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) { 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" )); } 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) { 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 { 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)) { 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 ; }
整体逻辑大致如下:
检查缓冲区是否读取完毕byteBuffer.position()
-> byteBuffer.limit()
,是的话则调用org.apache.coyote.http11. Http11InputBuffer#fill
从底层Socket
继续读
byteBuffer
逐字节读取,当读到空格或者制表符时chr == Constants.SP || chr == Constants.HT
,方法名读取完毕,终止循环
检查当前字节是否是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,可以泄露出几乎整个请求