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
src/ui/interactive.py ADDED
@@ -0,0 +1,742 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrinterXPL-Forge — Interactive Guided CLI
5
+ =======================================
6
+ Provides a guided menu-driven interface for operators who prefer
7
+ not to memorize CLI flags. Every option maps directly to a CLI
8
+ command shown on screen before execution.
9
+
10
+ Launch: python src/main.py (no arguments)
11
+ python src/main.py --interactive
12
+ """
13
+ # Author : Andre Henrique (@mrhenrike)
14
+ # GitHub : https://github.com/mrhenrike
15
+ # LinkedIn : https://linkedin.com/in/mrhenrike
16
+ # X/Twitter : https://x.com/mrhenrike
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import shlex
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ import textwrap
26
+ from typing import List, Optional, Tuple
27
+
28
+ # ── ANSI palette ──────────────────────────────────────────────────────────────
29
+ _RST = '\033[0m'
30
+ _BLD = '\033[1m'
31
+ _DIM = '\033[2;37m'
32
+ _CYN = '\033[1;36m'
33
+ _GRN = '\033[1;32m'
34
+ _YEL = '\033[1;33m'
35
+ _RED = '\033[1;31m'
36
+ _MGT = '\033[1;35m'
37
+ _BLU = '\033[1;34m'
38
+ _WHT = '\033[1;37m'
39
+
40
+ W = shutil.get_terminal_size((80, 24)).columns
41
+
42
+ # ── Session state (persists across menu actions) ──────────────────────────────
43
+ _session: dict = {
44
+ 'target': '',
45
+ 'vendor': '',
46
+ 'serial': '',
47
+ }
48
+
49
+
50
+ # ── Low-level I/O helpers ────────────────────────────────────────────────────
51
+
52
+ def _clr() -> None:
53
+ os.system('cls' if os.name == 'nt' else 'clear')
54
+
55
+
56
+ def _hr(char: str = '─', color: str = _DIM) -> None:
57
+ print(f" {color}{char * min(64, W - 4)}{_RST}")
58
+
59
+
60
+ def _ask(prompt: str, default: str = '') -> str:
61
+ """Prompt for input, returning default on blank enter."""
62
+ hint = f" [{_DIM}{default}{_RST}]" if default else ''
63
+ try:
64
+ val = input(f" {_CYN}?{_RST} {prompt}{hint}: ").strip()
65
+ except (EOFError, KeyboardInterrupt):
66
+ print()
67
+ return default
68
+ return val or default
69
+
70
+
71
+ def _ask_yn(prompt: str, default: bool = False) -> bool:
72
+ hint = f"[{_GRN}Y{_RST}/{_DIM}n{_RST}]" if default else f"[{_DIM}y{_RST}/{_GRN}N{_RST}]"
73
+ try:
74
+ val = input(f" {_CYN}?{_RST} {prompt} {hint}: ").strip().lower()
75
+ except (EOFError, KeyboardInterrupt):
76
+ print()
77
+ return default
78
+ if not val:
79
+ return default
80
+ return val.startswith('y')
81
+
82
+
83
+ def _choose(options: List[Tuple[str, str]], title: str = '',
84
+ allow_back: bool = True) -> Optional[str]:
85
+ """
86
+ Display numbered menu and return the selected key.
87
+
88
+ Args:
89
+ options: List of (key, label) pairs.
90
+ title: Section title.
91
+ allow_back: Whether to add a [B]ack/[Q]uit option.
92
+ Returns:
93
+ key string, or None if user chose back/quit.
94
+ """
95
+ if title:
96
+ print(f"\n {_CYN}{_BLD}{title}{_RST}")
97
+ _hr()
98
+
99
+ for i, (key, label) in enumerate(options, 1):
100
+ print(f" {_YEL}[{i}]{_RST} {label}")
101
+
102
+ if allow_back:
103
+ print(f"\n {_DIM}[0] ← Back / Main menu{_RST}")
104
+
105
+ print()
106
+ while True:
107
+ try:
108
+ raw = input(f" {_CYN}▶{_RST} Select: ").strip()
109
+ except (EOFError, KeyboardInterrupt):
110
+ print()
111
+ return None
112
+ if raw in ('0', 'b', 'B', 'q', 'Q', ''):
113
+ return None
114
+ try:
115
+ idx = int(raw) - 1
116
+ if 0 <= idx < len(options):
117
+ return options[idx][0]
118
+ except ValueError:
119
+ pass
120
+ print(f" {_DIM} Invalid choice — try again{_RST}")
121
+
122
+
123
+ def _print_cmd(cmd: List[str]) -> None:
124
+ """Show the equivalent CLI command before execution."""
125
+ print()
126
+ print(f" {_DIM}Running command:{_RST}")
127
+ print(f" {_BLU}$ python src/main.py {' '.join(cmd)}{_RST}")
128
+ print()
129
+
130
+
131
+ def _run_cmd(cmd: List[str], pause: bool = True) -> None:
132
+ """Execute a main.py command in subprocess and show output."""
133
+ _print_cmd(cmd)
134
+ py = sys.executable
135
+ full = [py, '-W', 'ignore', 'src/main.py'] + cmd
136
+ try:
137
+ subprocess.run(full, check=False)
138
+ except KeyboardInterrupt:
139
+ print(f"\n {_YEL}[!] Interrupted{_RST}")
140
+ if pause:
141
+ print()
142
+ try:
143
+ input(f" {_DIM}Press Enter to continue...{_RST}")
144
+ except (EOFError, KeyboardInterrupt):
145
+ pass
146
+
147
+
148
+ # ── Sections ──────────────────────────────────────────────────────────────────
149
+
150
+ def _banner_mini() -> None:
151
+ """Compact banner for interactive mode."""
152
+ from version import __version__, __release_date__
153
+ print()
154
+ print(f" {_RED}██████{_RST}{_WHT}╗ {_RED}███████╗{_RST}{_WHT}╗{_RST} PrinterXPL-Forge "
155
+ f"{_DIM}v{__version__} ({__release_date__}){_RST}")
156
+ print(f" {_RED}██╔══██{_RST}{_WHT}╗{_RED}██╔════╝{_RST} "
157
+ f"{_DIM}Advanced Printer Penetration Testing{_RST}")
158
+ print(f" {_RED}██████╔╝{_RED}█████╗ {_RST} "
159
+ f"{_DIM}@mrhenrike · linkedin.com/in/mrhenrike{_RST}")
160
+ print()
161
+ _hr('═', _CYN)
162
+ print()
163
+
164
+
165
+ def _target_prompt(current: str = '') -> str:
166
+ """Ask for target IP/hostname, reusing the session target if already set.
167
+
168
+ If a target was used in a previous menu action this session, it becomes
169
+ the default — the user can press Enter to confirm it, or type a new one.
170
+ """
171
+ effective = current or _session.get('target', '')
172
+ while True:
173
+ t = _ask("Target IP or hostname", effective or '192.168.x.x')
174
+ if t and t not in ('192.168.x.x', ''):
175
+ _session['target'] = t
176
+ return t
177
+ print(f" {_DIM} Please enter a valid IP or hostname{_RST}")
178
+
179
+
180
+ def _serial_prompt() -> str:
181
+ val = _ask("Serial number (leave blank if unknown)", _session.get('serial', ''))
182
+ if val:
183
+ _session['serial'] = val
184
+ return val
185
+
186
+
187
+ def _vendor_prompt(auto: str = '') -> str:
188
+ hint = auto or _session.get('vendor', '') or 'epson'
189
+ val = _ask(
190
+ "Printer vendor (epson/hp/ricoh/xerox/kyocera/brother/canon/generic)",
191
+ hint,
192
+ )
193
+ if val:
194
+ _session['vendor'] = val
195
+ return val
196
+
197
+
198
+ # ── Menu sections ─────────────────────────────────────────────────────────────
199
+
200
+ def _menu_discover() -> None:
201
+ choice = _choose([
202
+ ('local', 'Local network discovery (SNMP scan — finds printers on LAN)'),
203
+ ('online', 'Online discovery (Shodan/Censys — requires API keys)'),
204
+ ('local_installed', 'Locally installed printers (installed on this machine/OS)'),
205
+ ], title='Discover Printers')
206
+ if choice is None:
207
+ return
208
+
209
+ if choice == 'local':
210
+ _run_cmd(['--discover-local'])
211
+ elif choice == 'online':
212
+ _run_cmd(['--discover-online'])
213
+ elif choice == 'local_installed':
214
+ _run_cmd(['--discover-local'])
215
+
216
+
217
+ def _menu_scan() -> None:
218
+ target = _target_prompt()
219
+ choice = _choose([
220
+ ('quick', 'Quick scan (banner + CVEs, no NVD API — fast, offline)'),
221
+ ('full', 'Full scan (banner + NVD CVE lookup + exploit matching)'),
222
+ ('ml', 'Full + ML scan (full + ML-assisted fingerprint & scoring)'),
223
+ ], title=f'Scan → {target}', allow_back=True)
224
+ if choice is None:
225
+ return
226
+
227
+ if choice == 'quick':
228
+ _run_cmd([target, '--scan', '--no-nvd'])
229
+ elif choice == 'full':
230
+ _run_cmd([target, '--scan'])
231
+ elif choice == 'ml':
232
+ _run_cmd([target, '--scan-ml'])
233
+
234
+
235
+ def _menu_bruteforce() -> None:
236
+ target = _target_prompt()
237
+ print()
238
+ print(f" {_DIM}Brute-force tests default vendor credentials against HTTP, FTP, SNMP, Telnet.{_RST}")
239
+ print(f" {_DIM}Password variations generated: normal, reverse, leet, CamelCase, UPPER, lower.{_RST}")
240
+ print()
241
+
242
+ vendor = _vendor_prompt()
243
+ serial = _serial_prompt()
244
+ mac = _ask("MAC address (for OKI/Brother/Kyocera KR2 — leave blank if unknown)", '')
245
+ delay = _ask("Delay between attempts in seconds (0.3 = default, increase to avoid lockouts)", '0.3')
246
+ variations = _ask_yn("Enable password variations (reverse, leet, CamelCase...)?", True)
247
+
248
+ cmd = [target, '--bruteforce', '--bf-vendor', vendor]
249
+ if serial:
250
+ cmd += ['--bf-serial', serial]
251
+ if mac:
252
+ cmd += ['--bf-mac', mac]
253
+ if delay and delay != '0.3':
254
+ cmd += ['--bf-delay', delay]
255
+ if not variations:
256
+ cmd.append('--bf-no-variations')
257
+
258
+ _run_cmd(cmd)
259
+
260
+
261
+ def _menu_attack() -> None:
262
+ target = _target_prompt()
263
+ choice = _choose([
264
+ ('ipp', 'IPP attacks (job submit/purge, queue dump, attr manipulation)'),
265
+ ('matrix', 'Full attack matrix (BlackHat 2017 + 2024-2025 CVEs — all categories)'),
266
+ ('pivot', 'Network pivot (SSRF internal host discovery, port scan via printer)'),
267
+ ('storage', 'Storage access (FTP filesystem, web file mgr, SNMP MIB dump)'),
268
+ ('firmware','Firmware audit (version, upload check, NVRAM probe)'),
269
+ ('xsp', 'Cross-Site Printing (XSP + CORS spoofing payload generator)'),
270
+ ('netmap', 'Network mapping (subnet scan, SNMP routing, WSD neighbors)'),
271
+ ('payload', 'Inject payload (PJL/PS/ESC-P payload, display message)'),
272
+ ('implant', 'Persistent implant (SMTP/DNS/NTP config change, NVRAM write)'),
273
+ ], title=f'Attack → {target}', allow_back=True)
274
+ if choice is None:
275
+ return
276
+
277
+ dry_note = (
278
+ f"\n {_YEL}[!]{_RST} Default: DRY-RUN mode — no destructive actions.\n"
279
+ f" {_YEL} {_RST} Use --no-dry to execute live (authorized labs only).\n"
280
+ )
281
+
282
+ if choice == 'ipp':
283
+ print(dry_note)
284
+ _run_cmd([target, '--ipp'])
285
+
286
+ elif choice == 'matrix':
287
+ print(dry_note)
288
+ nodry = _ask_yn("Enable LIVE mode (execute exploits, not just probe)?", False)
289
+ cmd = [target, '--attack-matrix']
290
+ if nodry:
291
+ cmd.append('--no-dry')
292
+ _run_cmd(cmd)
293
+
294
+ elif choice == 'pivot':
295
+ internal = _ask("Internal host to port-scan via printer SSRF (leave blank to skip)", '')
296
+ cmd = [target, '--pivot']
297
+ if internal:
298
+ cmd += ['--pivot-scan', internal]
299
+ _run_cmd(cmd)
300
+
301
+ elif choice == 'storage':
302
+ _run_cmd([target, '--storage'])
303
+
304
+ elif choice == 'firmware':
305
+ _run_cmd([target, '--firmware'])
306
+
307
+ elif choice == 'xsp':
308
+ xtype = _choose([
309
+ ('info', 'info — extract printer ID'),
310
+ ('capture', 'capture — job sniffer'),
311
+ ('dos', 'dos — PS infinite loop via browser'),
312
+ ('nvram', 'nvram — NVRAM damage'),
313
+ ('exfil', 'exfil — exfiltrate captured jobs'),
314
+ ], title='XSP Payload Type', allow_back=True)
315
+ if xtype:
316
+ callback = _ask("Exfiltration callback URL (optional)", '')
317
+ cmd = [target, '--xsp', xtype]
318
+ if callback:
319
+ cmd += ['--xsp-callback', callback]
320
+ _run_cmd(cmd)
321
+
322
+ elif choice == 'netmap':
323
+ _run_cmd([target, '--network-map'])
324
+
325
+ elif choice == 'payload':
326
+ lang = _choose([
327
+ ('pjl:info', 'PJL info — extract printer variables'),
328
+ ('pjl:reset', 'PJL reset — factory reset via PJL'),
329
+ ('ps:custom', 'PS custom — custom PostScript payload'),
330
+ ('escpr:info', 'ESC/P info — Epson ESC/P-R device info'),
331
+ ], title='Payload Type', allow_back=True)
332
+ if lang:
333
+ _run_cmd([target, '--payload', lang])
334
+
335
+ elif choice == 'implant':
336
+ kv = _ask("Config to implant (e.g. smtp_host=attacker.com)", '')
337
+ if kv:
338
+ _run_cmd([target, '--implant', kv])
339
+
340
+
341
+ def _menu_send_job() -> None:
342
+ """Send a print job (file or raw text) to a target printer."""
343
+ target = _target_prompt()
344
+ print()
345
+ print(f" {_DIM}Send any file or text directly to the printer for printing.{_RST}")
346
+ print(f" {_DIM}Supported: .txt, .pdf, .ps, .pcl, .png, .jpg, .doc — or raw text.{_RST}")
347
+ print()
348
+ choice = _choose([
349
+ ('file', 'Send a file (PDF, PS, PCL, PNG, JPG, TXT, DOC...)'),
350
+ ('text', 'Send raw text (type text directly, printer outputs it)'),
351
+ ('ps', 'Send PostScript (raw PS code — advanced)'),
352
+ ], title=f'Send Print Job -> {target}', allow_back=True)
353
+ if choice is None:
354
+ return
355
+
356
+ proto = _choose([
357
+ ('raw', 'RAW / JetDirect (port 9100 — fastest, no job tracking)'),
358
+ ('ipp', 'IPP (port 631 — standard, job tracking)'),
359
+ ('lpd', 'LPD (port 515 — legacy line printer)'),
360
+ ], title='Printing Protocol', allow_back=True)
361
+ if proto is None:
362
+ return
363
+
364
+ port_defaults = {'raw': '9100', 'ipp': '631', 'lpd': '515'}
365
+ port = _ask(f"Port [{port_defaults.get(proto, '9100')}]", port_defaults.get(proto, '9100'))
366
+ copies = _ask("Number of copies [1]", '1')
367
+
368
+ if choice == 'file':
369
+ path = _ask("File path (absolute or relative)", '')
370
+ if not path:
371
+ return
372
+ cmd = [target, '--send-job', path, '--send-proto', proto, '--port', port]
373
+ if copies and copies != '1':
374
+ cmd += ['--send-copies', copies]
375
+ _run_cmd(cmd)
376
+
377
+ elif choice == 'text':
378
+ print(f" {_DIM}Type your text below. End with a blank line + Enter:{_RST}")
379
+ lines = []
380
+ try:
381
+ while True:
382
+ line = input()
383
+ if line == '' and lines:
384
+ break
385
+ lines.append(line)
386
+ except (EOFError, KeyboardInterrupt):
387
+ pass
388
+ if not lines:
389
+ return
390
+ import tempfile, os
391
+ tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt',
392
+ delete=False, encoding='utf-8')
393
+ tmp.write('\n'.join(lines) + '\n')
394
+ tmp.close()
395
+ cmd = [target, '--send-job', tmp.name, '--send-proto', proto, '--port', port]
396
+ if copies and copies != '1':
397
+ cmd += ['--send-copies', copies]
398
+ _run_cmd(cmd)
399
+ try:
400
+ os.unlink(tmp.name)
401
+ except OSError:
402
+ pass
403
+
404
+ elif choice == 'ps':
405
+ print(f" {_DIM}Enter PostScript code (end with blank line):{_RST}")
406
+ lines = []
407
+ try:
408
+ while True:
409
+ line = input()
410
+ if line == '' and lines:
411
+ break
412
+ lines.append(line)
413
+ except (EOFError, KeyboardInterrupt):
414
+ pass
415
+ if not lines:
416
+ return
417
+ import tempfile, os
418
+ tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.ps',
419
+ delete=False, encoding='utf-8')
420
+ tmp.write('\n'.join(lines) + '\n')
421
+ tmp.close()
422
+ cmd = [target, '--send-job', tmp.name, '--send-proto', proto, '--port', port]
423
+ _run_cmd(cmd)
424
+ try:
425
+ os.unlink(tmp.name)
426
+ except OSError:
427
+ pass
428
+
429
+
430
+ def _menu_exploits() -> None:
431
+ choice = _choose([
432
+ ('list', 'List all exploits (sorted by severity: critical → info)'),
433
+ ('check', 'Check target (non-destructive — is target vulnerable?)'),
434
+ ('run', 'Run exploit (dry-run by default)'),
435
+ ('fetch', 'Download exploit (fetch raw exploit from ExploitDB by ID)'),
436
+ ('update', 'Update index (rebuild xpl/index.json from loaded exploits)'),
437
+ ], title='Exploit Library', allow_back=True)
438
+ if choice is None:
439
+ return
440
+
441
+ if choice == 'list':
442
+ _run_cmd(['--xpl-list'])
443
+
444
+ elif choice == 'check':
445
+ target = _target_prompt()
446
+ print()
447
+ print(f" {_DIM}Run --xpl-list first to see available exploit IDs.{_RST}")
448
+ xid = _ask("Exploit ID (e.g. EDB-15631, CVE-2025-26508)", '')
449
+ if xid:
450
+ _run_cmd([target, '--xpl-check', xid])
451
+
452
+ elif choice == 'run':
453
+ target = _target_prompt()
454
+ print()
455
+ print(f" {_DIM}Run --xpl-list first to see available exploit IDs.{_RST}")
456
+ xid = _ask("Exploit ID", '')
457
+ if not xid:
458
+ return
459
+ nodry = _ask_yn(
460
+ f"{_YEL}Enable LIVE mode?{_RST} (default: DRY-RUN — safe probe only)", False
461
+ )
462
+ cmd = [target, '--xpl-run', xid]
463
+ if nodry:
464
+ cmd.append('--no-dry')
465
+ _run_cmd(cmd)
466
+
467
+ elif choice == 'fetch':
468
+ print(f"\n {_DIM}Browse https://www.exploit-db.com/search for printer exploits.{_RST}")
469
+ edb_id = _ask("ExploitDB numeric ID (e.g. 45273)", '')
470
+ if edb_id:
471
+ _run_cmd(['--xpl-fetch', edb_id])
472
+
473
+ elif choice == 'update':
474
+ _run_cmd(['--xpl-update'])
475
+
476
+
477
+ def _menu_config() -> None:
478
+ choice = _choose([
479
+ ('check', 'Check API configuration (shows which features are active)'),
480
+ ('help', 'Full help / all CLI flags'),
481
+ ('about', 'About PrinterXPL-Forge'),
482
+ ], title='Configuration & Help', allow_back=True)
483
+ if choice is None:
484
+ return
485
+
486
+ if choice == 'check':
487
+ _run_cmd(['--check-config'])
488
+ elif choice == 'help':
489
+ _run_cmd(['--help'])
490
+ elif choice == 'about':
491
+ _show_about()
492
+
493
+
494
+ def _show_about() -> None:
495
+ print()
496
+ print(f" {_CYN}{'═'*60}{_RST}")
497
+ print(f" {_BLD}PrinterXPL-Forge — Advanced Printer Penetration Testing{_RST}")
498
+ print(f" {_CYN}{'═'*60}{_RST}")
499
+ lines = [
500
+ ('Author', 'Andre Henrique (@mrhenrike)'),
501
+ ('GitHub', 'https://github.com/mrhenrike'),
502
+ ('LinkedIn', 'https://linkedin.com/in/mrhenrike'),
503
+ ('X', 'https://x.com/mrhenrike'),
504
+ ('License', 'MIT'),
505
+ ('Purpose', 'Authorized security testing of network printers'),
506
+ ]
507
+ for label, value in lines:
508
+ print(f" {_DIM}{label:<12}{_RST} {value}")
509
+ print()
510
+ try:
511
+ input(f" {_DIM}Press Enter to continue...{_RST}")
512
+ except (EOFError, KeyboardInterrupt):
513
+ pass
514
+
515
+
516
+ # ── Workflow shortcuts ────────────────────────────────────────────────────────
517
+
518
+ def _workflow_full_audit() -> None:
519
+ """Guided full audit: scan + BF + exploit check in sequence."""
520
+ print()
521
+ print(f" {_CYN}{_BLD}Full Audit Workflow{_RST}")
522
+ print(f" {_DIM}Runs: Scan → Exploit matching → Brute-force → Attack matrix{_RST}")
523
+ _hr()
524
+ target = _target_prompt()
525
+ serial = _serial_prompt()
526
+ vendor = _vendor_prompt()
527
+
528
+ steps = [
529
+ (f"Step 1/4 Scan (banner + CVEs)",
530
+ [target, '--scan']),
531
+ (f"Step 2/4 Brute-force login",
532
+ [target, '--bruteforce', '--bf-vendor', vendor]
533
+ + (['--bf-serial', serial] if serial else [])),
534
+ (f"Step 3/4 Attack matrix (dry-run)",
535
+ [target, '--attack-matrix']),
536
+ (f"Step 4/4 Network map",
537
+ [target, '--network-map']),
538
+ ]
539
+
540
+ for title, cmd in steps:
541
+ print(f"\n {_YEL}{'─'*54}{_RST}")
542
+ print(f" {_YEL}▶ {title}{_RST}")
543
+ print(f" {_YEL}{'─'*54}{_RST}")
544
+ _run_cmd(cmd, pause=False)
545
+
546
+ print(f"\n {_GRN}Full audit complete.{_RST}")
547
+ print(f" {_DIM}Review results above. Re-run individual steps for deeper analysis.{_RST}")
548
+ try:
549
+ input(f"\n {_DIM}Press Enter to return to main menu...{_RST}")
550
+ except (EOFError, KeyboardInterrupt):
551
+ pass
552
+
553
+
554
+ # ── Main interactive loop ─────────────────────────────────────────────────────
555
+
556
+ _MAIN_MENU = [
557
+ ('discover', '[~] Discover printers Find printers on LAN or via Shodan/Censys'),
558
+ ('scan', '[?] Scan target Fingerprint + CVE lookup + exploit matching'),
559
+ ('bruteforce', '[*] Brute-force login Test default credentials (all protocols)'),
560
+ ('attack', '[!] Attack / Exploit IPP, pivot, firmware, payload, XSP, matrix'),
561
+ ('exploits', '[X] Exploit library List, check, run or download exploits'),
562
+ ('destructive', '[D] DESTRUCTIVE AUDIT Irreversible / physical-damage attack check'),
563
+ ('send', '[>] Send print job Send text/doc/pdf/image to target printer'),
564
+ ('workflow', '[>>] Full audit workflow Scan -> BF -> Attack matrix -> Netmap in one go'),
565
+ ('config', '[=] Config & help API keys, settings, documentation'),
566
+ ]
567
+
568
+
569
+ def _menu_destructive() -> None:
570
+ """Destructive / irreversible physical-damage attack audit."""
571
+ print()
572
+ print(f" {_RED}{_BLD}!!! DESTRUCTIVE ATTACK AUDIT !!!{_RST}")
573
+ print(f" {_RED}The following checks probe for IRREVERSIBLE physical damage vectors.{_RST}")
574
+ print(f" {_DIM}Default: DRY-RUN (assess only). You must explicitly enable LIVE mode.{_RST}")
575
+ _hr()
576
+ print()
577
+
578
+ target = _target_prompt()
579
+
580
+ print()
581
+ print(f" {_YEL}Attack modules available:{_RST}")
582
+ print(f" {_DIM} [1] Fuser Thermal Runaway — overheat fuser unit (fire/melt risk){_RST}")
583
+ print(f" {_DIM} [2] Motor Jamming — strip gears/rollers (mechanical failure){_RST}")
584
+ print(f" {_DIM} [3] Laser Scanner Damage — degrade diode/drum (optical failure){_RST}")
585
+ print(f" {_DIM} [4] NVRAM Exhaustion — burn NVRAM write cycles (brick){_RST}")
586
+ print(f" {_DIM} [5] SNMP Factory Reset — unauthenticated wipe (config loss){_RST}")
587
+ print(f" {_DIM} [6] Firmware Brick — Xerox DLM/HTTP firmware injection{_RST}")
588
+ print(f" {_DIM} [0] ALL (recommended — run all 10 modules){_RST}")
589
+ print()
590
+
591
+ raw_sel = _ask("Modules to test (0=all, or IDs comma-separated e.g. 1,4)", '0')
592
+
593
+ _module_map = {
594
+ '1': 'research-fuser-thermal-attack',
595
+ '2': 'research-motor-jam-attack',
596
+ '3': 'research-laser-scanner-attack',
597
+ '4': 'research-pjl-nvram-damage,research-brother-nvram,research-generic-pjl-nvram',
598
+ '5': 'research-snmp-factory-reset',
599
+ '6': 'research-xerox-pjl-dlm,research-xerox-firmware-root,edb-45273',
600
+ }
601
+
602
+ selected_modules = ''
603
+ if raw_sel and raw_sel != '0':
604
+ parts = []
605
+ for token in raw_sel.split(','):
606
+ token = token.strip()
607
+ if token in _module_map:
608
+ parts.append(_module_map[token])
609
+ elif token:
610
+ parts.append(token)
611
+ selected_modules = ','.join(parts)
612
+
613
+ print()
614
+ print(f" {_YEL}Execution mode:{_RST}")
615
+ print(f" {_GRN} [1] DRY-RUN (assess only — SAFE, default){_RST}")
616
+ print(f" {_RED} [2] LIVE EXECUTION (destructive payloads sent — IRREVERSIBLE){_RST}")
617
+ print()
618
+
619
+ mode_choice = _ask("Select mode", '1')
620
+ live_mode = mode_choice == '2'
621
+
622
+ if live_mode:
623
+ print()
624
+ print(f" {_RED}{_BLD}!!! WARNING: LIVE MODE SELECTED !!!{_RST}")
625
+ print(f" {_RED}This will send DESTRUCTIVE payloads to {target}.{_RST}")
626
+ print(f" {_RED}Hardware damage is PERMANENT and IRREVERSIBLE.{_RST}")
627
+ print(f" {_RED}Use ONLY in authorized lab environments with fire safety controls.{_RST}")
628
+ print()
629
+ confirm = _ask_yn("Type YES to confirm you have written authorization", False)
630
+ if not confirm:
631
+ print(f"\n {_YEL}Aborted — reverting to DRY-RUN mode.{_RST}")
632
+ live_mode = False
633
+
634
+ cmd = [target, '--destructive-audit']
635
+ if live_mode:
636
+ cmd.append('--no-dry')
637
+ if selected_modules:
638
+ cmd += ['--destructive-modules', selected_modules]
639
+
640
+ _run_cmd(cmd)
641
+
642
+
643
+ def _menu_header() -> None:
644
+ from version import __version__
645
+ print()
646
+ _w = 58 # inner box width (number of ═ chars)
647
+ _ver = f"PrinterXPL-Forge v{__version__}"
648
+ _sub = "Advanced Printer Penetration Testing Toolkit"
649
+ _act = "Choose an action:"
650
+ # Each content line: 2 leading spaces + text + padding + 2 trailing spaces = _w
651
+ def _row(text: str, bold: str = '') -> str:
652
+ pad = ' ' * (_w - 4 - len(text))
653
+ inner = f" {bold}{text}{_RST}{pad} "
654
+ return f" {_CYN}║{_RST}{inner}{_CYN}║{_RST}"
655
+ print(f" {_CYN}╔{'═'*_w}╗{_RST}")
656
+ print(_row(_ver, f"{_RED}{_BLD}"))
657
+ print(_row(_sub, _DIM))
658
+ print(f" {_CYN}╠{'═'*_w}╣{_RST}")
659
+ print(_row(_act))
660
+ print(f" {_CYN}╚{'═'*_w}╝{_RST}")
661
+ print()
662
+
663
+
664
+ def run_interactive() -> None:
665
+ """
666
+ Main interactive loop.
667
+
668
+ Called by main.py when no meaningful arguments are provided.
669
+ """
670
+ # Enable ANSI on Windows
671
+ if os.name == 'nt':
672
+ os.system('')
673
+
674
+ while True:
675
+ _clr()
676
+ _menu_header()
677
+
678
+ for i, (key, label) in enumerate(_MAIN_MENU, 1):
679
+ # Split label at first double-space into icon+title and description
680
+ parts = label.split(' ', 2)
681
+ if len(parts) >= 2:
682
+ icon_title = parts[0] + ' ' + parts[1]
683
+ desc = parts[2] if len(parts) > 2 else ''
684
+ else:
685
+ icon_title = label
686
+ desc = ''
687
+ print(f" {_YEL}[{i}]{_RST} {icon_title:<36} {_DIM}{desc}{_RST}")
688
+
689
+ print()
690
+ # Show current session target if one is set
691
+ if _session.get('target'):
692
+ print(f" {_DIM}Session target:{_RST} {_GRN}{_session['target']}{_RST}"
693
+ f" {_DIM}· type {_RST}[T]{_DIM} to change{_RST}")
694
+ print(f" {_DIM}[T] Set/change session target [Q] Exit PrinterXPL-Forge{_RST}")
695
+ print()
696
+
697
+ try:
698
+ raw = input(f" {_CYN}▶{_RST} Select: ").strip()
699
+ except (EOFError, KeyboardInterrupt):
700
+ print(f"\n {_DIM}Bye.{_RST}\n")
701
+ sys.exit(0)
702
+
703
+ if raw.lower() in ('q', 'quit', 'exit', ''):
704
+ print(f"\n {_DIM}Bye.{_RST}\n")
705
+ sys.exit(0)
706
+
707
+ # Allow typing 't' to change the session target quickly
708
+ if raw.lower() == 't':
709
+ new_t = _ask("New session target IP or hostname", _session.get('target', ''))
710
+ if new_t and new_t not in ('192.168.x.x',):
711
+ _session['target'] = new_t
712
+ continue
713
+
714
+ try:
715
+ idx = int(raw) - 1
716
+ if 0 <= idx < len(_MAIN_MENU):
717
+ key = _MAIN_MENU[idx][0]
718
+ else:
719
+ continue
720
+ except ValueError:
721
+ continue
722
+
723
+ _clr()
724
+
725
+ if key == 'discover':
726
+ _menu_discover()
727
+ elif key == 'scan':
728
+ _menu_scan()
729
+ elif key == 'bruteforce':
730
+ _menu_bruteforce()
731
+ elif key == 'attack':
732
+ _menu_attack()
733
+ elif key == 'exploits':
734
+ _menu_exploits()
735
+ elif key == 'destructive':
736
+ _menu_destructive()
737
+ elif key == 'send':
738
+ _menu_send_job()
739
+ elif key == 'workflow':
740
+ _workflow_full_audit()
741
+ elif key == 'config':
742
+ _menu_config()