一、前言
“破壳之路”是破壳平台推出的一个系列文章,旨在通过复现多种漏洞挖掘过程,深入讲解破壳平台的使用。作为该系列的开篇之作,本文将通过两个具体漏洞的复现,展示在面对不同规模目标时,如何巧妙运用破壳平台进行高效的漏洞挖掘。
二、细粒度查询:TP-Link SR20命令执行
2.1 漏洞介绍
该漏洞出现在TP-Link SR20在处理TDDP协议内容时,TDDP(TP-Link设备发现协议,TP-Link Device Discovery Protocol)是一种专有协议,用于在网络中发现和管理TP-Link设备。TDDP协议允许网络管理员通过网络自动发现并配置TP-Link的路由器、交换机和其他设备。其具备以下功能:
- 发现设备:TDDP可以自动扫描网络,找到所有连接的TP-Link设备。这对于大规模网络管理非常有用,因为管理员不需要手动查找每个设备的IP地址。
- 配置设备:通过TDDP协议,管理员可以远程配置设备的参数。这可以包括网络设置、安全设置、固件更新等。
- 管理设备:TDDP还允许管理员监控设备的状态,如在线状态、设备负载等,以便及时进行维护和故障排除。
TDDP协议有两个版本:v1
和v2
。其中v1
不支持身份验证和对数据包载荷的加密,而v2
要求身份验证和加密。所以若TP-Link SR20 运行了 V1
版本的 TDDP 协议,在无需认证的情况下,往 SR20 设备的 UDP 1040 端口发送特定数据就可以造成命令注入漏洞。其原理如下:
结合图一可知,recvfrom函数的第一个参数param_1接受信息,然后传递到FUN_00015E74(图二),再次传递到FUN_00009944(图三)。在FUN_00009944中,通过sscanf(local_c,"%[^;];%s",local_68,local_a8)
语句对其内容以 ;
进行分割,字符串中 ;
前面的部分放入 local_68
中,;
后面的部分放入 local_a8
中,随后进入 FUN_000091dc
函数处理流程(图四)。
查看变量定义的位置, local_130
是一个指针数组,execve
函数的第二个参数就是指针数组(是 execve
执行程序的命令行参数),这里 execve("/bin/sh",(char **)local_130,(char **)0x0);
这个 local_130
也就是变量 local_68
,由 vsprintf
函数拼接而成,原始的数据为 FUN_000091dc
函数中调用 sscanf(local_c,"%[^;];%s",local_68,local_a8)
进行传递。
所以当控制 TDDP协议的版本号为 v1
,然后输入任意命令以 ;
结尾。这里虽然无法使用命令分隔符 ;
,但是 &&
和 ||
并没有进行过滤,依然可以造成命令注入。
2.2 查询思路与流程
该漏洞的漏洞模式较为清晰,而且分析对象较为简单,可以直接在变量层追踪变量的数据流,这种查询方式较为底层,误报较少,所以在本文中,将这种方式称呼为细粒度查询。即其是一个典型的污点类型可检测的漏洞,只需要检测类似recvfrom这样的可以被控制的数据引入点,有没有数据流传递到类似于execve这样的危险函数点即可。在静态漏洞挖掘中,这种检测方法被称作污点分析。
简单来说,污点分析要做的就是查询可控的数据源source点和危险函数sink点之间是否存在数据流关系。为了用户可以快速进行污点分析,破壳平台提供了VQL.taintPropagation函数给用户使用,其有以下的参数:
List<Long>source
List<Long>sink
Integer flag
List<Long>should
List<Long>shouldNot
source为source点ID集合;sink为sink点ID集合;should为期望经过的点ID集合;shouldNot为排除点ID的集合;flag为污点传播的标志位,默认为空,为从source和sink中数量较少的一方开始传递 ,flag=1
时为从source到sink进行有向传播,flag=2
时为从sink到source进行有向传播,flag=3
时进行无向污点分析。
该漏洞的漏洞模式可以用以下的语句表示:
MATCH (n)
WHERE n.callee="recvfrom" and n.index=0
WITH collect(id(n)) as source
MATCH (m)
WHERE m.callee="execve" and m.index=1
WITH source,collect(id(m)) as sink
CALL VQL.taintPropagation(source,sink) yield taintPropagationPath
RETURN taintPropagationPath
第一步:查询recvfrom的第一个参数作为source点集合
MATCH (n) where n.callee="recvfrom" and n.index=0
WITH collect(id(n)) as source;
第二步:查询execve的第二个参数作为sink点集合
MATCH (m) where m.callee="execve" and m.index=1
WITH source,collect(id(m)) as sink;
第三步:使用污点查询原语查询可达路径
CALL VQL.taintPropagation(source,sink) yield taintPropagationPath
RETURN taintPropagationPath;
最后产生的结果如图五所示,即通过污点分析最后定位了漏洞所在位置:
三、粗粒度查询:MikroTik CVE-2018-14847
3.1 漏洞介绍
MikroTik作为网络基础设施供应商,RouterOS 是 MikroTik 的旗舰软件产品,在线运行RouterOS 系统的设备至少有 300 万台。
本次复现的漏洞是CVE-2018-14847的一个目录遍历漏洞,漏洞原理网络上,参考文献比较多,本文的重点需要理解这种漏洞模式,然后如何编写脚本。该漏洞的流程如图六所示:
- 首先
nv::message::get_string
函数的第一个参数v73是一个可控的变量,而sub_804FCFA
函数是一个赋值作用的函数,其将v73的值传递给了p_path
; - 追寻
p_path
的来源,可见p_path的值来源于(string *)(v5+3)
; - 由于
v5[3]
和v5+3
指向同一块地址,而v5[3]
的值又传递给了v11
; - v11的值最后传到了open的第一个参数,导致目录遍历漏洞形成。
3.2 攻击面分析
使用ps -a 查看routeros中运行的进程,发现许多进程都是/nova/bin目录下的二进制文件所启动的。通过逆向分析可知,用户发送的数据通过80端口或8291端口进行处理后,会通过进程间通信的方式转发给对应的nova/bin目录下的二进制文件进行处理。因此,我们可以将nova/bin目录下的二进制文件一起打包上传到破壳平台上进行分析。
80 0 0:00 /nova/bin/sstore
83 0 0:03 /nova/bin/loader
85 0 0:00 /nova/bin/mproxy
86 0 0:00 /nova/bin/log
87 0 0:00 /nova/bin/moduler
88 0 0:02 /nova/bin/discover
89 0 0:00 /nova/bin/mactel
90 0 0:00 /nova/bin/bridge2
91 0 0:00 /nova/bin/macping
93 0 0:12 /nova/bin/net
94 0 0:00 /nova/bin/sys2
95 0 0:00 /nova/bin/btest
97 0 0:01 /pckg/ipv6/nova/bin/radvd
98 0 0:00 /pckg/dhcp/nova/bin/dhcpclient
99 0 0:00 {route_MAIN} /nova/bin/route
100 0 0:00 /pckg/ups/nova/bin/ups
102 0 0:00 /nova/bin/sermgr
103 0 0:00 /nova/bin/diskd
104 0 0:00 /pckg/wireless/nova/bin/wireless
106 0 0:00 /nova/bin/quickset
108 0 0:00 /nova/bin/watchdog
110 0 0:01 /nova/bin/resolver
111 0 0:00 /nova/bin/www
112 0 0:00 /nova/bin/modprobed
113 0 0:00 /nova/bin/keyman
114 0 0:00 route_NETLINK
115 0 0:00 route_POLICY
116 0 0:00 route_BGP
117 0 0:00 route_RIP
118 0 0:00 route_OSPF
119 0 0:00 route_PIMSM
120 0 0:00 route_LDP
121 0 0:00 route_FANTASY
122 0 0:00 route_STATIC
183 0 0:00 /nova/bin/user
185 0 0:00 /nova/bin/mesh
190 0 0:00 /pckg/security/nova/bin/sshd
191 0 0:00 /nova/bin/cerm-worker
201 0 0:00 /nova/bin/graphing
203 0 0:00 /nova/bin/romon
215 0 0:00 [kworker/u4:2-ev]
217 0 0:00 /bin/login -z admin
218 0 0:01 /nova/bin/console
232 0 0:00 /rw/disk/busybox telnetd -l /rw/disk/ash -p 1270
250 0 0:00 /rw/disk/ash
251 0 0:00 {ps} /rw/disk/ash
3.3初步分析
在该漏洞案例中,因为分析的对象比较多,如果直接对所有分析对象使用污点传播,速度会非常慢,所以我们这里使用函数调用图(call-graph)来疑似点进行过滤。与上文介绍的细粒度查询不同的是函数的抽象程度更高,是一种更上层的查询,所以在本文中称作粗粒度查询。
函数调用图是一种图形表示方式,用于描述程序中函数或方法之间的调用关系。在函数调用图中,每个函数或方法被表示为一个节点,函数或方法的调用关系被表示为边。
既然是图,那么一定有节点和边,该图还是一张有向图,那么就存在方向。在call-graph中,节点的类型应该全是函数或方法,但是通过调用和被调用关系分为caller和callee,Caller指的是调用其他函数的函数。在函数调用过程中,caller负责执行函数调用操作,即将控制权转移到被调用的函数(callee)中;Callee则是指被调用的函数,即接收来自caller传递的参数并执行相应操作的函数。根据上面的关系caller有一条边函数调用边指向callee,代表在caller中调用了callee,在破壳中记作(caller)-[:cg]→(callee)。
如果大家希望通过自己变成来生成二进制文件的call-graph,可以借助ghidra和其提供的API,具体实现代码可参考https://github.com/HackOvert/GhidraSnippets。
基础知识介绍完毕,就可以开始编写语句进行分析。
首先,需要明确该漏洞是一个目录遍历漏洞,那么其对应的危险参数是open,所以基本思想就是寻找哪些函数具有open函数的调用链。可以获得下面的语句:
MATCH p=((m:function)-[:cg*1..]->(n:function{name:"open"})),(k:file)-[:own]->(m)
WHERE not ()-[:cg*]->(m)
WITH p as path_set,k as own_file
RETURN [p IN nodes(path_set)|p.name],own_file.name limit 1000
这条语句是获得的一条较好的cg查询实践,下面将对其分解说明:
- 首先我们查询call-graph,p=((m:function)-[:cg*1..]->(n:function{name:”open”}))的含义是查询哪些函数调用了函数名为open的函数,记作m;同时通过(k:file)-[:own]->(m)查询函数m归属于哪个文件,记作k;
- 因为函数调用图cg会查询出来很多子路径,所以通过WHERE not ()-[:cg*]->(m)限制输出的路径path,使得查询出来的函数调用链是最长的,因为m是没有前继节点的;
- 第三步是是将查询出来的路径p重命名为path_set,k重命名为own_file;
- 由于查询出来的结果全是Path和Node这种特殊结构,不方方便查看,所以通过[p IN nodes(path_set)|p.name],提取函数调用路径path_set中所有节点集合,并提取出其中所有节点的名称,即函数调用名称;最后提取own_file的name来展示其位于哪个二进制。
通过上述语句查询出来的结果有80多条,通过人工审计还有一些难度。
3.4 细化分析
通过初步分析查询出80多条结果,可以把重点放在认证前的逻辑上,routeros通过下面的set_policy函数对进程间通信的接口进行鉴权,其中第三个参数如果是0的话就说明这个接口是无需认证的,代码中的表示如图七所示:
使用破壳平台对所有set_policy
的第三个参数为0
的调用处进行检测:
MATCH (f:file)-[:own]->(:function)<-[:use]-(:operator)-[:ast]->(n:const)
WHERE n.callee contains "set_policy" and n.index = 2 and n.name="0"
RETURN distinct(f.name)
在这句话中(f:file)-[:own]->(:function)<-[:use]-(:operator)-[:ast]->(n:const)是指在文件f中找到归属于这个文件的常量n,这个常量n必须是set_policy函数的第三个参数,且值为0。
如下图八所示,发现在众多进程中只有结果文件中拥有未鉴权接口:
此时再针对这几个进程进行open函数的调用栈查询,并且保证open函数的参数为一个变量。查询命令如下:
MATCH (n:identifier)
WHERE n.callee = "open" and n.index = 0 and n.function <> "open"
WITH collect(DISTINCT n.function) as func_set
MATCH p=((m:function)-[:cg*0..]->(n:function)),(k:file)-[:own]->(n)
WHERE n.name in func_set and not ()-[:cg]->(m) and k.name in ["console","mepty", "mproxy", "user"]
WITH p as path_set,k as own_file
RETURN [p IN nodes(path_set)|p.name]
1. 首先第一行是查询open的第一个参数是变量,同时避免进入plt表,所以这个变量所在的函数不是open,并将调用open的函数记作func_set;
2. 后面两行和上述的代码初步分析的流程和思想一样,只不过,其中加入了限制,需要在[“console”,”mepty”, “mproxy”, “user”]这些文件里面。
这时函数调用栈的结构如图九所示,只剩22条,此时可以根据这个结果进行人工排除。
经过人工确认,最后认证前目录穿越如下,此处open的路径没有经过检测。也即是查询到的[“FUN_0805016a”]这条函数调用栈,而这个地方就是cve-2018-14847的漏洞位置。
如果还要进行细化分析,那就需要借助污点分析的能力了,将nv::message::get_string的第一个函数作为sourceSet,将open的第一个参数作为sinkSet,查看之间是否存在数据流可达。
MATCH (n:identifier)
WHERE n.callee IN ['nv::message::get_string'] AND n.index = 0
WITH collect(id(n)) AS sourceSet
MATCH (n:identifier)
WHERE n.callee IN ['open'] AND n.index=0
WITH sourceSet, collect(id(n)) AS sinkSet
CALL VQL.taintPropagation(sourceSet, sinkSet,1) YIELD taintPropagationPath
RETURN taintPropagationPath
运行完还剩下5条结果,如图十所示,除了已知漏洞,目标均在console里面,经过人工识别,其余四条均认证后,且后续有拼接后缀,利用困难。
第一处
第二处
第三处
第四处:本文所述漏洞位置
第五处
五、总结
在实际漏洞挖掘过程中,面对不同的分析对象的时候,若需考虑时间成本,往往通过不同粒度的查询方式是一个不错的选择。
本文主要介绍了面对不同的目标时,在破壳平台上针对不同粒度的程序元素编写不同的语句,来快速地进行漏洞筛查。利用函数调用图进行粗粒度查询的优势就是在分析关系复杂的对象时能快速定位漏洞,相比于直接使用污点分析进行细粒度查询可以更换快速搜索获得结果,但劣势就显而易见,就是相较于污点分析,其精度较差,会有较多误报,需要更高的人工审计成本。
本文中提及的两个漏洞项目已在破壳平台示例项目中上线(https://poc.qianxin.com/example-project),感兴趣的同学可以注册破壳平台,进入示例项目,查看分析结果或者复制为私有项目进行破壳分析。