cisco 中小型企业路由器rv34x漏洞复现(利用失败)
前言:
某公司一面技术面时,让我提交一份无poc的shell漏洞报告,切设备选择需要从cisco,飞塔等厂家的企业级设备进行选择,才有了今天的这篇文章记录
在翻找漏洞的时候看到这个漏洞
d
看到是缓冲区溢出类型的漏洞想着去复现一下
漏洞版本区间
其中CVE-2022-20827和CVE-2022-20841为命令注入攻击,通过中间人的攻击去进行利用并且已经有了相关了漏洞信息和复现,所以我们的目标就盯上了CVE-2022-20842,猜测是缓冲区溢出攻击的漏洞
所以我下载了1.0.0.3.26和1.0.03.28版本
下载链接:https://software.cisco.com/download/home/286287791/type/282465789/release/1.0.03.29?catid=268437899
固件提取及环境搭建:
本人ubuntu环境是22.04 binwalk对ubuntu18以上的版本不是很有好,经过尝试binwalk并不能提取出openwrt-comcerto2000-hgw-rootfs-ubi_nand.img里的文件系统,所以我这里选择使用了unblob这个工具(还可以避免binwalk将var重定向到/dev/null的这个问题)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
unblob最初由ONEKEY开发和维护,再加上binwalk的更新频率在逐渐减少,所以我个人挺推荐这个工具的(最主要的是它对高版本ubuntu友好)
安装顺序如下:
git clone https://github.com/onekey-sec/unblob.git cd unblob poetry install --no-dev sudo apt install e2fsprogs p7zip-full unar zlib1g-dev liblzo2-dev lzop lziprecover img2simg libhyperscan-dev zstd poetry run unblob --show-external-dependencies
安装成功后我们就可以看到所以的依赖组件是否安装成功
工具介绍及下载链接Installation - unblob - extract everything!(有多种下载方式)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
然后我们使用 unblob去进行文件提取
poetry run unblob RV34X-v1.0.03.26-2022-01-06-02-47-46-AM.img
接下来就是固件模拟
该款设备是 arm32位 具体的模拟过程看下图
这是我改写的sh脚本(别忘了下载依赖的文件)
#!/bin/bash sudo tunctl -t tap0 -u `whoami` sudo ifconfig tap0 192.168.2.1/24 qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2 -append "root=/dev/mmcblk0p2 console=tty0" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographicz
运行 sudo start-armhf.sh
在qemu中设置ip属性
ifconfig eth0 192.168.2.2/24
qemu跟虚拟机能ping通即可
然后在虚拟机中
首先将文件系统压缩(避免scp传文件时扰乱文件中的软连接) tar zcvf 1.tar rootfs/ 然后将文件系统上传至qemu sudo scp -r 1.tar root@192.168.2.2:/root/
回到qemu中
tar zxvf 1.tar chmod -R 777 rootfs cd rootfs
mount --bind /proc proc mount --bind /dev dev chroot . sh
这样的话我们就完成了基础的环境模拟,接下来就是运行起nginx的过程
完整的探索过程我就不放了,这里我直接放上启动服务的步骤,如果想自己一步一步的开始乐意直接启动nginx服务然后根据报错去向上逆推
/etc/init.d/boot boot //初始化环境的创建 generate_default_cert //生成ssl证书文件 /etc/init.d/confd start //启动confd服务 /etc/init.d/nginx start //启动nginx服务
然后访问192.168.2.2
可以看到web服务能成功运行
漏洞复现:
在漏洞diff之前,又出现了一个致命问题,burp不能抓取到该款路由器的数据包,一旦开启burp,页面会报错,不能进行下一步操作
在相关文章中看到作者说删除掉lan.http.conf文件的最后一行,就可以自由的进行抓包,
也就是return 301 这行,但经过测试依旧是抓不到数据包,后来偶然看到cyberangel师傅的文章说是burp版本过高导致的(本人burp版本2021.12),所以将jdk版本降级到8,重新下载了低版本的burp(又是一天多时间的浪费,我真的是,别忘了保存快照,复现完返回去)
成功抓到了数据包
通过对数据包的简单分析,再对文件上传的类型的数据由/cgi-bin/upload.cgi去进行处理,而对数据修改和信息定义则是由/cgi-bin/jsonrpc.cgi中进行处理,因为漏洞信息提示由于web输入导致所有我们则对俩版本中的jsonrpc.cgi进行diff判断
diff界面如下:
0.66差别的一般都是函数的修改,比如这样的:
而补丁的话则是在原代码逻辑中加上响应的长度或者输入判断,所有代码的差异不会特别大,这里我们着重看0.96-0.81这四个栏
其中在函数sub_12C1C中有了明显的一个修补
到1.28版本去进行查看代码逻辑如下
相较于1.26没有^[A-Fa-f0-9]{64}$这块正则处理
在追随上层逻辑的时候可以看出来
nt __fastcall sub_13500(int *a1) { int v2; // r4 char *v3; // r8 int v4; // r1 char *v5; // r2 char *v6; // r0 char *v7; // r5 int v8; // r7 int v9; // r0 const char *v10; // r2 int v11; // r11 int v12; // r6 int v13; // r9 int v14; // r0 int v15; // r0 int v16; // r0 const char *v17; // r1 const char *v18; // r2 int v19; // r0 int v20; // r0 int v21; // r0 int v22; // r0 int result; // r0 char s[296]; // [sp+8h] [bp-128h] BYREF v2 = json_object_new_object(); v3 = getenv("REMOTE_ADDR"); v6 = sub_12C1C((int)v3, v4, v5); v7 = v6; if ( v6 ) { v8 = StrBufCreate((int)v6); if ( is_file_exist() ) { v9 = json_object_from_file("/tmp/websession/session"); v11 = v9; if ( v9 ) { v12 = *(_DWORD *)(json_object_get_object(v9) + 32); while ( v12 ) { v13 = *(_DWORD *)(v12 + 4); v12 = *(_DWORD *)(v12 + 8); if ( json_object_object_get_ex() ) { memset(s, 0, 0x100u); if ( json_object_object_get_ex() ) { v14 = json_object_get_string(0); StrBufSetStr(v8, v14); } sprintf(s, "%s/%s", "/tmp/websession/token", v7); remove(s); json_object_object_del(v13, v7); } } json_object_to_file_ext("/tmp/websession/session", v11, 2); } else { error((int)"(%d)session file access error. ", 775, v10); } } if ( StrBufToStr(v8) ) { v15 = json_object_new_int(0); json_object_object_add(v2, "code", v15); v16 = json_object_new_string("remove sessionid success"); json_object_object_add(v2, "errstr", v16); v17 = (const char *)StrBufToStr(v8); v18 = "";
这段逻辑是在用户退出的时候用于对session值得删除,一开始以为是个任意文件删除,试了半天后也没成功才开始正式得看正则,
^[A-Fa-f0-9]{64}$这段正则得意识是从头到尾匹配64个字符串同时不区分大小写,后来猜测是对cve-2022-20705得统一修复(也有可能是本人过于菜没有复现出来...) 但是并没有找到相关了缓冲区溢出得补丁,故怀疑是不是diff错了地方,所以重新去分析了数据包,
当我们在web端设置数据得时候都是get_什么什么得形式所以我们在jsonrpc.cgi进行搜索
程序会判断method后得值来根据以此跳转程序逻辑
但是程序只给了"set_" 并没有给出后续得内容 所以继续跟进sub_13044 和sub13e7c
其中sub_13044函数
它是用来判断session值是否合法
而sub_13e7c函数
则是由jsonrpc_set_config函数进行处理,处理后得返回值再依次判断,看来函数判断得主战场并不在这里,根据这个函数去进行搜索
所以转到libjsess.so文件去进行代码审计,搜索关键词“set"可以看到所以得set类型以及json 参数定义
比如我们设置log页面得时候,它有这些参数
而设置一些页面得时候他的程序逻辑如下
其中setpre_什么什么是程序得一个分支处理程序,diff完整截图如下
其中setpre_snmp引起了我得注意
很明显得一个长度限制得补丁,猜测是CVE-2022-20842,后来再查阅官方得版本更新文档里面才发现是CVE-2022-20753,我擦那20842这个洞在哪呢
关键代码逻辑如下:
int __fastcall setpre__snmp(int a1, int a2, _DWORD *a3) { int v4; // r7 int v5; // r6 const char *v6; // r4 int v7; // r10 int v8; // r5 int v9; // r1 const char *v10; // r9 int v11; // r1 const char *v12; // r9 int v13; // r0 int v14; // r9 int v15; // r0 const char *v16; // r3 unsigned __int8 *v17; // r1 char *v18; // r3 int v19; // r2 int v20; // t1 bool v21; // zf int v22; // r3 const char *v23; // r2 int v24; // r3 const char *v25; // r3 bool v26; // zf int v27; // r0 char *v28; // r0 char *v29; // r9 int v30; // r1 int v31; // r0 char *v32; // r0 char *v33; // r9 int v34; // r1 int v35; // r0 int v36; // r0 int v37; // r2 int v38; // r0 int v39; // r2 int v40; // r0 int v41; // r0 int v42; // r0 const char *v43; // r2 bool v44; // zf int v45; // r5 int i; // r4 int v47; // r8 int v48; // r0 int result; // r0 const char *v50; // [sp+10h] [bp-468h] const char *v51; // [sp+14h] [bp-464h] char *s1; // [sp+18h] [bp-460h] int v53; // [sp+1Ch] [bp-45Ch] FILE *stream; // [sp+20h] [bp-458h] int v55; // [sp+24h] [bp-454h] int v56; // [sp+28h] [bp-450h] int v57; // [sp+30h] [bp-448h] int v58; // [sp+34h] [bp-444h] int v60; // [sp+40h] [bp-438h] BYREF int v61; // [sp+44h] [bp-434h] BYREF int v62; // [sp+48h] [bp-430h] BYREF char *v63; // [sp+4Ch] [bp-42Ch] BYREF char v64[128]; // [sp+50h] [bp-428h] BYREF char s[128]; // [sp+D0h] [bp-3A8h] BYREF char v66[11]; // [sp+150h] [bp-328h] BYREF char v67[245]; // [sp+15Bh] [bp-31Dh] BYREF char str[552]; // [sp+250h] [bp-228h] BYREF v4 = jsess_get_sess_sock(g_h_sess_db); v5 = jsess_get_sess_tid(g_h_sess_db); v56 = jsonutil_get(a2, "SNMP-USER-BASED-SM-MIB"); if ( v56 ) { if ( maapi_exists(v4, v5, "/SNMP-USER-BASED-SM-MIB/usmUserTable/usmUserEntry") ) maapi_delete(v4, v5, "/SNMP-USER-BASED-SM-MIB/usmUserTable/usmUserEntry"); v6 = 0; v7 = 0; v51 = 0; v50 = 0; v53 = 0; s1 = 0; v55 = 0; while ( v7 < json_object_array_length(v56) ) { v8 = json_object_array_get_idx(v56, v7); if ( json_object_object_get_ex(v8, "usmUserEngineID", &v60) ) { v9 = json_object_get_string(v60); if ( v9 ) { if ( !match_regex("(^(([0-9a-fA-F]){2}(:([0-9a-fA-F]){2})*)?$)", v9) ) v55 = json_object_get_string(v60); } } if ( json_object_object_get_ex(v8, "usmUserSecurityName", &v60) ) s1 = (char *)json_object_get_string(v60); if ( json_object_object_get_ex(v8, "usmUserPrivProtocol", &v60) ) { v10 = (const char *)json_object_get_string(v60); if ( strcmp(v10, "1.3.6.1.6.3.10.1.2.1") ) { if ( !strcmp(v10, "1.3.6.1.6.3.10.1.2.2") ) { v6 = "des"; } else if ( !strcmp(v10, "1.3.6.1.6.3.10.1.2.4") ) { v6 = "aes"; } } } if ( json_object_object_get_ex(v8, "usmUserPrivKey", &v60) ) { v11 = json_object_get_string(v60); if ( v11 ) { if ( !match_regex("(^[^%&\\\"'\\s]*$)", v11) ) v53 = json_object_get_string(v60); } } if ( json_object_object_get_ex(v8, "usmUserAuthProtocol", &v60) ) { v12 = (const char *)json_object_get_string(v60); if ( strcmp(v12, "1.3.6.1.6.3.10.1.1.1") ) { if ( !strcmp(v12, "1.3.6.1.6.3.10.1.1.2") ) { v51 = "md5"; } else if ( !strcmp(v12, "1.3.6.1.6.3.10.1.1.3") ) { v51 = "sha"; } } } if ( json_object_object_get_ex(v8, "usmUserAuthKey", &v60) ) { v13 = json_object_get_string(v60); v14 = v13; if ( v13 ) { v15 = match_regex("(^[^%&\\\"'\\s]*$)", v13); v16 = v50; if ( !v15 ) v16 = (const char *)v14; v50 = v16; } } if ( v55 ) { v61 = 0; v62 = 0; memset(v64, 0, sizeof(v64)); memset(s, 0, sizeof(s)); v17 = (unsigned __int8 *)v55; v18 = v64; do { v20 = *v17++; v19 = v20; if ( !v20 ) break; if ( v19 != ':' ) *v18++ = v19; } while ( v17 ); *v18 = 0; v21 = v50 == 0; if ( v50 ) v21 = v51 == 0; v22 = !v21; v57 = v22; if ( v21 ) { v41 = json_object_new_string((int)""); json_object_object_add(v8, "usmUserAuthKey", v41); v42 = json_object_new_string((int)""); json_object_object_add(v8, "usmUserPrivKey", v42); } else { if ( v6 ) v23 = v6; else v23 = ""; v24 = (int)v6; if ( v6 ) v24 = 1; v58 = v24; v25 = (const char *)v53; v26 = v53 == 0; if ( v53 ) v26 = v6 == 0; if ( v26 ) v25 = ""; v27 = sprintf(str, "perl /usr/bin/snmpkey %s %s %s %s %s", v51, v50, v64, v23, v25); v61 = StrBufCreate(v27); v62 = StrBufCreate(v61); stream = (FILE *)popen(str, "r"); if ( stream ) {
可以看到在接收usmUserPrivKey和usmUserPrivKey这俩参数的时候没有限制长度之间sprintf 复制进了str缓冲区,其中的正则表达是修复了cve-2021-1414这个命令注入漏洞
没办法,只能尝试复现这个漏洞,这个漏洞发生在snmp界面得设置处,我们即可抓包进行溢出尝试
第二个问题又来了,jsonrpc.cgi是以uwsgi的子进程开始的,,每次请求单独出现一个进程,请求结束进程结束,如果我们直接调试jsonrpc.cgi,那么可能会缺少uwsgi传递的一些关键数据和环境变量,那么调试的问题要如何解决
这里采用的方法是将jsonrpc.cgi patch,从而死循环,然后在gdb中修改程序正常运行从而调试
查看__libc_start_main 运行的第一个函数
然后再sub_1214c中将第一个bl修改为跳转自身,这样我们就实现了一个死循环
然后开启gdb-multiarch ,设置好小端序和arm属性,
发起服务,然后查看进程
然后使用响应的gdbserver去开启端口
./gdbserver-armel-static-8.0.1 192.168.2.2:4444 --attach 28903
在gdb中
target remote 192.168.2.2:4444
程序成功卡在bl,0x12158
利用gdb修改内存数据的功能,将程序改回正常逻辑
set {int}0x12158=0xebfffed8 //数据内容根据ida的数据来,版本之间有些差别
程序就会回到一个正常的流程当中,但是这种调试方法需要吐槽的是,不知道断点该如何设置,因为漏洞发生在.so文件当中,所以不能直接一步到位的去设置断点,需要运行到响应的函数处,才可以之间去设置漏洞前处断点
程序卡在了这步,在sprintf(str, "perl /usr/bin/snmpkey %s %s %s %s %s", v51, v50, v64, v23, v25); 执行完后成功的溢出了,但是致命的几个问题来了,
1当用五千杂乱字符串时,程序不能成功卡住,但是当使用五千个a时,即可成功卡在该页面,中间也没有正则或者其它字符判断,
2程序无法覆盖pc寄存器,经过测试,3000多字符即可溢出,但是输入五六千个字符也不能溢出pc, 所以也就无法劫持程序流
猜测原因如下,因为溢出点发生在libjsess.so中, 无法覆盖jsonrpc.cgi的pc寄存器????
漏洞信息如下,应该是可以成功shell的
本人较为愚钝,希望有成功复现的师傅可以指点迷津