printerxpl-forge 6.2.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.
Files changed (97) hide show
  1. nse/README.md +204 -0
  2. nse/__init__.py +6 -0
  3. nse/install_nse.py +412 -0
  4. nse/lib/printerxpl.lua +238 -0
  5. nse/scripts/cups-info.nse +74 -0
  6. nse/scripts/cups-queue-info.nse +43 -0
  7. nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
  8. nse/scripts/http-device-mac.nse +107 -0
  9. nse/scripts/http-hp-ilo-info.nse +121 -0
  10. nse/scripts/http-info-xerox-enum.nse +101 -0
  11. nse/scripts/http-vuln-cve2022-1026.nse +158 -0
  12. nse/scripts/lexmark-config.nse +89 -0
  13. nse/scripts/pjl-ready-message.nse +106 -0
  14. nse/scripts/printer-banner.nse +217 -0
  15. nse/scripts/printer-cups-rce.nse +189 -0
  16. nse/scripts/printer-cve-detect.nse +279 -0
  17. nse/scripts/printer-discover.nse +205 -0
  18. nse/scripts/printer-firmware-exposed.nse +219 -0
  19. nse/scripts/printer-hp-pjl.nse +192 -0
  20. nse/scripts/printer-http-ews.nse +293 -0
  21. nse/scripts/printer-ipp-info.nse +235 -0
  22. nse/scripts/printer-lexmark-ipp.nse +203 -0
  23. nse/scripts/printer-passback.nse +204 -0
  24. nse/scripts/printer-pjl-info.nse +146 -0
  25. nse/scripts/printer-printnightmare.nse +211 -0
  26. nse/scripts/printer-snmp-info.nse +176 -0
  27. nse/scripts/printer-vuln-check.nse +256 -0
  28. nse/scripts/snmp-device-mac.nse +93 -0
  29. nse/scripts/snmp-info.nse +146 -0
  30. nse/scripts/snmp-sysdescr.nse +70 -0
  31. printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
  32. printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
  33. printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
  34. printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
  35. printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
  36. printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
  37. src/assets/fonts/gunplay.pfa +1671 -0
  38. src/assets/fonts/kshandwrt.pfa +315 -0
  39. src/assets/fonts/laksoner.pfa +2402 -0
  40. src/assets/fonts/paintcans.pfa +9699 -0
  41. src/assets/fonts/stencilod.pfa +4076 -0
  42. src/assets/fonts/takecover.pfa +26138 -0
  43. src/assets/fonts/topsecret.pfa +6652 -0
  44. src/assets/fonts/whoa.pfa +773 -0
  45. src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
  46. src/assets/mibs/Printer-MIB +4389 -0
  47. src/assets/mibs/README.md +9 -0
  48. src/assets/mibs/SNMPv2-MIB +854 -0
  49. src/assets/overlays/hacker.eps +596 -0
  50. src/assets/overlays/smiley.eps +214 -0
  51. src/assets/overlays/smiley2.eps +240 -0
  52. src/core/attack_orchestrator.py +1025 -0
  53. src/core/capabilities.py +323 -0
  54. src/core/destructive_audit.py +430 -0
  55. src/core/discovery.py +488 -0
  56. src/core/osdetect.py +74 -0
  57. src/core/poly_runner.py +579 -0
  58. src/core/printer.py +1426 -0
  59. src/main.py +2134 -0
  60. src/modules/install_printer.py +318 -0
  61. src/modules/login_bruteforce.py +852 -0
  62. src/modules/pcl.py +506 -0
  63. src/modules/pjl.py +3575 -0
  64. src/modules/print_job.py +1290 -0
  65. src/modules/ps.py +1102 -0
  66. src/payloads/__init__.py +98 -0
  67. src/payloads/assets/overlays/notice.eps +9 -0
  68. src/protocols/__init__.py +19 -0
  69. src/protocols/firmware.py +738 -0
  70. src/protocols/ipp.py +216 -0
  71. src/protocols/ipp_attacks.py +609 -0
  72. src/protocols/lpd.py +141 -0
  73. src/protocols/network_map.py +1004 -0
  74. src/protocols/raw.py +173 -0
  75. src/protocols/smb.py +359 -0
  76. src/protocols/ssrf_pivot.py +427 -0
  77. src/protocols/storage.py +587 -0
  78. src/ui/__init__.py +6 -0
  79. src/ui/interactive.py +742 -0
  80. src/ui/spinner.py +112 -0
  81. src/ui/tables.py +132 -0
  82. src/utils/banner_grabber.py +852 -0
  83. src/utils/codebook.py +456 -0
  84. src/utils/config.py +522 -0
  85. src/utils/cve_loader.py +158 -0
  86. src/utils/default_creds.py +134 -0
  87. src/utils/discovery_online.py +1327 -0
  88. src/utils/exploit_manager.py +805 -0
  89. src/utils/fuzzer.py +220 -0
  90. src/utils/helper.py +732 -0
  91. src/utils/local_printers.py +307 -0
  92. src/utils/ml_engine.py +491 -0
  93. src/utils/operators.py +474 -0
  94. src/utils/ports.py +234 -0
  95. src/utils/vuln_scanner.py +823 -0
  96. src/utils/wordlist_loader.py +412 -0
  97. src/version.py +36 -0
@@ -0,0 +1,805 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrinterXPL-Forge — Exploit Manager
5
+ ================================
6
+ Loads, indexes, matches and executes exploits from the xpl/ directory.
7
+
8
+ Directory structure expected:
9
+ xpl/
10
+ ├── index.json # Auto-regenerated master index
11
+ ├── edb-15631/
12
+ │ ├── metadata.json # Exploit metadata
13
+ │ └── exploit.py # check() + run() functions
14
+ ├── custom/
15
+ │ ├── TEMPLATE.py # Reference template
16
+ │ └── my_exploit.py # User-created exploit
17
+ └── ...
18
+
19
+ Exploit contract (exploit.py must export):
20
+ - check(host, port, timeout) -> bool # non-destructive vulnerability check
21
+ - run(host, port, timeout, **opts) -> dict # exploit execution
22
+ - METADATA = {...} (optional — falls back to metadata.json)
23
+
24
+ Integration points:
25
+ - Called by vuln_scanner / banner_grabber to show matched exploits on scan
26
+ - Called by main.py for --xpl-list, --xpl-run, --xpl-check, --xpl-update
27
+ """
28
+ # Author : Andre Henrique (@mrhenrike)
29
+ # GitHub : https://github.com/mrhenrike
30
+ # LinkedIn : https://linkedin.com/in/mrhenrike
31
+ # X/Twitter : https://x.com/mrhenrike
32
+
33
+ from __future__ import annotations
34
+
35
+ import importlib.util
36
+ import json
37
+ import logging
38
+ import os
39
+ import re
40
+ import sys
41
+ import time
42
+ from dataclasses import dataclass, field
43
+ from pathlib import Path
44
+ from typing import Any, Callable, Dict, List, Optional, Tuple
45
+
46
+ _log = logging.getLogger(__name__)
47
+
48
+ # ── Paths ──────────────────────────────────────────────────────────────────────
49
+ # xpl/ sits at project root (one level above src/)
50
+ _SRC_DIR = Path(__file__).resolve().parent.parent # src/
51
+ _ROOT_DIR = _SRC_DIR.parent # project root
52
+ XPL_DIR = _ROOT_DIR / 'xpl'
53
+ INDEX_FILE = XPL_DIR / 'index.json'
54
+
55
+ # Severity sort order
56
+ _SEV_ORDER = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3, 'info': 4}
57
+
58
+ # ANSI colours
59
+ _RED = '\033[1;31m'
60
+ _YEL = '\033[1;33m'
61
+ _GRN = '\033[0;32m'
62
+ _CYN = '\033[1;36m'
63
+ _DIM = '\033[2;37m'
64
+ _RST = '\033[0m'
65
+ _SEV_CLR = {
66
+ 'critical': '\033[1;31m',
67
+ 'high': '\033[0;31m',
68
+ 'medium': '\033[1;33m',
69
+ 'low': '\033[1;34m',
70
+ 'info': '\033[2;37m',
71
+ }
72
+
73
+
74
+ # ── Data model ────────────────────────────────────────────────────────────────
75
+
76
+ @dataclass
77
+ class Exploit:
78
+ """Loaded exploit module with metadata."""
79
+ id: str
80
+ path: Path
81
+ metadata: Dict = field(default_factory=dict)
82
+
83
+ # Callable attributes set after load
84
+ _check_fn: Optional[Callable] = field(default=None, repr=False)
85
+ _run_fn: Optional[Callable] = field(default=None, repr=False)
86
+
87
+ @property
88
+ def title(self) -> str:
89
+ return self.metadata.get('title', self.id)
90
+
91
+ @property
92
+ def cve(self) -> str:
93
+ return self.metadata.get('cve', '')
94
+
95
+ @property
96
+ def severity(self) -> str:
97
+ return self.metadata.get('severity', 'info')
98
+
99
+ @property
100
+ def cvss(self) -> float:
101
+ try:
102
+ return float(self.metadata.get('cvss', 0))
103
+ except (TypeError, ValueError):
104
+ return 0.0
105
+
106
+ @property
107
+ def vendors(self) -> List[str]:
108
+ return self.metadata.get('vendor', [])
109
+
110
+ @property
111
+ def model_patterns(self) -> List[str]:
112
+ return self.metadata.get('model_patterns', [])
113
+
114
+ @property
115
+ def category(self) -> str:
116
+ return self.metadata.get('category', '')
117
+
118
+ @property
119
+ def protocol(self) -> str:
120
+ return self.metadata.get('protocol', '')
121
+
122
+ @property
123
+ def port(self) -> int:
124
+ return int(self.metadata.get('port', 0))
125
+
126
+ @property
127
+ def requires(self) -> List[str]:
128
+ return self.metadata.get('requires', [])
129
+
130
+ @property
131
+ def tags(self) -> List[str]:
132
+ return self.metadata.get('tags', [])
133
+
134
+ @property
135
+ def source_url(self) -> str:
136
+ return self.metadata.get('url', '')
137
+
138
+ def check(self, host: str, port: int = None,
139
+ timeout: float = 8) -> bool:
140
+ """Run the non-destructive vulnerability check."""
141
+ if not self._check_fn:
142
+ return False
143
+ p = port or self.port
144
+ try:
145
+ return bool(self._check_fn(host, p, timeout))
146
+ except Exception as exc:
147
+ _log.debug("[%s] check() error: %s", self.id, exc)
148
+ return False
149
+
150
+ def run(self, host: str, port: int = None, timeout: float = 15,
151
+ dry_run: bool = True, **kwargs) -> Dict:
152
+ """Execute the exploit."""
153
+ if not self._run_fn:
154
+ return {'success': False, 'error': 'exploit.py not loaded', 'evidence': ''}
155
+ p = port or self.port
156
+ try:
157
+ return self._run_fn(host, p, timeout, dry_run=dry_run, **kwargs)
158
+ except Exception as exc:
159
+ return {'success': False, 'error': str(exc)[:120], 'evidence': ''}
160
+
161
+
162
+ # ── Loader ────────────────────────────────────────────────────────────────────
163
+
164
+ def _load_metadata(directory: Path) -> Dict:
165
+ """Load metadata.json from an exploit directory."""
166
+ meta_path = directory / 'metadata.json'
167
+ if meta_path.exists():
168
+ try:
169
+ with open(meta_path, encoding='utf-8') as f:
170
+ return json.load(f)
171
+ except Exception as exc:
172
+ _log.warning("metadata.json error in %s: %s", directory.name, exc)
173
+ return {}
174
+
175
+
176
+ def _load_module(directory: Path) -> Tuple[Optional[Callable], Optional[Callable], Dict]:
177
+ """
178
+ Dynamically import exploit.py from directory.
179
+
180
+ Returns: (check_fn, run_fn, embedded_metadata)
181
+ """
182
+ py_path = directory / 'exploit.py'
183
+ if not py_path.exists():
184
+ return None, None, {}
185
+
186
+ try:
187
+ spec = importlib.util.spec_from_file_location(
188
+ f'xpl_{directory.name}', py_path
189
+ )
190
+ mod = importlib.util.module_from_spec(spec)
191
+ spec.loader.exec_module(mod)
192
+
193
+ check_fn = getattr(mod, 'check', None)
194
+ run_fn = getattr(mod, 'run', None)
195
+ meta = getattr(mod, 'METADATA', {})
196
+ return check_fn, run_fn, meta
197
+ except Exception as exc:
198
+ _log.warning("Failed to load %s: %s", py_path, exc)
199
+ return None, None, {}
200
+
201
+
202
+ def load_all_exploits(source_filter: str = None) -> List[Exploit]:
203
+ """
204
+ Discover and load all exploits from xpl/ directory tree.
205
+
206
+ Scans:
207
+ - Direct subdirectories of xpl/ (edb-15631/, etc.)
208
+ - xpl/custom/ — standalone .py exploits (user-created)
209
+ - xpl/msf/ — Metasploit-ported modules
210
+ - xpl/research/ — original research / PoC modules
211
+
212
+ Args:
213
+ source_filter: if set, only load exploits where metadata['source'] matches
214
+ one of: 'exploit-db', 'metasploit', 'research', 'custom'
215
+
216
+ Returns list of loaded Exploit instances, sorted by severity.
217
+ """
218
+ if not XPL_DIR.exists():
219
+ _log.warning("xpl/ directory not found at %s", XPL_DIR)
220
+ return []
221
+
222
+ exploits: List[Exploit] = []
223
+
224
+ # ── Category subfolders (msf/, research/) ─────────────────────────────────
225
+ _CATEGORY_DIRS = {'msf', 'research'}
226
+
227
+ for item in sorted(XPL_DIR.iterdir()):
228
+ if not item.is_dir() or item.name.startswith('.'):
229
+ continue
230
+
231
+ # standalone .py exploits (custom/)
232
+ if item.name == 'custom':
233
+ for sub in sorted(item.iterdir()):
234
+ if sub.is_file() and sub.suffix == '.py' and not sub.name.startswith('_'):
235
+ _load_single_py_exploit(sub, exploits)
236
+ continue
237
+
238
+ # category subdirectory (msf/, research/) — recurse one level
239
+ if item.name in _CATEGORY_DIRS:
240
+ for sub_dir in sorted(item.iterdir()):
241
+ if not sub_dir.is_dir() or sub_dir.name.startswith('.'):
242
+ continue
243
+ _load_dir_exploit(sub_dir, exploits)
244
+ continue
245
+
246
+ # Standard edb-XXXXX/ directory
247
+ _load_dir_exploit(item, exploits)
248
+
249
+ # Filter by source if requested
250
+ if source_filter:
251
+ sf = source_filter.lower()
252
+ exploits = [x for x in exploits
253
+ if x.metadata.get('source', '').lower() == sf]
254
+
255
+ return sorted(exploits, key=lambda x: _SEV_ORDER.get(x.severity, 99))
256
+
257
+
258
+ def _load_dir_exploit(directory: Path, exploits: list) -> None:
259
+ """Load a standard directory-based exploit (metadata.json + exploit.py)."""
260
+ meta_file = directory / 'metadata.json'
261
+ py_file = directory / 'exploit.py'
262
+ if not meta_file.exists() and not py_file.exists():
263
+ return
264
+
265
+ meta = _load_metadata(directory)
266
+ check_fn, run_fn, emb_meta = _load_module(directory)
267
+ merged_meta = {**meta, **emb_meta} if emb_meta else meta
268
+
269
+ exploit_id = merged_meta.get('id', directory.name.upper())
270
+ xpl = Exploit(
271
+ id=exploit_id,
272
+ path=directory,
273
+ metadata=merged_meta,
274
+ _check_fn=check_fn,
275
+ _run_fn=run_fn,
276
+ )
277
+ exploits.append(xpl)
278
+ _log.debug("Loaded exploit: %s", exploit_id)
279
+
280
+
281
+ def _load_single_py_exploit(py_path: Path, exploits: list) -> None:
282
+ """Load a standalone .py exploit file (custom/ style)."""
283
+ try:
284
+ spec = importlib.util.spec_from_file_location(f'xpl_custom_{py_path.stem}', py_path)
285
+ mod = importlib.util.module_from_spec(spec)
286
+ spec.loader.exec_module(mod)
287
+
288
+ meta = getattr(mod, 'METADATA', {})
289
+ check_fn = getattr(mod, 'check', None)
290
+ run_fn = getattr(mod, 'run', None)
291
+
292
+ if not meta and not check_fn:
293
+ return # Skip files without the required interface
294
+
295
+ exploit_id = meta.get('id', py_path.stem.upper())
296
+ xpl = Exploit(
297
+ id=exploit_id,
298
+ path=py_path.parent,
299
+ metadata=meta,
300
+ _check_fn=check_fn,
301
+ _run_fn=run_fn,
302
+ )
303
+ exploits.append(xpl)
304
+ except Exception as exc:
305
+ _log.debug("Custom exploit load error %s: %s", py_path.name, exc)
306
+
307
+
308
+ # ── Matcher ───────────────────────────────────────────────────────────────────
309
+
310
+ def match_exploits(
311
+ exploits: List[Exploit],
312
+ make: str = '',
313
+ model: str = '',
314
+ firmware: str = '',
315
+ open_ports: List[int] = None,
316
+ langs: List[str] = None,
317
+ cves: List[str] = None,
318
+ ) -> List[Exploit]:
319
+ """
320
+ Filter exploits matching a specific printer fingerprint.
321
+
322
+ Matching criteria (ANY match = included):
323
+ - CVE match against known CVE list
324
+ - Vendor/make name match (case-insensitive)
325
+ - Model pattern regex match
326
+ - Port requirement satisfied
327
+ - Language requirement satisfied
328
+ - Firmware pattern match
329
+
330
+ Returns sorted list of matched exploits (critical first).
331
+ """
332
+ ports = set(open_ports or [])
333
+ langs_upper = {l.upper() for l in (langs or [])}
334
+ cves_upper = {c.upper() for c in (cves or [])}
335
+ target_str = f"{make} {model} {firmware}".lower()
336
+
337
+ matched = []
338
+ for xpl in exploits:
339
+ score = 0
340
+
341
+ # CVE match
342
+ if xpl.cve and xpl.cve.upper() in cves_upper:
343
+ score += 10
344
+
345
+ # Vendor match
346
+ vendors_lower = [v.lower() for v in xpl.vendors]
347
+ if make.lower() and any(v in make.lower() or make.lower() in v
348
+ for v in vendors_lower if v):
349
+ score += 5
350
+
351
+ # Model pattern match
352
+ for pat in xpl.model_patterns:
353
+ if pat and re.search(pat, target_str, re.I):
354
+ score += 3
355
+ break
356
+
357
+ # Firmware pattern match
358
+ for pat in xpl.metadata.get('firmware_patterns', []):
359
+ if pat and re.search(pat, firmware, re.I):
360
+ score += 2
361
+ break
362
+
363
+ # Port requirement match
364
+ req_ports_met = True
365
+ for req in xpl.requires:
366
+ if req.startswith('port:'):
367
+ p = int(req.split(':')[1])
368
+ if p not in ports:
369
+ req_ports_met = False
370
+ break
371
+ elif req.startswith('lang:'):
372
+ lang = req.split(':')[1].upper()
373
+ if lang not in langs_upper:
374
+ score = max(score - 1, 0) # penalise but don't exclude
375
+
376
+ if not req_ports_met:
377
+ continue
378
+
379
+ # If vendors specified but none match, still include if port matches
380
+ if xpl.vendors and score == 0 and ports & {xpl.port}:
381
+ score = 1 # low-confidence match
382
+
383
+ # No vendor specified = generic (matches anything with right port)
384
+ if not xpl.vendors and ports & {xpl.port}:
385
+ score = max(score, 1)
386
+
387
+ if score > 0:
388
+ matched.append((score, xpl))
389
+
390
+ # Sort: higher score first, then severity
391
+ matched.sort(key=lambda x: (-x[0], _SEV_ORDER.get(x[1].severity, 99)))
392
+ return [x[1] for x in matched]
393
+
394
+
395
+ # ── Printer ───────────────────────────────────────────────────────────────────
396
+
397
+ _SRC_BADGE = {
398
+ 'metasploit': '\033[1;35m[MSF]\033[0m',
399
+ 'exploit-db': '\033[1;34m[EDB]\033[0m',
400
+ 'research': '\033[1;33m[RES]\033[0m',
401
+ 'custom': '\033[1;32m[USR]\033[0m',
402
+ 'nvd': '\033[1;36m[NVD]\033[0m',
403
+ }
404
+
405
+
406
+ def print_exploit_list(exploits: List[Exploit], title: str = 'Available Exploits') -> None:
407
+ """Pretty-print a list of exploits to stdout."""
408
+ if not exploits:
409
+ print(f" {_DIM}No exploits found.{_RST}")
410
+ return
411
+
412
+ # Group by source for summary
413
+ by_source: Dict[str, int] = {}
414
+ for x in exploits:
415
+ s = x.metadata.get('source', 'unknown')
416
+ by_source[s] = by_source.get(s, 0) + 1
417
+
418
+ print(f"\n {_CYN}{'='*72}{_RST}")
419
+ print(f" {_CYN}{title} ({len(exploits)}){_RST}")
420
+ src_summary = ' '.join(
421
+ f"{_SRC_BADGE.get(s, f'[{s}]')} {n}"
422
+ for s, n in sorted(by_source.items())
423
+ )
424
+ print(f" {src_summary}")
425
+ print(f" {_CYN}{'='*72}{_RST}")
426
+ print(f" {'SRC':<6} {'ID':<30} {'SEV':<10} {'CVSS':<6} {'CAT':<16} TITLE")
427
+ print(f" {'-'*72}")
428
+ for xpl in exploits:
429
+ sev_clr = _SEV_CLR.get(xpl.severity, _DIM)
430
+ cve_str = f"[{xpl.cve}]" if xpl.cve else ''
431
+ src = xpl.metadata.get('source', '?')
432
+ badge = _SRC_BADGE.get(src, f'[{src[:3].upper()}]')
433
+ print(f" {badge} {_GRN}{xpl.id:<30}{_RST} "
434
+ f"{sev_clr}{xpl.severity:<10}{_RST} "
435
+ f"{xpl.cvss:<6.1f} "
436
+ f"{xpl.category:<16} "
437
+ f"{xpl.title[:35]}")
438
+ if cve_str:
439
+ print(f" {'':6} {'':30} {_DIM}{cve_str}{_RST}")
440
+ print()
441
+
442
+
443
+ def print_matched_exploits(matched: List[Exploit], target: str) -> None:
444
+ """Print matched exploits for a scanned target (compact format for --scan output)."""
445
+ if not matched:
446
+ return
447
+
448
+ print(f"\n {_RED}{'!'*3} EXPLOITS AVAILABLE FOR {target} {'!'*3}{_RST}")
449
+ print(f" {'-'*65}")
450
+ for xpl in matched[:10]:
451
+ sev_clr = _SEV_CLR.get(xpl.severity, _DIM)
452
+ cve_str = f" CVE: {xpl.cve}" if xpl.cve else ''
453
+ ref_str = f" {_DIM}{xpl.source_url[:55]}{_RST}" if xpl.source_url else ''
454
+ print(f" {sev_clr}[{xpl.severity.upper()}]{_RST} {_GRN}{xpl.id}{_RST} {xpl.title}")
455
+ if cve_str:
456
+ print(f" {_DIM}{cve_str}{_RST}")
457
+ if ref_str:
458
+ print(ref_str)
459
+ print(f" {_DIM}Run: python src/main.py {target} --xpl-run {xpl.id} --dry{_RST}")
460
+ print()
461
+
462
+
463
+ def print_run_result(result: Dict, exploit_id: str) -> None:
464
+ """Print the result of running an exploit."""
465
+ success = result.get('success', False)
466
+ evidence = result.get('evidence', '')
467
+ error = result.get('error', '')
468
+ creds = result.get('credentials', [])
469
+ files = result.get('files', [])
470
+ vuln = result.get('vulnerable', success)
471
+
472
+ icon = f"{_RED}[EXPLOITED]{_RST}" if (success and not result.get('dry_run')) else (
473
+ f"{_YEL}[VULN]{_RST}" if vuln else
474
+ f"{_GRN}[OK / NOT VULN]{_RST}")
475
+
476
+ print(f"\n {icon} {exploit_id}")
477
+ if evidence:
478
+ print(f" Evidence:")
479
+ for line in evidence.strip().splitlines():
480
+ print(f" {line}")
481
+ if creds:
482
+ print(f"\n {_RED}Credentials / Sensitive Data:{_RST}")
483
+ for c in creds[:10]:
484
+ print(f" {c}")
485
+ if files:
486
+ print(f"\n Files ({len(files)}):")
487
+ for f in files[:15]:
488
+ print(f" {f.get('type','?'):5} {f.get('path','?')}")
489
+ if error:
490
+ print(f" {_DIM}Error: {error}{_RST}")
491
+ print()
492
+
493
+
494
+ # ── Update / download ─────────────────────────────────────────────────────────
495
+
496
+ def update_index(exploits: List[Exploit]) -> None:
497
+ """Regenerate xpl/index.json from loaded exploits."""
498
+ index = {
499
+ '_comment': 'PrinterXPL-Forge exploit index. Auto-regenerated by --xpl-update.',
500
+ '_version': '2.0',
501
+ '_generated': time.strftime('%Y-%m-%d'),
502
+ 'exploits': [
503
+ {
504
+ 'id': x.id,
505
+ 'source': x.metadata.get('source', 'unknown'),
506
+ 'path': str(x.path.relative_to(XPL_DIR)),
507
+ 'cve': x.cve,
508
+ 'title': x.title,
509
+ 'severity': x.severity,
510
+ 'vendor': x.vendors,
511
+ 'category': x.category,
512
+ 'port': x.port,
513
+ 'protocol': x.protocol,
514
+ 'cvss': x.cvss,
515
+ 'url': x.source_url,
516
+ }
517
+ for x in exploits
518
+ ],
519
+ }
520
+ with open(INDEX_FILE, 'w', encoding='utf-8') as f:
521
+ json.dump(index, f, indent=2)
522
+ # Print breakdown by source
523
+ by_src: Dict[str, int] = {}
524
+ for x in exploits:
525
+ s = x.metadata.get('source', 'unknown')
526
+ by_src[s] = by_src.get(s, 0) + 1
527
+ src_str = ', '.join(f'{s}:{n}' for s, n in sorted(by_src.items()))
528
+ print(f" {_GRN}[+]{_RST} index.json updated — {len(exploits)} exploits [{src_str}]")
529
+
530
+
531
+ def fetch_exploit_db_raw(edb_id: str, output_dir: Path = None) -> Optional[Path]:
532
+ """
533
+ Download a raw exploit from ExploitDB by ID.
534
+
535
+ Creates xpl/edb-{id}/ directory and saves the raw file.
536
+ Returns the path to the saved file, or None on failure.
537
+ """
538
+ try:
539
+ import requests
540
+ import urllib3
541
+ urllib3.disable_warnings()
542
+
543
+ url = f"https://www.exploit-db.com/raw/{edb_id}"
544
+ r = requests.get(url, timeout=20, verify=False,
545
+ headers={'User-Agent': 'PrinterXPL-Forge/3.15'})
546
+ if r.status_code != 200:
547
+ print(f" {_YEL}[!]{_RST} EDB-{edb_id}: HTTP {r.status_code}")
548
+ return None
549
+
550
+ save_dir = output_dir or (XPL_DIR / f'edb-{edb_id}')
551
+ save_dir.mkdir(parents=True, exist_ok=True)
552
+
553
+ # Determine extension from content
554
+ content = r.text
555
+ ext = '.rb' if 'Msf::' in content else '.py' if 'import ' in content else '.txt'
556
+ out = save_dir / f'raw_edb-{edb_id}{ext}'
557
+ out.write_text(content, encoding='utf-8')
558
+
559
+ print(f" {_GRN}[+]{_RST} EDB-{edb_id}: saved to {out}")
560
+ return out
561
+ except Exception as exc:
562
+ print(f" {_YEL}[!]{_RST} EDB-{edb_id}: download failed — {exc}")
563
+ return None
564
+
565
+
566
+ # ── Convenience API ───────────────────────────────────────────────────────────
567
+
568
+ def get_matched_for_target(
569
+ make: str,
570
+ model: str,
571
+ firmware: str = '',
572
+ open_ports: List[int] = None,
573
+ langs: List[str] = None,
574
+ cves: List[str] = None,
575
+ source_filter: str = None,
576
+ ) -> List[Exploit]:
577
+ """
578
+ One-shot: load all exploits and return those matching the target.
579
+
580
+ Called from banner_grabber / vuln_scanner / scan pipeline.
581
+
582
+ Args:
583
+ source_filter: optionally restrict to 'metasploit', 'exploit-db',
584
+ 'research', or 'custom'.
585
+ """
586
+ all_exploits = load_all_exploits(source_filter=source_filter)
587
+ return match_exploits(
588
+ all_exploits,
589
+ make=make, model=model, firmware=firmware,
590
+ open_ports=open_ports, langs=langs, cves=cves,
591
+ )
592
+
593
+
594
+ def list_by_source(source: str) -> List[Exploit]:
595
+ """Return all exploits matching a specific source label."""
596
+ return load_all_exploits(source_filter=source)
597
+
598
+
599
+ # ── Auto Exploit ───────────────────────────────────────────────────────────────
600
+
601
+ from dataclasses import dataclass as _dc, field as _field
602
+
603
+
604
+ @_dc
605
+ class AutoExploitResult:
606
+ """Result of an auto_exploit() run for a single exploit module."""
607
+ exploit: Exploit
608
+ vulnerable: bool
609
+ ran: bool = False
610
+ result: Dict = _field(default_factory=dict)
611
+ params: Dict = _field(default_factory=dict) # pre-filled params used
612
+
613
+
614
+ def auto_exploit(
615
+ host: str,
616
+ *,
617
+ make: str = '',
618
+ model: str = '',
619
+ firmware: str = '',
620
+ open_ports: List[int] = None,
621
+ langs: List[str] = None,
622
+ cves: List[str] = None,
623
+ serial: str = '',
624
+ mac: str = '',
625
+ source_filter: str = None,
626
+ custom_xpl_path: str = None,
627
+ dry_run: bool = True,
628
+ check_limit: int = 8,
629
+ run_top_n: int = 1,
630
+ timeout: float = 10.0,
631
+ verbose: bool = True,
632
+ ) -> List[AutoExploitResult]:
633
+ """
634
+ Automatic exploit selection, verification, and execution.
635
+
636
+ Algorithm:
637
+ 1. Load all available exploits (or from custom_xpl_path if provided).
638
+ 2. Match against the target fingerprint (make/model/firmware/ports/cves).
639
+ 3. Sort candidates by CVSS score descending.
640
+ 4. Run check() on the top `check_limit` candidates (non-destructive).
641
+ 5. For confirmed-vulnerable exploits, pre-fill all required parameters
642
+ (host, port, serial, mac, vendor) automatically.
643
+ 6. Execute run() on the top `run_top_n` confirmed exploits (dry-run by default).
644
+ 7. Return ordered list of AutoExploitResult.
645
+
646
+ Args:
647
+ host: Target IP or hostname.
648
+ make: Vendor name from fingerprint (e.g. 'Epson').
649
+ model: Model string from fingerprint (e.g. 'L3250 Series').
650
+ firmware: Firmware version string.
651
+ open_ports: List of open ports detected during scan.
652
+ langs: Printer languages supported (e.g. ['PJL', 'PS']).
653
+ cves: Known CVEs from NVD scan.
654
+ serial: Device serial number (from --bf-serial or scan).
655
+ mac: Device MAC address (from --bf-mac or scan).
656
+ source_filter: Restrict to 'exploit-db', 'metasploit', 'research', 'custom'.
657
+ custom_xpl_path: Path to a custom exploit .py file to force-use.
658
+ dry_run: If True, run() is called in dry-run mode (no destructive actions).
659
+ check_limit: Maximum number of candidates to probe with check().
660
+ run_top_n: Execute run() on this many top confirmed-vulnerable exploits.
661
+ timeout: Socket timeout for check/run calls.
662
+ verbose: Print progress to stdout.
663
+
664
+ Returns:
665
+ List of AutoExploitResult, ordered by CVSS descending.
666
+ Only entries where vulnerable=True and ran=True have result populated.
667
+ """
668
+ results: List[AutoExploitResult] = []
669
+
670
+ # ── Step 1 & 2: load and match ──────────────────────────────────────────
671
+ if custom_xpl_path:
672
+ from pathlib import Path as _Path
673
+ candidates: List[Exploit] = []
674
+ _load_single_py_exploit(_Path(custom_xpl_path), candidates)
675
+ if not candidates:
676
+ if verbose:
677
+ print(f" {_YEL}[!]{_RST} Could not load custom exploit from {custom_xpl_path}")
678
+ return []
679
+ else:
680
+ all_xpls = load_all_exploits(source_filter=source_filter)
681
+ candidates = match_exploits(
682
+ all_xpls,
683
+ make=make, model=model, firmware=firmware,
684
+ open_ports=open_ports, langs=langs, cves=cves,
685
+ )
686
+
687
+ if not candidates:
688
+ if verbose:
689
+ print(f" {_DIM}[auto-exploit] No matching exploits found for {make} {model}{_RST}")
690
+ return []
691
+
692
+ if verbose:
693
+ print(f"\n {_CYN}{'='*65}{_RST}")
694
+ print(f" {_CYN}AUTO EXPLOIT — {host}{_RST}")
695
+ print(f" Target : {make} {model} {firmware}".strip())
696
+ print(f" Matched : {len(candidates)} exploit(s)")
697
+ print(f" Mode : {'DRY-RUN' if dry_run else 'LIVE'}")
698
+ print(f" {_CYN}{'='*65}{_RST}")
699
+
700
+ # ── Step 3: sort by CVSS ────────────────────────────────────────────────
701
+ candidates.sort(key=lambda x: (-x.cvss, _SEV_ORDER.get(x.severity, 99)))
702
+
703
+ # ── Step 4: check() — verify vulnerability ──────────────────────────────
704
+ confirmed: List[Exploit] = []
705
+ probed = 0
706
+ for xpl in candidates:
707
+ if probed >= check_limit:
708
+ break
709
+ probed += 1
710
+
711
+ port = xpl.port or (open_ports[0] if open_ports else 9100)
712
+ sev_clr = _SEV_CLR.get(xpl.severity, _DIM)
713
+
714
+ if verbose:
715
+ print(f" {_DIM}[check]{_RST} {sev_clr}{xpl.severity.upper():<8}{_RST} "
716
+ f"CVSS {xpl.cvss:<5.1f} {_GRN}{xpl.id}{_RST} {xpl.title[:40]}")
717
+
718
+ vuln = xpl.check(host, port=port, timeout=timeout)
719
+
720
+ result_entry = AutoExploitResult(
721
+ exploit = xpl,
722
+ vulnerable = vuln,
723
+ params = _prefill_params(xpl, host, port, serial, mac, make),
724
+ )
725
+
726
+ if verbose:
727
+ icon = f"{_RED}[VULN]{_RST}" if vuln else f"{_GRN}[NOT VULN]{_RST}"
728
+ print(f" {icon}")
729
+
730
+ results.append(result_entry)
731
+ if vuln:
732
+ confirmed.append(xpl)
733
+
734
+ # ── Step 5 & 6: run() on top confirmed ──────────────────────────────────
735
+ run_count = 0
736
+ for res in results:
737
+ if not res.vulnerable:
738
+ continue
739
+ if run_count >= run_top_n:
740
+ break
741
+ run_count += 1
742
+
743
+ xpl = res.exploit
744
+ port = res.params.get('port', xpl.port or 9100)
745
+ extra = {k: v for k, v in res.params.items() if k not in ('host', 'port')}
746
+
747
+ if verbose:
748
+ mode_str = f"{_YEL}DRY-RUN{_RST}" if dry_run else f"{_RED}LIVE{_RST}"
749
+ print(f"\n {mode_str} Running {_GRN}{xpl.id}{_RST} against {host}:{port}")
750
+
751
+ run_result = xpl.run(host, port=port, timeout=timeout, dry_run=dry_run, **extra)
752
+ res.ran = True
753
+ res.result = run_result
754
+ print_run_result(run_result, xpl.id)
755
+
756
+ if verbose and not any(r.vulnerable for r in results):
757
+ print(f"\n {_GRN}[OK]{_RST} No confirmed vulnerabilities in the top {probed} checked exploits.")
758
+
759
+ return results
760
+
761
+
762
+ def _prefill_params(xpl: Exploit, host: str, port: int,
763
+ serial: str, mac: str, make: str) -> Dict:
764
+ """
765
+ Build the kwargs dict to pass to exploit run(), pre-filling known values.
766
+
767
+ Maps standard exploit.requires keys (serial, mac, vendor) to values
768
+ extracted from fingerprint / CLI arguments.
769
+ """
770
+ params: Dict = {
771
+ 'host': host,
772
+ 'port': port or xpl.port or 9100,
773
+ }
774
+ requires = xpl.requires or []
775
+ for req in requires:
776
+ if req == 'serial' and serial:
777
+ params['serial'] = serial
778
+ elif req == 'mac' and mac:
779
+ params['mac'] = mac
780
+ elif req == 'vendor' and make:
781
+ params['vendor'] = make.lower()
782
+ return params
783
+
784
+
785
+ def print_auto_exploit_summary(results: List[AutoExploitResult]) -> None:
786
+ """Print a summary table of all auto-exploit results."""
787
+ if not results:
788
+ return
789
+ vuln_count = sum(1 for r in results if r.vulnerable)
790
+ ran_count = sum(1 for r in results if r.ran)
791
+
792
+ print(f"\n {_CYN}{'='*65}{_RST}")
793
+ print(f" {_CYN}AUTO EXPLOIT SUMMARY{_RST}")
794
+ print(f" Checked : {len(results)} exploit(s)")
795
+ print(f" Vulnerable : {_RED if vuln_count else _GRN}{vuln_count}{_RST}")
796
+ print(f" Executed : {ran_count}")
797
+ print(f" {_CYN}{'='*65}{_RST}")
798
+ print(f" {'EXPLOIT':<30} {'CVSS':<6} {'VULN':<8} {'RAN':<5} STATUS")
799
+ print(f" {'-'*65}")
800
+ for r in sorted(results, key=lambda x: -x.exploit.cvss):
801
+ vuln_icon = f"{_RED}YES{_RST}" if r.vulnerable else f"{_GRN}no{_RST}"
802
+ ran_icon = "YES" if r.ran else "no"
803
+ status = r.result.get('output', '')[:30] if r.result else ''
804
+ print(f" {r.exploit.id:<30} {r.exploit.cvss:<6.1f} {vuln_icon:<8} {ran_icon:<5} {status}")
805
+ print()