一个简单的漏洞分析。
环境搭建
首先下载虚拟机然后导入到 VMWare 中,可以参考官方文章的一部分1,需要注意的是:
- 默认密码 root/password
- 注意绑定的网卡,之后需要手动配置 IP。
之后访问 https://target_ip 就可以进入配置了。
获得 root shell
Info
旧版的 root 比较好拿,新版的可以参考 SonicWall SMA漏洞研究 这一篇文章。
参考 w0lfzhang 的文章2,首先用其他镜像挂载,然后修改 ./etc/passwd
文件,将 root 的登录环境修改为 /bin/sh
。
没看懂文章是怎么手动调用 sshd 的,只能把 admin 的权限改成 root,这样登录之后可以直接进入 sh,而且不会影响到其他功能的执行。然后在终端中执行:
只需要修改 root 的登陆 shell,然后在登录时用 root/password 登录,这样不会影响 admin/password 的登陆。
就可以在外面 ssh 了。默认用户名和密码是 root/password。ssh 后可以顺便设置一下 TERM:
上传下载文件
Sonicwall 的虚拟机只有 ftp 命令可以用来上传下载文件,需要在本机搭建 ftp 服务器:
然后在远程
# ftp <ip> <port>
> <Login>
> get <filename>
License 绕过
看一下 _isRemoteSupportLicensed
函数,这个函数位于 libSys.so
中。
犯懒了,以后需要的时候再看吧。
刚更新完文章就发现 badmonkey 师傅的文章3,因为我们已经有了 root 权限,绕过 license 检查还是很简单的,依次建立对应的文件,并写入相应的内容:
掉 license
在使用上述方法获得权限后一段时间会掉 license,猜测可能是 graphd 进行了内部检查。
可以将上述文件设为只读,暂时还未观察到掉 license。
漏洞分析
Sonicwall 使用了 Apache httpd 作为 Web 服务,大部分功能是自己实现的相关 cgi 且大部分 cgi 需要认证,不过有几个是不需要的。CVE-2019-7482 的漏洞点在 supportLogin.cgi 中,这个 cgi 就无需认证。漏洞位于 getSafariVersion
函数中,它的真正实现位于 libSys.so 中:
这一段函数会将 UA 中 “Version/” 后直到空格之间的内容都复制到栈缓冲区上,且没有检查长度限制。由于该缓冲区长度只有 44 字节,因此存在溢出。
执行环境研究
在 httpd 执行的过程中,带有 root 权限的 httpd 主进程会 fork 出多个 httpd 子进程,并将它们的权限设置为 nobody:
root@sslvpn:/tmp # ps aux | grep httpd
nobody 1217 0.0 0.1 16064 5084 ? S Mar02 0:00 /usr/src/EasyAccess/bin/httpd
nobody 1482 0.0 0.1 16068 5088 ? S Mar02 0:00 /usr/src/EasyAccess/bin/httpd
nobody 1503 0.0 0.1 16064 5084 ? S Mar02 0:00 /usr/src/EasyAccess/bin/httpd
nobody 1526 0.0 0.1 16068 5088 ? S Mar02 0:00 /usr/src/EasyAccess/bin/httpd
nobody 1582 0.0 0.1 16068 5088 ? S 00:03 0:00 /usr/src/EasyAccess/bin/httpd
root 1592 0.1 0.1 15944 5964 ? Ss Mar02 1:22 /usr/src/EasyAccess/bin/httpd
nobody 2683 0.0 0.1 16068 5088 ? S 01:48 0:00 /usr/src/EasyAccess/bin/httpd
然后 nobody 权限的 httpd 再去执行 cgi 程序,因此因此就算获得了权限也是 nobody。
APR 模型
APR 进程封装采用了传统的 fork-exec 配合方式(spawn),即父进程在 fork 出子进程后继续执行其自己的代码,而子进程调用 exec 函数加载新的程序映像到其地址空间,执行新的程序。4
看一下程序的保护措施:
$ checksec supportLogin && checksec libSys.so
[*] ...supportLogin'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] ...libSys.so'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
都没有开栈保护,supportLogin 没开 PIE,但是开了 NX。
接下来看地址随机化:
root@sslvpn:/tmp # cat /proc/sys/kernel/randomize_va_space
2
地址随机化也开了。可能到这你会发现这在 ctf 中就是个简单的栈溢出,但是一般来说 ctf 中的 pwn 是可以跟 stdio,stdout 进行交互的,就是说他输出的内容你可以正常接受,你也可以通过键盘输入你想输入的东西,一切都看似很正常。但是因为这里 cgi 运行的是在真实的 web 服务器上,跟它的交互也只能是 socket,而在真实的 web 服务器上你是无法知道 socket 对应的 fd 是什么的。而且就算知道 fd 了,fd 上的数据也是 httpd 在处理,而不是对应的 cgi 在处理。而且以前针对地址随机化的处理方法都是泄露 libc 地址等,但是这里也因为以上原因是无法通过 fd 来接受泄露数据。
调试环境搭建
还有一个问题,怎么调试 cgi?因为 cgi 的调试不可以像普通程序那样直接 attach 调试,cgi 的启动是由 httpd 执行的,把包发过去一瞬间就执行完了,根本没有机会 attach。
首先看一下调试方法,w0lfzhang 的文章 2 提到:
在虚拟机中运行:
然后正常不断的发送 http 数据包即可,会有一定几率 attach 到 cgi 程序。
我尝试了这一种方法,但是没能成功,发现产生了 gdbserver 参数的报错,在 echo 了一下 $a 的时候发现它有两行,因此我继续尝试只取头或只取尾:
效果可以认为没有,要么 No such process,要么 Operation not permitted (1), process 13150 is a zombie - the process has already terminated。
尝试抓一下是哪个 httpd 启动了这个进程吧,改造一下上面的命令,输出进程树:
呃,发现所有 nobody 权限的 httpd 都有可能启动 cgi 进程。
感谢嘉木,发现一种新的调试方法。由于有多个 httpd 子进程,它们是随机执行 cgi 程序的,因此我们可以先 attach 到一个 httpd 子进程上,先在 fork 上下断点:
b fork
接着 continue 执行,在外部发送多次 poc,触发我们 attach 的这个进程的断点,接下来在 gdb 中执行:
set follow-fork-mode child
catch exec
继续 continue 就走进 supportLogin 进程了。当然,在进入 supportLogin 后,别忘了删除过去的操作,要不然就会跳进其他进程了:
set follow-fork-mode parent
delete breakpoints 1
delete breakpoints 2
失败的尝试
我最开始也尝试了类似的方法,不过我是用 gdbserver 挂 httpd,尝试在外面调试,但是也许是 gdb 和 gdbserver 版本相差太大的原因,在外面可以 catch fork 但是没有办法 catch exec,两边都不能 catch syscall。
其他尝试
将 Apache 配置为单例模式,它的配置文件位于 /usr/src/EasyAccess/www/conf/httpd.conf
,将以下几个参数都设置为 1:
ServerLimit 750
StartServers 20
MinSpareServers 5
MaxSpareServers 20
MaxClients 750
但是不清楚怎么重启 httpd,重启机器的话会还原。
漏洞复现
继续调试,然后发现它会在这个逻辑退出:
简而言之就是没有 license。如果单纯想复现漏洞的话很简单,我们把汇编 patch 掉即可:
.text:080499F7 call _isRemoteSupportLicensed
.text:080499FC test eax, eax # 改为 nop 即可
Info
可以不用 patch,直接 bypass
正如上文提过的那样,httpd 只有在接收到请求之后才会拉起 cgi 进程,因此我们可以在 patch supportLogin 之后将原始文件覆盖。
接下来简单发包就可以触发 crash 了:
正常的返回值是 200,crash 的返回值是 500,当然,因为我们已经有了调试环境,也可以通过 GDB 查看程序状态:
0x08049a00 in ?? ()
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0xb76f2c98 in getSafariVersion () from /lib/libSys.so
发现确实是在 getSafariVersion
函数中触发了栈溢出。
漏洞利用
在这里我们已经完成一大半工作了,之前在执行环境研究中我们发现系统开启了随机化,而且我们只能通过 socket 进行交互,现在的问题只剩下两个了:
- 怎样控制输入实现 ROP;
- 如何绕过地址随机化。
对于第一个问题,cgi 程序的输入来自于 httpd 执行时的标准输入和环境变量5,因此我们可以将要执行的命令放在固定的 bss 段:
$ readelf -S supportLogin
There are 27 section headers, starting at offset 0x5c34:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
... ...
[24] .bss NOBITS 0804eb4c 005b4c 00000c 00 WA 0 0 4
... ...
在 ROP 的过程中还需要解决一个问题就是
\x00
是进不去的,所以不能调用 read 等函数来读取数据,但是可以调用 fgets 函数,该函数就一个参数,直接设置成 bss 段地址即可,不包含\x00
字符。
我看了半天才发现 w0lfzhang 师傅想说的应该是 gets 函数而不是 fgets 函数,我还想 fgets 函数有三个参数为什么没有做 ROPgadget,对照着师傅的 payload 也证明了是 gets 函数。
对于第二个问题,如果我们尝试手动执行 supportLogin,会发现它的 libc 的地址位于 0x40xxx000 开头的位置,而真正执行的时候会发现 libc 地址位于 0xb7xxx000
所在的位置。我们可以固定一个 libc 地址然后爆破。
最终 exp: