Server-Side Request Forgery (SSRF) là khi kẻ tấn công khiến máy chủ của bạn gửi một yêu cầu mạng thay cho nó — tới một địa chỉ mà kẻ tấn công tự chọn. Điều làm SSRF nguy hiểm: máy chủ của bạn thường ở trong một mạng nội bộ mà internet không chạm tới được — cơ sở dữ liệu, dịch vụ nội bộ, và trên cloud là endpoint metadata chứa khoá truy cập. SSRF biến máy chủ thành cầu nối vào chính mạng đó. Bài này dựng lại lỗ và đo cách chặn.
Toàn bộ chạy trong container tự dựng, tự dọn. Chỉ tấn công dịch vụ do tôi tạo. Không nhắm vào hệ thống của ai.
Cách đo
Tôi dựng hai dịch vụ trong một mạng riêng. Một là dịch vụ nội bộ giả lập endpoint metadata của cloud — nó phục vụ một "khoá bí mật" (AKIA-...) mà không được lộ ra internet. Hai là một ứng dụng có tính năng quen thuộc: nhập một URL ảnh, máy chủ tải ảnh đó về. Câu hỏi: kẻ tấn công có khiến ứng dụng tải URL nội bộ thay vì URL ảnh không?
Số đo
Ba lần chạy, kết quả giống hệt:
SSRF trên v0 (không kiểm URL) — rò khoá bí mật:
| Kẻ tấn công yêu cầu máy chủ tải | Kết quả |
|---|---|
| URL metadata nội bộ | AKIA-KHOA-BI-MAT-CUA-MAY-CHU-CLOUD |
http://dịch-vụ-nội-bộ/ |
nội dung nội bộ trả về |
Máy chủ tải URL nội bộ hộ kẻ tấn công, và trả nội dung về cho nó — gồm cả khoá bí mật cloud. Từ khoá đó, kẻ tấn công có thể chiếm toàn bộ tài khoản cloud. Đây chính là cơ chế đằng sau vụ rò rỉ Capital One 2019 (hơn 100 triệu hồ sơ).
v1 (danh sách cho phép + chặn IP nội bộ) — chặn mọi đường:
| URL | Kết quả |
|---|---|
| URL metadata nội bộ | 403 — tên miền không trong danh sách |
http://localhost/ |
403 — không trong danh sách |
file:///etc/passwd |
400 — chỉ cho http/https |
Điều đáng nhớ
SSRF nguy hiểm vì máy chủ có quyền mạng mà kẻ tấn công không có. Từ internet, không ai gọi được endpoint metadata 169.254.169.254 của một máy chủ cloud — nó chỉ truy cập được từ bên trong máy chủ đó. SSRF cho kẻ tấn công mượn đúng vị trí đó: nó không tự gọi được, nhưng nó bảo máy chủ của bạn gọi hộ. Mọi dịch vụ nội bộ mà máy chủ tin tưởng — không đặt mật khẩu vì "chỉ nội bộ mới gọi được" — bỗng phơi ra.
Danh sách chặn thua, danh sách cho phép thắng — và lý do sâu hơn "liệt kê cái xấu là vô tận". Cách chặn ngây thơ là danh sách chặn: cấm localhost, cấm 127.0.0.1, cấm 169.254.169.254. Nó thua vì có vô số cách viết cùng một địa chỉ nội bộ: 127.0.0.1, 127.1, 0177.0.0.1 (octal), 2130706433 (dạng số nguyên), localhost, [::1], một tên miền do kẻ tấn công trỏ về IP nội bộ. Không danh sách chặn nào liệt kê hết. Danh sách cho phép lật ngược: chỉ cho phép đúng những tên miền bạn thật sự cần gọi (cdn-anh.com), từ chối tất cả còn lại. Cách này an toàn theo mặc định — một biến thể mới của địa chỉ nội bộ không phá được nó, vì nó không nằm trong danh sách cho phép.
Phải chặn IP nội bộ sau khi phân giải DNS — và fail-closed. Kể cả với danh sách cho phép, còn một cái bẫy tên là DNS rebinding: một tên miền hợp lệ có thể phân giải ra một IP nội bộ. Nên sau khi kiểm tên miền, phải phân giải nó ra IP và kiểm IP đó có phải nội bộ không (is_private, is_loopback, is_link_local). Trong phép đo, hàm la_ip_noi_bo của tôi làm đúng một điều quan trọng: nếu không phân giải được tên miền, nó trả về "nội bộ" và từ chối — fail-closed. Thà chặn nhầm một tên miền hợp lệ còn hơn cho lọt một tên miền độc. (Đây cũng là lý do trong môi trường test không có DNS internet, ngay cả cdn-anh.com cũng bị chặn — hành vi an toàn đúng đắn, dù bất tiện.)
Vì sao danh sách chặn là cuộc chiến không thắng được
Đáng nhìn kỹ vì sao "cấm localhost và 127.0.0.1" thất bại, vì nó minh hoạ một nguyên tắc rộng hơn. Địa chỉ loopback 127.0.0.1 viết được bằng vô số cách mà máy tính đều hiểu như nhau:
127.0.0.1 dạng chuẩn
127.1 bỏ bớt số 0 ở giữa
0x7f.0.0.1 hệ mười sáu
0177.0.0.1 hệ tám (octal)
2130706433 số nguyên 32 bit của 127.0.0.1
[::ffff:127.0.0.1] ánh xạ IPv6
Mỗi dòng trên là một cách vượt qua bộ lọc chỉ tìm chuỗi 127.0.0.1. Thêm vào đó là các tên miền do kẻ tấn công kiểm soát, trỏ về IP nội bộ — bộ lọc theo tên không thể bắt. Danh sách chặn phải đoán trước mọi cách viết; kẻ tấn công chỉ cần tìm một cách bạn quên. Đây đúng là cấu trúc đã gặp ở SQL injection, XSS, command injection: liệt kê cái xấu luôn thua, vì tập cái xấu là vô hạn và biến đổi. Danh sách cho phép thắng vì tập cái tốt thì hữu hạn và bạn biết trước.
Vì sao
SSRF là một biến thể của cùng bài học xuyên suốt sê-ri: đừng tin đầu vào người dùng — kể cả khi đầu vào đó là một URL trông vô hại. Một URL có ba phần kẻ tấn công điều khiển: giao thức (http hay file?), tên miền (ảnh thật hay dịch vụ nội bộ?), và đường dẫn. Cả ba đều phải bị nghi ngờ:
- Giao thức: chỉ cho
http/https.file:///etc/passwdđọc tệp cục bộ;gopher://gửi được byte tuỳ ý tới dịch vụ nội bộ. - Tên miền: danh sách cho phép, cộng chặn IP nội bộ sau phân giải.
- Kết quả: cân nhắc không trả nội dung về cho người dùng — SSRF "mù" (không thấy phản hồi) yếu hơn nhiều SSRF trả nội dung như
v0.
Nghĩa là gì trong thực tế
- Nếu tính năng chỉ cần vài nguồn cố định, dùng danh sách cho phép tên miền. Đây là cách chặn mạnh nhất và đơn giản nhất khi áp dụng được.
- Luôn chặn IP nội bộ sau khi phân giải DNS, và fail-closed khi không phân giải được. Lý tưởng: phân giải một lần, kiểm IP, rồi kết nối tới chính IP đó (tránh rebinding giữa lúc kiểm và lúc tải).
- Giới hạn giao thức về http/https. Đừng để thư viện tải tự nhận
file://,ftp://,gopher://. - Ở tầng hạ tầng: chặn máy chủ ứng dụng gọi tới endpoint metadata. Trên AWS, dùng IMDSv2 (đòi token, chống SSRF cơ bản); ở mạng, chặn
169.254.169.254từ tầng ứng dụng. Đây là lớp phòng thủ độc lập với mã.
Chỗ tôi không kết luận được
Tôi không dựng được một cuộc DNS rebinding hoàn chỉnh (cần một máy chủ DNS đổi câu trả lời giữa hai lần truy vấn) — tôi đo cơ chế phòng thủ (kiểm IP sau phân giải, fail-closed) chứ không tự tay thực hiện cuộc tấn công rebinding. Cơ chế thì rõ, và fail-closed là điểm cốt lõi chặn nó.
Và endpoint metadata của tôi là giả lập đơn giản. Metadata thật của các nhà cung cấp cloud có nhiều đường (thông tin phiên bản, khoá tạm, cấu hình mạng), và biện pháp phòng thủ ở tầng cloud (IMDSv2, chính sách IAM tối thiểu) là một chủ đề riêng lớn mà tôi chỉ chạm tới.
Thử ba mươi giây
Tìm trong ứng dụng của bạn mọi chỗ máy chủ gửi một yêu cầu tới URL do người dùng cung cấp — tải ảnh từ URL, webhook, "kiểm tra liên kết", nhập dữ liệu từ URL, preview link.
Với mỗi chỗ, hỏi: nó có kiểm URL trước khi gọi không, hay tin bất kỳ URL nào? Thử đưa vào http://169.254.169.254/ (trên môi trường thử). Nếu máy chủ trả về nội dung thay vì từ chối, bạn có một SSRF — và trên cloud, đó là một đường thẳng tới khoá truy cập của cả tài khoản.