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
core/console.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""ExploitGraph - msfconsole-style interactive shell with tab completion."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os, sys, cmd, readline
|
|
4
|
+
from colorama import Fore, Style, init
|
|
5
|
+
init(autoreset=True)
|
|
6
|
+
|
|
7
|
+
from core.logger import log
|
|
8
|
+
from core.module_loader import loader
|
|
9
|
+
from core.session_manager import session_manager, Session
|
|
10
|
+
from core.risk_engine import risk_engine
|
|
11
|
+
from core.attack_graph import attack_graph
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def print_banner():
|
|
15
|
+
print(Fore.RED + r"""
|
|
16
|
+
███████╗██╗ ██╗██████╗ ██╗ ██████╗ ██╗████████╗ ██████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗
|
|
17
|
+
██╔════╝╚██╗██╔╝██╔══██╗██║ ██╔═══██╗██║╚══██╔══╝██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██║ ██║
|
|
18
|
+
█████╗ ╚███╔╝ ██████╔╝██║ ██║ ██║██║ ██║ ██║ ███╗██████╔╝███████║██████╔╝███████║
|
|
19
|
+
██╔══╝ ██╔██╗ ██╔═══╝ ██║ ██║ ██║██║ ██║ ██║ ██║██╔══██╗██╔══██║██╔═══╝ ██╔══██║
|
|
20
|
+
███████╗██╔╝ ██╗██║ ███████╗╚██████╔╝██║ ██║ ╚██████╔╝██║ ██║██║ ██║██║ ██║ ██║
|
|
21
|
+
╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
|
|
22
|
+
""" + Style.RESET_ALL)
|
|
23
|
+
print(f" {Fore.WHITE}ExploitGraph {Fore.YELLOW}v1.0.0{Style.RESET_ALL}")
|
|
24
|
+
print(f" {Fore.CYAN}Automated Attack Path Discovery & Exploitation Framework{Style.RESET_ALL}")
|
|
25
|
+
print(f" {Fore.RED}Misconfig → Exposure → Secrets → API Abuse → Compromise{Style.RESET_ALL}")
|
|
26
|
+
print(f" {Fore.WHITE}github.com/prajwal-infosec/ExploitGraph | Authorized use only{Style.RESET_ALL}\n")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Completer:
|
|
30
|
+
CMDS = ["help","use","set","unset","options","run","back","exit","quit",
|
|
31
|
+
"show","info","sessions","workspace","search","export","clear"]
|
|
32
|
+
|
|
33
|
+
def __init__(self, console):
|
|
34
|
+
self.console = console
|
|
35
|
+
|
|
36
|
+
def complete(self, text, state):
|
|
37
|
+
try:
|
|
38
|
+
buf = readline.get_line_buffer().lstrip()
|
|
39
|
+
parts = buf.split()
|
|
40
|
+
first = parts[0] if parts else ""
|
|
41
|
+
|
|
42
|
+
if not parts or (len(parts)==1 and not buf.endswith(" ")):
|
|
43
|
+
matches = [c for c in self.CMDS if c.startswith(text)]
|
|
44
|
+
elif first == "use":
|
|
45
|
+
prefix = parts[1] if len(parts)>1 else ""
|
|
46
|
+
matches = [m for m in loader.list_names() if m.startswith(prefix if buf.endswith(" ") else text)]
|
|
47
|
+
elif first == "show":
|
|
48
|
+
subs = ["modules","options","findings","secrets","sessions","attack-path","exploits","summary"]
|
|
49
|
+
matches = [s for s in subs if s.startswith(text)]
|
|
50
|
+
elif first == "sessions":
|
|
51
|
+
matches = [s for s in ["-i","-k","-l"] if s.startswith(text)]
|
|
52
|
+
elif first == "workspace":
|
|
53
|
+
matches = [s for s in ["list","new","switch","delete"] if s.startswith(text)]
|
|
54
|
+
elif first == "export":
|
|
55
|
+
matches = [s for s in ["html","json","all"] if s.startswith(text)]
|
|
56
|
+
elif first == "set":
|
|
57
|
+
m = self.console._active_module
|
|
58
|
+
opts = list(m.OPTIONS.keys()) if m else ["TARGET","MODE"]
|
|
59
|
+
matches = [o for o in opts if o.startswith(text.upper())]
|
|
60
|
+
else:
|
|
61
|
+
matches = []
|
|
62
|
+
|
|
63
|
+
return matches[state] if state < len(matches) else None
|
|
64
|
+
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ExploitGraphConsole(cmd.Cmd):
|
|
69
|
+
intro = ""
|
|
70
|
+
prompt = f"{Fore.RED}exploitgraph{Fore.WHITE}>{Style.RESET_ALL} "
|
|
71
|
+
|
|
72
|
+
def __init__(self):
|
|
73
|
+
super().__init__()
|
|
74
|
+
self._active_module = None
|
|
75
|
+
self._active_module_path = ""
|
|
76
|
+
self._mode = "offensive"
|
|
77
|
+
|
|
78
|
+
n = loader.discover()
|
|
79
|
+
for err in loader.load_errors():
|
|
80
|
+
log.warning(f"Module load error: {err}")
|
|
81
|
+
|
|
82
|
+
_c = _Completer(self)
|
|
83
|
+
readline.set_completer(_c.complete)
|
|
84
|
+
readline.parse_and_bind("tab: complete")
|
|
85
|
+
readline.set_completer_delims(" \t")
|
|
86
|
+
|
|
87
|
+
if not session_manager.active:
|
|
88
|
+
session_manager.new("http://127.0.0.1:5000", "default")
|
|
89
|
+
|
|
90
|
+
log.info(f"Loaded {n} modules | Session: {session_manager.active.session_id}")
|
|
91
|
+
log.info("Type 'help' for commands. Type 'run auto' to start full attack chain.\n")
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def session(self) -> Session:
|
|
95
|
+
return session_manager.active
|
|
96
|
+
|
|
97
|
+
def _update_prompt(self):
|
|
98
|
+
if self._active_module_path:
|
|
99
|
+
self.prompt = (f"{Fore.RED}exploitgraph{Fore.WHITE}"
|
|
100
|
+
f"({Fore.YELLOW}{self._active_module_path}{Fore.WHITE})"
|
|
101
|
+
f">{Style.RESET_ALL} ")
|
|
102
|
+
else:
|
|
103
|
+
self.prompt = f"{Fore.RED}exploitgraph{Fore.WHITE}>{Style.RESET_ALL} "
|
|
104
|
+
|
|
105
|
+
# ── Commands ──────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def do_use(self, args):
|
|
108
|
+
"""Select a module. use discovery/http_enum"""
|
|
109
|
+
path = args.strip()
|
|
110
|
+
if not path: log.error("Usage: use <module_path>"); return
|
|
111
|
+
cls = loader.get(path)
|
|
112
|
+
if not cls: log.error(f"Not found: {path} (try: show modules)"); return
|
|
113
|
+
self._active_module = cls()
|
|
114
|
+
self._active_module_path = path
|
|
115
|
+
if "TARGET" in self._active_module.OPTIONS:
|
|
116
|
+
self._active_module.set_option("TARGET", self.session.target)
|
|
117
|
+
if "MODE" in self._active_module.OPTIONS:
|
|
118
|
+
self._active_module.set_option("MODE", self._mode)
|
|
119
|
+
self._update_prompt()
|
|
120
|
+
log.success(f"Using: {path}")
|
|
121
|
+
log.info(self._active_module.DESCRIPTION)
|
|
122
|
+
if self._active_module.MITRE:
|
|
123
|
+
log.info(f"MITRE: {', '.join(self._active_module.MITRE)}")
|
|
124
|
+
log.info("Type 'options' to configure. Type 'run' to execute.")
|
|
125
|
+
|
|
126
|
+
def do_set(self, args):
|
|
127
|
+
"""Set option or session variable. set TARGET http://x.com"""
|
|
128
|
+
parts = args.strip().split(None, 1)
|
|
129
|
+
if len(parts) < 2: log.error("Usage: set OPTION value"); return
|
|
130
|
+
key, val = parts[0].upper(), parts[1]
|
|
131
|
+
if key == "TARGET":
|
|
132
|
+
self.session.target = val
|
|
133
|
+
log.success(f"TARGET => {val}")
|
|
134
|
+
if self._active_module and "TARGET" in self._active_module.OPTIONS:
|
|
135
|
+
self._active_module.set_option("TARGET", val)
|
|
136
|
+
return
|
|
137
|
+
if key == "MODE":
|
|
138
|
+
if val.lower() not in ("offensive","defensive"):
|
|
139
|
+
log.error("MODE must be offensive or defensive"); return
|
|
140
|
+
self._mode = val.lower()
|
|
141
|
+
log.success(f"MODE => {val.lower()}")
|
|
142
|
+
if self._active_module and "MODE" in self._active_module.OPTIONS:
|
|
143
|
+
self._active_module.set_option("MODE", val)
|
|
144
|
+
return
|
|
145
|
+
if not self._active_module: log.error("No module selected. use <module>"); return
|
|
146
|
+
ok, msg = self._active_module.set_option(key, val)
|
|
147
|
+
(log.success if ok else log.error)(msg)
|
|
148
|
+
|
|
149
|
+
def do_unset(self, args):
|
|
150
|
+
"""Reset an option to its default. unset OPTION"""
|
|
151
|
+
key = args.strip().upper()
|
|
152
|
+
if self._active_module and key in self._active_module.OPTIONS:
|
|
153
|
+
self._active_module.set_option(key, self._active_module.OPTIONS[key].get("default",""))
|
|
154
|
+
log.success(f"{key} reset to default")
|
|
155
|
+
else:
|
|
156
|
+
log.error(f"No option: {key}")
|
|
157
|
+
|
|
158
|
+
def do_options(self, args):
|
|
159
|
+
"""Show current module options."""
|
|
160
|
+
if not self._active_module:
|
|
161
|
+
log.kv("TARGET", self.session.target)
|
|
162
|
+
log.kv("MODE", self._mode); return
|
|
163
|
+
rows = self._active_module.show_options()
|
|
164
|
+
log.table(["Option","Value","Required","Description"], rows,
|
|
165
|
+
title=f"Options: {self._active_module_path}")
|
|
166
|
+
|
|
167
|
+
def do_run(self, args):
|
|
168
|
+
"""Execute module or full chain. run | run auto"""
|
|
169
|
+
if args.strip().lower() == "auto":
|
|
170
|
+
self._run_auto_chain(); return
|
|
171
|
+
if not self._active_module:
|
|
172
|
+
log.error("No module selected. use <module> OR run auto"); return
|
|
173
|
+
ok, err = self._active_module.validate(self.session)
|
|
174
|
+
if not ok: log.error(f"Validation: {err}"); return
|
|
175
|
+
log.info(f"Running: {self._active_module_path}")
|
|
176
|
+
try:
|
|
177
|
+
result = self._active_module.run(self.session)
|
|
178
|
+
if result.success:
|
|
179
|
+
log.success("Module completed")
|
|
180
|
+
for k,v in result.data.items():
|
|
181
|
+
if not isinstance(v,(list,dict)): log.result(f"{k}: {v}")
|
|
182
|
+
else:
|
|
183
|
+
log.error(f"Module failed: {result.error}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
log.error(f"Module error: {e}"); import traceback; traceback.print_exc()
|
|
186
|
+
|
|
187
|
+
def _run_auto_chain(self):
|
|
188
|
+
"""Run the full attack chain — static discovery first, then context-driven."""
|
|
189
|
+
log.banner("AUTO ATTACK CHAIN")
|
|
190
|
+
log.info(f"Target: {self.session.target}")
|
|
191
|
+
log.info(f"Mode: {self._mode}")
|
|
192
|
+
log.info(f"Session: {self.session.session_id}\n")
|
|
193
|
+
|
|
194
|
+
from core.context_engine import context_engine
|
|
195
|
+
|
|
196
|
+
# Phase A: Static discovery chain (always runs)
|
|
197
|
+
static_chain = [
|
|
198
|
+
"discovery/http_enum",
|
|
199
|
+
"cloud/s3_enum",
|
|
200
|
+
"secrets/git_secrets",
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
total_modules = len(static_chain)
|
|
204
|
+
for i, path in enumerate(static_chain, 1):
|
|
205
|
+
self._run_module_in_chain(path, i, total_modules)
|
|
206
|
+
|
|
207
|
+
# Phase B: Context-driven dynamic chain
|
|
208
|
+
log.newline()
|
|
209
|
+
log.info("Building dynamic chain based on discoveries...")
|
|
210
|
+
dynamic_chain = context_engine.build_dynamic_chain(self.session)
|
|
211
|
+
log.info(f"Context engine selected {len(dynamic_chain)} additional modules:")
|
|
212
|
+
for m in dynamic_chain:
|
|
213
|
+
log.step(m)
|
|
214
|
+
log.newline()
|
|
215
|
+
|
|
216
|
+
for i, path in enumerate(dynamic_chain, len(static_chain) + 1):
|
|
217
|
+
self._run_module_in_chain(path, i, len(static_chain) + len(dynamic_chain))
|
|
218
|
+
|
|
219
|
+
# Final summary
|
|
220
|
+
print()
|
|
221
|
+
log.banner("ATTACK CHAIN COMPLETE")
|
|
222
|
+
s = self.session.summary()
|
|
223
|
+
score, label = risk_engine.session_score(self.session)
|
|
224
|
+
log.kv("Session ID", s["session_id"])
|
|
225
|
+
log.kv("Target", s["target"])
|
|
226
|
+
log.kv("Risk Score", f"{score}/10 [{label}]")
|
|
227
|
+
log.kv("Endpoints", str(s["endpoints"]))
|
|
228
|
+
log.kv("Exposed Files", str(s["exposed_files"]))
|
|
229
|
+
log.kv("Secrets Found", str(s["secrets"]))
|
|
230
|
+
log.kv("Findings", str(s["findings"]))
|
|
231
|
+
log.kv("CRITICAL", str(s["severity"]["CRITICAL"]))
|
|
232
|
+
log.kv("HIGH", str(s["severity"]["HIGH"]))
|
|
233
|
+
print()
|
|
234
|
+
log.info("View report: export html")
|
|
235
|
+
|
|
236
|
+
def _run_module_in_chain(self, path: str, step: int, total: int):
|
|
237
|
+
"""Execute a single module within the auto chain."""
|
|
238
|
+
cls = loader.get(path)
|
|
239
|
+
if not cls:
|
|
240
|
+
log.warning(f"Skipping (not found): {path}")
|
|
241
|
+
return
|
|
242
|
+
log.phase(step, total, path)
|
|
243
|
+
mod = cls()
|
|
244
|
+
for opt, key in [("TARGET", self.session.target), ("MODE", self._mode)]:
|
|
245
|
+
if opt in mod.OPTIONS:
|
|
246
|
+
mod.set_option(opt, key)
|
|
247
|
+
ok, err = mod.validate(self.session)
|
|
248
|
+
if not ok:
|
|
249
|
+
log.error(f"Skip {path}: {err}")
|
|
250
|
+
return
|
|
251
|
+
try:
|
|
252
|
+
result = mod.run(self.session)
|
|
253
|
+
if result.success:
|
|
254
|
+
for k, v in result.data.items():
|
|
255
|
+
if not isinstance(v, (list, dict)):
|
|
256
|
+
log.result(f"{k}: {v}")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
log.error(f"{path} error: {e}")
|
|
259
|
+
|
|
260
|
+
def do_show(self, args):
|
|
261
|
+
"""show modules | findings | secrets | sessions | attack-path | exploits | summary | options"""
|
|
262
|
+
arg = args.strip().lower()
|
|
263
|
+
if arg in ("","help"):
|
|
264
|
+
for cmd, desc in [("show modules","List all modules"),("show options","Current options"),
|
|
265
|
+
("show findings","Security findings"),("show secrets","Extracted credentials"),
|
|
266
|
+
("show sessions","All sessions"),("show attack-path","ASCII kill chain"),
|
|
267
|
+
("show exploits","Exploitation results"),("show summary","Stats + risk score")]:
|
|
268
|
+
log.kv(f" {cmd}", desc)
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
if arg == "modules":
|
|
272
|
+
for cat, mods in loader.all_modules().items():
|
|
273
|
+
if mods:
|
|
274
|
+
print(f"\n {Fore.YELLOW}{cat.upper()}{Style.RESET_ALL}")
|
|
275
|
+
print(f" {'─'*58}")
|
|
276
|
+
for m in mods:
|
|
277
|
+
sc = {"CRITICAL":Fore.RED,"HIGH":Fore.YELLOW,"MEDIUM":Fore.CYAN}.get(m["severity"],Fore.WHITE)
|
|
278
|
+
print(f" {Fore.CYAN}{m['path']:<38}{Style.RESET_ALL}"
|
|
279
|
+
f"{sc}{m['severity']:<10}{Style.RESET_ALL}{m['description'][:40]}")
|
|
280
|
+
print(f"\n Total: {loader.count()} modules | Tab-complete: use <Tab>")
|
|
281
|
+
|
|
282
|
+
elif arg == "options": self.do_options("")
|
|
283
|
+
|
|
284
|
+
elif arg == "findings":
|
|
285
|
+
if not self.session.findings: log.warning("No findings yet."); return
|
|
286
|
+
log.section(f"Findings ({len(self.session.findings)})")
|
|
287
|
+
for i,f in enumerate(self.session.findings,1):
|
|
288
|
+
print(f" {log.severity_badge(f.get('severity','INFO'))} {i:>3}. {f['title']}")
|
|
289
|
+
if f.get("cvss_score"): log.step(f"CVSS: {f['cvss_score']} | {f.get('mitre_technique','')}")
|
|
290
|
+
|
|
291
|
+
elif arg == "secrets":
|
|
292
|
+
if not self.session.secrets: log.warning("No secrets yet."); return
|
|
293
|
+
log.section(f"Secrets ({len(self.session.secrets)})")
|
|
294
|
+
for s in self.session.secrets:
|
|
295
|
+
log.secret(s["secret_type"], s["value"][:60])
|
|
296
|
+
log.step(f"Source: {s['source']}")
|
|
297
|
+
|
|
298
|
+
elif arg == "sessions": self.do_sessions("")
|
|
299
|
+
|
|
300
|
+
elif arg == "attack-path":
|
|
301
|
+
if not self.session.graph_nodes: log.warning("No graph yet. Run modules first."); return
|
|
302
|
+
attack_graph.build(self.session)
|
|
303
|
+
print(); print(attack_graph.print_ascii())
|
|
304
|
+
stats = attack_graph.stats()
|
|
305
|
+
print()
|
|
306
|
+
log.info(f"Nodes: {stats['nodes']} | Edges: {stats['edges']} | MITRE: {', '.join(stats['mitre_techniques'])}")
|
|
307
|
+
|
|
308
|
+
elif arg == "exploits":
|
|
309
|
+
if not self.session.exploit_results: log.warning("No exploits yet."); return
|
|
310
|
+
log.section(f"Exploit Results ({len(self.session.exploit_results)})")
|
|
311
|
+
for r in self.session.exploit_results:
|
|
312
|
+
st = f"{Fore.GREEN}[OK]{Style.RESET_ALL}" if r["success"] else f"{Fore.RED}[FAIL]{Style.RESET_ALL}"
|
|
313
|
+
print(f" {st} {r['action']}")
|
|
314
|
+
log.step(r["detail"])
|
|
315
|
+
|
|
316
|
+
elif arg == "summary":
|
|
317
|
+
s = self.session.summary()
|
|
318
|
+
score, label = risk_engine.session_score(self.session)
|
|
319
|
+
log.banner("SESSION SUMMARY")
|
|
320
|
+
for k,v in s.items():
|
|
321
|
+
if isinstance(v,dict):
|
|
322
|
+
for k2,v2 in v.items(): log.kv(f" {k2}", str(v2))
|
|
323
|
+
else: log.kv(k, str(v))
|
|
324
|
+
log.kv("Risk Score", f"{score}/10 [{label}]")
|
|
325
|
+
else:
|
|
326
|
+
log.error(f"Unknown: show {arg}")
|
|
327
|
+
|
|
328
|
+
def do_sessions(self, args):
|
|
329
|
+
"""sessions | sessions -i ID | sessions -k ID"""
|
|
330
|
+
arg = args.strip()
|
|
331
|
+
rows = session_manager.list_all()
|
|
332
|
+
if not arg or arg == "-l":
|
|
333
|
+
if not rows: log.warning("No sessions."); return
|
|
334
|
+
active_id = self.session.session_id if self.session else ""
|
|
335
|
+
for r in rows:
|
|
336
|
+
m = f"{Fore.GREEN}*{Style.RESET_ALL}" if r["session_id"]==active_id else " "
|
|
337
|
+
print(f" {m} {Fore.CYAN}{r['session_id']}{Style.RESET_ALL} | "
|
|
338
|
+
f"{r['name']:<15} | {r['target']:<35} | risk={r['risk_score']:.1f} | {r['status']}")
|
|
339
|
+
return
|
|
340
|
+
parts = arg.split()
|
|
341
|
+
if len(parts) >= 2:
|
|
342
|
+
flag, sid = parts[0], parts[1]
|
|
343
|
+
if flag == "-i":
|
|
344
|
+
s = session_manager.switch(sid)
|
|
345
|
+
log.success(f"Switched to: {sid}") if s else log.error(f"Not found: {sid}")
|
|
346
|
+
self._update_prompt()
|
|
347
|
+
elif flag == "-k":
|
|
348
|
+
log.success(f"Killed: {sid}") if session_manager.kill(sid) else log.error(f"Not found: {sid}")
|
|
349
|
+
|
|
350
|
+
def do_workspace(self, args):
|
|
351
|
+
"""workspace list | workspace new <name> <url> | workspace switch <id>"""
|
|
352
|
+
parts = args.strip().split(None, 2)
|
|
353
|
+
if not parts: log.error("Usage: workspace [list|new|switch]"); return
|
|
354
|
+
sub = parts[0].lower()
|
|
355
|
+
if sub == "list": self.do_sessions("")
|
|
356
|
+
elif sub == "new":
|
|
357
|
+
if len(parts) < 3: log.error("Usage: workspace new <name> <url>"); return
|
|
358
|
+
t = parts[2] if parts[2].startswith(("http://","https://")) else "http://"+parts[2]
|
|
359
|
+
s = session_manager.new(t, parts[1], self._mode)
|
|
360
|
+
log.success(f"Workspace '{parts[1]}' created → {t} [{s.session_id}]")
|
|
361
|
+
self._update_prompt()
|
|
362
|
+
elif sub == "switch":
|
|
363
|
+
if len(parts) < 2: log.error("Usage: workspace switch <id>"); return
|
|
364
|
+
self.do_sessions(f"-i {parts[1]}")
|
|
365
|
+
else: log.error(f"Unknown: workspace {sub}")
|
|
366
|
+
|
|
367
|
+
def do_export(self, args):
|
|
368
|
+
"""Export report. export html | export json | export all"""
|
|
369
|
+
fmt = args.strip().lower() or "all"
|
|
370
|
+
fmts = ["html","json"] if fmt=="all" else [fmt]
|
|
371
|
+
for f in fmts:
|
|
372
|
+
mod = loader.instantiate(f"html_report" if f=="html" else "json_export")
|
|
373
|
+
if mod: mod.run(self.session)
|
|
374
|
+
else: log.error(f"Reporting module not found: {f}")
|
|
375
|
+
|
|
376
|
+
def do_info(self, args):
|
|
377
|
+
"""Module details. info discovery/http_enum"""
|
|
378
|
+
path = args.strip() or self._active_module_path
|
|
379
|
+
if not path: log.error("Usage: info <module>"); return
|
|
380
|
+
cls = loader.get(path)
|
|
381
|
+
if not cls: log.error(f"Not found: {path}"); return
|
|
382
|
+
log.banner(f"MODULE: {path.upper()}")
|
|
383
|
+
for k,v in [("Name",cls.NAME),("Description",cls.DESCRIPTION),("Author",cls.AUTHOR),
|
|
384
|
+
("Version",cls.VERSION),("Category",cls.CATEGORY),("Severity",cls.SEVERITY),
|
|
385
|
+
("MITRE",", ".join(cls.MITRE) or "N/A"),("AWS Parallel",cls.AWS_PARALLEL or "N/A")]:
|
|
386
|
+
log.kv(k, v)
|
|
387
|
+
if cls.OPTIONS:
|
|
388
|
+
log.table(["Option","Default","Required","Description"], cls().show_options())
|
|
389
|
+
|
|
390
|
+
def do_search(self, args):
|
|
391
|
+
"""Search modules by keyword. search jwt | search s3 | search cloud"""
|
|
392
|
+
kw = args.strip().lower()
|
|
393
|
+
if not kw: log.error("Usage: search <keyword>"); return
|
|
394
|
+
results = [m for mods in loader.all_modules().values() for m in mods
|
|
395
|
+
if kw in m["name"] or kw in m["description"].lower()
|
|
396
|
+
or kw in m.get("mitre","").lower()]
|
|
397
|
+
if not results: log.warning(f"No results for: {kw}"); return
|
|
398
|
+
print(f"\n {Fore.YELLOW}Results for '{kw}':{Style.RESET_ALL}")
|
|
399
|
+
for m in results:
|
|
400
|
+
print(f" {Fore.CYAN}{m['path']:<38}{Style.RESET_ALL}{m['description'][:48]}")
|
|
401
|
+
|
|
402
|
+
def do_back(self, args):
|
|
403
|
+
"""Deselect current module."""
|
|
404
|
+
self._active_module = None; self._active_module_path = ""
|
|
405
|
+
self._update_prompt(); log.info("Returned to main console.")
|
|
406
|
+
|
|
407
|
+
def do_clear(self, args):
|
|
408
|
+
"""Clear screen."""
|
|
409
|
+
os.system("clear"); print_banner()
|
|
410
|
+
|
|
411
|
+
def do_exit(self, args):
|
|
412
|
+
"""Exit ExploitGraph."""
|
|
413
|
+
log.info("Exiting. Run 'export html' first if you haven't saved your report.")
|
|
414
|
+
if self.session: self.session.close()
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
def do_quit(self, args): return self.do_exit(args)
|
|
418
|
+
def do_EOF(self, args): print(); return self.do_exit(args)
|
|
419
|
+
def emptyline(self): pass
|
|
420
|
+
def default(self, line):
|
|
421
|
+
log.error(f"Unknown: {line.split()[0] if line.split() else line} (type 'help')")
|
|
422
|
+
|
|
423
|
+
def do_help(self, args):
|
|
424
|
+
"""Show all commands."""
|
|
425
|
+
if args: super().do_help(args); return
|
|
426
|
+
log.banner("EXPLOITGRAPH COMMANDS")
|
|
427
|
+
sections = [
|
|
428
|
+
("SESSION", [
|
|
429
|
+
(" workspace new <n> <url>", "Create named workspace"),
|
|
430
|
+
(" set TARGET <url>", "Set scan target"),
|
|
431
|
+
(" set MODE offensive|defensive", "Set scan mode"),
|
|
432
|
+
]),
|
|
433
|
+
("ATTACK", [
|
|
434
|
+
(" run auto", "Full automatic attack chain"),
|
|
435
|
+
(" use <module>", "Select a module (Tab to complete)"),
|
|
436
|
+
(" run", "Execute selected module"),
|
|
437
|
+
(" options", "Show module options"),
|
|
438
|
+
(" set OPTION value", "Configure option"),
|
|
439
|
+
(" back", "Deselect module"),
|
|
440
|
+
]),
|
|
441
|
+
("INFORMATION", [
|
|
442
|
+
(" show modules", "All modules by category"),
|
|
443
|
+
(" show findings", "Security findings"),
|
|
444
|
+
(" show secrets", "Extracted credentials"),
|
|
445
|
+
(" show attack-path", "ASCII kill chain"),
|
|
446
|
+
(" show summary", "Risk score + stats"),
|
|
447
|
+
(" info <module>", "Module details"),
|
|
448
|
+
(" search <keyword>", "Search modules"),
|
|
449
|
+
]),
|
|
450
|
+
("SESSIONS", [
|
|
451
|
+
(" sessions", "List all sessions"),
|
|
452
|
+
(" sessions -i <id>", "Switch to session"),
|
|
453
|
+
(" sessions -k <id>", "Kill session"),
|
|
454
|
+
]),
|
|
455
|
+
("OUTPUT", [
|
|
456
|
+
(" export html", "HTML report (D3.js graph)"),
|
|
457
|
+
(" export json", "JSON for SIEM/ticketing"),
|
|
458
|
+
(" export all", "All formats"),
|
|
459
|
+
]),
|
|
460
|
+
("MISC", [
|
|
461
|
+
(" clear", "Clear screen"),
|
|
462
|
+
(" exit / quit", "Exit framework"),
|
|
463
|
+
]),
|
|
464
|
+
]
|
|
465
|
+
for section, cmds in sections:
|
|
466
|
+
print(f"\n {Fore.YELLOW}{section}{Style.RESET_ALL}")
|
|
467
|
+
for name, desc in cmds: log.kv(name, desc)
|
|
468
|
+
print()
|
|
469
|
+
log.info("Quick start: workspace new test http://target.com → run auto")
|
core/context_engine.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ExploitGraph - Context Engine
|
|
3
|
+
Analyses what modules have found and decides dynamically which modules
|
|
4
|
+
to run next. Replaces the static 6-step chain with intelligent sequencing.
|
|
5
|
+
|
|
6
|
+
Logic:
|
|
7
|
+
Discovery → Cloud Enum → [if .gz found] CloudTrail Analysis
|
|
8
|
+
→ [if secrets found] Credential Validation
|
|
9
|
+
→ [if valid creds] IAM Enum + Priv-Esc
|
|
10
|
+
→ Reporting
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from core.session_manager import Session
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContextEngine:
|
|
20
|
+
"""
|
|
21
|
+
Examines session state after each module and returns the next
|
|
22
|
+
module path(s) to execute.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Static discovery chain — always runs in this order
|
|
26
|
+
DISCOVERY_CHAIN = [
|
|
27
|
+
"discovery/http_enum",
|
|
28
|
+
"cloud/s3_enum",
|
|
29
|
+
"secrets/git_secrets",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Always runs at the end
|
|
33
|
+
REPORTING_CHAIN = [
|
|
34
|
+
"reporting/html_report",
|
|
35
|
+
"reporting/json_export",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
def build_dynamic_chain(self, session: "Session") -> list[str]:
|
|
39
|
+
"""
|
|
40
|
+
Build the full module execution chain based on what was discovered.
|
|
41
|
+
Call this AFTER the discovery chain has run.
|
|
42
|
+
"""
|
|
43
|
+
chain = []
|
|
44
|
+
|
|
45
|
+
# ── Conditional: CloudTrail logs found ─────────────────────────────
|
|
46
|
+
if self._has_cloudtrail_logs(session):
|
|
47
|
+
chain.append("cloud/cloudtrail_analyzer")
|
|
48
|
+
|
|
49
|
+
# ── Conditional: Subdomains not yet checked ────────────────────────
|
|
50
|
+
if not self._already_ran("subdomain_enum", session):
|
|
51
|
+
chain.append("discovery/subdomain_enum")
|
|
52
|
+
|
|
53
|
+
# ── Conditional: Azure/GCP indicators in responses ─────────────────
|
|
54
|
+
body = self._all_response_text(session)
|
|
55
|
+
if "blob.core.windows.net" in body or "azurewebsites" in body:
|
|
56
|
+
chain.append("cloud/azure_enum")
|
|
57
|
+
if "storage.googleapis.com" in body or "appspot.com" in body:
|
|
58
|
+
chain.append("cloud/gcp_enum")
|
|
59
|
+
|
|
60
|
+
# ── Conditional: IMDS / metadata hints ─────────────────────────────
|
|
61
|
+
if "169.254.169.254" in body or "metadata" in body.lower():
|
|
62
|
+
chain.append("cloud/metadata_check")
|
|
63
|
+
|
|
64
|
+
# ── Always: scan for secrets in all discovered content ──────────────
|
|
65
|
+
chain.append("secrets/file_secrets")
|
|
66
|
+
|
|
67
|
+
# ── Conditional: AWS credentials found → validate them ──────────────
|
|
68
|
+
if self._has_aws_credentials(session):
|
|
69
|
+
chain.append("cloud/aws_credential_validator")
|
|
70
|
+
chain.append("cloud/iam_enum")
|
|
71
|
+
chain.append("cloud/iam_privilege_escalation")
|
|
72
|
+
|
|
73
|
+
# ── Conditional: Login/auth endpoints found → try exploitation ──────
|
|
74
|
+
if self._has_auth_endpoints(session):
|
|
75
|
+
chain.append("exploitation/api_exploit")
|
|
76
|
+
chain.append("exploitation/jwt_attack")
|
|
77
|
+
|
|
78
|
+
# ── Conditional: SSRF-prone parameters found ────────────────────────
|
|
79
|
+
if self._has_ssrf_params(session):
|
|
80
|
+
chain.append("exploitation/ssrf_scanner")
|
|
81
|
+
|
|
82
|
+
# ── Always: reporting at the end ────────────────────────────────────
|
|
83
|
+
chain.extend(self.REPORTING_CHAIN)
|
|
84
|
+
|
|
85
|
+
# Deduplicate while preserving order
|
|
86
|
+
seen = set()
|
|
87
|
+
return [x for x in chain if not (x in seen or seen.add(x))]
|
|
88
|
+
|
|
89
|
+
def next_modules(self, completed: str, session: "Session") -> list[str]:
|
|
90
|
+
"""
|
|
91
|
+
After a specific module completes, return immediate follow-up modules.
|
|
92
|
+
Used for real-time chaining during execution.
|
|
93
|
+
"""
|
|
94
|
+
follow_ups = []
|
|
95
|
+
|
|
96
|
+
if completed == "cloud/s3_enum":
|
|
97
|
+
if self._has_cloudtrail_logs(session):
|
|
98
|
+
follow_ups.append("cloud/cloudtrail_analyzer")
|
|
99
|
+
if session.exposed_files:
|
|
100
|
+
follow_ups.append("secrets/file_secrets")
|
|
101
|
+
|
|
102
|
+
elif completed in ("secrets/file_secrets", "cloud/cloudtrail_analyzer"):
|
|
103
|
+
if self._has_aws_credentials(session):
|
|
104
|
+
follow_ups.append("cloud/aws_credential_validator")
|
|
105
|
+
|
|
106
|
+
elif completed == "cloud/aws_credential_validator":
|
|
107
|
+
if self._has_valid_credentials(session):
|
|
108
|
+
follow_ups.extend(["cloud/iam_enum", "cloud/iam_privilege_escalation"])
|
|
109
|
+
|
|
110
|
+
return follow_ups
|
|
111
|
+
|
|
112
|
+
# ── Detection helpers ──────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
def _has_cloudtrail_logs(self, session: "Session") -> bool:
|
|
115
|
+
"""Check if any downloaded files are CloudTrail .json.gz logs."""
|
|
116
|
+
for f in session.exposed_files:
|
|
117
|
+
path = f.get("path", "") or f.get("url", "")
|
|
118
|
+
if "CloudTrail" in path or (
|
|
119
|
+
path.endswith(".json.gz") and "AWSLogs" in path
|
|
120
|
+
):
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def _has_aws_credentials(self, session: "Session") -> bool:
|
|
125
|
+
"""Check if any AWS access key + secret pair exists in secrets."""
|
|
126
|
+
types = {s["secret_type"] for s in session.secrets}
|
|
127
|
+
return "AWS_ACCESS_KEY" in types
|
|
128
|
+
|
|
129
|
+
def _has_valid_credentials(self, session: "Session") -> bool:
|
|
130
|
+
"""Check if credential validator confirmed valid credentials."""
|
|
131
|
+
for r in session.exploit_results:
|
|
132
|
+
if "credential_validator" in r.get("module", "") and r.get("success"):
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def _has_auth_endpoints(self, session: "Session") -> bool:
|
|
137
|
+
"""Check if login/auth endpoints were discovered."""
|
|
138
|
+
for ep in session.endpoints:
|
|
139
|
+
p = ep.get("path", "").lower()
|
|
140
|
+
if any(kw in p for kw in ["login", "auth", "token", "signin"]):
|
|
141
|
+
return True
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def _has_ssrf_params(self, session: "Session") -> bool:
|
|
145
|
+
"""Check if any discovered endpoints have SSRF-prone parameters."""
|
|
146
|
+
for ep in session.endpoints:
|
|
147
|
+
url = ep.get("url", "")
|
|
148
|
+
if any(p in url for p in ["url=", "redirect=", "proxy=", "fetch=", "dest="]):
|
|
149
|
+
return True
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def _already_ran(self, module_name: str, session: "Session") -> bool:
|
|
153
|
+
"""Check if a module already produced results in this session."""
|
|
154
|
+
for r in session.exploit_results:
|
|
155
|
+
if module_name in r.get("module", ""):
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def _all_response_text(self, session: "Session") -> str:
|
|
160
|
+
"""Concatenate all downloaded file content for pattern matching."""
|
|
161
|
+
parts = []
|
|
162
|
+
for f in session.exposed_files:
|
|
163
|
+
content = f.get("content", "") or ""
|
|
164
|
+
if isinstance(content, str):
|
|
165
|
+
parts.append(content[:500])
|
|
166
|
+
for ep in session.endpoints:
|
|
167
|
+
parts.append(ep.get("url", ""))
|
|
168
|
+
return " ".join(parts)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Global singleton
|
|
172
|
+
context_engine = ContextEngine()
|