漏洞分析 | Apache Tomcat 信息泄露漏洞 (CVE-2024-21733)

漏洞分析 | Apache Tomcat 信息泄露漏洞 (CVE-2024-21733)

塞讯安全验证 2024-08-21 18:16

Apache Tomcat 信息泄露漏洞 (CVE-2024-21733),影响范围包括 Tomcat 9.0.0-M11 至 9.0.43 版本及 8.5.7 至 8.5.63 版本。当受影响版本的 Tomcat 无法正确处理 POST 请求的 Content-Length 字段时,会发生客户端不同步漏洞 (CSD)。通过这种方式,攻击者可以强制受害者的浏览器取消与网站的连接同步,从而导致敏感数据从服务器或客户端连接中被走私。

▌漏洞复现

1、靶场环境搭建使用 Docker 启动以下环境****

version:'2'
services:
      web:
            image: *****.secvision.***:8443/vulhub/tomcat:v3.0.0-cve-2024-21733-amd
            ports:        
            - "80:8080"        
            healthcheck:        
            test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080 || exit 1"]        
            interval: 40s        
            timeout: 5s        
            retries: 5        
            start_period: 40s

在等待几分钟后,访问 http://ip 确认 Tomcat 页面是否正常显示,环境启动成功。

2、具体复现步骤

使用 BurpSuite 发送以下数据包,模拟用户的登录请求:

POST /test.jsp HTTP/1.1
Host: 192.168.76.1:9090
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: 
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 48

name=admin&passwd=123456&id=1000001&action=login

随后发送恶意构造的数据包,窃取用户请求数据。(注意,发送时需设置禁止更新 Content-Length)

▌漏洞分析

该漏洞的产生由多方面的因素组成。从简单来讲,当 Tomcat 接收到一个 HTTP 请求时,会从套接字中读取请求内容并保存到一个 byteBuffer 对象中,并且如果使用的是同一个 TCP 连接(即请求头中设置了 Connection为keep-alive),从套接字中读取到的数据会保存到同一个 byteBuffer 对象中。而在保存二进制数据时,Tomcat 并没有将数据进行完全擦除,而是进行了覆盖,这时就出现了一种情况:若第一个数据包比较长,而第二个数据包比较短,在处理第二个请求时 byteBuffer 对象的字节数组中可能仍存在第一个数据包的数据。由于 Tomcat 在读取 byteBuffer 对象的字节数据时采用了 position 和 limit 来限制读取区间,正常情况下并不会读取到上一个数据包的内容。但是由于 Content-Length 的设置错误,会导致 limit 出现异常,使请求的解析出现错误,并在默认错误页面泄露上一个数据包的内容。

1、泄露数据的来源

可以利用 BurpSuite 来复现这种情况。在 Http11Processor 类的 service 方法中添加断点,该方法用于处理并解析接收到的数据,并且通过 while 循环能够实现对同一个 TCP 流的多个数据包进行解析。

在使用同一个 TCP 连接的前提下,我们首先发送下面的数据包。

当程序在执行 inputBuffer.parseRequestLine 方法并中断时,我们可以查看当前 byteBuffer 对象的缓冲区还没有任何数据,这是由于该 Tomcat 程序刚启动,从连接池中取出的连接未接收到过任何数据。

随后进入 parseRequestLine 方法的实现中,该方法用于解析请求行,即 POST /test.jsp HTTP/1.1 部分,但在正式解析之前它会调用 fill 方法尝试将数据读入到 byteBuffer 中。

在 fill 方法中,函数执行到 socketWrapper.read(block, byteBuffer) 时,会将从 socket 套接字读取请求数据并保存到 byteBuffer 中。

可以在缓冲区中看到请求的数据。

随后我们再次利用 BurpSuite 发送下面的数据包,该数据包比上一个数据包的长度小很多。

并且,在执行完 fill 方法中的 socketWrapper.read(block, byteBuffer) 之后,我们可以在 byteBuffer 中看到 Tomcat 采用了覆盖的方式将数据读入到缓冲区,上一个请求的数据在缓冲区并没有完全删除,而导致信息泄露的正是这部分数据。

2、Content-Length 和请求体的解析过程

在 Http11Processor 类的 service 方法中,除了请求行的解析,还包括请求头的解析、协议的解析等。并且还调用了 prepareRequest 方法,该方法会读取请求头中的内容,并设置一些过滤器。

其中还包括了 Content-Length 的解析。

通过 request.getContentLengthLong() 方法从 headers 中获取到了 Content-Length 的值,并且如果 Content-Length 的值大于等于 0,会通过 addActiveFilter 方法增加一个 IDENTITY_FILTER 过滤器,该过滤器用于解析请求体。

在 addActiveFilter 方法的实现中,又调用了 filter.setRequest(request)。

setRequest 方法设置了两个值,一个是 Content-Length,另一个是 remaining,remaining 用于记录剩余待读取的字节数,这会在后续的分析中用到,这里我们可以看到 remaining 就等于 Content-Length。

随着代码的执行,Tomcat 会根据请求路径找到对应的 jsp 文件,并根据 jsp 文件中的代码决定是否需要解析请求参数,以及如何解析请求参数。我们根据漏洞复现的需要创建了 test.jsp,并在其中调用了 request.getParameter(“id”) 方法。

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

通过不断跟踪 getParameter 方法的实现,我们根据下面的调用链,进入了 parseParameters 方法。

parseParameters 方法中会调用 readPostBody 方法尝试根据请求头中 Content-Length 的值读取请求体实际的长度。

readPostBody 方法中通过 getStream().read() 从输入流中循环读取指定长度的数据直到符合 Content-Length。

而 getStream().read() 方法会根据下面的调用链执行到 IdentityInputFilter 的 doRead 方法。

过滤器 IdentityInputFilter 的 doRead 方法中包含以下内容。

buffer.doRead 用于读取缓冲区并获取缓冲区中可以读取的长度,而根据 remaining(剩余读取的长度)和 nRead(需要读取的长度)之间的差值,Tomcat 有着不同的处理方式。当 nRead > remaining 时,会通过 handler.getByteBuffer().limit(handler.getByteBuffer().position() + (int) remaining) 进行截断,而 nRead < remaining 时 remaining 会记录剩余待读取的字节数。

根据 buffer.doRead 的实现,如果当前 buffer 的指针位置超过了缓冲区的限制大小(即已读取到缓冲区最后,但仍有数据需要读取),代码会调用 Http11InputBuffer 类的 fill 方法去从请求输入流中获取数据并添加到缓冲区,并且这次调用是阻塞的。

3、Content-Length 异常导致缓冲区范围重置失败

当我们设置的 Content-Length 超过实际数据的大小时,就会出现异常。在 org/apache/catalina/connector/Request.java 文件中 readPostBody 方法添加断点。

并发送以下数据包。

在第一次调用 getStream().read(body, offset, len – offset) 代码时,我们实际只能读取到 1 个字节,即 x,且获取到的长度为 1,由于我们需要的长度大于获取到的长度,所以还会进行一次循环。

根据上面的分析,我们最终会调用到 Http11InputBuffer 类的 fill 方法。此时 position 和 limit 的值都为 144(即请求体的大小)。

并且由于后续没有数据传入,在 20 秒后会导致连接超时,并出现异常,使得 byteBuffer.limit(byteBuffer.position()).reset() 代码无法执行,导致缓冲区范围重置失败。在出现异常后我们可以看到 limit 的值变成了 8336(144+8192)。

4、缓冲区限制范围异常导致后续缓冲区更新错误

随后 Http11Processor 类中的 service() 方法继续执行,直到进入 endRequest(); 方法。

该方法会调用 inputBuffer.endRequest()。

然后调用了 IdentityInputFilter 过滤器中的 end 方法。

此处又尝试通过 buffer.doRead(this) 读取数据。

而 byteBuffer.remaining() 方法的实现如下:

经过计算,当前 byteBuffer 对象在返回时 position 由 144 变成了 8336,而 doRead() 方法会返回 8192。

由于 nread 的值为 8192,并且在异常之前我们仍存在 5 个字节未读,所以 remaining(剩余读取字节数)就变成了 5-8192=-8187,并在该函数返回 8187。

所以在 endRequest() 函数结束时,buffer 的 position 值就变成了 8336-8187 = 149。

在 inputBuffer.endRequest() 调用完成之后,inputBuffer.nextRequest() 又影响了缓冲区的配置。

在该方法中,由于当前读取位置大于 0,同时剩余读取字节数大于 0,会调用 byteBuffer.compact() 方法。

byteBuffer.compact() 方法会将待读取的内容复制到缓冲区最前面。

随后执行 byteBuffer.flip() 更新 limit 和 position 值。

5、解析请求行时,异常的缓冲区导致报错

由于循环接收请求数据的需要,会再次进入 inputBuffer.parseRequestLine 方法中,尝试读取缓冲区的数据。

parseRequestLine 方法解析请求 Method,通过逐字节读取缓冲区的方式,当读取到空格或制表符时,会将该字符前面部分设置为 Method,但如果读取的字符中存在分隔符或控制字符(由 HttpParser.isToken 方法判断),就会出现异常。由于当前缓冲区第一个字符为 =,所以会抛出异常。

随后通过 parseInvalid(parsingRequestLineStart, byteBuffer) 以空格为分隔符提取出存在问题的数据。

并最终通过默认错误页面将上个请求的数据泄露到当前请求的响应。

需要注意的是,该漏洞实际导致 Tomcat 误以为接收到了第三个数据包,进行处理并产生报错,实际的报错内容会跟在第二个响应页面之后。

▌塞讯验证规则

针对该漏洞的攻击模拟已经加入到塞讯安全度量验证平台中,您可以在塞讯安全度量验证平台中搜索关键词“CVE-2024-21733”或“Apache Tomcat”获取相关攻击模拟验证动作,从而验证您的安全防御体系是否能够有效应对该漏洞,平台以业界独有方式确保您的验证过程安全无害。

参考来源:
HackerOne Report

塞讯验证提供
真实勒索软件攻击样本,如需
了解更多信息,欢迎拨打官方电话 400-860-6366 或发送邮件至
[email protected] 联系我们。
您也可以扫描下方二维码添加官方客服,我们将竭诚为您服务。

用持续验证   建长久安全


长按图片扫码添加【官方客服】

塞讯验证是国内网络安全度量验证平台开创者,率先提出利用真实自动化APT攻击场景来持续验证安全防御有效性概念, 旨在用安全验证技术来帮助客户实现365天持续评估自身安全防御体系效果,已在金融、高科技、关键信息基础设施等重点行业多家标杆客户中获得商业化落地验证。

核心团队均来自于全球知名网络安全公司和APT研究机构,拥有业界突出的安全研究与APT组织追踪能力。两大研发团队分别位于上海和杭州,致力于为客户打造最优秀的安全验证产品。我们在北京、上海、深圳、杭州均设有分支机构,服务可覆盖全国各个角落。

关注【塞讯安全验证】,了解塞讯安全度量验证平台以及更多安全资讯

关注【塞讯业务观测验证】,
了解最前沿的业务观测与IT运营相关技术、观点及趋势