Analyzing CVE-2024-34750 Apache Tomcat DoS
Preface
Bài viết sẽ phân tích lại CVE-2024-34750, CVE mà mình tìm được gần đây. Về web server Tomcat chắc cũng không còn phải giới thiệu nhiều, trong các ứng dụng java web servlet hay spring framework thì tomcat luôn là server được lựa chọn để deploy phổ biến nhất cho đến nay.
Nói ngắn gọn, CVE-2024-34750 cho phép DoS sập HTTP/2 connector của tomcat bằng cách trigger lỗi StreamException
làm giá trị activeRemoteStreamCount
bị đếm sai dẫn đến connection timeout bị set vô thời hạn (connection không được đóng sau khi xử lý xong). Khi số lượng connection được mở đạt đến giới hạn, tomcat connector không thể handle thêm request dẫn đến DoS.
Bài viết yêu cầu người đọc có hiểu biết basic về giao thức HTTP/2, có thể tham khảo chuẩn RFC: https://datatracker.ietf.org/doc/html/rfc9113
Versions affect:
Apache Tomcat 11.0.0-M1 to 11.0.0-M20
Apache Tomcat 10.1.0-M1 to 10.1.24
Apache Tomcat 9.0.0-M1 to 9.0.89
....và các versions cũ hơn mà apache không còn support
Conditions: Sử dụng giao thức HTTP/2 của tomcat (connector) Advisory: https://lists.apache.org/thread/4kqf0bc9gxymjc2x7v3p7dvplnl77y8l
Http2 Notes:
- Connection: Http2 connection hay đúng hơn là TCP connection
- Stream: Stream là một khối data chứa một hoặc nhiều các frames được gửi đi trong connection
- Frame: Là một frame data theo chuẩn RFC ứng với các thành phần trong http / events như HEADERS, CONTINUATION, DATA, SETTINGS frames
Setup
Tại <tomcat-home>/conf/server.xml
uncomment block sau để enable HTTP/2 connector:
|
|
conf/localhost-rsa.jks
có thể lấy từ đây
Analysis
Http2UpgradeHandler#upgradeDispatch()
Đầu tiên đến với Http2UpgradeHandler#upgradeDispatch()
|
|
Tất cả các http2 frame của client đều đi qua method này để phân loại và xử lý. Connection sẽ được set timeout vô thời hạn trong quá trình xử lý các frame này (line 27, giá trị -1
ứng với vô thời hạn). Các frame có thể đến từ nhiều streams khác nhau, sau khi đọc hết tất cả các frame, tại line 75 gọi setConnectionTimeoutForStreamCount()
:
|
|
Để kiểm tra còn active stream nào tồn tại cần xử lý tiếp không, nếu không (ứng với line 2-10) sẽ thực hiện set timeout cho connection (default: protocol.getKeepAliveTimeout()
~ 20000ms
). Nếu vẫn còn (line 11-15) tiếp tục set timeout vô hạn để tiếp tục xử lý.
Headers Frame
Một Frame chính trong http2 request mà không thể thiếu đó chính là Headers Frame, khi tomcat parse frame này tại Http2Parser#readHeadersFrame()
có 2 nơi cần chú ý:
|
|
Tại line 10 sẽ khởi tạo stream mới thông qua Http2UpgradeHandler#headersStart()
để lưu / xử lý headers, cũng như sau khi đọc hết header data (line 18), sẽ gọi lần lượt onHeadersComplete()
(line 26)–> Http2UpgradeHandler#headersEnd()
:
|
|
Tại line 11 ở câu điều kiện if sẽ gọi activeRemoteStreamCount.incrementAndGet()
để tăng giá trị activeRemoteStreamCount
lên += 1
đồng thời lấy số lượng stream đang active để so sánh
Sau khi đọc / xử lý xong Headers Frame này để trả về response sẽ gọi lại Http2UpgradeHandler#sentEndOfStream()
:
|
|
Do stream không còn active (đã hoàn thành), gọi decrementActiveRemoteStreamCount()
:
|
|
Từ đó sẽ activeRemoteStreamCount
giảm -= 1
==> Stream đã xử lý xong, loại stream khỏi active stream. activeRemoteStreamCount
lúc này sẽ bằng 0
, connection được set timeout trừ khi client request nhiều Headers Frame khác từ đó sẽ còn stream để tiếp tục xử lý.
Ngoài Http2UpgradeHandler#sentEndOfStream()
gọi đến decrementActiveRemoteStreamCount()
thì các method sau cũng gọi đến để check và set timeout sau mỗi lần xử lý stream:
Http2AsyncUpgradeHandler:
sendStreamReset
Http2UpgradeHandler:
sendStreamReset
sentEndOfStream
receivedEndOfStream
reset
headersEnd
Có thể thấy các method này được gọi chủ yếu khi stream gặp lỗi / kết thúc stream
Break the logic
Như đã đề cập, activeRemoteStreamCount
được += 1
khi đọc xong header data và thực hiện Http2UpgradeHandler#headersEnd()
, cùng xem lại method readHeadersFrame
Vậy nếu ta có thể tìm cách trigger lỗi gì đó để khi tomcat đọc header mà không reach đến được Http2UpgradeHandler#headersEnd()
-> activeRemoteStreamCount
vẫn bằng 0
. Tuy nhiên do lỗi mà tomcat phải end/reset stream mà gọi đến decrementActiveRemoteStreamCount()
==> activeRemoteStreamCount
là -1
–> setConnectionTimeoutForStreamCount()
set timeout vô thời hạn.
Trong lúc test mình tình cờ phát hiện method Http2Parser#readHeaderPayload()
(line 18) (tức ngay trước khi headersEnd()
kết thúc frame):
|
|
Nếu isHeaderSizeExceeded()
–> set StreamException
/ Tuy nhiên không vào isHeaderSwallowSizeExceeded()
để tránh ConnectionException
- luồng chương trình sau method này sẽ rơi về catch block StreamException
của Http2UpgradeHandler#upgradeDispatch() (line 33-59) đầu bài
Đặc biệt tại line 57 sẽ gọi stream.close(se)
để close stream, code:
|
|
Tại method này lại thấy có gọi đến sendStreamReset()
, một method mà mình đã đề cập ở trên sẽ gọi đến được decrementActiveRemoteStreamCount()
==> Lúc này activeRemoteStreamCount
sẽ giảm đi một và giá trị sẽ là -1
. Ở đây cần né các exception gọi closeConnection()
do nó sẽ không trigger được decrementActiveRemoteStreamCount()
.
Luồng chương trình lúc này sẽ đi qua vài lần method setConnectionTimeoutForStreamCount()
nữa nhưng với activeRemoteStreamCount
là số âm (khác 0
) ==> luôn set timeout vô hạn ==> connection ở server không bị đóng và treo dù client đã send hết stream và drop connection.
PoC
Để trigger StreamExcetion
qua isHeaderSizeExceeded()
, headerSize
cần nằm trong khoảng x
và y
. Nếu headerSize
quá y
sẽ gây lỗi ConnectionException
-> fail. Phần còn lại người đọc có thể review code để ra số chính xác.
Default maxConnections
được cấu hình trong AbstractEndpoint
là 8*1024
~ 8192
:
Do đó có thể DoS tomcat khi gửi đủ 8192
connections trigger StreamExcetion
, mọi requests sau 8192
đều sẽ bị connection refused cho đến khi tomcat được restart lại. Ngoài ra nếu server cấu hình memory có giới hạn cũng sẽ gây lỗi java.lang.OutOfMemoryError
và cũng gây ra DoS.
Fix
Bản fix sẽ tăng activeRemoteStreamCount
khi headersStart
thay vì headersEnd
:
Thêm method Stream#decrementAndGetActiveRemoteStreamCount()
để đảm bảo khi giảm activeRemoteStreamCount
, mỗi stream chỉ giảm đúng 1 lần:
Chi tiết commit: https://github.com/apache/tomcat/commit/2344a4c0d03e307ba6b8ab6dc8b894cc8bac63f2
Thoughts
CVE này mình tìm ra khi đang reproduce lại CVE-2024-24549, thanks for disclosure! Lúc mới làm tomcat mình khá mới với codebase cho nên khi PoC được bug này mình cũng chưa thể hiểu hết được logic xử lý, sau khi xem lại commit fix của tomcat kết hợp dynamic debug thì mình mới hiểu sâu hơn về lỗ hổng này.
Cảm ơn vì đã đọc đến đây :)