前言
Bash有一个隐藏的功能,可以在不使用curl或wget等工具的情况下进行HTTP请求。这个功能最初是从KornShell (ksh)中引入的,旨在简化通过网络发送数据的过程,比如生成报告,虽然它的设计初衷是为了方便用户,但也可能被黑客或渗透测试人员利用,创建反向shell等。本文将介绍它是如何工作的。
Bash发送http请求
阅读 Bash 手册:man bash, 在 “REDIRECTION(重定向)” 部分,会发现一个有趣的细节:
当某些文件名用于重定向时,Bash 会对它们进行特殊处理。如果你运行 Bash 的操作系统支持这些特殊文件(如 /dev/tcp 和 /dev/udp),Bash 将直接使用它们;如果不支持,Bash 则会内部模拟这些功能,采用相应的行为。
Bash handles several filenames specially when they are used in redirections, as described in the following ta‐ ble. If the operating system on which bash is running provides these special files, bash will use them; oth‐ erwise it will emulate them internally with the behavior described below. /dev/fd/fd If fd is a valid integer, file descriptor fd is duplicated. /dev/stdin File descriptor 0 is duplicated. /dev/stdout File descriptor 1 is duplicated. /dev/stderr File descriptor 2 is duplicated. # 如果 host 是有效的主机名或 Internet 地址,并且 port 是整数端口号或服务名称,则 Bash 会尝试打开相应的 TCP 套接字。 /dev/tcp/host/port If host is a valid hostname or Internet address, and port is an integer port number or service name, bash attempts to open the corresponding TCP socket. /dev/udp/host/port If host is a valid hostname or Internet address, and port is an integer port number or service name, bash attempts to open the corresponding UDP socket.
如果尝试列出 /dev/tcp 或 /dev/upd 目录,会出现以下错误:
jpzhang-dev:~# ls -lat /dev/tcp ls: 无法访问 '/dev/tcp': 没有那个文件或目录
这是因为 /dev/tcp 并不是文件系统上的一个实际路径,它是 Bash 本身的一个特性,而不是底层 Linux 系统的一个特性。
要使用该功能,需要使用 exec 命令将其作为 Bash 执行环境的一部分来调用(使用 exec 命令创建一个与目标主机和端口的连接):
root@ubuntu:~# exec 3<> /dev/tcp/...: 创建一个用于输入和输出操作的套接字,文件描述符的进程标识符等于 3 exec 3<>/dev/tcp/yok8s.com/80 # 例如:使用以下命令打开与目标主机的 TCP 连接 root@ubuntu:~# exec 3<>/dev/tcp/baidu.com/80
运行这条命令后,看似什么也没发生,但其背后做了很多事情。可通过 strace 进一步研究,如下:
使用 strace 工具可以监视系统调用和信号,帮助观察进程的运行情况,包括创建套接字和连接的过程,以下是如何使用 strace 观察 Bash 创建 TCP 套接字和连接的步骤(注:脚本所出现不明白的命令将在下文中解释)。
准备命令
假设我们要观察的命令是使用 /dev/tcp 进行 HTTP 请求的 Bash 脚本。例如,创建一个名为 request.sh 的脚本
#!/bin/bash exec 3<>/dev/tcp/baidu.com/80 echo -ne "GET / HTTP/1.1\r\nHost: baidu.com\r\nConnection: close\r\n\r\n" >&3 cat <&3 exec 3<&-
添加可执行权限:chmod +x request.sh
strace监视脚本
strace -f -e trace=network ./request.sh
-f 选项用于跟踪由该进程创建的所有子进程;
-e trace=network 选项限制输出为与网络相关的系统调用;
分析输出
在运行脚本时,strace 会输出相关的系统调用,包括创建套接字和连接的细节,你会看到类似以下内容的输出
....... socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3 setsockopt(3, SOL_IP, IP_RECVERR, [1], 4) = 0 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, 16) = 0
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP):创建一个 TCP 套接字,返回的 3 是文件描述符;
connect(…):连接到指定的 IP 地址和端口,返回 0 表示成功;
从上述输出中,证明执行 exec 3<>/dev/tcp/baidu.com/80 命令创建了绑定到 baidu.com 的 TCP/IP 套接字。
在 Linux 中,系统中的几乎所有内容都被视为文件,这包括常规文件、设备文件以及网络连接等。网络连接通过所谓的文件描述符(file descriptors)来表示,文件描述符是一个非负整数,用于指代系统打开的文件或连接。
文件描述符
标准文件描述符:
0:标准输入(stdin);
1:标准输出(stdout);
2:标准错误(stderr);
访问文件描述符
要查看当前进程打开的所有文件描述符,可以检查 /proc/self/fd/ 目录。/proc/self 是指当前进程,而 fd 子目录包含了该进程打开的所有文件描述符的链接。可以使用以下命令来查看当前进程的文件描述符:ls -l /proc/self/fd/ 例如:
root@ubuntu:~# ls -lat /proc/self/fd/ 总用量 0 dr-x------ 2 root root 0 Sep 25 14:40 . dr-xr-xr-x 9 root root 0 Sep 25 14:40 .. lrwx------ 1 root root 64 Sep 25 14:40 0 -> /dev/pts/0 lrwx------ 1 root root 64 Sep 25 14:40 1 -> /dev/pts/0 lrwx------ 1 root root 64 Sep 25 14:40 2 -> /dev/pts/0 lrwx------ 1 root root 64 Sep 25 14:40 3 -> 'socket:[236152582]' lr-x------ 1 root root 64 Sep 25 14:40 4 -> /proc/3076814/fd
这里的 0, 1, 2 分别表示标准输入、输出和错误;
3 是一个额外的文件描述符,可能代表一个打开的网络套接字。在这里 3 表示:连接到 baidu.com 的套接字所对应的文件描述符,用于指代这个连接;
接下来,向服务器发送一个 HTTP 请求,例如 GET 请求,使用 echo 命令构造请求并将其发送到文件描述符:
echo -ne "GET / HTTP/1.1\r\nHost: baidu.com\r\nConnection: close\r\n\r\n" >&3
-n 选项确保没有换行;
-e 选项允许使用转义字符(如 \r\n),以满足 HTTP 协议的要求;
虽然看起来什么也没发生,但实际上通过套接字发送了请求数据。使用 cat 命令从文件描述符中读取服务器的响应并输出:
cat <&3
输出
root@ubuntu:~# cat <&3 -------------------------------------------------------------------------------- HTTP/1.1 200 OK Date: Wed, 25 Sep 2024 07:23:11 GMT Server: Apache Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT ETag: "51-47cf7e6ee8400" Accept-Ranges: bytes Content-Length: 81 Cache-Control: max-age=86400 Expires: Thu, 26 Sep 2024 07:23:11 GMT Connection: Close Content-Type: text/html <html> <meta http-equiv="refresh" content="0;url=http://www.baidu.com/"> </html>
如上输出,显示服务器的响应,例如 HTML 内容和 HTTP 状态码。
关闭连接
# 完成所有操作后,关闭文件描述符以释放资源 exec 3<&-
完整示例
以下是完整的 Bash 脚本,展示如何使用套接字描述符实现一个简单的 HTTP GET 请求:
#!/bin/bash # 打开一个 TCP 连接到 baidu.com 的 80 端口,并将其分配给文件描述符 3 exec 3<>/dev/tcp/baidu.com/80 # 向文件描述符 3 发送 HTTP 请求 echo -ne "GET / HTTP/1.1\r\nHost: baidu.com\r\nConnection: close\r\n\r\n" >&3 # 从文件描述符 3 读取响应并输出到标准输出 cat <&3 # 关闭文件描述符 3 exec 3<&-
发送 HTTP POST 请求
root@ubuntu:~# exec 3<>/dev/tcp/baidu.com/80 root@ubuntu:~# echo -e "POST /submit HTTP/1.1\r\nHost: baidu.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 27\r\nConnection: close\r\n\r\nparam1=value1¶m2=value2" >&3 root@ubuntu:~# cat <&3 ------------------------------------------------------------------ 输出: HTTP/1.1 302 Found Date: Wed, 25 Sep 2024 07:28:14 GMT Server: Apache Location: http://www.baidu.com/search/error.html Cache-Control: max-age=86400 Expires: Thu, 26 Sep 2024 07:28:14 GMT Content-Length: 222 Connection: Close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>302 Found</title> </head><body> <h1>Found</h1> <p>The document has moved <a href="http://www.baidu.com/search/error.html">here</a>.</p> </body></html> ------------------------------------------------------------------ root@ubuntu:~# exec 3>&-
脚本模板
http-request.sh,对应替换 HTTP 请求地址:
#!/usr/bin/env bash exec 3<>/dev/tcp/ysap.daveeddy.com/80 lines=( 'GET /ping HTTP/1.1' 'Host: ysap.daveeddy.com' 'Connection: close' '' ) printf '%s\r\n' "${lines[@]}" >&3 while read -r data <&3; do echo "got server data: $data" done exec 3>&-
总结
Bash 的 /dev/tcp 功能为用户提供了一个直接的方式,通过 TCP 套接字发送 HTTP 请求,这一功能可以在没有额外工具的情况下执行简单的网络操作,非常适合快速测试和脚本自动化。