3 điểm bởi GN⁺ 2024-04-06 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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ý CONTINUATION vớ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 HEADERS dù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ằng HPACK
  • Khung HEADERS có các cờ để báo hiệu phần cuối của header và stream
    • END_HEADERS: tín hiệu rằng khung đó chứa toàn bộ header cần gửi
    • END_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 HEADERS duy nhất, các khung CONTINUATION sẽ nối tiếp sau khung HEADERS không có END_HEADERS
    • Khung HEADERS không có END_HEADERS
    • Các khung CONTINUATION bổ sung cũng không có END_HEADERS
    • Khung CONTINUATION cuối cùng được đặt END_HEADERS
  • Sau khung header cuối cùng, sẽ có khung DATA chứ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 HEADERSCONTINUATION mà tuyệt đối không đặt END_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 CONTINUATION Flood
  • Triển khai của Go dùng lớp trừu tượng http2MetaHeadersFrame để gộp một khung HEADERS, không hoặc nhiều khung CONTINUATION, và bộ giải mã HPACK
  • readMetaFrame gọi SetEmitEnabled(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ành true, đ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, readMetaFrame sẽ 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 CONTINUATION cuối cùng để giữ kết nối sống
  • CONTINUATION Flood 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_ERROR nế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 CONTINUATION vô 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 trong Http2Session::~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à sau session_.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 khung CONTINUATION
    • Khung CONTINUATION đến khi đang ở trạng thái NGHTTP2_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_callback tiếp tục diễn ra
    • OnFrameReceiveHandleHeadersFrame của Node.js cập nhật bộ đếm bộ nhớ
  • Nếu HandleHeadersFrameHttp2Session::~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ến CHECK_EQ thấ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
  • CONTINUATION Flood 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
  • CONTINUATION Flood 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 HEADERS có đặt END_STREAMEND_HEADERS cùng với khung RST_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 CONTINUATION Flood, 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, CONTINUATION Flood 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

 
GN⁺ 2024-04-06
Ý 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ệ

    • Bạn biết điều gì xảy ra khi chúng ta giả định rồi đấy. Nó biến bạn và tôi thành tiêu đề trang nhất
  • 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

    • Mọi người cứ tập trung và tối ưu cho đường đi bình thường, nhưng không dừng lại để nghĩ xem điều gì xảy ra nếu kẻ tấn công cố ý lặp đi lặp lại tình huống xấu nhất
      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/

    • Nói cách khác, đó là các triển khai đã phản đối việc dùng CONTINUATION từ 10 năm trước vì rủi ro từ chối dịch vụ. Đọc các thread dài sẽ thấy trọng tâm luôn là làm sao tránh CONTINUATION phiền phức: https://lists.w3.org/Archives/Public/ietf-http-wg/2014JulSep...
      Í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
    • Còn Caddy thì sao? Đây là một dự án tuyệt vời nên đáng có một dòng riêng ;)
  • 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/1.1 dễ triển khai hơn nhiều, nên hợp lý khi cho rằng sẽ có ít lỗi hơn
      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
    • Nâng cấp chỉ để nâng cấp không phải là thực hành kỹ thuật tốt. Nếu việc nâng cấp không mang lại lợi ích bổ sung thì khó biện minh
    • Không hẳn vậy. Những người nói HTTP/1.1 đơn giản là những người chưa từng triển khai một parser hoàn chỉnh tương thích với môi trường thực tế
      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
    • Tôi cũng từng thắc mắc điều đó. Vì trưởng thành hơn và ít phức tạp hơn nên có vẻ có khả năng an toàn hơn
    • Có lẽ là vậy. HTTP/2 tốt cho streaming, mà ngay cả việc đó cũng đang được thay thế bằng các giao thức mới hơn
      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