(本文写于几个月前,由于官方还没公开推送补丁,就迟迟没发布) 本来只是打算分析思科RV160W这款路由器最近的无条件RCE漏洞。我diff固件查看变动代码,结果有了一些新发现
https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-rv160-260-rce-XZeFkNHf.html
根据官方的通告,Cisco Small Business RV160、RV160W、RV260、RV260P 和 RV260W VPN 路由器基于 Web 的管理界面中存在多个漏洞,可能允许未经身份验证的远程攻击者以root用户身份在受影响的设备上执行任意代码。这些漏洞在固件v1.0.01.02版本被修复
我去Cisco Software Center 下载了两个版本固件
版本v1.0.01.01就是存在漏洞的固件
binwalk解压固件,寻找提供web服务的二进制文件,其实可以根据rc.d与init.d目录的一些初始化脚本盲猜,这里本着通用性与研究性的目的用另一种方式寻找。
由于手中没有现成设备,firmadyne有点鸡肋,于是我在油管上找了个RV160W的配置教程,观察浏览器上方的url,根据关键字符“configurationManagement”定位到admin.cgi,进而定位到web组件是mini_httpd(32位arm小端程序)。
定位到web服务程序后,我们对两个版本固件的mini_httpd程序进行二进制代码对比(当然也有对 admin.cgi 进行对比,也存在不少改动代码,但这次我只关注mini_httpd组件部分)
最经典的是bindiff 这款工具,但是我用它并没有找到关键代码改动部分
之后又使用了一款IDA插件diaphora ,导出sqlite数据库后进行对比,发现 mini_httpd2 的 sub_1B034 函数(对应mini_httpd1的sub_1AF58函数)和sub_15CE4函数(对应mini_httpd1的sub_15CE4函数)改动较明显。
分析sub_1AF58函数改动部分
分别定位到两个程序相关代码部分
mini_httpd1
mini_httpd2
在新版本的sub_1B340中,将格式化后的v3作为参数传递给system,在这之前v3经过了一次sub_1B034函数
sub_1b034函数是新加入的,里面大概是过滤字符的功能
这里已经不能再明显了,system函数处存在命令注入,于是mini_httpd2用过滤危险字符的方式修复了这个漏洞。
分析sub_15CE4函数改动部分
mini_httpd1
mini_httpd2
将strcpy函数替换成了strncpy函数。断定此处strcpy存在栈溢出漏洞。
对固件的初步逆向分析后,基本判定老版本固件至少存在命令注入和栈溢出这两个漏洞。
模拟httpd服务,为了之后附加进程调试,我选用qemu系统模式进行模拟。
拷贝1.0.01.01固件文件系统进qemu,挂载一些关键目录后以chroot执行sh.
在运行mini_httpd之前可能需要初始化环境目录,这些往往都在rc.d与init.d启动脚本里面,所以全局搜索引用“mini_httpd”的地方
发现etc/scripts/mini_httpd/mini_httpd.sh
与 etc/rc.d/S23mini_httpd.init
与 etc/init.d/mini_httpd.init
这些里面的内容都差不多,大概都是初始化一些文件然后最终启动/usr/sbin/mini_httpd
我试着运行了一个
发现报错can't bind to any address,这个报错在mini_httpd程序中,此时一些文件其实已经初始化了,我们可以只关注mini_httpd程序本身。
我们直接运行mini_httpd,同样的报错
错误原因是setsockopt函数在调用时协议参数不合法,经过一番尝试后无果;最后想到这种设置套接字属性的函数,其实hook掉对连接的影响也不是很大,关键服务能起来就行。
定位到程序报错点,是setsockopt失败返回负值导致的错误。
我索性将setsockopt函数hook全返回1
可以看到服务跑起来了,访问试试
根据403字符定位到程序中
执行sub_1b5f0函数后就退出了,猜测是某些环境变量的问题,这里为了不改变代码逻辑,我直接patch mini_httpd程序代码块,把跳转 sub_1B5F0的地方nop掉
再次执行程序后访问web
接下来就是对mini_httpd的详细逆向过程,其实就是寻找触发路径。
我把漏洞触发函数改成了vuln,以及调用vuln的上层函数vuln_back1,上上层函数vuln_back2........
在vuln_back2中看出,我们只需要contrl_arg字符串里面包含"dniapi/"
即可进入vuln_back1
在往上看,contrl_arg被赋值成dword_34F60 + 1,并且dword_34F60第一个字符必须为'/'
,这里可以猜测contrl_arg是一个请求行的URL
为了验证我的猜测,动态调试一下,由于所有的请求都被放在了fork子进程中处理。我这里暂时为了方便调试,hook了fork函数返回0,构造了GET /hello.txt
在vuln_back1中判断了contrl_arg是否是相关字符,不是就会去执行vuln,调用vuln时传入了contrl_arg这个参数
vuln函数里面还有最后一层判断,必须让contrl_arg的前9个字符等于"download/"
所以综上,我们可以构造GET请求URL为"/download/dniapi/",为了能触发system函数,我还需要调整contrl_cmd的值为"Basic "
查找引用,发现在vuln_back2函数中给contrl_cmd赋值
大胆猜测,这里就是HTTP的head的Authorization字段
综上,最终能触发到system函数的请求大致如下
vuln通过sub_1E19C函数处理contrl_cmd + 6
(Basic
之后的字符串),结果放入v5,最终将v5作为拼接命令的一部分。
3字节一组,以及明显的base64解码字符表,经验证sub_1E19C就是base64解密函数。
所以只需要将请求Authorization: Basic
后的字符换成想要执行命令的base64编码形式,同时用;
字符截断原有的curl命令,就可以执行任意命令,下面以date命令为例。
同样我将漏洞触发函数重命名成overflow,上层函数overflow_back1、overflow_back2
在vuln_back2函数里,处理 cookie 并且作为参数传入overflow_back2函数。
在这之前会有一个check,检查当前请求的url资源内容是否是需要登录才能访问
所以这里需要请求这些资源,使其返回1。
overflow_back2里判断cookie不为空后进入overflow_back1
然后就是检索cookie是否包含sessionID,包含的话,将其以空格分隔后,传入overflow函数
overflow函数中形参a2指向空字符,所以其实最后就是未限制sessionID长度在strcpy时的溢出。
POC如下:
分析crash结果,得知是在overflow函数返回前抛出的内存引用异常。原因是数据过长,覆盖了压入栈中的第五个参数a5,它刚好位于返回地址下方,导致最后一个strcpy往a5变量地址写数据时,该地址不合法。
所以为了能够保证利用,需要刚好覆盖到overflow函数的返回地址。
经过调试,测得偏移为268,POC如下:
可以看到返回地址被覆盖成了bbb(0x626262),且可以观察到此时的r0寄存器指向sessionID=后的字符串1234...,因此可以在sessionID=后放置命令字符串,然后控制PC跳转到system函数上。由于cookie在overflow_back1函数中被过滤了空格,在overflow中被过滤了等号,导致命令执行受限,空格可以用${IFS}替换,等号只能避免使用了。
关于system执行命令,我并不想去另寻僻径去ROP或者是上传后门程序,我尝试了很多技巧,最终以这种较为简单且通用的方式弹shell(输入与输出分离)。
以Command Inject EXP为例:
由于我手里没有真实设备,只能找一些公网上的设备验证。
用burpsuit抓包获取设备特征
Content-Security-Policy:
是一个很明显的特征
在fofa上搜索 header="frame-ancestors 'self' 'unsafe-inline' 'unsafe-eval'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
我这里只搜索了巴西的
用exp打来试试
一开始我补丁比较的时候就发现,v1.0.01.02固件在sprintf之前加入了字符过滤函数
该函数完整代码如下
经分析,它的功能主要是检索字符串,一旦检测到敏感字符,就将在该字符前面加上一个转义符"\"
比如我们构造命令ls -all
,被它处理后字符变成了ls\ -all
我试了各种方法,都无法绕过
但是我发现它并没有过滤换行符"\n"
,这可以让我们使用换行符截断命令,但是我们只能执行一些环境变量目录下的程序,并且还不能跟参数,可能的用处是开启一些对外的接口比如telnetd或其它。
但这也是一个很严重的过滤缺陷,因为我们可以构造"\npoweroff\n"
来使路由器关机,这直接是拒绝服务攻击了
根据上面Command Inject EXP代码,将字符拼接处的;换成\n即可。
尝试过注入telnetd命令来开启telnetd服务 但经1day打进去的公网设备验证发现,该系列路由器里没有配置用户密码。 下面只尝试poweroff的命令注入(效果最直观明显)。POC如下
在公网上找了个用1day打getshell失败 但使用POC成功宕机的
目前该漏洞已经提交给了思科厂商,思科已确认了该漏洞并修复。https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-rv-code-execution-9UVJr7k4.html
b0ldfrev@ubuntu:~
/
blog
/
v1.
0.01
.
01
/
rootfs$ grep
-
Rnl
"configurationManagement"
*
2
>
/
dev
/
null
usr
/
sbin
/
admin.cgi
usr
/
lib
/
opkg
/
info
/
sbr
-
gui.
list
www
/
gettingStarted.htm
www
/
configurationManagement.htm
www
/
app.min20200813.js
www
/
home.htm
b0ldfrev@ubuntu:~
/
blog
/
v1.
0.01
.
01
/
rootfs$ grep
-
Rnl
"admin.cgi"
*
2
>
/
dev
/
null
usr
/
sbin
/
mini_httpd
usr
/
sbin
/
admin.cgi
usr
/
lib
/
opkg
/
info
/
sbr
-
gui.
list
b0ldfrev@ubuntu:~
/
blog
/
v1.
0.01
.
01
/
rootfs$
file
.
/
usr
/
sbin
/
mini_httpd
.
/
usr
/
sbin
/
mini_httpd: ELF
32
-
bit LSB executable, ARM, EABI5 version
1
(SYSV), dynamically linked, interpreter
/
lib
/
ld
-
linux.so.
3
,
for
GNU
/
Linux
2.6
.
16
, stripped
b0ldfrev@ubuntu:~
/
blog
/
v1.
0.01
.
01
/
rootfs$ grep
-
Rnl
"configurationManagement"
*
2
>
/
dev
/
null
usr
/
sbin
/
admin.cgi
usr
/
lib
/
opkg
/
info
/
sbr
-
gui.
list
www
/
gettingStarted.htm
www
/
configurationManagement.htm
www
/
app.min20200813.js
www
/
home.htm
b0ldfrev@ubuntu:~
/
blog
/
v1.
0.01
.
01
/
rootfs$ grep
-
Rnl
"admin.cgi"
*
2
>
/
dev
/
null
usr
/
sbin
/
mini_httpd
usr
/
sbin
/
admin.cgi
usr
/
lib
/
opkg
/
info
/
sbr
-
gui.
list
b0ldfrev@ubuntu:~
/
blog
/
v1.
0.01
.
01
/
rootfs$
file
.
/
usr
/
sbin
/
mini_httpd
.
/
usr
/
sbin
/
mini_httpd: ELF
32
-
bit LSB executable, ARM, EABI5 version
1
(SYSV), dynamically linked, interpreter
/
lib
/
ld
-
linux.so.
3
,
for
GNU
/
Linux
2.6
.
16
, stripped
root@debian
-
armhf:~
/
rootfs
bin
etc media overlay rom sbin test_scripts usr www
dev lib mnt proc root sys tmp var
root@debian
-
armhf:~
/
rootfs
root@debian
-
armhf:~
/
rootfs
root@debian
-
armhf:~
/
rootfs
BusyBox v1.
23.2
(
2020
-
08
-
17
10
:
59
:
42
IST) built
-
in
shell (ash)
/
root@debian
-
armhf:~
/
rootfs
bin
etc media overlay rom sbin test_scripts usr www
dev lib mnt proc root sys tmp var
root@debian
-
armhf:~
/
rootfs
root@debian
-
armhf:~
/
rootfs
root@debian
-
armhf:~
/
rootfs
BusyBox v1.
23.2
(
2020
-
08
-
17
10
:
59
:
42
IST) built
-
in
shell (ash)
/
b0ldfrev@ubuntu:~
/
cve
/
RV160W
/
rootfs$ grep
-
Rnl
"mini_httpd"
*
2
>
/
dev
/
null
etc
/
scripts
/
mini_httpd
/
mini_httpd.sh
etc
/
rc.d
/
S23mini_httpd.init
etc
/
init.d
/
mini_httpd.init
etc
/
init.d
/
config_update.sh
usr
/
sbin
/
mini_httpd
usr
/
sbin
/
admin.cgi
usr
/
lib
/
opkg
/
info
/
sbr
-
gui.
list
b0ldfrev@ubuntu:~
/
cve
/
RV160W
/
rootfs$ grep
-
Rnl
"mini_httpd"
*
2
>
/
dev
/
null
etc
/
scripts
/
mini_httpd
/
mini_httpd.sh
etc
/
rc.d
/
S23mini_httpd.init
etc
/
init.d
/
mini_httpd.init
etc
/
init.d
/
config_update.sh
usr
/
sbin
/
mini_httpd
usr
/
sbin
/
admin.cgi
usr
/
lib
/
opkg
/
info
/
sbr
-
gui.
list
START
=
23
version_gt() {
test
"$(echo "
$@
" | tr "
" "
\n
" | sort -n | head -n 1)"
!
=
"$1"
;
}
get_version() {
version
=
`cat $
1
| grep
"\"VERSION\""
| awk
-
F
'{print $4}'
`
fw_version
=
$(get_version ${fwLgPath}
/
${i}.js)
mnt_version
=
$(get_version ${mntLgPath}
/
${i}.js)
if
[[
"${tmp_version/V/}"
!
=
"$tmp_version"
]]; then
cp
-
f ${fwLgPath}
/
${i}.js ${mntLgPath}
/
${i}.js
elif
! version_gt $mnt_version $fw_version; then
cp
-
f ${fwLgPath}
/
${i}.js ${mntLgPath}
/
${i}.js
fi
else
cp ${fwLgPath}
/
${i}.js ${mntLgPath}
/
${i}.js
fi
fi
done
fi
/
etc
/
scripts
/
mini_httpd
/
mini_httpd.sh start
}
stop() {
/
etc
/
scripts
/
mini_httpd
/
mini_httpd.sh stop
}
reload
() {
/
etc
/
scripts
/
mini_httpd
/
mini_httpd.sh
reload
}
START
=
23
version_gt() {
test
"$(echo "
$@
" | tr "
" "
\n
" | sort -n | head -n 1)"
!
=
"$1"
;
}
get_version() {
version
=
`cat $
1
| grep
"\"VERSION\""
| awk
-
F
'{print $4}'
`
fw_version
=
$(get_version ${fwLgPath}
/
${i}.js)
mnt_version
=
$(get_version ${mntLgPath}
/
${i}.js)
if
[[
"${tmp_version/V/}"
!
=
"$tmp_version"
]]; then
cp
-
f ${fwLgPath}
/
${i}.js ${mntLgPath}
/
${i}.js
elif
! version_gt $mnt_version $fw_version; then
cp
-
f ${fwLgPath}
/
${i}.js ${mntLgPath}
/
${i}.js
fi
else
cp ${fwLgPath}
/
${i}.js ${mntLgPath}
/
${i}.js
fi
fi
done
fi
/
etc
/
scripts
/
mini_httpd
/
mini_httpd.sh start
}
stop() {
/
etc
/
scripts
/
mini_httpd
/
mini_httpd.sh stop
}
reload
() {
/
etc
/
scripts
/
mini_httpd
/
mini_httpd.sh
reload
}
/
uci: Entry
not
found
Syntax:
/
etc
/
init.d
/
mini_httpd.init [command]
Available commands:
start Start the service
stop Stop the service
restart Restart the service
reload
Reload
configuration files (
or
restart
if
that fails)
enable Enable service autostart
disable Disable service autostart
/
uci: Entry
not
found
ls:
/
mnt
/
configcert
/
confd
/
startup
/
: No such
file
or
directory
use backup cert
for
mini
-
httpd ...
1
0
0
0
setsockopt SO_REUSEADDR: Protocol
not
available
setsockopt SO_REUSEADDR: Protocol
not
available
/
usr
/
sbin
/
mini_httpd: can't bind to
any
address
/
/
uci: Entry
not
found
Syntax:
/
etc
/
init.d
/
mini_httpd.init [command]
Available commands:
start Start the service
stop Stop the service
restart Restart the service
reload
Reload
configuration files (
or
restart
if
that fails)
enable Enable service autostart
disable Disable service autostart
/
uci: Entry
not
found
ls:
/
mnt
/
configcert
/
confd
/
startup
/
: No such
file
or
directory
use backup cert
for
mini
-
httpd ...
1
0
0
0
setsockopt SO_REUSEADDR: Protocol
not
available
setsockopt SO_REUSEADDR: Protocol
not
available
/
usr
/
sbin
/
mini_httpd: can't bind to
any
address
/
/
setsockopt SO_REUSEADDR: Protocol
not
available
setsockopt SO_REUSEADDR: Protocol
not
available
/
usr
/
sbin
/
mini_httpd: can't bind to
any
address
/
setsockopt SO_REUSEADDR: Protocol
not
available
setsockopt SO_REUSEADDR: Protocol
not
available
/
usr
/
sbin
/
mini_httpd: can't bind to
any
address
/
*
arm
-
linux
-
gnueabi
-
gcc
-
shared
-
fPIC hook.c
-
o hook
*
/
int
setsockopt(
int
sockfd,
int
level,
int
optname,
const void
*
optval, socklen_t optlen)
{
return
1
;
}
/
*
arm
-
linux
-
gnueabi
-
gcc
-
shared
-
fPIC hook.c
-
o hook
*
/
int
setsockopt(
int
sockfd,
int
level,
int
optname,
const void
*
optval, socklen_t optlen)
{
return
1
;
}
BusyBox v1.
23.2
(
2020
-
08
-
17
10
:
59
:
42
IST) built
-
in
shell (ash)
/
bind: Address already
in
use
/
/
2364
root
3540
S .
/
usr
/
sbin
/
mini_httpd
2369
root
3120
S grep mini_httpd
BusyBox v1.
23.2
(
2020
-
08
-
17
10
:
59
:
42
IST) built
-
in
shell (ash)
/
bind: Address already
in
use
/
/
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2021-8-8 01:03
被b0ldfrev编辑
,原因: 目录调整
上传的附件: