被动漏洞扫描器-代理设计与实现
- 前言
赶着毕业前一直打算写一款自己的扫描器,最后毕设的时候各种忙,最后还是没有完成我的想法采用了GourdScan V2.0的设计。这种传统的扫描器个人感觉放在五年前十年前还有用,可以扫到很多常见的Web漏洞。可是现在的web环境下使用这种直接扫描的方式,除了发出一大堆非常明显的流量以外,检测的结果还是比较局限的。所以自己设计扫描器这个想法一直没有放弃,刚好趁着离职的这一段时间研究研究,终于把当时没有搞下来的代理搞定了。
- 方案
首先,先说一下扫描器大概的一个方案。刚开始设想这个扫描器的时候很简单,就是能从手机或者各种终端设备中产生获取流量,扫描任务在对这些流量进行扫描。其实这些流量在我们生产生活中往往被忽视掉,可是对于企业来说这些接口是他们对用户的一个虚拟资产,在漏洞挖掘或者扫描过程,这些流量往往是爬虫最大的痛点,这也正是基于流量扫描的一个最大的优势。
想好最原始的方案后,我开始想如何去设计代理模块,最早的时候是想自己去造轮子,想法很简单就是Socks5代理到服务器,服务器解析Socks5包分析出http流量导出。当自己尝试写了一版Socks5协议最简单的功能的时候,且不说性能,不知道是不是操作系统的原因,发现不同的客户端Socks5协议都有所差异,流量小还行流量大了服务端经常蹦,不稳定。后面灵机一动想到ss开源的,而且流量也是加密传输,安全性也高,客户端也比较丰富,就选择了ss进行一些定制化的设计。
- Socks5协议
说到代理,不得不从Socks5协议说起,其实Socks5协议很简单:
sock5代理的工作程序是:
1.需要向代理方服务器发出请求信息。
2.代理方应答
3.需要代理方接到应答后发送向代理方发送目的ip和端口
4.代理方与目的连接
5.代理方将需要代理方发出的信息传到目的方,将目的方发出的信息传到需要代理方。代理完成。
首先服务器和客户端建立连接后进行握手,这个握手的格式在官方文档可以看到:
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1 to 255 |
+----+----------+----------+
- VER:socks的版本(socks5对应的是0x05)
- NMETHODS:在METHODS字段中出现的方法的数目.
- METHODS:客户端支持的认证方式列表,每个方法占1字节。
服务器返回信息如下:
+----+--------+
|VER | METHOD |
+----+--------+
| 1 | 1 |
+----+--------+
- VER:socks版本(在socks5中是0x05);
- METHOD:服务端选中的方法(若返回0xFF表示没有方法被选中,客户端需要关闭连接);
讲到这里后面看文档就知道怎么连接,详细可以看文档,很短的。既然介绍,那把一个TCP代理流程写一下吧:
1.向服务器的1080端口建立tcp连接。
2.向服务器发送 05 01 00 (此为16进制码,以下同)
3.如果接到 05 00 则是可以代理
4.发送 05 01 00 01 + 目的地址(4字节) + 目的端口(2字节),目的地址和端口都是16进制码。 例 202.103.190.27 -7201 则发送的信息为:05 01 00 01 CA 67 BE 1B 1C 21 (CA=202 67=103 BE=190 1B=27 1C21=7201)
5.接受服务器返回的自身地址和端口,连接完成
6.以后操作和直接与目的方进行TCP连接相同。
RFC文档在这里:https://www.ietf.org/rfc/rfc1928.txt
- SS源码分析
这一部分的分析比较长,读了很久的源码,理解源码简单,但是具体稳定应用比较难。ss是一个非常优秀和稳定的工具,如果想理解源码的流程,可以拜读一下这几片文章:
主要读前两篇就可以了,这里不再啰嗦了,直接发一张ss连接的结构图:
我们主要抓取在ssserver和目标服务器连接的时候的流量,这一部分流量是ss解密过的。下面是创建和远程服务器连接的流程,首先dns解析,成功了进行创建一个与remote通信的套接字。
def _handle_dns_resolved(self, result, error):
if error:
self.destroy()
return
if not (result and result[1]):
self.destroy()
return
ip = result[1]
self._stage = STAGE_CONNECTING
remote_addr = ip
if self._is_local:
remote_port = self._chosen_server[1]
else:
remote_port = self._remote_address[1]
remote_sock = self._create_remote_socket(remote_addr,
remote_port)
try:
remote_sock.connect((remote_addr, remote_port))
except (OSError, IOError) as e:
pass
self._loop.add(remote_sock,
eventloop.POLL_ERR | eventloop.POLL_OUT,
self._server)
self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING)
self._update_stream(STREAM_DOWN, WAIT_STATUS_READING)
remote_sock,这个就是和远程服务器连接的套接字,直接把这一块导出出来就ok了。
Https流量
看似万事大吉的时候,又有一个问题来了,如果是Https流量的话,那该怎么办?这里套接字导出的数据也都是ssl加密了的。我想了很久,觉得还是像Burp这种,中间做一个ca服务器,自己颁发证书自己信任比较可行。这里就找到python的一个很好用的库mitmproxy,这个库内置的实现了一整套的代理和认证,可以把这个库和ss融合到一起。为了不让代码过于耦合,首先本地用mimtproxy起一个socks5的服务端,同时我在ss向远程主机发出请求的时候把套接字改一下,加一层socks5协议,强行将ss的流量导入mitmproxy,这样两部分的耦合也比较小,同时流量管理起来也比较方便。下面是修改后的_create_remote_socket方法。
def _create_remote_socket(self, ip, port):
addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM,
socket.SOL_TCP)
#print("-------------------------------------------目标地址----------------------------------------------")
#print(addrs)
#print("-------------------------------------------END----------------------------------------------")
print(self._client_address[0],self._config['server_port'])
if len(addrs) == 0:
raise Exception("getaddrinfo failed for %s:%d" % (ip, port))
af, socktype, proto, canonname, sa = addrs[0]
if self._forbidden_iplist:
if common.to_str(sa[0]) in self._forbidden_iplist:
raise Exception('IP %s is in forbidden list, reject' %
common.to_str(sa[0]))
remote_sock = socket.socket(af, socktype, proto)
remote_sock = socks.socksocket()
remote_sock.set_proxy(socks.SOCKS5, "localhost", 8080,username='tttest')
self._remote_sock = remote_sock
self._fd_to_handlers[remote_sock.fileno()] = self
remote_sock.setblocking(False)
remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
return remote_sock
此时本地8080端口用mimtproxy起一个socks5服务端。
$ curl -xsocks5://127.0.0.1:1086 https://www.baidu.com
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
看到mitmproxy也能收到流量:
后面流量存储可以直接mitmproxy加载脚本将流量导入mongodb当中。代理部分框架差不多这样设计完成了,目前已经达到预期的效果。但是后期迭代的时候还有一些坑需要填补,就是目前只是单用户使用,如果是多用户使用,如何去区分不同用户或者客户端的流量是一个问题。这部分目前的设想是ss可以通过起不同端口来认证不同的用户,或者通过源ip来区分,ss区分的不同的用户在通过不同的用户特征给mitmproxy,但是mitmproxy不支持socks5用户认证,源码上面socks5认证这一块留了空,需要后人进行填补。