CVE-2024-21733 猫心滴血漏洞

CVE-2024-21733 Tomcat 请求走私漏洞

漏洞简述

Apache Tomcat版本 9.0.0-M11 - 9.0.43、8.5.7 - 8.5.63中存在信息泄露漏洞,由于coyote/http11/Http11InputBuffer.java中在抛出CloseNowException异常后没有重置缓冲区位置和限制,威胁者可发送不完整的POST请求触发错误响应,从而可能导致获取其他用户先前请求的数据。

影响版本

Apache Tomcat 9.0.0-M11 to 9.0.43

Apache Tomcat 8.5.7 to 8.5.63

漏洞分析

漏洞的产生

没有做好错误之后的请求缓存区管理,导致触发错误后缓存区被带出至下一次请求.

coyote/http11/Http11InputBuffer.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
private boolean fill(boolean block) throws IOException {

if (log.isDebugEnabled()) {
log.debug("Before fill(): [" + parsingHeader +
"], parsingRequestLine: [" + parsingRequestLine +
"], parsingRequestLinePhase: [" + parsingRequestLinePhase +
"], parsingRequestLineStart: [" + parsingRequestLineStart +
"], byteBuffer.position() [" + byteBuffer.position() + "]");
}

if (parsingHeader) {
if (byteBuffer.limit() >= headerBufferSize) {
if (parsingRequestLine) {
// Avoid unknown protocol triggering an additional error
request.protocol().setString(Constants.HTTP_11);
}
throw new IllegalArgumentException(sm.getString("iib.requestheadertoolarge.error"));
}
} else {
byteBuffer.limit(end).position(end);
}

byteBuffer.mark();
if (byteBuffer.position() < byteBuffer.limit()) {
byteBuffer.position(byteBuffer.limit());
}
byteBuffer.limit(byteBuffer.capacity());
SocketWrapperBase<?> socketWrapper = this.wrapper;
int nRead = -1;
if (socketWrapper != null) {
nRead = socketWrapper.read(block, byteBuffer);
} else {
throw new CloseNowException(sm.getString("iib.eof.error"));
//这个地方throw了,没有执行reset方法导致缓冲区残留
}
byteBuffer.limit(byteBuffer.position()).reset();

if (log.isDebugEnabled()) {
log.debug("Received ["
+ new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.remaining(), StandardCharsets.ISO_8859_1) + "]");
}

if (nRead > 0) {
return true;
} else if (nRead == -1) {
throw new EOFException(sm.getString("iib.eof.error"));
} else {
return false;
}

}

可以看到在上面的代码中,如果我们触发了CloseNowException,就会导致byteBuffer跳过执行重置的阶段,从而导致触发一个的脏缓存区的状态。

修复版本

coyote/http11/Http11InputBuffer.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
56
57
58
private boolean fill(boolean block) throws IOException {

if (log.isDebugEnabled()) {
log.debug("Before fill(): parsingHeader: [" + parsingHeader +
"], parsingRequestLine: [" + parsingRequestLine +
"], parsingRequestLinePhase: [" + parsingRequestLinePhase +
"], parsingRequestLineStart: [" + parsingRequestLineStart +
"], byteBuffer.position(): [" + byteBuffer.position() +
"], byteBuffer.limit(): [" + byteBuffer.limit() +
"], end: [" + end + "]");
}

if (parsingHeader) {
if (byteBuffer.limit() >= headerBufferSize) {
if (parsingRequestLine) {
// Avoid unknown protocol triggering an additional error
request.protocol().setString(Constants.HTTP_11);
}
throw new IllegalArgumentException(sm.getString("iib.requestheadertoolarge.error"));
}
} else {
byteBuffer.limit(end).position(end);
}

int nRead = -1;
byteBuffer.mark();
try {
if (byteBuffer.position() < byteBuffer.limit()) {
byteBuffer.position(byteBuffer.limit());
}
byteBuffer.limit(byteBuffer.capacity());
SocketWrapperBase<?> socketWrapper = this.wrapper;
if (socketWrapper != null) {
nRead = socketWrapper.read(block, byteBuffer);
} else {
throw new CloseNowException(sm.getString("iib.eof.error"));
}
} finally {
// Ensure that the buffer limit and position are returned to a
// consistent "ready for read" state if an error occurs during in
// the above code block.
byteBuffer.limit(byteBuffer.position()).reset();
}

if (log.isDebugEnabled()) {
log.debug("Received ["
+ new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.remaining(), StandardCharsets.ISO_8859_1) + "]");
}

if (nRead > 0) {
return true;
} else if (nRead == -1) {
throw new EOFException(sm.getString("iib.eof.error"));
} else {
return false;
}

}

使用Try-Finally语句块来确保最后真实清空了缓存区,不会影响下一次请求.

回显的来源

首先需要明白一个问题,为什么会报错

1
2
HTTP method names must be tokens
at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:417) ~[tomcat-embed-core-9.0.43.jar:9.0.43]
1
2
3
4
5
6
7
8
chr = byteBuffer.get();
...
else if (!HttpParser.isToken(chr)) {
// Avoid unknown protocol triggering an additional error
request.protocol().setString(Constants.HTTP_11);
String invalidMethodValue = parseInvalid(parsingRequestLineStart, byteBuffer);
throw new IllegalArgumentException(sm.getString("iib.invalidmethod", invalidMethodValue));
}

也就是当数据没有正常的token的时候会抛出异常,把不合法的invalidMethodValue抛了出来,而这个就是我们数据的value值.

下面是isToken判断逻辑的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static boolean isToken(String s) {
if (s == null) {
return false;
}
if (s.isEmpty()) {
return false;
}
for (char c : s.toCharArray()) {
if (!isToken(c)) {
return false;
}
}
return true;
}

整体逻辑大致如下:

  1. 检查缓冲区是否读取完毕byteBuffer.position() -> byteBuffer.limit(),是的话则调用org.apache.coyote.http11. Http11InputBuffer#fill从底层Socket继续读
  2. byteBuffer逐字节读取,当读到空格或者制表符时chr == Constants.SP || chr == Constants.HT,方法名读取完毕,终止循环
  3. 检查当前字节是否是http方法名合法字符!HttpParser.isToken(chr),不是的话就将byteBuffer的数据抛出为异常.

利用漏洞的最佳实践

len(leakage) = len(previous body) + len(actually length) + 1 -len(Content-Length)

漏洞复现

Docker-compose搭建漏洞环境.

1
2
3
4
5
6
7
8
9
version: '2'
services:
tomcat:
image: 'tomcat:9.0.43'
restart: on-failure:3
ports:
- '8080:8080'
volumes:
- ./ROOT:/usr/local/tomcat/webapps/ROOT

写一个简单的jsp文件来处理请求,接受请求中的id的值并输出

1
2
3
4
5
6
7
8
9
10
<%
String
id = request.getParameter("id");

if (id != null) {
out.println("The ID is: " + id);
} else {
out.println("No ID parameter provided.");
}
%>

发个包,回显正常

关闭BurpSuite的自动更新Content-length功能,发送一个不完整的数据包,等待Tomcat超时抛出错误

最佳实践是控制Content-Length为请求包Body的长度+1,可以泄露出几乎整个请求