一、被动式漏洞扫描器基本原理
被动式漏洞扫描器,也叫交互式漏洞扫描器,英文叫Interactive application security testing,顾名思义,指非主动进行攻击测试的一类漏洞扫描器。这类扫描器从实现原理上来说又可以分为两类,一类是基于代理流量的黑盒扫描器,另一类是基于字节码插桩的灰盒扫描器。两者的核心引擎都不主动通过爬虫的方式收集目标系统的web接口信息。基于代理流量的漏洞扫描器一般通过接收外部转发来的http/https数据包,被动的触发黑盒扫描任务,而基于字节码插桩的灰盒漏洞扫描器一般是通过静态方式修改字节码,在关键的函数上织入hook逻辑,进行运行时数据流分析,这种检测方案也需要接收外部的输入,被动地进行漏洞检测分析,就web层面来说,这里的输入一般指http/https请求数据包。
基于代理流量的被动式漏洞扫描器与传统的主动式扫描器相比的优势是,被动式漏洞扫描器在对目标系统攻击面的收集更加的全面,因为其攻击面收集的方式相对多元化,在继承传统的主动式的攻击面的收集基础之上,还可以通过代理流量/镜像的方式收集测试人员人工提交的数据包信息进行攻击面的采集,另外在鉴权检测方面也有不错的效果。
本文主要分享下基于代理流量的扫描器的相关实现原理和架构设计,在后续的文章中,我们将会详细的剖析基于字节码插桩的灰盒扫描器的技术实现。
(注:以下涉及到的漏洞信息均为模拟数据。)
二、HTTP/HTTPS流量采集方案研究
谈到被动式漏洞扫描器,其核心之一就是流量的采集,所以,我们先来聊一聊流量的采集问题。就http/https流量采集来说,总的来说有两种方案,一种是基于http/https代理的流量收集方案,另一种是基于nginx mirror模块的流量收集方案,下面详细的剖析下这两种技术实现方案的原理。
基于代理
基于http/https代理的流量采集方案基本原理
简单来说,所谓的http/https流量代理劫持其实就是在服务端和客户单之间插入一个代理服务器,通过代理服务器,将经过代理服务器的tcp流以http报文格式解析,做一些处理后,然后再通过socket方式将这些报文进行转发。如果是https协议通信的话,则需要通过自签名/商业的ca证书进行https认证劫持,来获取https通信信道中的明文数据包。在python中,实现了一个基类BaseHTTPRequestHandler,用于将tcp流数据解析为http 请求数据包格式的报文,这是python的webserver实现的主要基类之一。python中可通过重写BaseHTTPRequestHandler内置函数do_GET, do_POST, do_PUT, do_HEAD, do_DELETE, do_OPTIONS, 来对这些方法对应的http请求/响应报文进行篡改,这是一些代理流量劫持的基本操作。这里我们要进行漏洞检测的话,则需要对http请求对报文进行一些处理。由于我们需要同时测试非常多的漏洞插件,所以就需要基于原始的数据包构建多个攻击数据包,攻击数据包的数量取决于插件的数量。
https流量代理实现示意图
如图所示,在正常的https加密通信过程中,客户端首先向服务端发送一个携带了加密方法的hash,服务端记录下加密方法,并返回相应的证书和公钥信息。客户端接收到公钥之后,首先会判断公钥和证书的有效性,如果是用的自签名的ca证书,则需要手动将证书导入到浏览器中,让浏览器信任该证书,以便进行后续的流量加解密工作。然后,浏览器端将生成一串aes加密密钥,并通过公钥进行加密发送给https服务端,https服务端通过私钥解密获得aes加密密钥,此后两者之间便使用这串aes密钥进行数据的加解密通信。而我们如果需要劫持https流量的话,则需要破解这个https证书认证过程。
那么如何破解这个认证过程呢?我们可以在代理端通过自签名的ca证书做中间人劫持,具体的做法是让流向server端http/https流量首先经过代理端,这时候,代理端主要做两件事,第一件事是伪造https服务端身份,与客户端进行https认证通信,第二件事是伪造客户端与服务端进行https认证通信,通过这个两层欺骗,代理端在完成https认证之后,便获得了此后用于数据包加解密的aes密钥信息,便可对往来的数据包进行加解密。这部分具体的原理及劫持流程见上图,基于获取的明文数据包,我们可以重新构造带有攻击性payload的数据包,对目标web应用进行安全测试。
代理端https流量解密详细方案
1、如何生成自签名证书
def _gen_ca(self,again=False): # Generate key #如果证书存在而且不是强制生成,直接返回证书信息 if os.path.exists(self.ca_file_path) and os.path.exists(self.cert_file_path) and not again: self._read_ca(self.ca_file_path) #读取证书信息 return self.key = PKey() self.key.generate_key(TYPE_RSA, 2048) # Generate certificate self.cert = X509() self.cert.set_version(2) self.cert.set_serial_number(1) self.cert.get_subject().CN = 'baseproxy' self.cert.gmtime_adj_notBefore(0) self.cert.gmtime_adj_notAfter(315360000) self.cert.set_issuer(self.cert.get_subject()) self.cert.set_pubkey(self.key) self.cert.add_extensions([ X509Extension(b"basicConstraints", True, b"CA:TRUE, pathlen:0"), X509Extension(b"keyUsage", True, b"keyCertSign, cRLSign"), X509Extension(b"subjectKeyIdentifier", False, b"hash", subject=self.cert), ]) self.cert.sign(self.key, "sha256") with open(self.ca_file_path, 'wb+') as f: f.write(dump_privatekey(FILETYPE_PEM, self.key)) f.write(dump_certificate(FILETYPE_PEM, self.cert)) with open(self.cert_file_path, 'wb+') as f: f.write(dump_certificate(FILETYPE_PEM, self.cert))
2、自签名证书在https加解密中的作用
0x1 为什么要配置自签名证书
在上文https流量代理实现示意图中我们可以知道,在进行https请求连接的过程中,服务端需要下发证书给客户端,这一步如果下发的是自签名的证书的话,客户端浏览器(如chrome,firefox)会默认不信任自签名的证书,所以我们需要先手动配置安装证书。
0x2 自签名证书作用
上述的代码生成的文件有两个,一个是crt证书文件,一个是pem证书认证文件,crt证书文件中包含了非对称加密的公钥信息,pem证书认证文件中既包含了公钥信息,又包含了私钥信息。非对称加密,如上文https流量代理实现示意图中所述,应用于https安全连接通道建立时的认证操作,即代理端(https服务端)下发证书,浏览器端保存并信任证书的这一过程。证书中的公钥信息用于后续的对称加密密钥的加密,服务端中的私钥信息用于解密浏览器端发来的对称加密密钥信息,以实现后续的加密通信。
3、具体应用(回答如何配置http/https代理进行被动式安全扫描测试)
代理端实现https流量解密可以采用自签名证书方案或商业证书方案,自签名证书需要在客户端安装一个证书,让浏览器信任我们的https服务。这里以mac下的chrome浏览器为例,演示如何进行证书安装:
0x1 配置代理
用到的工具有chrome浏览器和switchyomega proxy管理插件。这里建议大家先安装一个switchyomega代理管理软件,这样方便进行代理的设置及切换。以proxy管理工具switchyomega为例:
0x2 导入证书
在chrome浏览器的设置中搜索证书并找到管理证书选项
点击管理证书,进入证书管理页面。如下图所示,点击左上角的新增证书按钮将我们的自签名证书添加到chrome中
刚添加到证书chrome默认是不信任的,需要手动设置始终信任,如下图所示,右键选中导入的自签名证书,设置为信任,这样以来客户端的chrome浏览器在后续建立https连接的过程中就会信任我们的代理服务端下发的证书文件:
0x3 https流量解密测试
这里我们以访问https://www.baidu.com为例,可以看到如下信息,代理端已经对发往https://www.baidu.com的https数据包进行了解密。
基于nginx mirror模块
基于nginx mirror模块的流量采集方案基本原理
当然,流量的收集不仅仅可以通过代理流量的方式,也可以通过nginx mirror模块,。
在基于nginx mirror模块的流量采集方案中,我们将通过以下步骤来实现对目标系统的流量采集:
1、配置nginx配置文件,增加配置一个mirror服务器,指定backend地址为我们魔改的一个webserver地址。这样一来,nginx就会在请求到来的时候,镜像一份原始的http请求包,并将请求转发到mirror 服务器。
2、在mirror服务器上将这些http请求数据包进行解析,解析出明文请求数据包后再将这些请求数据包push到消息队列中进行任务分发。
实验验证基于nginx mirror模块进行流量收集的可行性
为了验证这个思路的可行性,我们来做一个实验。这里,我们需要搭建一个支持mirror模块的nginx服务器,搭建过程就不再赘述,读者可自行搭建。
下面我们开始实验,首先我们在服务器a(192.168.1.10)上安装并配置好nginx,在服务器192.168.1.11上安装并配置好我们魔改后的webserver,这个魔改的webserver负责解析nginx转发过来的http数据包,并将解析后的http数据包push到消息队列中。
服务器a中的nginx配置文件如下:
worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 8181; mirror_request_body on; access_log /var/log/nginx/test.log; root html/test; } server { mirror_request_body on; listen 8282; access_log /var/log/nginx/mir1.log; root html/mir1; } upstream backend { server 127.0.0.1:8181; } upstream test_backend1 { server 192.168.1.11:9008; #如果需要做负载均衡,多配置一些backend #server 192.168.1.12:9008; #server 192.168.1.13:9008; } server { listen 80; server_name localhost; mirror_request_body on; location / { mirror /mirror1; proxy_pass http://backend; } location = /mirror1 { #internal; proxy_pass http://test_backend1$request_uri; } } }
简单的解释下上述的配置信息。在nginx.conf配置中,配置启动三个server实例,80端口的server负责转发原始请求数据包到8181和8282端口,8181端口http服务负责将原始请求转发给web后端,8282端口http服务负责将mirror的请求包转发到其他的webserver(基于python的BaseHTTPRequestHandler修改实现的webserver)。配置完毕后我们启动nginx,并且在http服务更目录下执行以下命令创建实验所需的一些文件:
cd /usr/share/nginx/html/ mkdir test mir1 echo "test page" >test/index.html
下面我们执行curl命令 curl http://127.0.0.1/index.html
执行完这个命令之后,我们期望的结果是,在/var/log/nginx/mir1.log和/var/log/nginx/test.log中都有相应的访问记录,这说明,nginx成功地将流量mirror了一份,并分别转发给了8282和8181端口对应的http服务。下面我们tail一下看看实际的访问结果
没错,一切正如开始所料,从access日志来看,已经成功对mirror服务器转发来http请求数据包进行接收和解析。这里我们给出一个简单的实例:
from http.server import HTTPServer, BaseHTTPRequestHandler import json class Resquest(BaseHTTPRequestHandler): def handler(self): print("data:", self.rfile.readline().decode()) self.wfile.write(self.rfile.readline()) def do_GET(self): print(self.requestline) print(self.headers) data = { "status":200, "info":"test" } self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(data).encode()) def do_POST(self): print(self.requestline) print(self.headers) req_datas = self.rfile.read(int(self.headers['content-length'])) print(req_datas.decode()) data = { "status":200, "info":"test" } self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps(data).encode('utf-8')) if __name__ == '__main__': host = ('0.0.0.0', 9008) server = HTTPServer(host, Resquest) print("Starting server, listen at: %s:%s" % host) server.serve_forever()
启动该服务之后,即可接收来自mirror服务器转发来的http请求数据包。这里我们通过重写do_GET及do_POST函数来对请求的数据包进行解析和打印。实验效果如下:
post数据包效果信息如下(curl -X POST -d 'name=张三' http://localhost/api/basic)
这个过程可以用以下示意图表示:
但是,通过nginx mirror模块收集流量的话,只能针对企业内部接入nginx的项目,而如果希望检测下未接入nginx的项目,或者网上其他的一些web站点,则还需要使用代理模式。这两者之间还是有互补的,nginx方面可以解决配置代理的麻烦和证书的麻烦(当然nginx本身也需要配置https证书),而代理模式可以让被动式漏洞扫描系统的作用范围更大一些。但就企业内部来说,基于nginx mirror模块的流量收集方案在自动化方面会相对比较有优势一些。
三、分布式漏洞检测框架实现方案
上文讲解了基于http/https代理流量劫持的基本原理,那么如何据此开发一款被动式漏洞检测系统呢?而我们又需要考虑到哪些问题呢?在大规模的被动式漏洞扫描的场景下,往往会有大量的流量被转发到代理服务器上,如果没有进行架构上的合理设计,那么代理服务器很可能会陷入数据包泛洪的困境,类似于dos攻击(ps:要进行漏洞检测,必然要构造请求数据包,设想一下,如果是500个漏洞检测插件,那么针对每一个请求过来代理服务器就得转发至少500个http请求包给目标webserver,倘若没有进行妥善的处理,这是一件非常恐怖的事情)。那么如何处理这个问题呢?下面是我们设计的基于代理流量的被动式漏洞扫描器的原理示意图,我们将基于此做进一步的讨论。
基于代理流量的分布式漏洞检测框架设计
在基于代理的被动流量收集方案中,用户通过在chrome端配置https/http代理,让流量首先走向我们的代理服务器,代理服务器端将原始数据包push到redis消息队列中,进行任务分发,slave workers监听redis消息队列,并通过抢占式方法获取到任务信息,加载漏洞检测插件,向服务端发送带有攻击payload的请求数据包,进行漏洞检测分析。另一方面代理服务端也会将原始的请求包转发到服务端,以便让用户端获得期望的请求结果。
在性能方面的优化方面,一方面可以通过适当的sleep将这些http攻击数据包分批次进行发送,另一方面可以通过分布式的方式来减轻自身的漏洞检测分析服务器的负担。
基于nginx mirror模块的分布式漏洞检测框架设计
基于nginx mirror模块的被动流量收集方案中,通过配置mirror server镜像一份原始请求流量,并将镜像的流量转发到aeacus 服务器,以便对http流量进行解析,并将解析后到http请求报文信息push到消息队列中。而后slave workers通过监听redis消息队列,并通过强占式方法获取到任务信息,加载漏洞检测插件,向服务端发送带有攻击payload的请求数据包,进行漏洞检测分析。
基于nginx mirror模块的被动式流量收集方面,可以从以下两个方面进行性能优化。一方面针对大量的并发请求,可通过配置nginx负载均衡方式,将这些请求平均分配给各个mirror服务器,这样每个mirror服务器处理的任务就会相对少一些,可根据实际的业务量来增减mirror服务器的数量,另一方面为减轻各个slave worker的漏洞分析压力,可根据实际情况拓展分布式任务节点的数量。当然,重要的一点是要对数据包进行去重处理,否则会增加任务节点的检测压力。
脏数据解决方案讨论
在进行被动式漏洞分析检测过程中,如果某些post请求数据包具有存储功能的话,那么大量的带有攻击性的数据包,无疑会带来大量的脏数据,这也是很多安全人员在推一些安全检测产品的时候比较经常考虑的一个问题。本人在推的时候也担心因为这个问题会受阻碍,但其实被动式漏洞检测系统主要是接在test环境中的,脏数据影响面比较小,几乎可忽略不计。如果测试方面觉得大量的脏数据会影响测试的效果,那么这里我们不妨讨论下如何处理测试过程中产生的脏数据问题。网上有些同学的解决方案是对一些可能涉及存储的接口进行一些过滤,不过个人觉得这个得慎重考虑,因为你无法保证这些接口没有存在漏洞,容易造成漏报。而如果不想脏数据误导测试同学的话,可以在payload中插入特定的识别码,以便测试同学辨认。但是我觉得最好的方法是将安全检测排在测试环节之后,在测试环节中只进行流量的收集而不进行安全检测分析,等测试同学确保功能测试没问题之后,再通过向检测服务端发送相应的检测指令,进行安全检测分析。
四、插件模块设计
1、支持动态加载的指纹识别模块
0x1 指纹识别模块加载执行流程图示意图
2、支持动态加载的漏洞检测模块
0x0 aeacus漏洞检测模块动态插件加载原理说明
插件加载在扫描作业的初始化阶段进行,通过python中的动态导入模块技术,将指定文件中的模块导入到当前进程中,然后对导入到模块进行合法性校验,如果符合期望的模块格式要求,则将该模块保存在模块缓存区域。在漏洞检测作业进行的时候,只需要遍历执行该模块缓存区域中的插件即可。动态加载的目的是为了支持漏洞检测插件的热更新,即,后续的插件维护人员只需要将编写好的插件放到指定的目录下,而无需重启系统就可以让系统支持更多的漏洞检测能力。
代码实现
//python中动态加载指定文件中的模块信息 def module_dynamic_loader(file_path): if '' not in importlib.machinery.SOURCE_SUFFIXES: importlib.machinery.SOURCE_SUFFIXES.append('') try: module_name = 'plugin_{0}'.format(get_filename(file_path, with_ext=False)) spec = importlib.util.spec_from_file_location(module_name, file_path, loader=PocLoader(module_name, file_path)) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod except ImportError: error_msg = "load module failed! '{}'".format(file_path) print(error_msg) raise
0x1 漏洞检测模块加载执行流程图示意图
五、爬虫模块与Aeacus被动式漏洞分析引擎的结合
爬虫方面可以配置https/http代理与aeacus系统进行联动,这样爬虫爬取的数据包就会转交给后端的被动式漏洞分析引擎处理,如果是采用nginx mirror流量采集方案则无需配置代理,直接爬取,aeacus系统即可自动获得相关的请求数据包。
六、数据包去重存储方案
数据包去重必要性
被动式漏洞扫描器在收集流量的过程中难免会收集到大量的重复数据包,而重复的数据包会增加后端漏洞分析引擎的负荷,所以我们需要对收集到的http/https请求数据包进行去重处理。
具体实现
主要依赖的去重算法:布隆去重算法
算法库:redisbloom<https://github.com/RedisBloom/redisbloom-py>
算法库安装配置:
#服务端安装配置 docker pull redislabs/rebloom:latest docker run -p 5276:6379 --name redis-redisbloom redislabs/rebloom:latest #客户端处理安装 pip install redisbloom
算法原理
基本原理
布隆过滤器内部维护一个bitArray(位数组), 开始所有数据全部置 0 。当一个元素过来时,能过多个哈希函数(hash1,hash2,hash3....)计算不同的在哈希值,并通过哈希值找到对应的bitArray下标处,将里面的值 0 置为 1 。 需要说明的是,布隆过滤器有一个误判率的概念,误判率越低,则数组越长,所占空间越大。误判率越高则数组越小,所占的空间越小。
初始化
插入lixiang.com,经过3个hash函数的计算,得到的hash值分别为1,4,7,然后在bitarray的相应索引处标记bit位为1。
数据包布隆去重代码实现
这里主要是根据port,hostname,path,method,params,protocal这几个参数对数据包进行签名,示例代码如下:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # desc: 数据包布隆去重 # author: pOny from redisbloom.client import Client from configure import bloom_server_address,bloom_server_port class bloomfilter(): rb = Client(host=bloom_server_address, port=bloom_server_port) @staticmethod def add_packet_hash(**kwargs): ''' 添加hash信息 :param kwargs: datadict={ "method":"post", "protocal":"http", "hostname":"pony.com", "port":"80", "path":"/docs", "params":["p1","p2","p3"] } :return: None ''' port,hostname,path,method,params,protocal=kwargs.get("port"),\ kwargs.get("hostname"),kwargs.get("path"),\ kwargs.get("method"),kwargs.get("params"),\ kwargs.get("protocal") params= "".join(params) if params else "" if isinstance(port,int): port=str(port) data="{}{}{}{}{}{}".format(port,hostname,path,method,params,protocal) bloomfilter.rb.bfAdd(kwargs.get("projectid"),data) @staticmethod def dofilter(**kwargs): ''' 布隆去重复 :param kwargs: datadict={ "method":"post", "protocal":"http", "hostname":"lixiang.com", "port":"80", "path":"/docs", "params":["p1","p2","p3"], } :return:Boolean ''' port,hostname,path,method,params,protocal=kwargs.get("port"),\ kwargs.get("hostname"),kwargs.get("path"),\ kwargs.get("method"),kwargs.get("params"),\ kwargs.get("protocal") params= params="".join(params) if params else "" if isinstance(port,int): port=str(port) data="{}{}{}{}{}{}".format(port,hostname,path,method,params,protocal) return bloomfilter.rb.bfExists(kwargs.get("projectid"),data)
七、Aeacus被动式漏洞扫描器介绍
理想汽车安全部devsecops小组在过去的q2季度里,一直致力于被动式漏洞扫描器的研究和研发,在被动式漏洞扫描器的研发方面积累了一些经验,在此给各位做一些心得分享,希望对各位之后的工作有一些帮助。
注:以下涉及到的漏洞数据均为模拟数据。
0x0 整体架构图
0x1 漏洞数据可视化展示面板
0x2 漏洞管理
漏洞列表
漏洞详情
0x3 项目管理
项目列表
项目添加
0x4 插件管理
插件列表
插件添加页面
0x5 报告订阅
可通过报告订阅模块订阅漏洞报告信息。
0x6 工单模块
拉取工单
飞书自动同步漏洞信息进行漏洞修复讨论
八、流程自动化及闭环
aeacus被动式漏洞扫描器闭环流程示意图(基础型自动化)
如下为aeacus被动式漏洞扫描器在devops流程中的接入示意图,在流程上,目前基本上算是实现了自动化及流程闭环。目前一个标准的上线发布流程中,包括了镜像构建前的白盒检测,和test环境中的被动式漏洞扫描检测,这里只展示被动式漏洞扫描检测相关的流程信息。
在test环境中,aeacus被动式漏洞扫描器将通过nginx mirror模块/http代理采集项目相关的http请求数据包,并将这些数据包解析后push到redis消息队列中,然后slave worker节点通过监听redis消息队列,获取任务信息,对test环境中的系统进行漏洞扫描检测,并将漏洞检出结果同步到devsecops飞书群和数据库中,相关的负责人可在飞书群或devsecops平台上查看相关的漏洞信息。针对单个漏洞修复,devsecops平台提供漏洞修复工单模块,可自动拉取飞书临时讨论群进行漏洞修复讨论。在漏洞修复完毕之后将通过邮件方式通知项目发布者,然后由项目发布者再次执行构建发布。
此外,devsecops平台还将定期给项目的负责人发送项目的风险日报/周报/月报/季报,以让相关人员对项目目前的风险情况有一个比较清晰的了解。
飞书自动同步漏洞信息进行漏洞修复讨论
每日风险检测情况汇报
每周风险检测情况汇报
月度风险检测情况汇报
季度风险检测情况汇报
漏洞修复生命周期监控和展示
九、CONTACT WITH ME
email:747289639@qq.com(微信同)
By 郑斯碟
理想汽车安全部DevSecOps负责人
:)