guest@websec: ~/websec/13-ssrf/th — zsh — 100×40 เขียนโดย SaKKo ENTH โค้ด:

guest@websec ~ % man 13-ssrf

13 · Server-Side Request Forgery (SSRF)

ฟีเจอร์ไหนก็ตามที่เซิร์ฟเวอร์เราไปดึง URL ของผู้ใช้ (รูปจากลิงก์, webhook, ตัวอย่างลิงก์, render PDF) ถูกใช้ย้อนกลับมาได้ ผู้โจมตีเข้าเครือข่ายภายในเราไม่ได้ แต่เซิร์ฟเวอร์เราเข้าได้ และ SSRF ทำให้มันไปดึงให้เขา

ผู้โจมตีเล็งอะไร

  • Cloud metadata (169.254.169.254) ให้ข้อมูลรับรองคลาวด์ของเซิร์ฟเวอร์ นี่คือเบื้องหลังการเจาะ Capital One ปี 2019
  • บริการภายใน ที่เชื่อทุกอย่างในเครือข่ายส่วนตัว
  • บริการ localhost ในเครื่องเดียวกัน

ทำไม blocklist ง่ายๆ จึงล้มเหลว

บล็อกคำว่า "localhost" ในสตริงใช้ไม่ได้ IP เขียนได้หลายแบบ hostname ชี้เข้าภายในได้ และ redirect ก็พาเข้าไปได้ ลองดู:

sim: ตัวตรวจ URL ก่อนดึงใช้ URL parser จริงของเบราว์เซอร์คุณ
try:
การตรวจผลตัดสินสิ่งที่เกิดขึ้นจริง

วิธีป้องกัน

  1. เลี่ยงได้ก็เลี่ยง: รับไฟล์อัปโหลดแทน หรือดึงเฉพาะจาก host ที่รู้จัก
  2. ถ้าเลี่ยงไม่ได้: resolve hostname เอง บล็อก IP ภายใน แล้วเชื่อมต่อไปที่ IP นั้นตรงๆ
  3. อย่าตาม redirect แบบไม่ตรวจ
  4. ตั้ง timeout สั้น และอย่าแสดง response กลับไปให้ผู้ใช้
  5. บน AWS บังคับ IMDSv2 เพื่อให้ GET ธรรมดาอ่าน metadata ไม่ได้
Rails ไม่ได้ช่วยอะไร ระวัง URI.open(params[…]), HTTP client ที่ใช้ URL ของผู้ใช้ และ attach(io: URI.open(url)) gem ssrf_filter ทำส่วนที่ยากให้
# Gemfile: gem "ssrf_filter"
def import_avatar(url)
  uri = URI.parse(url)
  raise ArgumentError, "http(s) only" unless %w[http https].include?(uri.scheme)
  response = SsrfFilter.get(uri, max_redirects: 0)   # resolves, blocks private IPs, pins IP
  raise "not an image" unless response["content-type"].to_s.start_with?("image/")
  avatar.attach(io: StringIO.new(response.body), filename: "avatar")
rescue SsrfFilter::Error, URI::InvalidURIError
  errors.add(:avatar_url, "can't be fetched")         # generic message, no body echo
end

avatar.attach(io: URI.open(params[:avatar_url]), filename: "a")   # SSRF
// Node: request-filtering-agent blocks private IPs after DNS resolution.
// It needs an agent-aware client (node-fetch, axios, got): Node's built-in fetch ignores `agent`.
import fetch from "node-fetch";
import { useAgent } from "request-filtering-agent";
const res = await fetch(url, { agent: useAgent(url), redirect: "manual" });
await fetch(req.body.url);    // SSRF