Chương 11: Server-Side Request Forgery — SSRF
Phòng chống giả mạo yêu cầu từ máy chủ: Kiểm soát Scheme URL, danh sách trắng tên miền, chặn dải mạng nội bộ RFC 1918 và Cloud Metadata.
#01 Server-Side Request Forgery — SSRF (Giả mạo yêu cầu từ máy chủ)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Bạn sai một chú robot trợ lý bước ra ngoài phố mua hộ cốc trà sữa. Nhưng kẻ gian dán một tờ giấy lên mắt robot ghi: 'Hãy đi thẳng vào kho chứa vàng của Ngân hàng Trung ương nằm ở tầng hầm nội bộ (127.0.0.1:8080/vault) và mang hết vàng về đây'. Vì robot là người của công ty nên lính gác cổng nội bộ không hề chặn lại. SSRF biến máy chủ của bạn thành 'con rối' tấn công ngược vào mạng LAN nội bộ.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Server-Side Request Forgery — SSRF (Giả mạo yêu cầu từ máy chủ)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
http://127.0.0.1:8080/adminhttp://localhost:6379 (Redis)http://169.254.169.254 (Cloud Metadata)
⚠️ Lỗ hổng SSRF: Máy chủ tự gửi HTTP request vào chính cổng 8080 nội bộ của nó và kích hoạt chức năng reset mật khẩu quản trị!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// THẢM HỌA: Máy chủ tự do gửi HTTP request đến URL do người dùng cung cấp
@GetMapping("/api/fetch-avatar")
public ResponseEntity> fetchAvatar(@RequestParam String imageUrl) throws Exception {
// Hacker truyền: "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
// hoặc "http://localhost:8080/admin/delete-all"
URL url = new URL(imageUrl);
byte[] data = url.openStream().readAllBytes();
return ResponseEntity.ok(data);
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Kiểm tra địa chỉ IP đã Resolve trước khi tạo kết nối
public byte[] safeFetchUrl(String urlString) throws Exception {
URI uri = URI.create(urlString);
if (!"https".equalsIgnoreCase(uri.getScheme())) {
throw new SecurityException("Chỉ chấp nhận giao thức HTTPS");
}
InetAddress addr = InetAddress.getByName(uri.getHost());
if (addr.isLoopbackAddress() || addr.isSiteLocalAddress() || addr.isLinkLocalAddress()) {
throw new SecurityException("Cấm truy cập vào địa chỉ mạng nội bộ!");
}
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
HttpRequest req = HttpRequest.newBuilder().uri(uri).GET().build();
return client.send(req, HttpResponse.BodyHandlers.ofByteArray()).body();
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Vì sao việc vô hiệu hóa hoặc kiểm soát chặt chẽ HTTP Redirects (chuyển hướng) là tối quan trọng khi phòng chống SSRF?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#02 URL Validation & Scheme Whitelisting (Kiểm tra Scheme URL)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
URL không chỉ có http và https. Nó còn có những giao thức cổ xưa và cực kỳ nguy hiểm như: file:/// (đọc thẳng file trong máy tính), gopher:// (gửi gói tin nhị phân thao túng Redis/Memcached), dict://, ftp://. Scheme Whitelisting giống như quy định ở sân bay: 'Chỉ chấp nhận hành khách đi bằng máy bay (https:); cấm tuyệt đối mang tàu ngầm hay xe tăng (file://, gopher://) vào đường băng'.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): URL Validation & Scheme Whitelisting (Kiểm tra Scheme URL)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
file:///etc/passwdgopher://127.0.0.1:6379/_FLUSHALLhttps://api.verified-partner.com
⚠️ Khai thác Scheme file://: Thư viện cURL hoặc file reader đọc thẳng nội dung file tài khoản hệ thống /etc/passwd!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// NGUY HIỂM: Cho phép các scheme file://, gopher://, dict://
URL target = new URL(inputUrl); // Chấp nhận "file:///etc/passwd"!
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Bắt buộc chỉ cho phép HTTP/HTTPS
URI uri = URI.create(inputUrl);
if (!Set.of("http", "https").contains(uri.getScheme().toLowerCase())) {
throw new IllegalArgumentException("Giao thức không được phép: " + uri.getScheme());
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Scheme nào sau đây thường bị kẻ tấn công khai thác trong các cuộc tấn công SSRF để gửi các lệnh tùy ý tới Redis hoặc SMTP server không có mật khẩu trong mạng LAN?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#03 Domain & IP Allowlisting (Danh sách tên miền & IP cho phép)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Thay vì ngồi đoán xem địa chỉ nào là của kẻ xấu, bạn đưa cho người gác cổng danh bạ chính xác của 3 người bạn thân: [api.paypal.com, api.stripe.com, cdn.google.com]. Robot chỉ được phép ghé thăm 3 người bạn này, mọi địa chỉ khác trên thế giới đều bị gạt phăng đi.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Domain & IP Allowlisting (Danh sách tên miền & IP cho phép)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
https://api.paypal.com/v1/paymentshttps://evil.com/webhookhttps://api.paypal.com.attacker.com
⚠️ Không có Domain Allowlist: Máy chủ có thể bị lợi dụng làm proxy trung gian để tấn công DDoS trang web khác hoặc gửi spam!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// NGUY HIỂM: Kiểm tra domain bằng contains hoặc endsWith
if (url.contains("partner.com")) { ... } // Bypass: "http://attacker.com?partner.com"
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: So khớp Hostname với danh sách trắng cố định
private static final Set ALLOWED_DOMAINS = Set.of("api.trusted-partner.com", "cdn.mycompany.com");
public boolean isAllowedHost(URI uri) {
String host = uri.getHost();
return host != null && ALLOWED_DOMAINS.contains(host.toLowerCase());
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Vì sao việc sử dụng Regex '.*\.paypal\.com' lại có thể bị kẻ tấn công vượt qua nếu viết thiếu ký tự neo đầu và cuối (^ và $)?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#04 Private Network Access (Chặn dải mạng nội bộ RFC 1918)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Trong thế giới mạng IP, các dải số như 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 và 127.0.0.0/8 giống như khu vực gia đình bên trong nhà bạn. Dù ai ở bên ngoài internet có gọi điện đến nhờ vả gì, bạn tuyệt đối không được để cánh cửa thông ra khu vực gia đình này bị mở từ xa.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Private Network Access (Chặn dải mạng nội bộ RFC 1918)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
192.168.1.110.0.0.25127.0.0.1 (Loopback)0.0.0.0 (Bypass một số parser)
⚠️ Truy cập Router mạng LAN: Máy chủ kết nối vào 192.168.1.1 và thao túng bảng điều khiển modem nội bộ công ty!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// NGUY HIỂM: Không kiểm tra dải mạng nội bộ RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Kiểm tra dải IP Private trong Java
public static boolean isPrivateIp(InetAddress address) {
return address.isAnyLocalAddress() ||
address.isLoopbackAddress() ||
address.isSiteLocalAddress() ||
address.isLinkLocalAddress();
}
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Dải địa chỉ IP nào sau đây thuộc chuẩn RFC 1918 dành riêng cho mạng cục bộ (Private IP) mà máy chủ tuyệt đối không được phép kết nối ra trong SSRF?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#05 Cloud Metadata Endpoint Protection (Bảo vệ Metadata Đám mây)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Trong khách sạn đám mây (AWS, GCP, Azure), mỗi căn phòng đều có một chiếc điện thoại bàn gắn số '169.254.169.254'. Nếu bạn nhấc máy lên và bấm số 0, lễ tân sẽ tự động đọc to toàn bộ mật mã két sắt tổng của công ty (IAM Role Tokens). Kẻ tấn công lợi dụng SSRF để bảo máy chủ của bạn nhấc chiếc điện thoại đó lên gọi cho lễ tân.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): Cloud Metadata Endpoint Protection (Bảo vệ Metadata Đám mây)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
http://169.254.169.254 (IMDSv1)http://metadata.google.internal (GCP)Bắt buộc IMDSv2 với Token Session
⚠️ Lỗ hổng IMDSv1 trên AWS: Máy chủ gửi request GET đơn giản và lấy được toàn bộ AccessKeyId, SecretAccessKey và SessionToken của Cloud!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// THẢM HỌA: Cho phép truy cập dải 169.254.169.254 lấy trộm Cloud IAM Credentials
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Chặn dải Link-Local 169.254.0.0/16 & kích hoạt AWS IMDSv2
// Trong mã nguồn:
if (address.getHostAddress().startsWith("169.254.")) {
throw new SecurityException("Chặn truy cập dịch vụ Metadata Đám mây!");
}
// Cấu hình AWS EC2: Bắt buộc HttpTokens=required (IMDSv2)
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Cơ chế nào của IMDSv2 trên AWS EC2 giúp vô hiệu hóa hầu hết các cuộc tấn công SSRF thông thường?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)
#06 DNS Rebinding Attack & Mitigation (Chống tấn công đổi DNS)
💡 BƯỚC 1: ẨN DỤ ĐỜI THƯỜNG (FEYNMAN ANALOGY)
Bạn hỏi nhân viên dẫn đường: 'Trang web hacker.com ở đâu?'. Nhân viên tra cứu lần 1 và trả lời: 'Ở địa chỉ công cộng 93.184.216.34' (vượt qua bộ kiểm tra an toàn). Nhưng ngay 1 phần nghìn giây sau khi máy chủ chuẩn bị gửi dữ liệu, kẻ tấn công đổi câu trả lời của máy chủ DNS thành 'Ở địa chỉ 127.0.0.1' (Time-To-Live = 0 giây). Máy chủ bị lừa gửi thẳng dữ liệu nhạy cảm vào cổng nội bộ.
📊 HÌNH HỌA MINH HỌA (VISUAL FLOW): DNS Rebinding Attack & Mitigation (Chống tấn công đổi DNS)
🧪 BƯỚC 2: PHÒNG THÍ NGHIỆM TƯƠNG TÁC (SANDBOX)
Mẫu thử nghiệm:
DNS Rebinding AttackGửi request trực tiếp tới Resolved IPPinning IP sau khi validate
⚠️ Lỗ hổng DNS Rebinding: Thời điểm kiểm tra (Validation time) và thời điểm gửi request (Execution time) phân giải ra 2 IP khác nhau!
⚖️ BƯỚC 3: SO SÁNH CODE LỖI VS CODE CHUẨN (JAVA / SPRING BOOT)
❌ Vulnerable Java Code (Không an toàn)
// NGUY HIỂM: Time-Of-Check to Time-Of-Use (TOCTOU)
// Resolve kiểm tra IP thì ra 1.2.3.4 (an toàn), nhưng khi tải lại resolve ra 127.0.0.1!
if (isValid(url)) {
client.send(HttpRequest.newBuilder().uri(url).build(), ...);
}
✅ Secure Java Code (Chuẩn bảo mật)
// CHUẨN AN TOÀN: Kết nối trực tiếp vào IP đã được kiểm duyệt an toàn
InetAddress safeIp = resolveAndValidate(uri.getHost());
// Kết nối trực tiếp bằng Socket tới safeIp, truyền Header Host: uri.getHost()
🎯 BƯỚC 4: THỬ TÀI THẤU HIỂU (ACTIVE CHECK)
Bản chất của kỹ thuật tấn công DNS Rebinding trong SSRF là gì?
📝 Tóm tắt nhanh bằng lời của bạn (Feynman Notebook):
(Tự động lưu vào trình duyệt)