flawopen.com/ssrf/C-cpp

● CWE-918 · Hoch
Sicherheitsforschung · FlawOpen

Server-Side Request Forgery in C/C++

How libcurl requests allow internal network pivots in C/C++, and how to enforce IP address validation with CURLOPT_RESOLVE.

💡 Einfache Erklärung (ELI5)

Stellen Sie sich vor, Sie schicken einen Boten, um ein Paket in einem öffentlichen Geschäft abzuholen, geben ihm aber die Adresse des Tresors im Büro des Geschäftsführers. Da der Bote einen internen Ausweis besitzt, öffnet er den Tresor und händigt Firmengeheimnisse aus.

Kernkonzepte & Begriffe

Web Application Security
Kernkomponente der Architektur, die von CWE-918 betroffen ist.
CWE-918
Standardmäßige Common Weakness Enumeration (CWE)-Klassifizierung für ssrf-c-cpp.
Defense-in-Depth
Mehrschichtige technische Verifikation und Laufzeit-Grenzenisolation.

Schritt-für-Schritt Angriffsablauf

Step 1

Eingabe Nicht Vertrauenswürdiger URLs

Die Anwendung akzeptiert externe URL-Parameter für Webhooks, Avatare oder PDF-Voransichten.

Step 2

Adressierung Interner Netzwerke

Der Angreifer übergibt eine Ziel-URL auf Cloud-Metadaten oder interne Loopback-Adressen (z. B. http://169.254.169.254/latest/meta-data/).

Step 3

Ungefilterter Socket-Ausgang

Der HTTP-Client des Servers sendet die Anfrage aus der geschützten VPC, ohne die Ziel-IP gegen private Subnetzbereiche abzugleichen.

Step 4

Exfiltration von Cloud-Zugangsdaten

Der interne Metadatendienst vertraut der Server-Anfrage und liefert temporäre IAM-Schlüssel und Tokens zurück.

Quellcode: Verwundbar vs. Sicher

✕ VERWUNDBARE IMPLEMENTIERUNG
# VULNERABLE: Naive requests.get to user-provided URL
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/fetch-avatar')
def fetch_avatar():
    url = request.args.get('url')
    # Attacker passes: http://169.254.169.254/latest/meta-data/iam/security-credentials/
    # Server queries AWS metadata and returns cloud credentials!
    resp = requests.get(url, timeout=5)
    return resp.content, 200, {'Content-Type': resp.headers.get('Content-Type', 'image/png')}
✓ GEHÄRTETER SICHERHEITS-PATCH
# HARDENED: Resolve DNS, parse IP address, deny private/link-local CIDRs
import socket
import ipaddress
import urllib.parse
import requests
from flask import Flask, request, abort

app = Flask(__name__)

BLOCKED_NETWORKS = [
    ipaddress.ip_network('127.0.0.0/8'),      # Loopback
    ipaddress.ip_network('10.0.0.0/8'),       # RFC 1918 Private
    ipaddress.ip_network('172.16.0.0/12'),    # RFC 1918 Private
    ipaddress.ip_network('192.168.0.0/16'),   # RFC 1918 Private
    ipaddress.ip_network('169.254.0.0/16'),   # Link-local / Cloud IMDS
    ipaddress.ip_network('::1/128'),          # IPv6 Loopback
    ipaddress.ip_network('fc00::/7'),         # IPv6 Private ULA
]

def is_safe_ip(ip_str):
    ip = ipaddress.ip_address(ip_str)
    return not any(ip in net for net in BLOCKED_NETWORKS)

@app.route('/fetch-avatar')
def fetch_avatar():
    url = request.args.get('url', '')
    parsed = urllib.parse.urlparse(url)
    
    if parsed.scheme not in ('http', 'https') or not parsed.hostname:
        abort(400, "Invalid URL scheme")
        
    # 1. Resolve all DNS IPs for hostname
    try:
        addr_info = socket.getaddrinfo(parsed.hostname, parsed.port or (443 if parsed.scheme == 'https' else 80))
        resolved_ips = [ai[4][0] for ai in addr_info]
    except socket.gaierror:
        abort(400, "Unable to resolve host")
        
    # 2. Verify all resolved IPs are public
    for ip in resolved_ips:
        if not is_safe_ip(ip):
            abort(403, "Access to internal IP address is prohibited")
            
    # 3. Fetch with redirects disabled to prevent DNS rebinding or redirect bypass
    resp = requests.get(url, timeout=3, allow_redirects=False)
    return resp.content, 200, {'Content-Type': resp.headers.get('Content-Type', 'image/png')}

Checkliste für Engineering & Systemsicherheit

References