exploitgraph 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/__init__.py +0 -0
- core/attack_graph.py +83 -0
- core/aws_client.py +284 -0
- core/config.py +83 -0
- core/console.py +469 -0
- core/context_engine.py +172 -0
- core/correlator.py +476 -0
- core/http_client.py +243 -0
- core/logger.py +97 -0
- core/module_loader.py +69 -0
- core/risk_engine.py +47 -0
- core/session_manager.py +254 -0
- exploitgraph-1.0.0.dist-info/METADATA +429 -0
- exploitgraph-1.0.0.dist-info/RECORD +42 -0
- exploitgraph-1.0.0.dist-info/WHEEL +5 -0
- exploitgraph-1.0.0.dist-info/entry_points.txt +2 -0
- exploitgraph-1.0.0.dist-info/licenses/LICENSE +21 -0
- exploitgraph-1.0.0.dist-info/top_level.txt +2 -0
- modules/__init__.py +0 -0
- modules/base.py +82 -0
- modules/cloud/__init__.py +0 -0
- modules/cloud/aws_credential_validator.py +340 -0
- modules/cloud/azure_enum.py +289 -0
- modules/cloud/cloudtrail_analyzer.py +494 -0
- modules/cloud/gcp_enum.py +272 -0
- modules/cloud/iam_enum.py +321 -0
- modules/cloud/iam_privilege_escalation.py +515 -0
- modules/cloud/metadata_check.py +315 -0
- modules/cloud/s3_enum.py +469 -0
- modules/discovery/__init__.py +0 -0
- modules/discovery/http_enum.py +235 -0
- modules/discovery/subdomain_enum.py +260 -0
- modules/exploitation/__init__.py +0 -0
- modules/exploitation/api_exploit.py +403 -0
- modules/exploitation/jwt_attack.py +346 -0
- modules/exploitation/ssrf_scanner.py +258 -0
- modules/reporting/__init__.py +0 -0
- modules/reporting/html_report.py +446 -0
- modules/reporting/json_export.py +107 -0
- modules/secrets/__init__.py +0 -0
- modules/secrets/file_secrets.py +358 -0
- modules/secrets/git_secrets.py +267 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ExploitGraph Module: IAM Privilege Escalation Scanner
|
|
3
|
+
Category: cloud
|
|
4
|
+
|
|
5
|
+
Enumerates IAM permissions and detects privilege escalation paths.
|
|
6
|
+
Based on Rhino Security Labs research on AWS privilege escalation methods.
|
|
7
|
+
|
|
8
|
+
Detects:
|
|
9
|
+
- Direct policy modifications (PutUserPolicy, AttachUserPolicy)
|
|
10
|
+
- Role assumption chains (PassRole → AssumeRole)
|
|
11
|
+
- Service-linked escalation (Lambda, EC2, CloudFormation)
|
|
12
|
+
- Wildcard permissions
|
|
13
|
+
- Cross-account trust misconfigurations
|
|
14
|
+
|
|
15
|
+
All operations are READ-ONLY enumeration.
|
|
16
|
+
|
|
17
|
+
References:
|
|
18
|
+
https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/
|
|
19
|
+
|
|
20
|
+
MITRE: T1078.004, T1548, T1134.001
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import json
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from modules.base import BaseModule, ModuleResult
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from core.session_manager import Session
|
|
30
|
+
|
|
31
|
+
# Known privilege escalation paths (method → required permissions)
|
|
32
|
+
# Source: Rhino Security Labs IAM Privilege Escalation research
|
|
33
|
+
PRIVESC_PATHS = [
|
|
34
|
+
{
|
|
35
|
+
"name": "CreateNewPolicyVersion",
|
|
36
|
+
"permissions": ["iam:CreatePolicyVersion"],
|
|
37
|
+
"severity": "CRITICAL",
|
|
38
|
+
"description": "Create new policy version with admin permissions",
|
|
39
|
+
"mitre": "T1548",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "SetExistingDefaultPolicyVersion",
|
|
43
|
+
"permissions": ["iam:SetDefaultPolicyVersion"],
|
|
44
|
+
"severity": "CRITICAL",
|
|
45
|
+
"description": "Set existing policy version with admin permissions as default",
|
|
46
|
+
"mitre": "T1548",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "CreateAccessKey",
|
|
50
|
+
"permissions": ["iam:CreateAccessKey"],
|
|
51
|
+
"severity": "CRITICAL",
|
|
52
|
+
"description": "Create access key for another IAM user (including admins)",
|
|
53
|
+
"mitre": "T1078.004",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "CreateLoginProfile",
|
|
57
|
+
"permissions": ["iam:CreateLoginProfile"],
|
|
58
|
+
"severity": "CRITICAL",
|
|
59
|
+
"description": "Create console login for a user without one",
|
|
60
|
+
"mitre": "T1078.004",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "UpdateLoginProfile",
|
|
64
|
+
"permissions": ["iam:UpdateLoginProfile"],
|
|
65
|
+
"severity": "CRITICAL",
|
|
66
|
+
"description": "Update console login password for any IAM user",
|
|
67
|
+
"mitre": "T1078.004",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "AttachUserPolicy",
|
|
71
|
+
"permissions": ["iam:AttachUserPolicy"],
|
|
72
|
+
"severity": "CRITICAL",
|
|
73
|
+
"description": "Attach AdministratorAccess policy to self or another user",
|
|
74
|
+
"mitre": "T1548",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "AttachGroupPolicy",
|
|
78
|
+
"permissions": ["iam:AttachGroupPolicy"],
|
|
79
|
+
"severity": "CRITICAL",
|
|
80
|
+
"description": "Attach admin policy to a group the attacker is in",
|
|
81
|
+
"mitre": "T1548",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "AttachRolePolicy",
|
|
85
|
+
"permissions": ["iam:AttachRolePolicy"],
|
|
86
|
+
"severity": "CRITICAL",
|
|
87
|
+
"description": "Attach admin policy to an assumable role",
|
|
88
|
+
"mitre": "T1548",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "PutUserPolicy",
|
|
92
|
+
"permissions": ["iam:PutUserPolicy"],
|
|
93
|
+
"severity": "CRITICAL",
|
|
94
|
+
"description": "Add inline policy with admin permissions to self",
|
|
95
|
+
"mitre": "T1548",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "PutGroupPolicy",
|
|
99
|
+
"permissions": ["iam:PutGroupPolicy"],
|
|
100
|
+
"severity": "CRITICAL",
|
|
101
|
+
"description": "Add inline admin policy to a group the attacker is in",
|
|
102
|
+
"mitre": "T1548",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "PutRolePolicy",
|
|
106
|
+
"permissions": ["iam:PutRolePolicy"],
|
|
107
|
+
"severity": "CRITICAL",
|
|
108
|
+
"description": "Add inline admin policy to an assumable role",
|
|
109
|
+
"mitre": "T1548",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "AddUserToGroup",
|
|
113
|
+
"permissions": ["iam:AddUserToGroup"],
|
|
114
|
+
"severity": "HIGH",
|
|
115
|
+
"description": "Add self to a group with admin or elevated permissions",
|
|
116
|
+
"mitre": "T1548",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "PassRole+LambdaCreate",
|
|
120
|
+
"permissions": ["iam:PassRole", "lambda:CreateFunction", "lambda:InvokeFunction"],
|
|
121
|
+
"severity": "CRITICAL",
|
|
122
|
+
"description": "Create Lambda with admin role → invoke to execute admin actions",
|
|
123
|
+
"mitre": "T1134.001",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "PassRole+EC2",
|
|
127
|
+
"permissions": ["iam:PassRole", "ec2:RunInstances"],
|
|
128
|
+
"severity": "HIGH",
|
|
129
|
+
"description": "Launch EC2 with admin instance profile → access IMDS for credentials",
|
|
130
|
+
"mitre": "T1134.001",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "PassRole+CloudFormation",
|
|
134
|
+
"permissions": ["iam:PassRole", "cloudformation:CreateStack"],
|
|
135
|
+
"severity": "CRITICAL",
|
|
136
|
+
"description": "Create CloudFormation stack with admin role → execute arbitrary actions",
|
|
137
|
+
"mitre": "T1134.001",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "UpdateAssumeRolePolicy",
|
|
141
|
+
"permissions": ["iam:UpdateAssumeRolePolicy"],
|
|
142
|
+
"severity": "CRITICAL",
|
|
143
|
+
"description": "Update role trust policy to allow self to assume admin role",
|
|
144
|
+
"mitre": "T1548",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "AssumeRole (direct)",
|
|
148
|
+
"permissions": ["sts:AssumeRole"],
|
|
149
|
+
"severity": "HIGH",
|
|
150
|
+
"description": "Assume a role with higher privileges than current identity",
|
|
151
|
+
"mitre": "T1134.001",
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class IamPrivilegeEscalation(BaseModule):
|
|
157
|
+
|
|
158
|
+
NAME = "iam_privilege_escalation"
|
|
159
|
+
DESCRIPTION = "Enumerate IAM permissions and detect privilege escalation paths using Rhino Security Labs methodology"
|
|
160
|
+
AUTHOR = "ExploitGraph Team"
|
|
161
|
+
VERSION = "1.0.0"
|
|
162
|
+
CATEGORY = "cloud"
|
|
163
|
+
SEVERITY = "CRITICAL"
|
|
164
|
+
MITRE = ["T1078.004", "T1548", "T1134.001"]
|
|
165
|
+
AWS_PARALLEL = "Pacu: iam__privesc_scan | PMapper: pmapper graph"
|
|
166
|
+
|
|
167
|
+
OPTIONS = {
|
|
168
|
+
"AWS_ACCESS_KEY": {"default": "", "required": False, "description": "AWS Access Key (auto-populated from secrets)"},
|
|
169
|
+
"AWS_SECRET_KEY": {"default": "", "required": False, "description": "AWS Secret Key"},
|
|
170
|
+
"AWS_SESSION_TOKEN":{"default": "", "required": False, "description": "AWS Session Token"},
|
|
171
|
+
"AWS_REGION": {"default": "us-east-1", "required": False, "description": "AWS region"},
|
|
172
|
+
"AWS_PROFILE": {"default": "", "required": False, "description": "AWS CLI profile"},
|
|
173
|
+
"SCAN_ROLES": {"default": "true", "required": False, "description": "Also scan assumable roles"},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
def run(self, session: "Session") -> ModuleResult:
|
|
177
|
+
from core.logger import log
|
|
178
|
+
from core.aws_client import is_available, get_client, _creds_from_session
|
|
179
|
+
|
|
180
|
+
self._timer_start()
|
|
181
|
+
log.section("IAM Privilege Escalation Scanner")
|
|
182
|
+
log.info("MITRE: T1548 — Abuse Elevation Control Mechanism")
|
|
183
|
+
log.info("Methodology: Rhino Security Labs IAM Privilege Escalation")
|
|
184
|
+
log.info("Reference: https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/")
|
|
185
|
+
|
|
186
|
+
if not is_available():
|
|
187
|
+
log.warning("boto3 required: pip install boto3")
|
|
188
|
+
return ModuleResult(False, {}, "boto3 required")
|
|
189
|
+
|
|
190
|
+
# Auto-populate credentials from session
|
|
191
|
+
self._auto_populate_creds(session)
|
|
192
|
+
|
|
193
|
+
region = self.get_option("AWS_REGION", "us-east-1")
|
|
194
|
+
ak = self.get_option("AWS_ACCESS_KEY")
|
|
195
|
+
sk = self.get_option("AWS_SECRET_KEY")
|
|
196
|
+
token = self.get_option("AWS_SESSION_TOKEN", "")
|
|
197
|
+
|
|
198
|
+
if not (ak and sk):
|
|
199
|
+
# Try from session
|
|
200
|
+
ak, sk, token = _creds_from_session(session)
|
|
201
|
+
|
|
202
|
+
if not (ak and sk):
|
|
203
|
+
log.warning("No AWS credentials available.")
|
|
204
|
+
log.step("Run aws_credential_validator first, or set AWS_ACCESS_KEY/SECRET_KEY")
|
|
205
|
+
return ModuleResult(True, {"escalation_paths": 0,
|
|
206
|
+
"skipped_reason": "No credentials"})
|
|
207
|
+
|
|
208
|
+
iam = get_client("iam", region=region, access_key=ak,
|
|
209
|
+
secret_key=sk, session_token=token)
|
|
210
|
+
sts = get_client("sts", region=region, access_key=ak,
|
|
211
|
+
secret_key=sk, session_token=token)
|
|
212
|
+
|
|
213
|
+
if not iam:
|
|
214
|
+
return ModuleResult(False, {}, "Failed to create IAM client")
|
|
215
|
+
|
|
216
|
+
# Get current identity
|
|
217
|
+
current_user = ""
|
|
218
|
+
current_arn = ""
|
|
219
|
+
try:
|
|
220
|
+
identity = sts.get_caller_identity()
|
|
221
|
+
current_arn = identity.get("Arn", "")
|
|
222
|
+
current_user = current_arn.split("/")[-1]
|
|
223
|
+
log.info(f"Current identity: {current_arn}")
|
|
224
|
+
except Exception as e:
|
|
225
|
+
pass # error handled upstream
|
|
226
|
+
except Exception as e:
|
|
227
|
+
|
|
228
|
+
# Enumerate current permissions
|
|
229
|
+
pass # error handled upstream
|
|
230
|
+
log.step("Enumerating current IAM permissions...")
|
|
231
|
+
current_permissions = self._get_effective_permissions(iam, current_user)
|
|
232
|
+
log.info(f"Permissions found: {len(current_permissions)}")
|
|
233
|
+
|
|
234
|
+
# Check each escalation path
|
|
235
|
+
escalation_paths = []
|
|
236
|
+
for path in PRIVESC_PATHS:
|
|
237
|
+
required = path["permissions"]
|
|
238
|
+
if self._has_permissions(required, current_permissions):
|
|
239
|
+
escalation_paths.append(path)
|
|
240
|
+
log.critical(f"ESCALATION PATH: {path['name']}")
|
|
241
|
+
log.step(f" Requires: {', '.join(required)}")
|
|
242
|
+
log.step(f" Method: {path['description']}")
|
|
243
|
+
|
|
244
|
+
# Enumerate all users and roles for broader assessment
|
|
245
|
+
users = self._enum_users(iam)
|
|
246
|
+
roles = self._enum_roles(iam)
|
|
247
|
+
|
|
248
|
+
log.info(f"IAM Users: {len(users)} | Roles: {len(roles)}")
|
|
249
|
+
|
|
250
|
+
# Check for wildcard permissions
|
|
251
|
+
wildcard_findings = self._check_wildcards(iam, current_user)
|
|
252
|
+
|
|
253
|
+
# Check assumable roles
|
|
254
|
+
assumable_admin_roles = []
|
|
255
|
+
if self.get_option("SCAN_ROLES", "true").lower() == "true":
|
|
256
|
+
assumable_admin_roles = self._find_assumable_admin_roles(iam, current_arn)
|
|
257
|
+
|
|
258
|
+
# Generate findings
|
|
259
|
+
self._generate_findings(
|
|
260
|
+
escalation_paths, wildcard_findings,
|
|
261
|
+
assumable_admin_roles, users, roles,
|
|
262
|
+
current_arn, session
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Update attack graph
|
|
266
|
+
if escalation_paths or wildcard_findings:
|
|
267
|
+
session.add_graph_node(
|
|
268
|
+
"priv_escalation",
|
|
269
|
+
f"Priv-Esc Paths\n{len(escalation_paths)} found",
|
|
270
|
+
"escalation", "CRITICAL",
|
|
271
|
+
f"Methods: {', '.join(p['name'] for p in escalation_paths[:3])}"
|
|
272
|
+
)
|
|
273
|
+
session.add_graph_edge("aws_access", "priv_escalation",
|
|
274
|
+
"privilege escalation", "T1548")
|
|
275
|
+
|
|
276
|
+
elapsed = self._timer_stop()
|
|
277
|
+
log.success(f"IAM priv-esc scan done in {elapsed}s — "
|
|
278
|
+
f"{len(escalation_paths)} paths, {len(wildcard_findings)} wildcard issues")
|
|
279
|
+
|
|
280
|
+
return ModuleResult(True, {
|
|
281
|
+
"escalation_paths": len(escalation_paths),
|
|
282
|
+
"wildcard_findings": len(wildcard_findings),
|
|
283
|
+
"assumable_admin_roles":len(assumable_admin_roles),
|
|
284
|
+
"users_found": len(users),
|
|
285
|
+
"roles_found": len(roles),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
def _auto_populate_creds(self, session: "Session"):
|
|
289
|
+
from core.aws_client import _creds_from_session
|
|
290
|
+
ak, sk, token = _creds_from_session(session)
|
|
291
|
+
if ak and not self.get_option("AWS_ACCESS_KEY"):
|
|
292
|
+
self.set_option("AWS_ACCESS_KEY", ak)
|
|
293
|
+
if sk and not self.get_option("AWS_SECRET_KEY"):
|
|
294
|
+
self.set_option("AWS_SECRET_KEY", sk)
|
|
295
|
+
|
|
296
|
+
def _get_effective_permissions(self, iam, username: str) -> set[str]:
|
|
297
|
+
"""Get all IAM permissions for the current user."""
|
|
298
|
+
from core.logger import log
|
|
299
|
+
permissions = set()
|
|
300
|
+
|
|
301
|
+
if not username:
|
|
302
|
+
return permissions
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
# Attached managed policies
|
|
306
|
+
resp = iam.list_attached_user_policies(UserName=username)
|
|
307
|
+
for policy in resp.get("AttachedPolicies", []):
|
|
308
|
+
perms = self._expand_policy(iam, policy["PolicyArn"])
|
|
309
|
+
permissions.update(perms)
|
|
310
|
+
|
|
311
|
+
# Inline policies
|
|
312
|
+
resp2 = iam.list_user_policies(UserName=username)
|
|
313
|
+
for policy_name in resp2.get("PolicyNames", []):
|
|
314
|
+
try:
|
|
315
|
+
doc = iam.get_user_policy(UserName=username, PolicyName=policy_name)
|
|
316
|
+
perms = self._extract_permissions_from_doc(
|
|
317
|
+
doc.get("PolicyDocument", {})
|
|
318
|
+
)
|
|
319
|
+
permissions.update(perms)
|
|
320
|
+
except Exception:
|
|
321
|
+
pass # network/connection error — continue scanning
|
|
322
|
+
|
|
323
|
+
# Group policies
|
|
324
|
+
resp3 = iam.list_groups_for_user(UserName=username)
|
|
325
|
+
for group in resp3.get("Groups", []):
|
|
326
|
+
gname = group["GroupName"]
|
|
327
|
+
try:
|
|
328
|
+
attached = iam.list_attached_group_policies(GroupName=gname)
|
|
329
|
+
for p in attached.get("AttachedPolicies", []):
|
|
330
|
+
perms = self._expand_policy(iam, p["PolicyArn"])
|
|
331
|
+
permissions.update(perms)
|
|
332
|
+
except Exception:
|
|
333
|
+
pass # network/connection error — continue scanning
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
pass # error handled upstream
|
|
337
|
+
except Exception as e:
|
|
338
|
+
|
|
339
|
+
pass # error handled upstream
|
|
340
|
+
return permissions
|
|
341
|
+
|
|
342
|
+
def _expand_policy(self, iam, policy_arn: str) -> set[str]:
|
|
343
|
+
"""Get all actions from a managed policy's default version."""
|
|
344
|
+
permissions = set()
|
|
345
|
+
try:
|
|
346
|
+
policy = iam.get_policy(PolicyArn=policy_arn)
|
|
347
|
+
version_id = policy["Policy"]["DefaultVersionId"]
|
|
348
|
+
doc = iam.get_policy_version(PolicyArn=policy_arn,
|
|
349
|
+
VersionId=version_id)
|
|
350
|
+
return self._extract_permissions_from_doc(
|
|
351
|
+
doc["PolicyVersion"].get("Document", {})
|
|
352
|
+
)
|
|
353
|
+
except Exception:
|
|
354
|
+
pass # error handled upstream
|
|
355
|
+
except Exception:
|
|
356
|
+
|
|
357
|
+
pass # error handled upstream
|
|
358
|
+
def _extract_permissions_from_doc(self, doc) -> set[str]:
|
|
359
|
+
"""Extract action strings from a policy document."""
|
|
360
|
+
permissions = set()
|
|
361
|
+
if isinstance(doc, str):
|
|
362
|
+
try:
|
|
363
|
+
doc = json.loads(doc)
|
|
364
|
+
except Exception:
|
|
365
|
+
pass # error handled upstream
|
|
366
|
+
except Exception:
|
|
367
|
+
|
|
368
|
+
pass # error handled upstream
|
|
369
|
+
for statement in doc.get("Statement", []):
|
|
370
|
+
if statement.get("Effect", "Deny") != "Allow":
|
|
371
|
+
continue
|
|
372
|
+
actions = statement.get("Action", [])
|
|
373
|
+
if isinstance(actions, str):
|
|
374
|
+
actions = [actions]
|
|
375
|
+
for action in actions:
|
|
376
|
+
permissions.add(action.lower())
|
|
377
|
+
# Wildcards: iam:* → all iam actions
|
|
378
|
+
if action == "*":
|
|
379
|
+
permissions.add("*")
|
|
380
|
+
return permissions
|
|
381
|
+
|
|
382
|
+
def _has_permissions(self, required: list[str], current: set[str]) -> bool:
|
|
383
|
+
"""Check if all required permissions are in current permission set."""
|
|
384
|
+
if "*" in current:
|
|
385
|
+
return True
|
|
386
|
+
for perm in required:
|
|
387
|
+
service, action = perm.split(":", 1) if ":" in perm else (perm, "*")
|
|
388
|
+
service_wildcard = f"{service}:*"
|
|
389
|
+
if (perm.lower() in current or
|
|
390
|
+
service_wildcard.lower() in current or
|
|
391
|
+
"*" in current):
|
|
392
|
+
continue
|
|
393
|
+
return False
|
|
394
|
+
return bool(required) # All required perms satisfied
|
|
395
|
+
|
|
396
|
+
def _enum_users(self, iam) -> list[dict]:
|
|
397
|
+
from core.logger import log
|
|
398
|
+
try:
|
|
399
|
+
resp = iam.list_users(MaxItems=50)
|
|
400
|
+
users = resp.get("Users", [])
|
|
401
|
+
for u in users[:10]:
|
|
402
|
+
log.step(f"User: {u['UserName']} (created: {str(u.get('CreateDate',''))[:10]})")
|
|
403
|
+
return users
|
|
404
|
+
except Exception:
|
|
405
|
+
pass # error handled upstream
|
|
406
|
+
except Exception:
|
|
407
|
+
|
|
408
|
+
pass # error handled upstream
|
|
409
|
+
def _enum_roles(self, iam) -> list[dict]:
|
|
410
|
+
from core.logger import log
|
|
411
|
+
try:
|
|
412
|
+
resp = iam.list_roles(MaxItems=50)
|
|
413
|
+
roles = resp.get("Roles", [])
|
|
414
|
+
for r in roles[:10]:
|
|
415
|
+
log.step(f"Role: {r['RoleName']}")
|
|
416
|
+
return roles
|
|
417
|
+
except Exception:
|
|
418
|
+
pass # error handled upstream
|
|
419
|
+
except Exception:
|
|
420
|
+
|
|
421
|
+
pass # error handled upstream
|
|
422
|
+
def _check_wildcards(self, iam, username: str) -> list[dict]:
|
|
423
|
+
from core.logger import log
|
|
424
|
+
wildcards = []
|
|
425
|
+
if not username:
|
|
426
|
+
return wildcards
|
|
427
|
+
permissions = self._get_effective_permissions(iam, username)
|
|
428
|
+
if "*" in permissions:
|
|
429
|
+
wildcards.append({"type": "full_admin", "permission": "*"})
|
|
430
|
+
log.critical("WILDCARD (*) PERMISSION FOUND — Full admin access!")
|
|
431
|
+
for perm in permissions:
|
|
432
|
+
if perm.endswith(":*"):
|
|
433
|
+
wildcards.append({"type": "service_wildcard", "permission": perm})
|
|
434
|
+
log.warning(f"Service wildcard: {perm}")
|
|
435
|
+
return wildcards
|
|
436
|
+
|
|
437
|
+
def _find_assumable_admin_roles(self, iam, caller_arn: str) -> list[str]:
|
|
438
|
+
from core.logger import log
|
|
439
|
+
admin_roles = []
|
|
440
|
+
try:
|
|
441
|
+
resp = iam.list_roles(MaxItems=100)
|
|
442
|
+
for role in resp.get("Roles", []):
|
|
443
|
+
trust = json.dumps(role.get("AssumeRolePolicyDocument", {}))
|
|
444
|
+
# Check if our identity can assume this role
|
|
445
|
+
if (caller_arn in trust or
|
|
446
|
+
'"*"' in trust or
|
|
447
|
+
"sts:AssumeRole" in trust):
|
|
448
|
+
role_name = role["RoleName"]
|
|
449
|
+
# Check if it has admin policies
|
|
450
|
+
try:
|
|
451
|
+
attached = iam.list_attached_role_policies(RoleName=role_name)
|
|
452
|
+
for p in attached.get("AttachedPolicies", []):
|
|
453
|
+
if any(ind in p["PolicyName"] for ind in
|
|
454
|
+
["Administrator", "FullAccess", "PowerUser"]):
|
|
455
|
+
admin_roles.append(role["Arn"])
|
|
456
|
+
log.critical(f"Assumable admin role: {role['Arn']}")
|
|
457
|
+
except Exception:
|
|
458
|
+
pass # network/connection error — continue scanning
|
|
459
|
+
except Exception:
|
|
460
|
+
pass # network/connection error — continue scanning
|
|
461
|
+
return admin_roles
|
|
462
|
+
|
|
463
|
+
def _generate_findings(self, escalation_paths, wildcard_findings,
|
|
464
|
+
assumable_admin_roles, users, roles,
|
|
465
|
+
current_arn, session: "Session"):
|
|
466
|
+
from core.logger import log
|
|
467
|
+
|
|
468
|
+
for path in escalation_paths:
|
|
469
|
+
session.add_finding(
|
|
470
|
+
module = self.NAME,
|
|
471
|
+
title = f"IAM Privilege Escalation Path: {path['name']}",
|
|
472
|
+
severity = path["severity"],
|
|
473
|
+
description = (
|
|
474
|
+
f"Current identity ({current_arn.split('/')[-1]}) has permissions "
|
|
475
|
+
f"to escalate privileges via: {path['description']}"
|
|
476
|
+
),
|
|
477
|
+
evidence = (
|
|
478
|
+
f"Method: {path['name']}\n"
|
|
479
|
+
f"Required permissions: {', '.join(path['permissions'])}\n"
|
|
480
|
+
f"Current identity: {current_arn}"
|
|
481
|
+
),
|
|
482
|
+
recommendation = (
|
|
483
|
+
f"Remove or restrict permission(s): {', '.join(path['permissions'])}. "
|
|
484
|
+
"Apply least-privilege IAM policies. Use IAM Access Analyzer."
|
|
485
|
+
),
|
|
486
|
+
cvss_score = 9.5 if path["severity"] == "CRITICAL" else 7.5,
|
|
487
|
+
aws_parallel = f"Pacu: iam__privesc_scan would detect: {path['name']}",
|
|
488
|
+
mitre_technique = path["mitre"],
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
for wc in wildcard_findings:
|
|
492
|
+
session.add_finding(
|
|
493
|
+
module = self.NAME,
|
|
494
|
+
title = f"Dangerous Wildcard IAM Permission: {wc['permission']}",
|
|
495
|
+
severity = "CRITICAL",
|
|
496
|
+
description = f"Identity has wildcard permission '{wc['permission']}' which grants unrestricted access.",
|
|
497
|
+
evidence = f"Permission: {wc['permission']}\nIdentity: {current_arn}",
|
|
498
|
+
recommendation = "Replace wildcard with specific required actions. Run aws iam generate-service-last-accessed-details.",
|
|
499
|
+
cvss_score = 10.0,
|
|
500
|
+
aws_parallel = "IAM policy with Action: '*' — violates least privilege",
|
|
501
|
+
mitre_technique = "T1548",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
for role_arn in assumable_admin_roles:
|
|
505
|
+
session.add_finding(
|
|
506
|
+
module = self.NAME,
|
|
507
|
+
title = f"Assumable Admin Role: {role_arn.split('/')[-1]}",
|
|
508
|
+
severity = "CRITICAL",
|
|
509
|
+
description = f"Current identity can assume admin role: {role_arn}",
|
|
510
|
+
evidence = f"Role ARN: {role_arn}\nCurrent: {current_arn}",
|
|
511
|
+
recommendation = "Restrict role trust policy. Add conditions (MFA, source IP).",
|
|
512
|
+
cvss_score = 9.8,
|
|
513
|
+
aws_parallel = "sts:AssumeRole to admin role — immediate full access",
|
|
514
|
+
mitre_technique = "T1134.001",
|
|
515
|
+
)
|