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,42 @@
|
|
|
1
|
+
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
core/attack_graph.py,sha256=Osej6YKoPJ6q5wrqcW0hQLgUsn95XV04HeqItOdzqIk,3856
|
|
3
|
+
core/aws_client.py,sha256=D70-URLTRQbUfufCaKLeY-So-QPwuFSUJtMF_COuaEI,9796
|
|
4
|
+
core/config.py,sha256=FgkykICftoB1slI3mzAls0kj1VOPYhtl2lqcExGUPLU,3086
|
|
5
|
+
core/console.py,sha256=MKeGRWPwNvQqM1uwZcGmIK3hxxBX6_W4BeNRR73NpnQ,23051
|
|
6
|
+
core/context_engine.py,sha256=6wVx-TZhAoY83xG8tIul_mKzmrK_tdmxRHzIieN2DK4,7226
|
|
7
|
+
core/correlator.py,sha256=R63X_eR08UMuyR0IOjam5YKxZ-7GhurS20ibSAlfm3o,22238
|
|
8
|
+
core/http_client.py,sha256=LY8W_To2vamHheCvpb9f1_iCJP8TDuScmbuPjSLQ_D8,8595
|
|
9
|
+
core/logger.py,sha256=iazXJqI-OVLCQtIovjzD-b30cjzyRQWWsVhiZN9ReOo,3893
|
|
10
|
+
core/module_loader.py,sha256=2MVVXcZUFdeM4PmY82FP7Pn22fMVxRjXdwRpELmfPkw,2886
|
|
11
|
+
core/risk_engine.py,sha256=2bz6sPjKVZjNQdRqrYzaxXy8FUuK-kEe4B4NVnTx2Dw,1968
|
|
12
|
+
core/session_manager.py,sha256=FdHzVADV_jWIEzYJDqMOV8CLBcg0hArYunnFaBQ58rQ,10688
|
|
13
|
+
exploitgraph-1.0.0.dist-info/licenses/LICENSE,sha256=Nt_gc-40jBtmfDF4rIXJXB_mQ_ApZdn49uTSXrhv5w0,1070
|
|
14
|
+
modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
modules/base.py,sha256=oiopwu3TOxR_iYKFx1vso0krmWnv_Cu06FNlUKiQAvg,3283
|
|
16
|
+
modules/cloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
modules/cloud/aws_credential_validator.py,sha256=6JAyDr4AaIdVBfCocb6SJMOlzz3dZ_Hyii5Nbi91riw,15443
|
|
18
|
+
modules/cloud/azure_enum.py,sha256=oqdjWM19gsGce1CmzvnLdggKcvSMhCw6r5_XJ_O7tZU,13277
|
|
19
|
+
modules/cloud/cloudtrail_analyzer.py,sha256=B6QGJqexJhJjCATgCiDoRlrkv8FwuKafeWNIeylbjpM,22866
|
|
20
|
+
modules/cloud/gcp_enum.py,sha256=5HC2Ln0Bqsj32gIbcEmQ1nGn4-eyOAvuz-TTh3i03bY,11758
|
|
21
|
+
modules/cloud/iam_enum.py,sha256=dStnYqqmKY6uWSPrgflqqlbWq4_Dr2dameLxPtYJ838,15591
|
|
22
|
+
modules/cloud/iam_privilege_escalation.py,sha256=Kfqz-sHUaom_uV29cVAjQ40VZWw267oK-zRBxlFhieU,21593
|
|
23
|
+
modules/cloud/metadata_check.py,sha256=fXG0UKSVD5P1EUqVa2Y79XhvMkmVJMNd_zIjUkeoXqA,14583
|
|
24
|
+
modules/cloud/s3_enum.py,sha256=YQdCkyjXWS31EgRWccfUZswktbuC2MDu5gZqZAalYW4,22203
|
|
25
|
+
modules/discovery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
modules/discovery/http_enum.py,sha256=Vup-c49TOamzgHHbm6prlnfxb22yZ_fz7YXXg3qTTeo,10866
|
|
27
|
+
modules/discovery/subdomain_enum.py,sha256=WTfQn3rQWMutIkFbDVLgUFjrDKmhOmx8YEeC8vRzGmQ,10548
|
|
28
|
+
modules/exploitation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
modules/exploitation/api_exploit.py,sha256=ldfvTS66oFUcTcM1SotWTEhmQnuLizjPW6S6msJ7x8w,19526
|
|
30
|
+
modules/exploitation/jwt_attack.py,sha256=qXMTqnGTBXCGKvpCceFaFskOQgUqmFQChpCIH8w5_R8,15591
|
|
31
|
+
modules/exploitation/ssrf_scanner.py,sha256=D8RtgYEftvCJUXiCk8unyPEyXC_9VtNAZUxD4zvGDbg,11402
|
|
32
|
+
modules/reporting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
modules/reporting/html_report.py,sha256=kgHxR_VXJni9c4NrVSpFrV0-VvQr782lVP1yeLqLNAY,24944
|
|
34
|
+
modules/reporting/json_export.py,sha256=KHn5kguSGYRZw213PX1CBsk-ylqzixSpm-Eh0YwEd1s,4078
|
|
35
|
+
modules/secrets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
+
modules/secrets/file_secrets.py,sha256=ic0NhwLbCvB1ARQMT8cyYUNmjrIWVYE0Picv-XPpR-g,16820
|
|
37
|
+
modules/secrets/git_secrets.py,sha256=HcNqg7YXSqf-XkYiVOoH_dxYeO9VyYHe-Ylz3ehgfC0,11418
|
|
38
|
+
exploitgraph-1.0.0.dist-info/METADATA,sha256=k8sSj2WrymQ-FxvnFUAHxjzkxj_D5vuM44N5LPM766o,15193
|
|
39
|
+
exploitgraph-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
40
|
+
exploitgraph-1.0.0.dist-info/entry_points.txt,sha256=dG0vV2jtfX9ZXJ7upLKyG6pgbPJN1Ox2Rit9PO0BjjU,51
|
|
41
|
+
exploitgraph-1.0.0.dist-info/top_level.txt,sha256=dXGiKnRy6F5ai_9PsEJ3Wl2UDtLkQJC3z8Q1MZJvAtw,13
|
|
42
|
+
exploitgraph-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Prajwal Pawar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
modules/__init__.py
ADDED
|
File without changes
|
modules/base.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""ExploitGraph - BaseModule: the contract every module must fulfill."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import abc, time, datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from core.session_manager import Session
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModuleResult:
|
|
10
|
+
def __init__(self, success: bool, data: dict = None, error: str = ""):
|
|
11
|
+
self.success = success
|
|
12
|
+
self.data = data or {}
|
|
13
|
+
self.error = error
|
|
14
|
+
self.timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
15
|
+
def __bool__(self): return self.success
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseModule(abc.ABC):
|
|
19
|
+
"""
|
|
20
|
+
Abstract base class for all ExploitGraph modules.
|
|
21
|
+
Modules communicate ONLY via the shared Session object.
|
|
22
|
+
Modules must NEVER import or call other modules directly.
|
|
23
|
+
"""
|
|
24
|
+
NAME: str = "unnamed_module"
|
|
25
|
+
DESCRIPTION: str = "No description"
|
|
26
|
+
AUTHOR: str = "ExploitGraph Community"
|
|
27
|
+
VERSION: str = "1.0.0"
|
|
28
|
+
CATEGORY: str = "misc"
|
|
29
|
+
SEVERITY: str = "INFO"
|
|
30
|
+
MITRE: list[str] = []
|
|
31
|
+
AWS_PARALLEL: str = ""
|
|
32
|
+
OPTIONS: dict = {}
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self._options: dict[str, Any] = {k: v["default"] for k,v in self.OPTIONS.items()}
|
|
36
|
+
self._start: float = 0.0
|
|
37
|
+
self._elapsed: float = 0.0
|
|
38
|
+
|
|
39
|
+
@abc.abstractmethod
|
|
40
|
+
def run(self, session: "Session") -> ModuleResult: ...
|
|
41
|
+
|
|
42
|
+
def validate(self, session: "Session") -> tuple[bool, str]:
|
|
43
|
+
for name, meta in self.OPTIONS.items():
|
|
44
|
+
if meta.get("required") and not self._options.get(name):
|
|
45
|
+
return False, f"Required option '{name}' not set. set {name} <value>"
|
|
46
|
+
return True, ""
|
|
47
|
+
|
|
48
|
+
def report(self, session: "Session") -> dict:
|
|
49
|
+
return {"module": self.NAME, "category": self.CATEGORY,
|
|
50
|
+
"severity": self.SEVERITY, "elapsed": self._elapsed}
|
|
51
|
+
|
|
52
|
+
def set_option(self, key: str, value: Any) -> tuple[bool, str]:
|
|
53
|
+
if key.upper() not in self.OPTIONS:
|
|
54
|
+
return False, f"Unknown option: {key}. Valid: {list(self.OPTIONS.keys())}"
|
|
55
|
+
self._options[key.upper()] = value
|
|
56
|
+
return True, f"{key.upper()} => {value}"
|
|
57
|
+
|
|
58
|
+
def get_option(self, key: str, default: Any = None) -> Any:
|
|
59
|
+
return self._options.get(key.upper(), default)
|
|
60
|
+
|
|
61
|
+
def show_options(self) -> list[tuple]:
|
|
62
|
+
return [(name, str(self._options.get(name, meta.get("default",""))),
|
|
63
|
+
"yes" if meta.get("required") else "no", meta.get("description",""))
|
|
64
|
+
for name, meta in self.OPTIONS.items()]
|
|
65
|
+
|
|
66
|
+
def _timer_start(self): self._start = time.monotonic()
|
|
67
|
+
def _timer_stop(self) -> float:
|
|
68
|
+
self._elapsed = round(time.monotonic() - self._start, 2)
|
|
69
|
+
return self._elapsed
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def info(m): from core.logger import log; log.module_info(m)
|
|
73
|
+
@staticmethod
|
|
74
|
+
def ok(m): from core.logger import log; log.module_success(m)
|
|
75
|
+
@staticmethod
|
|
76
|
+
def warn(m): from core.logger import log; log.module_warn(m)
|
|
77
|
+
@staticmethod
|
|
78
|
+
def err(m): from core.logger import log; log.module_error(m)
|
|
79
|
+
@staticmethod
|
|
80
|
+
def critical(m): from core.logger import log; log.module_critical(m)
|
|
81
|
+
|
|
82
|
+
def __repr__(self): return f"<Module {self.CATEGORY}/{self.NAME} v{self.VERSION}>"
|
|
File without changes
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ExploitGraph Module: AWS Credential Validator
|
|
3
|
+
Category: cloud
|
|
4
|
+
|
|
5
|
+
Validates AWS credentials discovered by file_secrets or cloudtrail_analyzer.
|
|
6
|
+
Uses read-only STS + IAM calls to:
|
|
7
|
+
1. Confirm the key is still active (GetCallerIdentity)
|
|
8
|
+
2. Enumerate accessible services and permissions
|
|
9
|
+
3. Detect privilege level (admin vs limited)
|
|
10
|
+
4. Map potential lateral movement paths
|
|
11
|
+
|
|
12
|
+
All operations are READ-ONLY and non-destructive.
|
|
13
|
+
|
|
14
|
+
MITRE: T1078.004 — Valid Accounts: Cloud Accounts
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import re
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from modules.base import BaseModule, ModuleResult
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from core.session_manager import Session
|
|
24
|
+
|
|
25
|
+
# Services to probe for access (safe list/describe calls only)
|
|
26
|
+
PROBE_SERVICES = [
|
|
27
|
+
("s3", "list_buckets", {}, "s3:ListAllMyBuckets"),
|
|
28
|
+
("iam", "list_users", {"MaxItems": 5}, "iam:ListUsers"),
|
|
29
|
+
("iam", "list_roles", {"MaxItems": 5}, "iam:ListRoles"),
|
|
30
|
+
("ec2", "describe_instances", {"MaxResults": 5}, "ec2:DescribeInstances"),
|
|
31
|
+
("ec2", "describe_security_groups", {"MaxResults": 5}, "ec2:DescribeSecurityGroups"),
|
|
32
|
+
("lambda", "list_functions", {"MaxItems": 5}, "lambda:ListFunctions"),
|
|
33
|
+
("rds", "describe_db_instances", {"MaxRecords": 5}, "rds:DescribeDBInstances"),
|
|
34
|
+
("sts", "get_caller_identity", {}, "sts:GetCallerIdentity"),
|
|
35
|
+
("secretsmanager","list_secrets", {"MaxResults": 5}, "secretsmanager:ListSecrets"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Policies indicating admin-level access
|
|
39
|
+
ADMIN_INDICATORS = [
|
|
40
|
+
"AdministratorAccess",
|
|
41
|
+
"PowerUserAccess",
|
|
42
|
+
"FullAccess",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AwsCredentialValidator(BaseModule):
|
|
47
|
+
|
|
48
|
+
NAME = "aws_credential_validator"
|
|
49
|
+
DESCRIPTION = "Validate discovered AWS credentials using STS + enumerate accessible services and IAM permissions"
|
|
50
|
+
AUTHOR = "ExploitGraph Team"
|
|
51
|
+
VERSION = "1.1.0"
|
|
52
|
+
CATEGORY = "cloud"
|
|
53
|
+
SEVERITY = "CRITICAL"
|
|
54
|
+
MITRE = ["T1078.004", "T1580"]
|
|
55
|
+
AWS_PARALLEL = "aws sts get-caller-identity && aws iam list-attached-user-policies"
|
|
56
|
+
|
|
57
|
+
OPTIONS = {
|
|
58
|
+
"AWS_ACCESS_KEY": {"default": "", "required": False, "description": "AWS Access Key ID to validate"},
|
|
59
|
+
"AWS_SECRET_KEY": {"default": "", "required": False, "description": "AWS Secret Access Key"},
|
|
60
|
+
"AWS_SESSION_TOKEN":{"default": "", "required": False, "description": "AWS Session Token (for temporary creds)"},
|
|
61
|
+
"AWS_REGION": {"default": "us-east-1", "required": False, "description": "AWS region"},
|
|
62
|
+
"AWS_PROFILE": {"default": "", "required": False, "description": "AWS CLI profile name"},
|
|
63
|
+
"PROBE_SERVICES": {"default": "true", "required": False, "description": "Probe accessible AWS services"},
|
|
64
|
+
"ENUM_POLICIES": {"default": "true", "required": False, "description": "Enumerate attached IAM policies"},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def run(self, session: "Session") -> ModuleResult:
|
|
68
|
+
from core.logger import log
|
|
69
|
+
from core.aws_client import verify_credentials, get_client, is_available, has_credentials
|
|
70
|
+
|
|
71
|
+
self._timer_start()
|
|
72
|
+
log.section("AWS Credential Validator")
|
|
73
|
+
log.info("MITRE: T1078.004 — Valid Accounts: Cloud Accounts")
|
|
74
|
+
log.info("All operations are READ-ONLY")
|
|
75
|
+
|
|
76
|
+
if not is_available():
|
|
77
|
+
log.warning("boto3 not installed. Install: pip install boto3")
|
|
78
|
+
log.step("Manual validation: aws sts get-caller-identity --profile <profile>")
|
|
79
|
+
return ModuleResult(False, {}, "boto3 required")
|
|
80
|
+
|
|
81
|
+
# Collect all credentials to test
|
|
82
|
+
creds_to_test = self._gather_credentials(session)
|
|
83
|
+
|
|
84
|
+
if not creds_to_test:
|
|
85
|
+
log.warning("No AWS credentials found in session.")
|
|
86
|
+
log.step("Run secrets/file_secrets or cloud/cloudtrail_analyzer first.")
|
|
87
|
+
return ModuleResult(True, {"validated": 0,
|
|
88
|
+
"skipped_reason": "No AWS credentials in session"})
|
|
89
|
+
|
|
90
|
+
log.info(f"Credentials to validate: {len(creds_to_test)}")
|
|
91
|
+
|
|
92
|
+
valid_count = 0
|
|
93
|
+
results = []
|
|
94
|
+
|
|
95
|
+
for cred in creds_to_test:
|
|
96
|
+
ak = cred["access_key"]
|
|
97
|
+
sk = cred.get("secret_key", "")
|
|
98
|
+
token = cred.get("session_token", "")
|
|
99
|
+
|
|
100
|
+
if not ak:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
log.step(f"Validating: {ak[:12]}... (from {cred.get('source', 'unknown')})")
|
|
104
|
+
|
|
105
|
+
# Step 1: STS GetCallerIdentity — proves the key works
|
|
106
|
+
identity = verify_credentials(ak, sk, token,
|
|
107
|
+
self.get_option("AWS_REGION", "us-east-1"))
|
|
108
|
+
|
|
109
|
+
if identity["valid"]:
|
|
110
|
+
valid_count += 1
|
|
111
|
+
arn = identity["arn"]
|
|
112
|
+
account = identity["account"]
|
|
113
|
+
|
|
114
|
+
log.critical(f"VALID AWS CREDENTIALS!")
|
|
115
|
+
log.secret("Access Key", ak)
|
|
116
|
+
log.secret("ARN", arn)
|
|
117
|
+
log.secret("Account ID", account)
|
|
118
|
+
log.info(f"AWS cmd: aws sts get-caller-identity "
|
|
119
|
+
f"--access-key-id {ak} --secret-access-key <key>")
|
|
120
|
+
|
|
121
|
+
result = {
|
|
122
|
+
"access_key": ak,
|
|
123
|
+
"arn": arn,
|
|
124
|
+
"account": account,
|
|
125
|
+
"source": cred.get("source", ""),
|
|
126
|
+
"services": {},
|
|
127
|
+
"policies": [],
|
|
128
|
+
"privilege": "UNKNOWN",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Step 2: Probe which services are accessible
|
|
132
|
+
if self.get_option("PROBE_SERVICES", "true").lower() == "true":
|
|
133
|
+
accessible = self._probe_services(ak, sk, token, session)
|
|
134
|
+
result["services"] = accessible
|
|
135
|
+
log.success(f"Accessible services: {', '.join(accessible.keys())}")
|
|
136
|
+
|
|
137
|
+
# Step 3: Enumerate IAM policies
|
|
138
|
+
if self.get_option("ENUM_POLICIES", "true").lower() == "true":
|
|
139
|
+
username = arn.split("/")[-1] if "/" in arn else ""
|
|
140
|
+
policies, privilege = self._enum_policies(ak, sk, token, username, session)
|
|
141
|
+
result["policies"] = policies
|
|
142
|
+
result["privilege"] = privilege
|
|
143
|
+
log.success(f"Privilege level: {privilege}")
|
|
144
|
+
|
|
145
|
+
results.append(result)
|
|
146
|
+
|
|
147
|
+
# Add to session
|
|
148
|
+
session.add_exploit_result(
|
|
149
|
+
module = self.NAME,
|
|
150
|
+
action = f"AWS Credential Validated: {arn}",
|
|
151
|
+
success = True,
|
|
152
|
+
detail = f"Key {ak[:12]}... is VALID | Account: {account}",
|
|
153
|
+
data = result,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
session.add_finding(
|
|
157
|
+
module = self.NAME,
|
|
158
|
+
title = f"Valid AWS Credentials — {result['privilege']} Access",
|
|
159
|
+
severity = "CRITICAL",
|
|
160
|
+
description = (
|
|
161
|
+
f"AWS Access Key {ak[:12]}... is valid and active. "
|
|
162
|
+
f"Identity: {arn}. "
|
|
163
|
+
f"Privilege level: {result['privilege']}. "
|
|
164
|
+
f"Accessible services: {', '.join(result['services'].keys())}"
|
|
165
|
+
),
|
|
166
|
+
evidence = (
|
|
167
|
+
f"Access Key: {ak}\n"
|
|
168
|
+
f"ARN: {arn}\n"
|
|
169
|
+
f"Account: {account}\n"
|
|
170
|
+
f"Accessible: {', '.join(result['services'].keys())}\n"
|
|
171
|
+
f"Policies: {', '.join(result['policies'][:5])}"
|
|
172
|
+
),
|
|
173
|
+
recommendation = (
|
|
174
|
+
"Immediately rotate this key:\n"
|
|
175
|
+
f"aws iam update-access-key --access-key-id {ak} --status Inactive\n"
|
|
176
|
+
"Then investigate how it was exposed and audit all actions taken."
|
|
177
|
+
),
|
|
178
|
+
cvss_score = 10.0 if result["privilege"] == "ADMIN" else 9.0,
|
|
179
|
+
aws_parallel = "Stolen IAM credential — immediate privilege escalation risk",
|
|
180
|
+
mitre_technique = "T1078.004",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Update attack graph
|
|
184
|
+
session.add_graph_node(
|
|
185
|
+
"aws_access",
|
|
186
|
+
f"AWS Access\n{arn.split('/')[-1]}",
|
|
187
|
+
"access", "CRITICAL",
|
|
188
|
+
f"Account: {account} | Privilege: {result['privilege']}"
|
|
189
|
+
)
|
|
190
|
+
session.add_graph_edge("cloudtrail_creds", "aws_access",
|
|
191
|
+
"credential validation", "T1078.004")
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
log.warning(f"Invalid/expired: {ak[:12]}... ({identity['error']})")
|
|
195
|
+
|
|
196
|
+
elapsed = self._timer_stop()
|
|
197
|
+
log.success(f"Validation done in {elapsed}s — {valid_count}/{len(creds_to_test)} valid")
|
|
198
|
+
|
|
199
|
+
return ModuleResult(True, {
|
|
200
|
+
"credentials_tested": len(creds_to_test),
|
|
201
|
+
"valid": valid_count,
|
|
202
|
+
"results": results,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
def _gather_credentials(self, session: "Session") -> list[dict]:
|
|
206
|
+
"""Build credential pairs from session secrets."""
|
|
207
|
+
# Explicit options override
|
|
208
|
+
if self.get_option("AWS_ACCESS_KEY"):
|
|
209
|
+
return [{
|
|
210
|
+
"access_key": self.get_option("AWS_ACCESS_KEY"),
|
|
211
|
+
"secret_key": self.get_option("AWS_SECRET_KEY"),
|
|
212
|
+
"session_token": self.get_option("AWS_SESSION_TOKEN", ""),
|
|
213
|
+
"source": "module options",
|
|
214
|
+
}]
|
|
215
|
+
|
|
216
|
+
# Pair up access keys and secret keys from session
|
|
217
|
+
creds = []
|
|
218
|
+
access_keys = [s["value"] for s in session.secrets
|
|
219
|
+
if s["secret_type"] == "AWS_ACCESS_KEY"]
|
|
220
|
+
secret_keys = [s["value"] for s in session.secrets
|
|
221
|
+
if s["secret_type"] == "AWS_SECRET_KEY"]
|
|
222
|
+
session_toks = [s["value"] for s in session.secrets
|
|
223
|
+
if s["secret_type"] == "AWS_SESSION_TOKEN"]
|
|
224
|
+
|
|
225
|
+
# Pair them positionally
|
|
226
|
+
for i, ak in enumerate(access_keys):
|
|
227
|
+
sk = secret_keys[i] if i < len(secret_keys) else ""
|
|
228
|
+
token = session_toks[i] if i < len(session_toks) else ""
|
|
229
|
+
source_info = next(
|
|
230
|
+
(s["source"] for s in session.secrets if s["value"] == ak), "session"
|
|
231
|
+
)
|
|
232
|
+
creds.append({"access_key": ak, "secret_key": sk,
|
|
233
|
+
"session_token": token, "source": source_info})
|
|
234
|
+
|
|
235
|
+
# Also try access-key-only validation (STS might still work)
|
|
236
|
+
for ak in access_keys:
|
|
237
|
+
if not any(c["access_key"] == ak for c in creds):
|
|
238
|
+
creds.append({"access_key": ak, "secret_key": "",
|
|
239
|
+
"session_token": "", "source": "session (key only)"})
|
|
240
|
+
|
|
241
|
+
return creds
|
|
242
|
+
|
|
243
|
+
def _probe_services(self, ak: str, sk: str, token: str,
|
|
244
|
+
session: "Session") -> dict[str, str]:
|
|
245
|
+
"""Try safe list/describe calls on common AWS services."""
|
|
246
|
+
from core.logger import log
|
|
247
|
+
from core.aws_client import get_client
|
|
248
|
+
import botocore.exceptions
|
|
249
|
+
|
|
250
|
+
accessible = {}
|
|
251
|
+
region = self.get_option("AWS_REGION", "us-east-1")
|
|
252
|
+
|
|
253
|
+
for service, method, kwargs, permission in PROBE_SERVICES:
|
|
254
|
+
try:
|
|
255
|
+
client = get_client(service, region=region,
|
|
256
|
+
access_key=ak, secret_key=sk,
|
|
257
|
+
session_token=token)
|
|
258
|
+
if not client:
|
|
259
|
+
continue
|
|
260
|
+
fn = getattr(client, method)
|
|
261
|
+
response = fn(**kwargs)
|
|
262
|
+
accessible[service] = permission
|
|
263
|
+
log.step(f" ✓ {service}: {method} succeeded ({permission})")
|
|
264
|
+
except Exception as e:
|
|
265
|
+
pass # error handled upstream
|
|
266
|
+
except Exception as e:
|
|
267
|
+
if "AccessDenied" in err or "Unauthorized" in err:
|
|
268
|
+
pass # Expected — no access
|
|
269
|
+
elif "is not subscribed" in err:
|
|
270
|
+
pass # Service not in region
|
|
271
|
+
else:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
return accessible
|
|
275
|
+
|
|
276
|
+
def _enum_policies(self, ak: str, sk: str, token: str,
|
|
277
|
+
username: str, session: "Session") -> tuple[list[str], str]:
|
|
278
|
+
"""Enumerate IAM policies attached to the user and determine privilege level."""
|
|
279
|
+
from core.logger import log
|
|
280
|
+
from core.aws_client import get_client
|
|
281
|
+
import botocore.exceptions
|
|
282
|
+
|
|
283
|
+
region = self.get_option("AWS_REGION", "us-east-1")
|
|
284
|
+
iam = get_client("iam", region=region, access_key=ak,
|
|
285
|
+
secret_key=sk, session_token=token)
|
|
286
|
+
if not iam:
|
|
287
|
+
return [], "UNKNOWN"
|
|
288
|
+
|
|
289
|
+
policies = []
|
|
290
|
+
privilege = "LIMITED"
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
# Attached managed policies
|
|
294
|
+
if username:
|
|
295
|
+
resp = iam.list_attached_user_policies(UserName=username)
|
|
296
|
+
for p in resp.get("AttachedPolicies", []):
|
|
297
|
+
name = p["PolicyName"]
|
|
298
|
+
policies.append(name)
|
|
299
|
+
log.step(f" Policy: {name}")
|
|
300
|
+
if any(ind in name for ind in ADMIN_INDICATORS):
|
|
301
|
+
privilege = "ADMIN"
|
|
302
|
+
log.critical(f" ADMIN POLICY FOUND: {name}")
|
|
303
|
+
|
|
304
|
+
# Inline policies
|
|
305
|
+
if username:
|
|
306
|
+
resp2 = iam.list_user_policies(UserName=username)
|
|
307
|
+
for name in resp2.get("PolicyNames", []):
|
|
308
|
+
policies.append(f"inline:{name}")
|
|
309
|
+
log.step(f" Inline policy: {name}")
|
|
310
|
+
# Fetch and check for wildcards
|
|
311
|
+
try:
|
|
312
|
+
doc = iam.get_user_policy(UserName=username, PolicyName=name)
|
|
313
|
+
doc_str = str(doc.get("PolicyDocument", ""))
|
|
314
|
+
if '"*"' in doc_str and '"Action": "*"' in doc_str:
|
|
315
|
+
privilege = "ADMIN"
|
|
316
|
+
log.critical(f" WILDCARD POLICY: {name} — full admin!")
|
|
317
|
+
except Exception:
|
|
318
|
+
pass # network/connection error — continue scanning
|
|
319
|
+
|
|
320
|
+
# Try GetAccountAuthorizationDetails for full picture
|
|
321
|
+
try:
|
|
322
|
+
details = iam.get_account_authorization_details(Filter=["User"])
|
|
323
|
+
for user_detail in details.get("UserDetailList", []):
|
|
324
|
+
if user_detail.get("UserName") == username:
|
|
325
|
+
attached = user_detail.get("AttachedManagedPolicies", [])
|
|
326
|
+
for p in attached:
|
|
327
|
+
name = p["PolicyName"]
|
|
328
|
+
if name not in policies:
|
|
329
|
+
policies.append(name)
|
|
330
|
+
if any(ind in name for ind in ADMIN_INDICATORS):
|
|
331
|
+
privilege = "ADMIN"
|
|
332
|
+
except Exception:
|
|
333
|
+
pass # network/connection error — continue scanning
|
|
334
|
+
|
|
335
|
+
except botocore.exceptions.ClientError as e:
|
|
336
|
+
pass # error handled upstream
|
|
337
|
+
except botocore.exceptions.ClientError as e:
|
|
338
|
+
log.warning(f"IAM enum error: {e}")
|
|
339
|
+
|
|
340
|
+
return policies, privilege
|