Chi tiết kỹ thuật của HTTP/2 CONTINUATION Flood
(nowotarski.info)- HTTP/2 CONTINUATION Flood là một họ lỗ hổng trong triển khai HTTP/2 có thể làm sụp đổ tính sẵn sàng của máy chủ bằng cách liên tục gửi các khung header mà không có
END_HEADERS - Do yêu cầu tấn công không bao giờ hoàn tất, nó không xuất hiện trong nhật ký truy cập HTTP, và việc xác định nguyên nhân có thể đòi hỏi phân tích các byte lưu lượng thô
- Tùy theo cách triển khai, phạm vi tác động khác nhau, từ cạn kiệt CPU, OOM trên nhiều kết nối, OOM trên một kết nối, cho đến crash do lỗi thời điểm ngắt kết nối
- Trong các trường hợp của Go, Firefox và Node.js, lần lượt bộc lộ việc tiếp tục giải mã HPACK, thiếu giới hạn kích thước header phản hồi, và xung đột giữa ngắt kết nối trong lúc xử lý
CONTINUATIONvới việc cập nhật bộ đếm bộ nhớ - Khác với Rapid Reset, ở nhiều triển khai, chỉ với một kết nối TCP duy nhất cũng có thể làm máy chủ crash, nên nhiều dịch vụ Internet dùng HTTP/2 có thể đã bị ảnh hưởng
Cách khung CONTINUATION được dùng trong HTTP/2
- HTTP/2 là giao thức trao đổi khung nhị phân thay vì gửi nhận các dòng văn bản như HTTP/1.1
- Khung
HEADERSdùng để gửi các header HTTP của yêu cầu và phản hồi, và dữ liệu header được lưu trong field block fragment được mã hóa bằngHPACK - Khung
HEADERScó các cờ để báo hiệu phần cuối của header và streamEND_HEADERS: tín hiệu rằng khung đó chứa toàn bộ header cần gửiEND_STREAM: tín hiệu rằng không còn phần thân yêu cầu hoặc phản hồi nữa
- Các khung có kích thước tối đa được xác định khi bắt đầu giao tiếp, và nếu khung nhận được vượt quá kích thước cho phép thì kết nối sẽ bị ngắt do lỗi giao thức
- Nếu không thể chứa toàn bộ header trong một khung
HEADERSduy nhất, các khungCONTINUATIONsẽ nối tiếp sau khungHEADERSkhông cóEND_HEADERS- Khung
HEADERSkhông cóEND_HEADERS - Các khung
CONTINUATIONbổ sung cũng không cóEND_HEADERS - Khung
CONTINUATIONcuối cùng được đặtEND_HEADERS
- Khung
- Sau khung header cuối cùng, sẽ có khung
DATAchứa dữ liệu yêu cầu hoặc stream HTTP/2 sẽ kết thúc
Cốt lõi của lỗ hổng: luồng header không bao giờ kết thúc
- Nếu client bắt đầu một stream HTTP/2 mới rồi gửi các khung
HEADERSvàCONTINUATIONmà tuyệt đối không đặtEND_HEADERS, máy chủ sẽ tiếp tục cố gắng phân tích và lưu trữ luồng header vô hạn - Máy chủ HTTP/1.1 thường có hai cơ chế để chặn header vô hạn
- Giới hạn kích thước header ngắt kết nối nếu danh sách header vượt quá kích thước cho phép
- Timeout yêu cầu/header ngắt kết nối nếu yêu cầu hoặc header không được gửi đúng thời hạn
- Trong nhiều triển khai HTTP/2, các cơ chế bảo vệ này hoặc không có, hoặc được triển khai sai; bao gồm Apache httpd, Envoy và nhiều gói cùng codec HTTP/2 khác
- Kết quả theo từng triển khai được chia thành bốn nhóm
- Cạn kiệt CPU: trong lúc đọc và giải mã thêm header, mức sử dụng CPU tăng lên làm chậm hoặc chặn phản hồi cho các yêu cầu khác
- OOM dựa trên nhiều kết nối: header
CONTINUATIONđược lưu trong bộ nhớ, và dù có giới hạn kích thước danh sách header, việc thiếu timeout header khiến mỗi kết nối tiếp tục chiếm bộ nhớ - OOM trên một kết nối duy nhất: một số triển khai tiếp tục đọc header cho đến khi hết bộ nhớ, khiến hệ điều hành buộc phải dừng tiến trình
- Crash chỉ với vài khung: lỗi triển khai làm máy chủ crash khi kết nối bị ngắt giữa chừng trong luồng
CONTINUATION
- Nếu không có
END_HEADERS, yêu cầu sẽ không được đóng đúng cách, nên yêu cầu của client độc hại sẽ không được ghi vào nhật ký truy cập
Trường hợp Go: giải mã HPACK không dừng dẫn đến cạn kiệt CPU
- Go là trường hợp nổi bật về cạn kiệt CPU trong
CONTINUATIONFlood - Triển khai của Go dùng lớp trừu tượng
http2MetaHeadersFrameđể gộp một khungHEADERS, không hoặc nhiều khungCONTINUATION, và bộ giải mã HPACK readMetaFramegọiSetEmitEnabled(false)khi chạm giới hạn kích thước header hoặc gặp lỗi, để dừng phát ra các header đã giải mã- Tuy nhiên, ngay cả sau khi ngừng phát header, bộ giải mã HPACK vẫn tiếp tục giải mã các byte đầu vào
- Vòng lặp cung cấp khung chỉ dừng khi
HeadersEnded()trở thànhtrue, điều này chỉ xảy ra khi cờEND_HEADERSđược đặt - Nếu kẻ tấn công không gửi
END_HEADERS,readMetaFramesẽ không bao giờ trả về, và bộ giải mã HPACK tiếp tục xử lý các byte mới miễn là kẻ tấn công còn gửi
Các trường hợp OOM và ảnh hưởng tới client Firefox
- OOM xảy ra trong các triển khai không giới hạn kích thước danh sách header được tạo bởi các khung
CONTINUATION - Với các triển khai không có timeout header, chỉ một kết nối HTTP/2 duy nhất cũng có thể làm máy chủ crash
- Ngay cả với các triển khai có idle timeout, vẫn có thể tạo nhiều kết nối HTTP/2 để mỗi kết nối chiếm gần tới giới hạn RAM theo kết nối, rồi cứ vài giây lại gửi theo từng byte khung
CONTINUATIONcuối cùng để giữ kết nối sống CONTINUATIONFlood không chỉ xảy ra ở máy chủ mà còn có thể xảy ra ở phía client như trình duyệt- Commit vá của Mozilla Firefox đã thêm kiểm tra để trả về lỗi phiên
PROTOCOL_ERRORnếu tổng kích thước header đã cộng dồn và kích thước khung mới vượt quánetwork_http_max_response_header_size()
Trường hợp Node.js: crash do assertion trong lúc ngắt kết nối
- Node.js xử lý đúng luồng
CONTINUATIONvô hạn, nhưng lại phát sinh data race khi kết nối bị ngắt trong luồng header - Trong khi mã tấn công chạy, Node.js crash do assertion
CHECK_EQ(current_nghttp2_memory_, 0)thất bại trongHttp2Session::~Http2Session() - Vụ crash gắn với đúng thời điểm client HTTP/2 ngắt kết nối khỏi máy chủ Node.js, và assertion nằm trong destructor của
Http2Session - Node.js nhúng thư viện nghttp2 để xử lý kết nối HTTP/2
current_nghttp2_memory_theo dõi bộ nhớ được cấp phát bên trong nghttp2, và sausession_.reset()trong destructor, nó kiểm tra xem mọi artifact của nghttp2 đã được gỡ khỏi bộ nhớ hay chưa- Kết quả điều tra cho thấy có những trường hợp callback của nghttp2 và
reset()cùng chạy trong lúc phân tích khungCONTINUATION- Khung
CONTINUATIONđến khi đang ở trạng tháiNGHTTP2_IB_EXPECT_CONTINUATION - Trạng thái chuyển sang
NGHTTP2_IB_READ_HEADER_BLOCK - Luồng
session_after_header_block_received,session_call_on_frame_received,on_frame_recv_callbacktiếp tục diễn ra OnFrameReceivevàHandleHeadersFramecủa Node.js cập nhật bộ đếm bộ nhớ
- Khung
- Nếu
HandleHeadersFramevàHttp2Session::~Http2Session()chạy đồng thời,current_session_memory_sẽ bị cập nhật đồng thời, làm giá trịcurrent_nghttp2_memory_trở thành âm và khiếnCHECK_EQthất bại
Khác biệt so với các lỗ hổng HTTP/2 năm 2019
- Gói lỗ hổng HTTP/2 do Netflix và Google báo cáo năm 2019 được tóm tắt trong CERT/CC Vulnerability Note VU#605641
- CVE-2019-9516, “0-Length Headers Leak”, là vấn đề khiến một số triển khai cấp phát bộ nhớ cho tên và giá trị header có độ dài 0 rồi giữ lại đến khi phiên kết thúc
CONTINUATIONFlood không dùng header rỗng, mà gửi nhiều header ngẫu nhiên cho tới giới hạn kích thước khung mà máy chủ cấu hình- CVE-2019-9518, “Empty Frame Flooding”, là vấn đề gửi các khung
DATA,HEADERS,CONTINUATION,PUSH_PROMISE... có payload rỗng mà không có end-of-stream, khiến bên kia tiêu tốn thời gian xử lý quá mức so với băng thông của cuộc tấn công CONTINUATIONFlood không dùng khung rỗng mà dùng các khung lớn nhất có thể để chiếm bộ nhớ và tiêu hao chu kỳ CPU trong quá trình giải mã
Vì sao nó có thể nghiêm trọng hơn Rapid Reset
- Tháng 10/2023, chi tiết về zero-day “Rapid Reset” của giao thức HTTP/2 được công bố và nó được gọi là “cuộc tấn công DDoS lớn nhất từng được biết đến”
- Rapid Reset dùng tổ hợp khung
HEADERScó đặtEND_STREAMvàEND_HEADERScùng với khungRST_STREAM - Với cách này, các biện pháp giảm thiểu tiêu chuẩn như rate limiting có thể làm giảm thiệt hại, và quản trị viên máy chủ có thể thấy nhiều yêu cầu đi vào trong nhật ký để nhận cảnh báo
- Trong
CONTINUATIONFlood, do không cóEND_HEADERS, không một yêu cầu nào hoàn tất, nên quản trị viên không thể thấy yêu cầu trong nhật ký - Ở nhiều triển khai,
CONTINUATIONFlood có thể làm máy chủ crash chỉ với một kết nối TCP duy nhất, và trong một số trường hợp chỉ cần rất ít dữ liệu - Rapid Reset đã được dùng cho các cuộc tấn công DDoS, và trong phần lớn trường hợp, để tấn công thành công vẫn cần một botnet
Tác động có thể xảy ra với các dịch vụ Internet
- Theo Cloudflare Radar, lưu lượng HTTP/2 chiếm khoảng 60% lưu lượng HTTP do con người tạo ra, không tính bot
- Cloudflare Radar ước tính lưu lượng HTTP chiếm hơn 70% toàn bộ truyền tải trên Internet
- Xét tới tầm quan trọng của các dự án bị ảnh hưởng và khả năng khai thác dễ dàng, một phần lớn Internet đã có thể bị phơi bày trước lỗ hổng này
- HTTP không chỉ dùng cho website mà còn cho nhiều RESTful API
- Các vấn đề về tính sẵn sàng của API và website quan trọng của doanh nghiệp hoặc chính phủ có thể gây thiệt hại hàng triệu đô la hoặc tạo ra hỗn loạn
- Nếu bị khai thác, việc gỡ lỗi có thể đã cực kỳ khó đối với quản trị viên máy chủ không có kiến thức HTTP/2
- Yêu cầu HTTP độc hại không được đóng đúng cách
- Yêu cầu không xuất hiện trong nhật ký truy cập máy chủ
- Phần lớn máy chủ HTTP/2 thiếu khả năng phân tích khung nâng cao
- Có thể phải phân tích thủ công dữ liệu kết nối thô
Công bố có phối hợp và phản ứng của CERT/CC
- Họ lỗ hổng này tạo ra rủi ro đáng kể đối với an toàn Internet
- Sau khi được báo cáo vào tháng 1/2024, CERT/CC đã mở một vụ Vulnerability Coordination để theo dõi vấn đề này
- Nhiều tập đoàn công nghệ lớn và các dự án mã nguồn mở đã tham gia vào quá trình công bố có trách nhiệm liên quan
- Vì một nhà nghiên cứu đơn lẻ khó có thể kiểm tra hết nhiều triển khai, nên các vấn đề ảnh hưởng tới nhiều nhà cung cấp cần có Vulnerability Coordination
- CERT/CC đã công bố Vulnerability Note về vấn đề này; mỗi năm chỉ có vài ghi chú như vậy được công bố
1 bình luận
Ý kiến trên Hacker News
Tháng trước, Bandit đã giảm thiểu đúng vấn đề này
https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
Từ góc nhìn của người triển khai, thành thật mà nói đây là thứ quá hiển nhiên phải chặn. Chúng tôi đã để ý từ lâu và cứ nghĩ các triển khai khác đương nhiên cũng đang phòng vệ
Trong vài tháng qua tôi đã kiểm tra hàng chục triển khai, nhưng kỳ lạ là ngay cả các máy chủ HTTP/2 lớn cũng không có biện pháp bảo vệ này hoặc triển khai sai
Về bản chất, tôi cho rằng đây là kết quả của một văn hóa phát triển đã quen với việc tự động mở rộng mọi thứ một cách động và không bận tâm kích thước có thể lớn đến đâu
Loại vấn đề này không chỉ giới hạn ở HTTP/2, nhưng rất có thể sự phức tạp nặng nề của HTTP/2 đã góp phần. Thời HTTP/1.x, có nhiều lập trình viên quen với các ngôn ngữ như C hơn nên họ luôn chú ý quản lý độ dài bộ đệm, và dù việc cấp phát header cho toàn bộ request nhiều lắm chỉ cần vài KB, họ cũng sẽ không để nó tăng vô hạn
Nhiều cuộc tấn công từ chối dịch vụ như slowloris hay va chạm hash của tham số truy vấn đã trở thành hiện thực vì mức dùng tài nguyên bị giới hạn chỉ được cân nhắc sau này
> Không bị ảnh hưởng: Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]0: https://nowotarski.info/http2-continuation-flood/
Ít nhất nếu đề xuất cấm nó sau một frame HEADERS chưa đầy được chấp nhận thì đã vững chắc hơn, nhưng khi đó người ta cho rằng bản thân việc mã hóa có thể trở nên khó hơn, vì các vấn đề như ranh giới byte của bộ nén
Thật buồn cười khi cứ mỗi 10 năm chúng ta lại “tái phát hiện” những thứ giống nhau. Gần đây là RESET_STREAM flood vốn đã nổi tiếng, lần này là CONTINUATION, và có lẽ sắp tới sẽ là frame DATA độ dài 0, WINDOW_UPDATE 1 byte, các SETTINGS INITIAL_WINDOW khiến CPU tốn nhiều tài nguyên, v.v. Miễn là có thể đặt tên và tốt nhất là gắn logo cho một vấn đề đã biết, thế giới sẽ tiếp tục quay vòng trong rạp xiếc bảo mật này
Bài trước của cùng tác giả tổng hợp các web server/reverse proxy bị ảnh hưởng
https://nowotarski.info/http2-continuation-flood/
Bài này đã ở đầu trang suốt cả ngày
Tôi tò mò, với một website có lưu lượng thấp thì liệu chỉ chạy bằng HTTP/1.1 có thể an toàn hơn không?
HTTP/2 và HTTP/3 khác biệt lớn về mặt tính năng. Việc thêm multiplexing, windowing, HPACK, v.v. đã biến kết nối gần như không trạng thái của HTTP/1.1 thành kết nối có trạng thái. Để duy trì kết nối có trạng thái cần lưu dữ liệu như trạng thái và thiết lập, và vì thế những vấn đề như thế này phát sinh
Trong HTTP/2 có thêm multiplexing nên đặc tính phòng vệ cũng khác. Ví dụ, nếu kết nối đến từ request origin của CDN thì có thể chỉ cho phép ít kết nối và đặt một pool kênh multiplexing lớn cho mỗi kết nối, còn nếu là truy cập trực tiếp từ người dùng thì có thể muốn cho phép nhiều kết nối nhưng giảm số kênh multiplexing trên mỗi kết nối. Trong HTTP/1, gần như mọi thứ trông khá giống nhau nên việc phòng vệ đơn giản hơn nhiều
HTTP/1 có nhiều điều kiện biên khó nhận thấy và hành vi ngoại lệ cũ. Định dạng văn bản linh hoạt hơn rất nhiều so với khi chỉ nhìn các header hợp lệ, và còn có các tính năng mơ hồ như header nhiều dòng, tính năng MIME cũ, điều kiện tranh chấp 100-continue, header hop-by-hop tùy chỉnh, body trong GET
May mắn là các RFC HTTP mới đã ghi lại nhiều cái bẫy. Nếu chỉ nhìn RFC 2616 để triển khai thì sẽ không có một triển khai an toàn
Kích thước thực tế của request hoặc response có thể được chỉ định đồng thời theo nhiều cách và các giá trị có thể xung đột. Ngoài ra, tùy vào nhiều tổ hợp tính năng và các giá trị header cần quy tắc parsing kỳ lạ để tương thích ngược, một triển khai HTTP “đơn giản” có thể bị đánh lừa bởi request smuggling
Dù chọn bên nào, cũng cần một triển khai trưởng thành, vững chắc và được kiểm thử tốt
Với việc phục vụ tài sản tĩnh thông thường, lợi thế duy nhất là HTTP/1 bị giới hạn số kết nối trên mỗi domain nên có thể tải nhiều tài sản song song hơn. Dùng CDN ở các domain khác nhau thường cũng tránh được vấn đề này
Về lý thuyết có thể dùng HTTP/2 để phục vụ các tài sản JavaScript chưa bundle, nhưng tôi chưa từng thấy trong môi trường vận hành thực tế. Có lẽ vì hầu hết trường hợp vẫn cần bước biên dịch
Nếu làm việc này chậm rãi thì có thể gọi là slowloris v2 nhỉ :(
HTTP/2, hay cách nhồi nhét một “nâng cấp” tầng truyền tải vào giao thức tầng ứng dụng