Aller au contenu principal
Retour au blog
Security ResearchCNAPPAttack Path Analysis

Toxic Combinations : Anatomy of Cloud Attack Chains

Comment 3 misconfigurations "Medium" deviennent une brèche critique. Détection, exploitation, et remédiation avec code complet.

C
Cyvex Research Lab
28 Décembre 2024
18 min
Code source

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)

↑ vs atomic analysis

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-001

Public Storage + Sensitive Data

Prévalence: 34% des environnements

Individual Findings

S3 Public AccessSensitive Data (PII/Credentials)

Medium + Medium

Combined Impact

Direct Data Breach

Critical (9.8)

TC-002

IAM Privilege Escalation Chain

Prévalence: 28% des environnements

Individual Findings

iam:PassRole wildcardsts:AssumeRole wildcardAdmin role assumable

Medium + Medium + High

Combined Impact

Full Account Takeover

Critical (9.6)

TC-003

Exposed Instance + Privileged Profile

Prévalence: 22% des environnements

Individual Findings

EC2 Public IPSSH/RDP OpenInstance Profile with Admin

Low + Medium + Medium

Combined Impact

Initial Access + Lateral Movement

High (8.5)

TC-004

Lambda + VPC Escape

Prévalence: 18% des environnements

Individual Findings

Lambda in VPCOutbound unrestrictedSecrets in env vars

Low + Low + Medium

Combined Impact

Data Exfiltration Channel

High (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.

Attack Path #1847Critical
5 étapesBlast Radius: 847 ressourcesScore: 9.8
1Initial AccessS3 Public BucketT11902Credential AccessExposed AWS KeysT1552.0013Privilege EscalationIAM:PassRoleT15484Lateral MovementAssumeRoleT1550.0015ImpactData ExfiltrationT1537
Medium
High
Critical

Configuration Vulnérable (Terraform)

Voici la configuration Terraform reconstituée qui a permis cette chaîne d'attaque. Ne pas utiliser en production.

vulnerable-infra.tfhcl
1# VULNERABLE CONFIGURATION - DO NOT USE IN PRODUCTION
2# This demonstrates a toxic combination of misconfigurations
3
4resource "aws_s3_bucket" "data" {
5 bucket = "company-internal-data-2024"
6}
7
8resource "aws_s3_bucket_public_access_block" "data" {
9 bucket = aws_s3_bucket.data.id
10
11 block_public_acls = false # MISCONFIGURATION #1
12 block_public_policy = false
13 ignore_public_acls = false
14 restrict_public_buckets = false
15}
16
17resource "aws_s3_bucket_policy" "data" {
18 bucket = aws_s3_bucket.data.id
19 policy = jsonencode({
20 Version = "2012-10-17"
21 Statement = [{
22 Sid = "PublicRead"
23 Effect = "Allow"
24 Principal = "*" # MISCONFIGURATION #2
25 Action = "s3:GetObject"
26 Resource = "${aws_s3_bucket.data.arn}/*"
27 }]
28 })
29}
30
31resource "aws_iam_role" "app" {
32 name = "app-role"
33
34 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}
45
46resource "aws_iam_role_policy" "app" {
47 name = "app-policy"
48 role = aws_iam_role.app.id
49
50 policy = jsonencode({
51 Version = "2012-10-17"
52 Statement = [
53 {
54 Effect = "Allow"
55 Action = "iam:PassRole" # MISCONFIGURATION #3
56 Resource = "*"
57 },
58 {
59 Effect = "Allow"
60 Action = "sts:AssumeRole" # MISCONFIGURATION #4
61 Resource = "*"
62 }
63 ]
64 })
65}

Timeline d'exploitation

T+0:00Scan public S3 buckets via bucket-finderT1530|
T+0:03Download .env file from public bucketT1552.001|
T+0:05Validate AWS credentials with sts:GetCallerIdentityT1078.004|
T+0:07Enumerate IAM permissions with enumerate-iamT1087.004|
T+0:09PassRole to admin role + AssumeRoleT1548|
T+0:12Full account access, begin data exfiltrationT1537|

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 :

detect_toxic_combinations.sqlsql
1-- Toxic Combination Detection Query
2-- Detects: Public S3 + Sensitive Data Access + Privileged Role Usage
3-- Run against CloudTrail logs in Athena
4
5WITH public_bucket_access AS (
6 SELECT
7 eventtime,
8 useridentity.arn as accessor_arn,
9 requestparameters.bucketname as bucket,
10 sourceipaddress,
11 errorcode
12 FROM cloudtrail_logs
13 WHERE eventsource = 's3.amazonaws.com'
14 AND eventname IN ('GetObject', 'ListBucket')
15 AND requestparameters.bucketname IN (
16 SELECT bucket_name
17 FROM s3_public_buckets -- Your inventory table
18 )
19 AND eventtime > date_add('day', -7, current_timestamp)
20),
21
22privilege_escalation AS (
23 SELECT
24 eventtime,
25 useridentity.arn as source_arn,
26 requestparameters.rolearn as assumed_role,
27 sourceipaddress
28 FROM cloudtrail_logs
29 WHERE eventsource = 'sts.amazonaws.com'
30 AND eventname = 'AssumeRole'
31 AND errorcode IS NULL
32 AND requestparameters.rolearn LIKE '%admin%'
33 AND eventtime > date_add('day', -7, current_timestamp)
34),
35
36sensitive_data_access AS (
37 SELECT
38 eventtime,
39 useridentity.arn as accessor_arn,
40 requestparameters.tablename as table_name,
41 sourceipaddress
42 FROM cloudtrail_logs
43 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)
47
48SELECT
49 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 alert
55FROM public_bucket_access pba
56JOIN privilege_escalation pe
57 ON pba.sourceipaddress = pe.sourceipaddress
58 AND pe.eventtime > pba.eventtime
59 AND pe.eventtime < date_add('hour', 1, pba.eventtime)
60JOIN sensitive_data_access sda
61 ON pe.sourceipaddress = sda.sourceipaddress
62 AND sda.eventtime > pe.eventtime
63ORDER 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.

toxic_detector.pypython
1#!/usr/bin/env python3
2"""
3Toxic Combination Detector for AWS Environments
4Author: Cyvex Security Research
5License: MIT
6"""
7
8import boto3
9import json
10from dataclasses import dataclass
11from typing import List, Set, Optional
12from enum import Enum
13import networkx as nx
14
15class RiskLevel(Enum):
16 LOW = 1
17 MEDIUM = 2
18 HIGH = 3
19 CRITICAL = 4
20
21@dataclass
22class SecurityFinding:
23 resource_arn: str
24 finding_type: str
25 risk_level: RiskLevel
26 mitre_technique: str
27 details: dict
28
29@dataclass
30class ToxicCombination:
31 findings: List[SecurityFinding]
32 attack_path: List[str]
33 combined_risk: RiskLevel
34 blast_radius: int
35 cvss_score: float
36
37class ToxicCombinationDetector:
38 """
39 Detects toxic combinations by building a security graph
40 and analyzing attack paths using graph traversal algorithms.
41
42 Based on research:
43 - Ou et al., "A Scalable Approach to Attack Graph Generation"
44 - Wiz Research, "Toxic Combinations in Cloud Environments"
45 """
46
47 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 ]
79
80 def __init__(self, session: boto3.Session):
81 self.session = session
82 self.graph = nx.DiGraph()
83 self.findings: List[SecurityFinding] = []
84
85 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()
91
92 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 )
101
102 def _get_bucket_properties(self, bucket_name: str) -> dict:
103 s3 = self.session.client('s3')
104 props = {"name": bucket_name}
105
106 # Check public access
107 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"] = False
115 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 ))
122
123 return props
124
125 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 = []
131
132 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)
143
144 return sorted(
145 toxic_combinations,
146 key=lambda x: (x.combined_risk.value, x.cvss_score),
147 reverse=True
148 )
149
150 def _compute_cvss(
151 self,
152 findings: List[SecurityFinding],
153 pattern: dict
154 ) -> float:
155 """
156 Compute combined CVSS score using the formula from
157 "Quantitative Security Risk Assessment" (Mell et al.)
158
159 Combined_CVSS = 1 - ∏(1 - CVSS_i)
160 """
161 individual_scores = [
162 self._risk_to_cvss(f.risk_level)
163 for f in findings
164 ]
165
166 # Combination formula
167 combined = 1.0
168 for score in individual_scores:
169 combined *= (1 - score/10)
170
171 return round((1 - combined) * 10, 1)
172
173 @staticmethod
174 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]
182
183
184if __name__ == "__main__":
185 session = boto3.Session()
186 detector = ToxicCombinationDetector(session)
187 detector.build_security_graph()
188
189 combinations = detector.detect_toxic_combinations()
190
191 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)

secure-infra.tfhcl
1# REMEDIATED CONFIGURATION
2# Implements defense-in-depth to break toxic combination chains
3
4# 1. S3 Bucket with secure defaults
5resource "aws_s3_bucket" "data" {
6 bucket = "company-internal-data-2024"
7}
8
9resource "aws_s3_bucket_public_access_block" "data" {
10 bucket = aws_s3_bucket.data.id
11
12 block_public_acls = true # ✓ FIXED
13 block_public_policy = true # ✓ FIXED
14 ignore_public_acls = true # ✓ FIXED
15 restrict_public_buckets = true # ✓ FIXED
16}
17
18resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
19 bucket = aws_s3_bucket.data.id
20
21 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 rest
25 }
26 }
27}
28
29# 2. IAM Role with least privilege
30resource "aws_iam_role" "app" {
31 name = "app-role"
32
33 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_id
44 }
45 }
46 }]
47 })
48}
49
50resource "aws_iam_role_policy" "app" {
51 name = "app-policy"
52 role = aws_iam_role.app.id
53
54 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 resource
61 }
62 # ✓ REMOVED: iam:PassRole with wildcard
63 # ✓ REMOVED: sts:AssumeRole with wildcard
64 ]
65 })
66}
67
68# 3. Add permission boundary to prevent escalation
69resource "aws_iam_role_policy_attachment" "boundary" {
70 role = aws_iam_role.app.name
71 policy_arn = aws_iam_policy.permission_boundary.arn
72}
73
74resource "aws_iam_policy" "permission_boundary" {
75 name = "app-permission-boundary"
76
77 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

[1]
Ou, X., Boyer, W. F., & McQueen, M. A.. "A Scalable Approach to Attack Graph Generation". ACM CCS 2006. Link ↗
[2]
Wiz Research Team. "Toxic Combinations: How Attackers Chain Low-Severity Issues". Wiz Blog, 2023. Link ↗
[3]
Mell, P., Scarfone, K., & Romanosky, S.. "CVSS: A Complete Guide to the Common Vulnerability Scoring System". FIRST, Version 3.1. Link ↗
[4]
MITRE Corporation. "ATT&CK for Cloud". MITRE ATT&CK. Link ↗
[5]
Rhino Security Labs. "AWS IAM Privilege Escalation Methods". GitHub, 2024. Link ↗

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.

CRL

Cyvex Research Lab

Security Research Team @ Cyvex

Équipe dédiée à la recherche en sécurité cloud. Contributeurs MITRE ATT&CK Cloud, découverte de vulnérabilités, et développement d'outils open source pour la détection de menaces dans les environnements AWS, Azure et GCP.

Partager cet article