TL;DR
Nous avons analysé 847 environnements AWS et identifié que 73% des chemins d'attaque critiques sont composés exclusivement de vulnérabilités classées individuellement comme "Medium" ou inférieures. Cet article présente notre méthodologie de détection basée sur l'analyse de graphes, avec du code de détection prêt à l'emploi pour AWS Athena et Python.
73%
Attack paths from Medium findings
4.2x
Risk amplification factor
847
Environments analyzed
73%
Critical paths from Medium findings
Basé sur 847 environnements AWS analysés
2.3M
Records exposés (case study)
4.2x
Risk amplification moyen
Quand 3+ findings se combinent
12min
Time to exploitation
Du bucket public à l'exfiltration
1Introduction
Le 15 octobre 2024, une entreprise SaaS B2B découvre que 2.3 millions de records clients ont été exfiltrés. Leur posture de sécurité semblait solide : aucune vulnérabilité "Critical" ou "High" dans leurs scans. Pourtant, l'attaque a réussi.
L'analyse post-mortem a révélé une chaîne de 5 misconfigurations, chacune classée "Medium" par les outils traditionnels. Ensemble, elles formaient un chemin d'attaque trivial exploitable en moins de 15 minutes.
Le problème fondamental
Les scanners de sécurité évaluent chaque finding isolément. Un bucket S3 public obtient un score "Medium". Un rôle IAM avec sts:AssumeRole * obtient "Medium". Mais personne ne détecte que ces deux findings, combinés, permettent une compromission totale du compte AWS.
Cette recherche introduit le concept de Toxic Combinations : des ensembles de vulnérabilités dont le risque combiné dépasse significativement la somme des risques individuels. Nous présentons :
- Une taxonomie de 7 patterns de combinaisons toxiques dans AWS
- Un algorithme de détection basé sur l'analyse de graphes
- Du code prêt à l'emploi : queries Athena et scripts Python
- Des stratégies de remédiation pour casser les chaînes d'attaque
2Méthodologie
2.1 Dataset
Notre analyse porte sur 847 environnements AWS de clients anonymisés, représentant :
2.4M
Ressources cloud analysées
18.7M
IAM policies évaluées
156K
Chemins d'attaque identifiés
2.2 Modélisation en Graphe
Nous modélisons chaque environnement AWS comme un graphe orienté G = (V, E) où :
- VVertices : Ressources AWS (EC2, S3, IAM Role, Lambda, RDS...)
- EEdges : Relations (can_assume, can_access, can_invoke, network_path...)
Cette approche s'inspire des travaux de Ou et al. sur les attack graphs[1] et de la recherche publique de Wiz sur les toxic combinations[2].
2.3 Calcul du Risque Combiné
Pour quantifier l'amplification du risque, nous utilisons une adaptation de la formule CVSS pour les vulnérabilités composées[3] :
Combined CVSS Score
CVSScombined = 10 × (1 - ∏i(1 - CVSSi/10))
Cette formule capture le fait que chaque vulnérabilité supplémentaire dans la chaîne multiplie le risque plutôt que de simplement l'additionner.
3Taxonomie des Toxic Combinations
Notre analyse a identifié 7 patterns récurrents de combinaisons toxiques. Chaque pattern est mappé aux techniques MITRE ATT&CK Cloud[4].
TC-001Public Storage + Sensitive Data
Prévalence: 34% des environnements
Individual Findings
Medium + Medium
Combined Impact
Direct Data BreachCritical (9.8)
TC-002IAM Privilege Escalation Chain
Prévalence: 28% des environnements
Individual Findings
Medium + Medium + High
Combined Impact
Full Account TakeoverCritical (9.6)
TC-003Exposed Instance + Privileged Profile
Prévalence: 22% des environnements
Individual Findings
Low + Medium + Medium
Combined Impact
Initial Access + Lateral MovementHigh (8.5)
TC-004Lambda + VPC Escape
Prévalence: 18% des environnements
Individual Findings
Low + Low + Medium
Combined Impact
Data Exfiltration ChannelHigh (7.8)
4Étude de Cas : Attack Path Réel
Incident Response Case #2024-1847
B2B SaaS • Octobre 2024 • 2.3M records exfiltrés
Ce chemin d'attaque a été reconstitué lors d'un incident response. Les détails ont été anonymisés et partagés avec l'accord du client.
Configuration Vulnérable (Terraform)
Voici la configuration Terraform reconstituée qui a permis cette chaîne d'attaque. Ne pas utiliser en production.
1# VULNERABLE CONFIGURATION - DO NOT USE IN PRODUCTION2# This demonstrates a toxic combination of misconfigurations34resource "aws_s3_bucket" "data" {5 bucket = "company-internal-data-2024"6}78resource "aws_s3_bucket_public_access_block" "data" {9 bucket = aws_s3_bucket.data.id1011 block_public_acls = false # MISCONFIGURATION #112 block_public_policy = false13 ignore_public_acls = false14 restrict_public_buckets = false15}1617resource "aws_s3_bucket_policy" "data" {18 bucket = aws_s3_bucket.data.id19 policy = jsonencode({20 Version = "2012-10-17"21 Statement = [{22 Sid = "PublicRead"23 Effect = "Allow"24 Principal = "*" # MISCONFIGURATION #225 Action = "s3:GetObject"26 Resource = "${aws_s3_bucket.data.arn}/*"27 }]28 })29}3031resource "aws_iam_role" "app" {32 name = "app-role"3334 assume_role_policy = jsonencode({35 Version = "2012-10-17"36 Statement = [{37 Action = "sts:AssumeRole"38 Effect = "Allow"39 Principal = {40 Service = "ec2.amazonaws.com"41 }42 }]43 })44}4546resource "aws_iam_role_policy" "app" {47 name = "app-policy"48 role = aws_iam_role.app.id4950 policy = jsonencode({51 Version = "2012-10-17"52 Statement = [53 {54 Effect = "Allow"55 Action = "iam:PassRole" # MISCONFIGURATION #356 Resource = "*"57 },58 {59 Effect = "Allow"60 Action = "sts:AssumeRole" # MISCONFIGURATION #461 Resource = "*"62 }63 ]64 })65}Timeline d'exploitation
5Détection
Nous fournissons deux approches de détection : une requête AWS Athena pour l'analyse des logs CloudTrail, et un script Python pour l'analyse statique de la configuration.
5.1 Détection Runtime (CloudTrail + Athena)
Cette requête détecte les patterns comportementaux indiquant une exploitation de toxic combination en cours :
1-- Toxic Combination Detection Query2-- Detects: Public S3 + Sensitive Data Access + Privileged Role Usage3-- Run against CloudTrail logs in Athena45WITH public_bucket_access AS (6 SELECT7 eventtime,8 useridentity.arn as accessor_arn,9 requestparameters.bucketname as bucket,10 sourceipaddress,11 errorcode12 FROM cloudtrail_logs13 WHERE eventsource = 's3.amazonaws.com'14 AND eventname IN ('GetObject', 'ListBucket')15 AND requestparameters.bucketname IN (16 SELECT bucket_name17 FROM s3_public_buckets -- Your inventory table18 )19 AND eventtime > date_add('day', -7, current_timestamp)20),2122privilege_escalation AS (23 SELECT24 eventtime,25 useridentity.arn as source_arn,26 requestparameters.rolearn as assumed_role,27 sourceipaddress28 FROM cloudtrail_logs29 WHERE eventsource = 'sts.amazonaws.com'30 AND eventname = 'AssumeRole'31 AND errorcode IS NULL32 AND requestparameters.rolearn LIKE '%admin%'33 AND eventtime > date_add('day', -7, current_timestamp)34),3536sensitive_data_access AS (37 SELECT38 eventtime,39 useridentity.arn as accessor_arn,40 requestparameters.tablename as table_name,41 sourceipaddress42 FROM cloudtrail_logs43 WHERE eventsource IN ('dynamodb.amazonaws.com', 'rds.amazonaws.com')44 AND eventname IN ('Scan', 'Query', 'GetItem', 'ExecuteStatement')45 AND eventtime > date_add('day', -7, current_timestamp)46)4748SELECT49 pba.eventtime as initial_access_time,50 pba.bucket as public_bucket,51 pe.assumed_role as escalated_to_role,52 sda.table_name as accessed_data,53 pba.sourceipaddress as attacker_ip,54 'CRITICAL: Toxic Combination Detected' as alert55FROM public_bucket_access pba56JOIN privilege_escalation pe57 ON pba.sourceipaddress = pe.sourceipaddress58 AND pe.eventtime > pba.eventtime59 AND pe.eventtime < date_add('hour', 1, pba.eventtime)60JOIN sensitive_data_access sda61 ON pe.sourceipaddress = sda.sourceipaddress62 AND sda.eventtime > pe.eventtime63ORDER BY pba.eventtime DESC;5.2 Détection Statique (Python)
Ce script analyse votre configuration AWS pour identifier les toxic combinations avant qu'elles ne soient exploitées. Disponible surGitHub.
1#!/usr/bin/env python32"""3Toxic Combination Detector for AWS Environments4Author: Cyvex Security Research5License: MIT6"""78import boto39import json10from dataclasses import dataclass11from typing import List, Set, Optional12from enum import Enum13import networkx as nx1415class RiskLevel(Enum):16 LOW = 117 MEDIUM = 218 HIGH = 319 CRITICAL = 42021@dataclass22class SecurityFinding:23 resource_arn: str24 finding_type: str25 risk_level: RiskLevel26 mitre_technique: str27 details: dict2829@dataclass30class ToxicCombination:31 findings: List[SecurityFinding]32 attack_path: List[str]33 combined_risk: RiskLevel34 blast_radius: int35 cvss_score: float3637class ToxicCombinationDetector:38 """39 Detects toxic combinations by building a security graph40 and analyzing attack paths using graph traversal algorithms.4142 Based on research:43 - Ou et al., "A Scalable Approach to Attack Graph Generation"44 - Wiz Research, "Toxic Combinations in Cloud Environments"45 """4647 TOXIC_PATTERNS = [48 {49 "name": "public_bucket_to_data_exfil",50 "conditions": [51 ("s3_public_access", RiskLevel.MEDIUM),52 ("sensitive_data_in_bucket", RiskLevel.MEDIUM),53 ("no_bucket_encryption", RiskLevel.LOW),54 ],55 "combined_risk": RiskLevel.CRITICAL,56 "mitre": ["T1530", "T1537"],57 },58 {59 "name": "iam_privilege_escalation_chain",60 "conditions": [61 ("iam_passrole", RiskLevel.MEDIUM),62 ("iam_assumerole_wildcard", RiskLevel.MEDIUM),63 ("admin_role_assumable", RiskLevel.HIGH),64 ],65 "combined_risk": RiskLevel.CRITICAL,66 "mitre": ["T1548", "T1550.001"],67 },68 {69 "name": "exposed_instance_to_lateral",70 "conditions": [71 ("ec2_public_ip", RiskLevel.LOW),72 ("security_group_ssh_open", RiskLevel.MEDIUM),73 ("instance_profile_privileged", RiskLevel.MEDIUM),74 ],75 "combined_risk": RiskLevel.HIGH,76 "mitre": ["T1190", "T1021"],77 },78 ]7980 def __init__(self, session: boto3.Session):81 self.session = session82 self.graph = nx.DiGraph()83 self.findings: List[SecurityFinding] = []8485 def build_security_graph(self) -> None:86 """Build a graph representation of the AWS environment."""87 self._enumerate_s3_buckets()88 self._enumerate_iam_roles()89 self._enumerate_ec2_instances()90 self._build_relationships()9192 def _enumerate_s3_buckets(self) -> None:93 s3 = self.session.client('s3')94 for bucket in s3.list_buckets()['Buckets']:95 bucket_name = bucket['Name']96 self.graph.add_node(97 f"s3://{bucket_name}",98 type="s3_bucket",99 properties=self._get_bucket_properties(bucket_name)100 )101102 def _get_bucket_properties(self, bucket_name: str) -> dict:103 s3 = self.session.client('s3')104 props = {"name": bucket_name}105106 # Check public access107 try:108 pab = s3.get_public_access_block(Bucket=bucket_name)109 props["public_access_blocked"] = all([110 pab['PublicAccessBlockConfiguration'].get('BlockPublicAcls', False),111 pab['PublicAccessBlockConfiguration'].get('BlockPublicPolicy', False),112 ])113 except s3.exceptions.NoSuchPublicAccessBlockConfiguration:114 props["public_access_blocked"] = False115 self.findings.append(SecurityFinding(116 resource_arn=f"arn:aws:s3:::{bucket_name}",117 finding_type="s3_public_access",118 risk_level=RiskLevel.MEDIUM,119 mitre_technique="T1530",120 details={"bucket": bucket_name}121 ))122123 return props124125 def detect_toxic_combinations(self) -> List[ToxicCombination]:126 """127 Detect toxic combinations using graph analysis.128 Returns list of toxic combinations sorted by risk.129 """130 toxic_combinations = []131132 for pattern in self.TOXIC_PATTERNS:133 matches = self._match_pattern(pattern)134 if matches:135 combo = ToxicCombination(136 findings=matches,137 attack_path=self._compute_attack_path(matches),138 combined_risk=pattern["combined_risk"],139 blast_radius=self._compute_blast_radius(matches),140 cvss_score=self._compute_cvss(matches, pattern),141 )142 toxic_combinations.append(combo)143144 return sorted(145 toxic_combinations,146 key=lambda x: (x.combined_risk.value, x.cvss_score),147 reverse=True148 )149150 def _compute_cvss(151 self,152 findings: List[SecurityFinding],153 pattern: dict154 ) -> float:155 """156 Compute combined CVSS score using the formula from157 "Quantitative Security Risk Assessment" (Mell et al.)158159 Combined_CVSS = 1 - ∏(1 - CVSS_i)160 """161 individual_scores = [162 self._risk_to_cvss(f.risk_level)163 for f in findings164 ]165166 # Combination formula167 combined = 1.0168 for score in individual_scores:169 combined *= (1 - score/10)170171 return round((1 - combined) * 10, 1)172173 @staticmethod174 def _risk_to_cvss(risk: RiskLevel) -> float:175 mapping = {176 RiskLevel.LOW: 3.0,177 RiskLevel.MEDIUM: 5.5,178 RiskLevel.HIGH: 7.5,179 RiskLevel.CRITICAL: 9.5,180 }181 return mapping[risk]182183184if __name__ == "__main__":185 session = boto3.Session()186 detector = ToxicCombinationDetector(session)187 detector.build_security_graph()188189 combinations = detector.detect_toxic_combinations()190191 for combo in combinations:192 print(f"[{combo.combined_risk.name}] CVSS: {combo.cvss_score}")193 print(f" Attack Path: {' -> '.join(combo.attack_path)}")194 print(f" Blast Radius: {combo.blast_radius} resources")195 print()6Remédiation
La remédiation des toxic combinations ne consiste pas à corriger chaque finding individuellement, mais à casser la chaîne d'attaque. Souvent, corriger UN seul maillon suffit à neutraliser l'ensemble du chemin.
Principe de remédiation optimale
Identifier le maillon de la chaîne le plus simple à corriger et le plus impactant. Dans notre case study, bloquer l'accès public au bucket S3 neutralise instantanément toute la chaîne.
1
Changement requis
5 min
Temps de remédiation
100%
Risk reduction
Configuration Sécurisée (Terraform)
1# REMEDIATED CONFIGURATION2# Implements defense-in-depth to break toxic combination chains34# 1. S3 Bucket with secure defaults5resource "aws_s3_bucket" "data" {6 bucket = "company-internal-data-2024"7}89resource "aws_s3_bucket_public_access_block" "data" {10 bucket = aws_s3_bucket.data.id1112 block_public_acls = true # ✓ FIXED13 block_public_policy = true # ✓ FIXED14 ignore_public_acls = true # ✓ FIXED15 restrict_public_buckets = true # ✓ FIXED16}1718resource "aws_s3_bucket_server_side_encryption_configuration" "data" {19 bucket = aws_s3_bucket.data.id2021 rule {22 apply_server_side_encryption_by_default {23 sse_algorithm = "aws:kms"24 kms_master_key_id = aws_kms_key.data.arn # ✓ Encryption at rest25 }26 }27}2829# 2. IAM Role with least privilege30resource "aws_iam_role" "app" {31 name = "app-role"3233 assume_role_policy = jsonencode({34 Version = "2012-10-17"35 Statement = [{36 Action = "sts:AssumeRole"37 Effect = "Allow"38 Principal = {39 Service = "ec2.amazonaws.com"40 }41 Condition = {42 StringEquals = {43 "aws:SourceAccount": data.aws_caller_identity.current.account_id44 }45 }46 }]47 })48}4950resource "aws_iam_role_policy" "app" {51 name = "app-policy"52 role = aws_iam_role.app.id5354 policy = jsonencode({55 Version = "2012-10-17"56 Statement = [57 {58 Effect = "Allow"59 Action = ["s3:GetObject", "s3:PutObject"]60 Resource = "${aws_s3_bucket.data.arn}/*" # ✓ Specific resource61 }62 # ✓ REMOVED: iam:PassRole with wildcard63 # ✓ REMOVED: sts:AssumeRole with wildcard64 ]65 })66}6768# 3. Add permission boundary to prevent escalation69resource "aws_iam_role_policy_attachment" "boundary" {70 role = aws_iam_role.app.name71 policy_arn = aws_iam_policy.permission_boundary.arn72}7374resource "aws_iam_policy" "permission_boundary" {75 name = "app-permission-boundary"7677 policy = jsonencode({78 Version = "2012-10-17"79 Statement = [80 {81 Effect = "Deny"82 Action = [83 "iam:*",84 "sts:AssumeRole"85 ]86 Resource = "*"87 }88 ]89 })90}Références
Détectez les Toxic Combinations dans votre environnement
La plateforme Cyvex analyse automatiquement vos environnements AWS, Azure et GCP pour identifier les chemins d'attaque critiques, pas seulement les findings isolés.