如何在 Bash 中不依赖 curl 或 wget 发出 HTTP 请求并实现文件传输——/dev/tcp的妙用
1. 前言
在 Bash 脚本编程中,发送 HTTP 请求通常依赖于像 curl
或 wget
这样的外部工具。然而,Bash 本身隐藏着一个鲜为人知的功能:通过内置的 /dev/tcp
或 /dev/udp
伪设备,可以直接与网络进行交互,而无需额外安装任何工具。这个特性最初由 KornShell (ksh) 引入,后来被 Bash 继承,其设计初衷是为了方便用户通过网络发送数据,例如生成报告或执行简单的网络操作。然而,这个功能也因其强大而灵活的特性,常常被黑客或渗透测试人员利用,比如创建反向 Shell。
本文将深入探讨这一功能的实现原理,展示如何利用它发送 HTTP 请求,并进一步扩展到使用 cat <
和 cat >
实现文件的上传和下载。我们将从基础用法开始,逐步深入到实际应用场景,并提供丰富的示例代码和分析。
2. Bash 中的 /dev/tcp 功能解析
2.1 手册中的秘密
要了解这个功能的奥秘,我们可以查阅 Bash 的手册(运行 man bash
),在 “REDIRECTION(重定向)” 部分会发现一些有趣的细节。手册中提到,Bash 在处理某些特殊文件名时会进行特殊操作,这些文件名包括:
/dev/fd/fd # 复制文件描述符 fd
/dev/stdin # 复制标准输入(文件描述符 0)
/dev/stdout # 复制标准输出(文件描述符 1)
/dev/stderr # 复制标准错误(文件描述符 2)
/dev/tcp/host/port # 打开与指定主机和端口的 TCP 连接
/dev/udp/host/port # 打开与指定主机和端口的 UDP 连接
其中,/dev/tcp/host/port
是本文的核心。如果 host
是一个有效的主机名或 IP 地址,而 port
是一个整数端口号或服务名称(如 80
或 http
),Bash 会尝试打开对应的 TCP 套接字。如果底层操作系统提供了这些特殊文件,Bash 会直接使用它们;否则,Bash 会通过内部机制模拟这些行为。
需要注意的是,/dev/tcp
并不是文件系统中的实际路径。尝试列出它(如 ls -l /dev/tcp
)会返回“没有那个文件或目录”的错误。这是因为它完全是 Bash 的内置特性,而非 Linux 内核或文件系统的一部分。
2.2 基本用法
要使用 /dev/tcp
,我们需要借助 exec
命令创建一个与目标主机和端口的连接。例如:
exec 3<>/dev/tcp/example.com/80
这条命令的作用是打开一个到 example.com
的 80 端口(HTTP 默认端口)的 TCP 连接,并将其绑定到文件描述符 3。<>
表示这是一个双向连接,既可以写入数据(发送请求),也可以读取数据(接收响应)。
运行后,表面上看似什么也没发生,但实际上 Bash 已经在后台建立了一个 TCP 套接字。我们可以通过 strace
工具验证这一过程:
strace -f -e trace=network bash -c 'exec 3<>/dev/tcp/baidu.com/80'
输出中会包含类似以下内容:
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("220.181.38.148")}, 16) = 0
这表明 Bash 创建了一个 TCP 套接字(文件描述符 3),并成功连接到了目标服务器。
3. 发送 HTTP 请求
3.1 GET 请求示例
让我们通过一个完整的脚本展示如何发送 HTTP GET 请求:
#!/bin/bash
# 打开 TCP 连接到 baidu.com 的 80 端口
exec 3<>/dev/tcp/baidu.com/80
# 发送 HTTP GET 请求
echo -ne "GET / HTTP/1.1\r\nHost: baidu.com\r\nConnection: close\r\n\r\n" >&3
# 读取并输出服务器响应
cat <&3
# 关闭文件描述符
exec 3<&-
逐步解析:
-
exec 3<>/dev/tcp/baidu.com/80
创建一个双向 TCP 连接,绑定到文件描述符 3。 -
echo -ne "GET / HTTP/1.1\r\nHost: baidu.com\r\nConnection: close\r\n\r\n" >&3
使用echo
构造一个标准的 HTTP GET 请求,并通过>&3
将其写入文件描述符 3。-n
避免多余的换行符。-e
启用转义字符(如\r\n
),以满足 HTTP 协议的换行要求。Connection: close
确保服务器在响应后关闭连接。
-
cat <&3
从文件描述符 3 读取服务器的响应并输出到终端。 -
exec 3<&-
关闭文件描述符,释放资源。
运行后,你可能会看到类似以下的输出:
HTTP/1.1 200 OK
Date: Sun, 16 Mar 2025 10:00:00 GMT
Server: Apache
Content-Length: 81
Connection: close
Content-Type: text/html
<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>
3.2 POST 请求示例
发送 POST 请求的步骤类似,只需调整 HTTP 请求头和添加请求体:
#!/bin/bash
# 打开 TCP 连接
exec 3<>/dev/tcp/baidu.com/80
# 发送 HTTP POST 请求
echo -ne "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
# 读取响应
cat <&3
# 关闭连接
exec 3<&-
输出可能是:
HTTP/1.1 302 Found
Date: Sun, 16 Mar 2025 10:05:00 GMT
Server: Apache
Location: http://www.baidu.com/search/error.html
Content-Length: 222
Connection: close
Content-Type: text/html
<!DOCTYPE HTML>
<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>
这里的关键是正确设置 Content-Length
,其值必须与请求体的字节数匹配。
4. 文件上传与下载的扩展
/dev/tcp
的强大之处不仅限于发送简单的 HTTP 请求,还可以通过文件描述符结合 cat
命令实现文件的上传和下载。以下是具体方法。
4.1 文件上传(使用 cat >)
假设我们有一个本地文件 upload.txt
,内容为:
Hello, this is a test file for upload.
我们可以通过以下脚本将文件上传到支持文件上传的服务器(如一个简单的 HTTP 文件接收端点):
#!/bin/bash
# 定义目标服务器和端口
HOST="example.com"
PORT=80
ENDPOINT="/upload"
# 计算文件内容的长度
FILE="upload.txt"
CONTENT_LENGTH=$(wc -c < "$FILE")
# 打开 TCP 连接
exec 3<>/dev/tcp/$HOST/$PORT
# 构造并发送 POST 请求
{
echo -ne "POST $ENDPOINT HTTP/1.1\r\n"
echo -ne "Host: $HOST\r\n"
echo -ne "Content-Type: text/plain\r\n"
echo -ne "Content-Length: $CONTENT_LENGTH\r\n"
echo -ne "Connection: close\r\n"
echo -ne "\r\n"
cat "$FILE"
} >&3
# 读取服务器响应
cat <&3
# 关闭连接
exec 3<&-
解析:
wc -c < "$FILE"
计算文件的字节数,用于设置Content-Length
。cat "$FILE" >&3
将文件内容直接写入文件描述符,实现上传。- 请求头和文件内容通过
{}
组合在一起,确保顺序发送。
如果服务器支持文件上传,响应可能是:
HTTP/1.1 200 OK
Date: Sun, 16 Mar 2025 10:10:00 GMT
Content-Type: text/plain
Content-Length: 20
Connection: close
File uploaded successfully
优化版本:支持大文件分块上传
对于大文件,可以使用分块上传的方式,避免一次性加载整个文件到内存:
#!/bin/bash
HOST="example.com"
PORT=80
ENDPOINT="/upload"
FILE="largefile.bin"
CHUNK_SIZE=1024 # 每次发送 1KB
# 获取文件总大小
TOTAL_SIZE=$(wc -c < "$FILE")
# 打开 TCP 连接
exec 3<>/dev/tcp/$HOST/$PORT
# 发送分块上传请求
{
echo -ne "POST $ENDPOINT HTTP/1.1\r\n"
echo -ne "Host: $HOST\r\n"
echo -ne "Content-Type: application/octet-stream\r\n"
echo -ne "Content-Length: $TOTAL_SIZE\r\n"
echo -ne "Connection: close\r\n"
echo -ne "\r\n"
dd if="$FILE" bs=$CHUNK_SIZE # 使用 dd 分块读取并发送
} >&3
# 读取响应
cat <&3
# 关闭连接
exec 3<&-
这里使用了 dd
工具按块读取文件并发送,适用于大文件场景。
4.2 文件下载(使用 cat <)
要从服务器下载文件,只需发送一个 GET 请求并将响应保存到本地文件。例如,下载一个远程文本文件:
#!/bin/bash
HOST="example.com"
PORT=80
ENDPOINT="/files/sample.txt"
# 打开 TCP 连接
exec 3<>/dev/tcp/$HOST/$PORT
# 发送 GET 请求
echo -ne "GET $ENDPOINT HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n" >&3
# 将响应保存到文件(跳过 HTTP 头)
cat <&3 > downloaded.txt
# 关闭连接
exec 3<&-
注意事项:
- 上面的脚本会将完整的 HTTP 响应(包括头信息)保存到
downloaded.txt
。如果只需要文件内容,可以用工具(如sed
或awk
)过滤掉头信息:
#!/bin/bash
HOST="example.com"
PORT=80
ENDPOINT="/files/sample.txt"
# 打开 TCP 连接
exec 3<>/dev/tcp/$HOST/$PORT
# 发送 GET 请求
echo -ne "GET $ENDPOINT HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n" >&3
# 跳过 HTTP 头,仅保存文件内容
sed '1,/^\r$/d' <&3 > downloaded.txt
# 关闭连接
exec 3<&-
这里 sed '1,/^\r$/d'
的作用是从开头删除到第一个空行(即 HTTP 头和正文之间的分隔符 \r\n
)。
下载二进制文件
对于图片、视频等二进制文件,直接使用 cat
保存即可,但需要确保服务器返回的是正确的 Content-Type
:
#!/bin/bash
HOST="example.com"
PORT=80
ENDPOINT="/images/sample.jpg"
# 打开 TCP 连接
exec 3<>/dev/tcp/$HOST/$PORT
# 发送 GET 请求
echo -ne "GET $ENDPOINT HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n" >&3
# 保存二进制文件(包含头信息)
cat <&3 > sample.jpg
# 关闭连接
exec 3<&-
如果需要纯二进制内容,可以结合 tail
或 dd
跳过头信息:
#!/bin/bash
HOST="example.com"
PORT=80
ENDPOINT="/images/sample.jpg"
# 打开 TCP 连接
exec 3<>/dev/tcp/$HOST/$PORT
# 发送 GET 请求
echo -ne "GET $ENDPOINT HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n" >&3
# 跳过头信息保存文件
tail -n +5 <&3 > sample.jpg # 假设头信息占前 4 行,具体行数需根据实际响应调整
# 关闭连接
exec 3<&-
5. 进阶应用与优化
5.1 处理 HTTPS
/dev/tcp
本身不支持 SSL/TLS 加密,因此无法直接处理 HTTPS 请求。不过,可以通过代理或外部工具(如 openssl s_client
)间接实现:
#!/bin/bash
HOST="example.com"
PORT=443
# 使用 openssl 建立 SSL 连接并绑定到文件描述符
exec 3<>/dev/tcp/$HOST/$PORT
openssl s_client -connect $HOST:$PORT -quiet <&3 >&3 2>/dev/null &
# 发送 HTTPS 请求
echo -ne "GET / HTTP/1.1\r\nHost: $HOST\r\nConnection: close\r\n\r\n" >&3
# 读取响应
cat <&3
# 关闭连接
exec 3<&-
这种方法依赖 openssl
,不算完全“无外部工具”,但展示了 /dev/tcp
的灵活性。
5.2 多线程下载
通过结合 Bash 的后台进程,可以实现简单的多线程下载。例如,分段下载一个大文件:
#!/bin/bash
HOST="example.com"
PORT=80
ENDPOINT="/largefile.bin"
TOTAL_SIZE=10485760 # 假设文件大小 10MB
CHUNK_SIZE=5242880 # 每个分段 5MB
# 分段下载函数
download_chunk() {
local OFFSET=$1
local LENGTH=$2
local OUTPUT="part_$OFFSET.bin"
exec 3<>/dev/tcp/$HOST/$PORT
echo -ne "GET $ENDPOINT HTTP/1.1\r\nHost: $HOST\r\nRange: bytes=$OFFSET-$(($OFFSET+$LENGTH-1))\r\nConnection: close\r\n\r\n" >&3
sed '1,/^\r$/d' <&3 > "$OUTPUT"
exec 3<&-
}
# 启动两个分段下载
download_chunk 0 $CHUNK_SIZE &
download_chunk $CHUNK_SIZE $CHUNK_SIZE &
# 等待所有分段完成
wait
# 合并文件
cat part_0.bin part_$CHUNK_SIZE.bin > largefile.bin
这里使用了 HTTP 的 Range
头实现分段下载,适用于支持范围请求的服务器。
6. 总结
Bash 的 /dev/tcp
功能提供了一种轻量、原生的方式来发送 HTTP 请求和实现文件传输。通过文件描述符和 cat <
、cat >
,我们可以在不依赖 curl
或 wget
的情况下完成 GET、POST 请求,甚至上传和下载文件。这一特性特别适合快速测试、脚本自动化或资源受限的环境。
通过掌握这一隐藏功能,你可以在 Bash 中解锁更多可能性,无论是网络调试还是自动化任务,都能游刃有余。
扩展阅读
- 查看文件描述符:
ls -l /proc/self/fd/
- 调试网络连接:
strace
或tcpdump
- 学习 HTTP 协议:RFC 2616(HTTP/1.1)