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/main.py ADDED
@@ -0,0 +1,2134 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ PrinterXPL-Forge - Advanced Printer Penetration Testing Toolkit
5
+ Main entry point.
6
+ """
7
+
8
+ # Author : Andre Henrique (@mrhenrike)
9
+ # GitHub : https://github.com/mrhenrike
10
+ # LinkedIn : https://linkedin.com/in/mrhenrike
11
+ # X/Twitter : https://x.com/mrhenrike
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import csv
17
+ import io
18
+ import sys
19
+ from typing import Callable, Dict, List
20
+
21
+ from core.osdetect import get_os
22
+ from core.discovery import discovery
23
+ from core.capabilities import capabilities
24
+ from modules.pjl import pjl
25
+ from modules.ps import ps
26
+ from modules.pcl import pcl
27
+ from utils.helper import output
28
+ from version import get_version_string
29
+
30
+ # --------------------------------------------------------------------------- #
31
+ # CLI helpers
32
+ # --------------------------------------------------------------------------- #
33
+
34
+ def _expand_csv(raw_values: List[str]) -> List[str]:
35
+ """Expand a list of raw CLI strings into individual values.
36
+
37
+ Accepts both repeated flags and comma-separated values (with optional
38
+ single- or double-quote quoting for compound tokens):
39
+
40
+ --dork-vendor hp --dork-vendor canon → ['hp', 'canon']
41
+ --dork-vendor "hp,canon,epson" → ['hp', 'canon', 'epson']
42
+ --dork-country BR,AR,US → ['BR', 'AR', 'US']
43
+
44
+ Quoting inside the CSV string follows standard CSV rules so compound
45
+ names with spaces work naturally:
46
+
47
+ --dork-city "São Paulo",Belém → ['São Paulo', 'Belém']
48
+ """
49
+ result: List[str] = []
50
+ for raw in raw_values:
51
+ raw = raw.strip()
52
+ # Use csv.reader to honour double-quote quoting inside the value
53
+ reader = csv.reader(io.StringIO(raw), skipinitialspace=True)
54
+ for row in reader:
55
+ for token in row:
56
+ token = token.strip().strip("'") # strip residual single-quotes
57
+ if token:
58
+ result.append(token)
59
+ return result
60
+
61
+
62
+ def _expand_csv_int(raw_values: List[str]) -> List[int]:
63
+ """Expand CSV values and convert each token to int (for port lists)."""
64
+ result: List[int] = []
65
+ for token in _expand_csv(raw_values):
66
+ try:
67
+ result.append(int(token))
68
+ except ValueError:
69
+ pass # non-numeric tokens silently ignored; argparse already validates type
70
+ return result
71
+
72
+
73
+ # --------------------------------------------------------------------------- #
74
+ # Metadata
75
+ # --------------------------------------------------------------------------- #
76
+ APP_NAME: str = "PrinterXPL-Forge"
77
+ VERSION: str = get_version_string()
78
+
79
+ # --------------------------------------------------------------------------- #
80
+ # Argument parsing
81
+ # --------------------------------------------------------------------------- #
82
+ def build_parser() -> argparse.ArgumentParser:
83
+ """Build and return the argparse parser (shared by CLI and help)."""
84
+ parser = argparse.ArgumentParser(
85
+ prog=APP_NAME.lower(),
86
+ description=f"{APP_NAME} - Advanced Printer Penetration Testing Toolkit",
87
+ )
88
+ # Make positionals optional to allow --discover-* without target/mode
89
+ parser.add_argument("target", nargs='?', help="Printer IP address or hostname")
90
+ parser.add_argument(
91
+ "mode",
92
+ nargs='?',
93
+ choices=["pjl", "ps", "pcl", "auto"],
94
+ help="Printer language to abuse (PJL, PostScript, PCL, or auto-detect)",
95
+ )
96
+ parser.add_argument(
97
+ "-s",
98
+ "--safe",
99
+ help="Verify if the chosen language is supported before attacking",
100
+ action="store_true",
101
+ )
102
+ parser.add_argument(
103
+ "-q", "--quiet", help="Suppress warnings and banner", action="store_true"
104
+ )
105
+ parser.add_argument(
106
+ "-d", "--debug", help="Enter debug mode (show raw traffic)", action="store_true"
107
+ )
108
+ parser.add_argument(
109
+ "-i", "--load", metavar="file", help="Load and run commands from file"
110
+ )
111
+ parser.add_argument(
112
+ "-o", "--log", metavar="file", help="Log raw data sent to the target"
113
+ )
114
+ parser.add_argument(
115
+ "--osint",
116
+ help="Check target exposure on public search engines (passive OSINT)",
117
+ action="store_true",
118
+ )
119
+ parser.add_argument(
120
+ "--auto-detect",
121
+ help="Automatically detect supported printer languages",
122
+ action="store_true",
123
+ )
124
+ # ── Custom port overrides ──────────────────────────────────────────────────
125
+ _port_group = parser.add_argument_group(
126
+ "custom port overrides",
127
+ "Override default protocol ports. When not specified, each module uses its own "
128
+ "default (RAW=9100, IPP=631, LPD=515, SNMP=161, FTP=21, HTTP=80, HTTPS=443, SMB=445, Telnet=23). "
129
+ "Use when the printer listens on non-standard ports."
130
+ )
131
+ _port_group.add_argument(
132
+ "--port-raw",
133
+ metavar="PORT",
134
+ type=int,
135
+ default=None,
136
+ help="Custom port for RAW/PJL/JetDirect (default: 9100). Example: --port-raw 3910",
137
+ )
138
+ _port_group.add_argument(
139
+ "--port-ipp",
140
+ metavar="PORT",
141
+ type=int,
142
+ default=None,
143
+ help="Custom port for IPP (default: 631). Example: --port-ipp 8631",
144
+ )
145
+ _port_group.add_argument(
146
+ "--port-lpd",
147
+ metavar="PORT",
148
+ type=int,
149
+ default=None,
150
+ help="Custom port for LPD/LPR (default: 515). Example: --port-lpd 5515",
151
+ )
152
+ _port_group.add_argument(
153
+ "--port-snmp",
154
+ metavar="PORT",
155
+ type=int,
156
+ default=None,
157
+ help="Custom port for SNMP (default: 161). Example: --port-snmp 1161",
158
+ )
159
+ _port_group.add_argument(
160
+ "--port-ftp",
161
+ metavar="PORT",
162
+ type=int,
163
+ default=None,
164
+ help="Custom port for FTP management (default: 21). Example: --port-ftp 2121",
165
+ )
166
+ _port_group.add_argument(
167
+ "--port-http",
168
+ metavar="PORT",
169
+ type=int,
170
+ default=None,
171
+ help="Custom port for HTTP embedded web server (default: 80). Example: --port-http 8080",
172
+ )
173
+ _port_group.add_argument(
174
+ "--port-https",
175
+ metavar="PORT",
176
+ type=int,
177
+ default=None,
178
+ help="Custom port for HTTPS embedded web server (default: 443). Example: --port-https 8443",
179
+ )
180
+ _port_group.add_argument(
181
+ "--port-smb",
182
+ metavar="PORT",
183
+ type=int,
184
+ default=None,
185
+ help="Custom port for SMB/CIFS (default: 445). Example: --port-smb 4445",
186
+ )
187
+ _port_group.add_argument(
188
+ "--port-telnet",
189
+ metavar="PORT",
190
+ type=int,
191
+ default=None,
192
+ help="Custom port for Telnet management (default: 23). Example: --port-telnet 2323",
193
+ )
194
+ _port_group.add_argument(
195
+ "--extra-ports",
196
+ metavar="PORT",
197
+ action="append",
198
+ dest="extra_ports",
199
+ default=[],
200
+ type=int,
201
+ help=(
202
+ "Extra port(s) to include in banner scan sweeps (repeatable). "
203
+ "Example: --extra-ports 9200 --extra-ports 7100"
204
+ ),
205
+ )
206
+ # Discovery helpers
207
+ parser.add_argument(
208
+ "--discover-local",
209
+ action="store_true",
210
+ help="Run local SNMP discovery to find printers on your networks",
211
+ )
212
+ parser.add_argument(
213
+ "--discover-online",
214
+ action="store_true",
215
+ help=(
216
+ "Search for exposed printers on the internet using indexed data from search engines "
217
+ "(Shodan, Censys, FOFA, ZoomEye, Netlas). "
218
+ "At least one --dork-* filter is mandatory to prevent unbounded searches. "
219
+ "Engine selection rules:\n"
220
+ " ONE engine → use the individual flag: --shodan | --censys | --fofa | --zoomeye | --netlas\n"
221
+ " MANY engines→ --dork-engine shodan,censys,fofa (comma-separated, the ONLY multi-engine option)\n"
222
+ " NO flag → all engines that have API keys configured in config.json are queried\n"
223
+ "Printer context is always implicit — no need to add 'printer' to any filter."
224
+ ),
225
+ )
226
+ # Dork filters for --discover-online
227
+ _dork_group = parser.add_argument_group("online discovery filters (--discover-online dorks)")
228
+ _dork_group.add_argument(
229
+ "--dork-vendor",
230
+ metavar="VENDOR[,VENDOR...]",
231
+ action="append",
232
+ dest="dork_vendors",
233
+ default=[],
234
+ help=(
235
+ "Vendor filter. Accepts a single value, comma-separated list, or repeated flags. "
236
+ "Choices: hp, epson, ricoh, brother, canon, kyocera, xerox, lexmark, samsung, oki, zebra. "
237
+ "Examples: --dork-vendor hp | --dork-vendor hp,canon,epson | --dork-vendor hp --dork-vendor canon"
238
+ ),
239
+ )
240
+ _dork_group.add_argument(
241
+ "--dork-model",
242
+ metavar="MODEL",
243
+ default=None,
244
+ help=(
245
+ "Model string to search for in banner (single value; wrap in quotes for spaces). "
246
+ "Example: --dork-model 'deskjet pro 5500'"
247
+ ),
248
+ )
249
+ _dork_group.add_argument(
250
+ "--dork-country",
251
+ metavar="COUNTRY[,COUNTRY...]",
252
+ action="append",
253
+ dest="dork_countries",
254
+ default=[],
255
+ help=(
256
+ "Country filter. ISO-2 codes or country names. "
257
+ "Accepts comma-separated list or repeated flags. "
258
+ "Examples: --dork-country BR | --dork-country BR,AR,US | --dork-country BR --dork-country AR. "
259
+ "Note: --dork-city is only allowed when exactly ONE country is specified."
260
+ ),
261
+ )
262
+ _dork_group.add_argument(
263
+ "--dork-city",
264
+ metavar="CITY[,CITY...]",
265
+ action="append",
266
+ dest="dork_cities",
267
+ default=[],
268
+ help=(
269
+ "City filter. Accepts comma-separated list or repeated flags. "
270
+ "Compound city names must be quoted (single or double quotes). "
271
+ "RESTRICTION: only allowed when exactly ONE --dork-country is provided. "
272
+ "Examples: --dork-city 'Sao Paulo' | "
273
+ "--dork-city \"Sao Paulo\",Belem | "
274
+ "--dork-city \"Sao Paulo\" --dork-city Belem"
275
+ ),
276
+ )
277
+ _dork_group.add_argument(
278
+ "--dork-region",
279
+ metavar="REGION[,REGION...]",
280
+ action="append",
281
+ dest="dork_regions",
282
+ default=[],
283
+ help=(
284
+ "Geographic region filter. Accepts comma-separated list or repeated flags. "
285
+ "Valid regions: latin_america, south_america, central_america, north_america, "
286
+ "europe, eastern_europe, asia, southeast_asia, middle_east, africa, north_africa, oceania. "
287
+ "Examples: --dork-region latin_america | --dork-region latin_america,europe"
288
+ ),
289
+ )
290
+ _dork_group.add_argument(
291
+ "--dork-port",
292
+ metavar="PORT[,PORT...]",
293
+ action="append",
294
+ dest="dork_ports",
295
+ default=[],
296
+ help=(
297
+ "Port filter. Accepts comma-separated list or repeated flags. "
298
+ "Common: 9100 (RAW/PJL), 515 (LPD), 631 (IPP), 80 (HTTP), 443 (HTTPS). "
299
+ "Examples: --dork-port 9100 | --dork-port 9100,515,631 | --dork-port 9100 --dork-port 631"
300
+ ),
301
+ )
302
+ _dork_group.add_argument(
303
+ "--dork-org",
304
+ metavar="ORG",
305
+ default=None,
306
+ help=(
307
+ "Organization/ISP filter (single value; wrap in quotes for spaces). "
308
+ "Example: --dork-org 'Telefonica'"
309
+ ),
310
+ )
311
+ _dork_group.add_argument(
312
+ "--dork-cpe",
313
+ metavar="CPE",
314
+ default=None,
315
+ help=(
316
+ "CPE filter (Censys and Netlas only). "
317
+ "Example: --dork-cpe 'cpe:/h:hp:laserjet'"
318
+ ),
319
+ )
320
+ _dork_group.add_argument(
321
+ "--dork-limit",
322
+ metavar="N",
323
+ type=int,
324
+ default=100,
325
+ help="Maximum results per query per engine (default: 100).",
326
+ )
327
+ # ── Per-engine shortcut flags (mutually exclusive — pick exactly ONE) ───────
328
+ _dork_group.add_argument(
329
+ "--shodan",
330
+ action="store_true",
331
+ dest="engine_shodan",
332
+ default=False,
333
+ help=(
334
+ "Query ONLY Shodan for this discovery run. "
335
+ "Requires shodan.api_key in config.json. "
336
+ "Cannot be combined with --censys / --fofa / --zoomeye / --netlas. "
337
+ "To query multiple engines at once use: --dork-engine shodan,censys"
338
+ ),
339
+ )
340
+ _dork_group.add_argument(
341
+ "--censys",
342
+ action="store_true",
343
+ dest="engine_censys",
344
+ default=False,
345
+ help=(
346
+ "Query ONLY Censys for this discovery run. "
347
+ "Requires censys.api_id + censys.api_secret in config.json. "
348
+ "Cannot be combined with other engine flags. "
349
+ "To query multiple engines use: --dork-engine censys,shodan"
350
+ ),
351
+ )
352
+ _dork_group.add_argument(
353
+ "--fofa",
354
+ action="store_true",
355
+ dest="engine_fofa",
356
+ default=False,
357
+ help=(
358
+ "Query ONLY FOFA for this discovery run. "
359
+ "Requires fofa.api_key in config.json (email field deprecated since Dec-2023). "
360
+ "Cannot be combined with other engine flags. "
361
+ "To query multiple engines use: --dork-engine fofa,shodan"
362
+ ),
363
+ )
364
+ _dork_group.add_argument(
365
+ "--zoomeye",
366
+ action="store_true",
367
+ dest="engine_zoomeye",
368
+ default=False,
369
+ help=(
370
+ "Query ONLY ZoomEye for this discovery run. "
371
+ "Requires zoomeye.api_key in config.json. "
372
+ "Cannot be combined with other engine flags. "
373
+ "To query multiple engines use: --dork-engine zoomeye,netlas"
374
+ ),
375
+ )
376
+ _dork_group.add_argument(
377
+ "--netlas",
378
+ action="store_true",
379
+ dest="engine_netlas",
380
+ default=False,
381
+ help=(
382
+ "Query ONLY Netlas for this discovery run. "
383
+ "Requires netlas.api_key in config.json. "
384
+ "Cannot be combined with other engine flags. "
385
+ "To query multiple engines use: --dork-engine netlas,shodan"
386
+ ),
387
+ )
388
+ _dork_group.add_argument(
389
+ "--dork-engine",
390
+ metavar="ENGINE,ENGINE,...",
391
+ default=None,
392
+ help=(
393
+ "ONLY valid way to query multiple search engines simultaneously. "
394
+ "Accepts a comma-separated list of engines. "
395
+ "Choices: shodan, censys, fofa, zoomeye, netlas. "
396
+ "Examples:\n"
397
+ " --dork-engine shodan,censys\n"
398
+ " --dork-engine fofa,zoomeye,netlas\n"
399
+ " --dork-engine shodan,censys,fofa,zoomeye,netlas\n"
400
+ "Cannot be combined with individual engine flags (--shodan, --fofa, etc.). "
401
+ "If no engine flag is provided, all engines with configured API keys are used."
402
+ ),
403
+ )
404
+ # Reconnaissance / scanning
405
+ parser.add_argument(
406
+ "--scan",
407
+ action="store_true",
408
+ help="Banner grab + CVE lookup + attack surface assessment (no payloads sent)",
409
+ )
410
+ parser.add_argument(
411
+ "--scan-ml",
412
+ action="store_true",
413
+ help="Same as --scan but also runs ML-assisted fingerprinting and attack scoring",
414
+ )
415
+ parser.add_argument(
416
+ "--no-nvd",
417
+ action="store_true",
418
+ help="Skip NVD API CVE lookup during --scan (faster, offline)",
419
+ )
420
+ parser.add_argument(
421
+ "--config",
422
+ metavar="PATH",
423
+ default=None,
424
+ help="Path to config.json (default: config.json next to src/)",
425
+ )
426
+ # ── Attack modules for non-PJL/PS/PCL printers ────────────────────────────
427
+ parser.add_argument(
428
+ "--ipp",
429
+ action="store_true",
430
+ help="Full IPP security audit: anonymous job, queue purge, attr manipulation",
431
+ )
432
+ parser.add_argument(
433
+ "--ipp-submit",
434
+ action="store_true",
435
+ help="Submit an anonymous IPP print job (dry-run by default; add --no-dry to actually print)",
436
+ )
437
+ parser.add_argument(
438
+ "--no-dry",
439
+ action="store_true",
440
+ help="Disable dry-run on --ipp-submit (actually sends the print job)",
441
+ )
442
+ parser.add_argument(
443
+ "--pivot",
444
+ action="store_true",
445
+ help="Lateral movement audit: SSRF via IPP/WSD, internal host discovery",
446
+ )
447
+ parser.add_argument(
448
+ "--pivot-scan",
449
+ metavar="INTERNAL_HOST",
450
+ default=None,
451
+ help="Port-scan INTERNAL_HOST via printer SSRF (e.g. --pivot-scan 192.168.1.1)",
452
+ )
453
+ parser.add_argument(
454
+ "--storage",
455
+ action="store_true",
456
+ help="Printer storage audit: FTP, web file manager, SNMP MIB dump, saved jobs",
457
+ )
458
+ parser.add_argument(
459
+ "--firmware",
460
+ action="store_true",
461
+ help="Firmware audit: version extraction, upload endpoint check, NVRAM probe",
462
+ )
463
+ parser.add_argument(
464
+ "--firmware-reset",
465
+ choices=["pjl", "web", "ipp"],
466
+ default=None,
467
+ help="Attempt factory reset via specified method (DANGEROUS — authorized labs only)",
468
+ )
469
+ parser.add_argument(
470
+ "--payload",
471
+ metavar="LANG:TYPE",
472
+ default=None,
473
+ help="Inject a language-specific payload: escpr:info, pjl:reset, ps:custom, etc.",
474
+ )
475
+ parser.add_argument(
476
+ "--payload-data",
477
+ metavar="STRING",
478
+ default='',
479
+ help="Custom payload string for --payload LANG:custom",
480
+ )
481
+ parser.add_argument(
482
+ "--implant",
483
+ metavar="KEY=VALUE",
484
+ default=None,
485
+ help="Persistent config implant (smtp_host=X, dns=Y, snmp_community=Z, etc.)",
486
+ )
487
+ parser.add_argument(
488
+ "--check-config",
489
+ action="store_true",
490
+ help="Show which API features are configured and exit",
491
+ )
492
+ # ── Full attack campaign (BlackHat matrix) ─────────────────────────────────
493
+ parser.add_argument(
494
+ "--attack-matrix",
495
+ action="store_true",
496
+ help=(
497
+ "Run the full attack matrix: DoS, Protection Bypass, Job Manipulation, "
498
+ "Info Disclosure. Probes all vectors from Müller et al. (2017) + 2024-2025 "
499
+ "CVEs. Use --no-dry to actually exploit (DANGEROUS)."
500
+ ),
501
+ )
502
+ parser.add_argument(
503
+ "--network-map",
504
+ action="store_true",
505
+ help=(
506
+ "Build a complete network map from the printer's perspective: SNMP routing, "
507
+ "PJL network vars, web config, subnet scan, WSD neighbors, attack paths."
508
+ ),
509
+ )
510
+ parser.add_argument(
511
+ "--xsp",
512
+ metavar="ATTACK_TYPE",
513
+ default=None,
514
+ choices=["info", "capture", "dos", "nvram", "exfil"],
515
+ help=(
516
+ "Generate Cross-Site Printing (XSP) + CORS spoofing payload. "
517
+ "Types: info (printer id), capture (job sniffer), dos (loop), "
518
+ "nvram (NVRAM damage), exfil (retrieve captured jobs)."
519
+ ),
520
+ )
521
+ parser.add_argument(
522
+ "--xsp-callback",
523
+ metavar="URL",
524
+ default="",
525
+ help="Attacker callback URL for XSP --exfil payloads",
526
+ )
527
+ # ── Auto exploit ───────────────────────────────────────────────────────────
528
+ parser.add_argument(
529
+ "--auto-exploit",
530
+ action="store_true",
531
+ help=(
532
+ "Automatic exploit selection: fingerprints the target, matches all applicable "
533
+ "exploit modules, verifies vulnerability with check(), pre-fills all required "
534
+ "parameters (host, port, serial, mac, vendor), and runs the best confirmed exploit. "
535
+ "Dry-run by default — add --no-dry to execute live. "
536
+ "Use --xpl-source to restrict to a specific exploit source."
537
+ ),
538
+ )
539
+ parser.add_argument(
540
+ "--auto-exploit-limit",
541
+ metavar="N",
542
+ type=int,
543
+ default=8,
544
+ help="Maximum number of exploits to probe with check() during --auto-exploit (default: 8).",
545
+ )
546
+ parser.add_argument(
547
+ "--auto-exploit-run",
548
+ metavar="N",
549
+ type=int,
550
+ default=1,
551
+ help="Number of confirmed-vulnerable exploits to execute during --auto-exploit (default: 1).",
552
+ )
553
+ parser.add_argument(
554
+ "--auto-exploit-file",
555
+ metavar="FILE",
556
+ default=None,
557
+ help=(
558
+ "Path to a custom exploit .py file to force-run via --auto-exploit. "
559
+ "The program pre-fills host/port/serial/vendor automatically. "
560
+ "Example: --auto-exploit-file /path/to/my_exploit.py"
561
+ ),
562
+ )
563
+ # ── Exploit module ─────────────────────────────────────────────────────────
564
+ parser.add_argument(
565
+ "--xpl-list",
566
+ action="store_true",
567
+ help="List all available exploits in xpl/ directory",
568
+ )
569
+ # ── Destructive audit mode ────────────────────────────────────────────────
570
+ parser.add_argument(
571
+ "--destructive-audit",
572
+ action="store_true",
573
+ dest="destructive_audit",
574
+ help=(
575
+ "Scan target for ALL irreversible/physical-damage vulnerabilities. "
576
+ "Checks: fuser thermal, motor jam, laser damage, NVRAM exhaustion, "
577
+ "firmware brick, SNMP factory reset, HP persistent root. "
578
+ "Default: assess-only (dry-run). Add --no-dry to send live payloads. "
579
+ "WARNING: --no-dry causes PERMANENT HARDWARE DAMAGE."
580
+ ),
581
+ )
582
+ parser.add_argument(
583
+ "--destructive-modules",
584
+ metavar="IDS",
585
+ default=None,
586
+ dest="destructive_modules",
587
+ help=(
588
+ "Comma-separated list of destructive module IDs to include in "
589
+ "--destructive-audit (default: all 10 modules). "
590
+ "Example: --destructive-modules research-fuser-thermal-attack,research-brother-nvram"
591
+ ),
592
+ )
593
+ parser.add_argument(
594
+ "--xpl-check",
595
+ metavar="EXPLOIT_ID",
596
+ default=None,
597
+ help="Check if target is vulnerable to a specific exploit (non-destructive)",
598
+ )
599
+ parser.add_argument(
600
+ "--xpl-run",
601
+ metavar="EXPLOIT_ID",
602
+ default=None,
603
+ help="Run a specific exploit against the target (dry-run by default; add --no-dry to execute)",
604
+ )
605
+ parser.add_argument(
606
+ "--xpl-update",
607
+ action="store_true",
608
+ help="Rebuild xpl/index.json from loaded exploits and re-scan xpl/ directory",
609
+ )
610
+ parser.add_argument(
611
+ "--xpl-fetch",
612
+ metavar="EDB_ID",
613
+ default=None,
614
+ help="Download a raw exploit from ExploitDB by ID (e.g. --xpl-fetch 45273)",
615
+ )
616
+ parser.add_argument(
617
+ "--xpl",
618
+ action="store_true",
619
+ help="Run exploit matching after --scan: shows available exploits for detected printer",
620
+ )
621
+ parser.add_argument(
622
+ "--xpl-source",
623
+ metavar="SOURCE",
624
+ default=None,
625
+ choices=["metasploit", "exploit-db", "research", "custom"],
626
+ help=(
627
+ "Filter --xpl-list or --xpl-run by exploit source. "
628
+ "Choices: metasploit, exploit-db, research, custom"
629
+ ),
630
+ )
631
+ # ── Brute force login ──────────────────────────────────────────────────────
632
+ parser.add_argument(
633
+ "--bruteforce",
634
+ action="store_true",
635
+ help=(
636
+ "Brute-force printer login using default vendor credentials. "
637
+ "Tests HTTP web admin, FTP, SNMP, Telnet. "
638
+ "Generates variations: normal, reverse, leet, CamelCase, UPPER."
639
+ ),
640
+ )
641
+ parser.add_argument(
642
+ "--bf-serial",
643
+ metavar="SERIAL",
644
+ default=None,
645
+ help=(
646
+ "Device serial number for brute-force (used as password for EPSON, HP, etc.). "
647
+ "Auto-detected from --scan if available. "
648
+ "Example: --bf-serial XAABT77481"
649
+ ),
650
+ )
651
+ parser.add_argument(
652
+ "--bf-mac",
653
+ metavar="MAC",
654
+ default=None,
655
+ help=(
656
+ "Device MAC address for brute-force (used for OKI, Brother, Kyocera KR2). "
657
+ "Example: --bf-mac AA:BB:CC:DD:EE:FF"
658
+ ),
659
+ )
660
+ parser.add_argument(
661
+ "--bf-vendor",
662
+ metavar="VENDOR",
663
+ default=None,
664
+ help=(
665
+ "Override vendor for credential selection (e.g. 'epson', 'hp', 'ricoh'). "
666
+ "Auto-detected from --scan if available."
667
+ ),
668
+ )
669
+ parser.add_argument(
670
+ "--bf-cred",
671
+ metavar="USER:PASS",
672
+ action="append",
673
+ default=[],
674
+ help=(
675
+ "Extra credential to test (can repeat). "
676
+ "Example: --bf-cred admin:MyPass --bf-cred root:"
677
+ ),
678
+ )
679
+ parser.add_argument(
680
+ "--bf-no-variations",
681
+ action="store_true",
682
+ help="Disable password variation generation (leet/reverse/camelcase). Faster but less thorough.",
683
+ )
684
+ parser.add_argument(
685
+ "--bf-delay",
686
+ metavar="SECS",
687
+ type=float,
688
+ default=0.3,
689
+ help="Delay in seconds between login attempts (default: 0.3s). Increase to avoid lockouts.",
690
+ )
691
+ parser.add_argument(
692
+ "--bf-wordlist",
693
+ metavar="FILE",
694
+ default=None,
695
+ help=(
696
+ "Custom wordlist file for brute-force (format: user:pass per line, # = comment). "
697
+ "REPLACES the default wordlist (wordlists/printer_default_creds.txt). "
698
+ "Supports vendor sections: '# ── Vendor ───'. "
699
+ "Use --bf-cred to add individual credentials on top of the wordlist. "
700
+ "Example: --bf-wordlist /path/to/my_creds.txt"
701
+ ),
702
+ )
703
+ # ── Send print job ─────────────────────────────────────────────────────────
704
+ parser.add_argument(
705
+ "--send-job",
706
+ metavar="FILE",
707
+ default=None,
708
+ help=(
709
+ "Send a file to the printer for printing. "
710
+ "Supported: .ps, .pcl, .pdf, .txt, .png, .jpg, .doc, .docx and any raw format. "
711
+ "Example: --send-job report.pdf"
712
+ ),
713
+ )
714
+ parser.add_argument(
715
+ "--send-proto",
716
+ metavar="PROTO",
717
+ default="auto",
718
+ choices=["auto", "raw", "ipp", "lpd"],
719
+ help=(
720
+ "Protocol for --send-job. "
721
+ "auto (default) = smart probe → picks best available protocol. "
722
+ "raw = JetDirect/9100 (HP/PCL), ipp = IPP/IPPS-631 (AirPrint), "
723
+ "lpd = LPD/515 (ESC-P, Epson inkjets)."
724
+ ),
725
+ )
726
+ parser.add_argument(
727
+ "--install-printer",
728
+ action="store_true",
729
+ default=False,
730
+ help=(
731
+ "Install the target printer on this host using the OS printer subsystem. "
732
+ "Windows: Add-Printer (PowerShell, requires admin). "
733
+ "Linux/macOS: CUPS lpadmin. "
734
+ "After installation, print normally from any application. "
735
+ "Use --install-driver to select the driver (auto|generic|epson|hp|cups-ipp)."
736
+ ),
737
+ )
738
+ parser.add_argument(
739
+ "--install-driver",
740
+ metavar="DRIVER",
741
+ default="auto",
742
+ choices=["auto", "generic", "epson", "hp", "cups-ipp"],
743
+ help=(
744
+ "Driver mode for --install-printer. "
745
+ "auto = IPP Everywhere / AirPrint if port 631 is open, else generic RAW. "
746
+ "epson = Epson universal inkjet. hp = HP Universal PCL6. "
747
+ "cups-ipp = CUPS IPP Everywhere (best for Linux/macOS). "
748
+ "generic = text/RAW passthrough (default fallback)."
749
+ ),
750
+ )
751
+ parser.add_argument(
752
+ "--install-name",
753
+ metavar="NAME",
754
+ default=None,
755
+ help="Custom printer name for --install-printer (default: PrinterXPL-Forge-<IP>).",
756
+ )
757
+ parser.add_argument(
758
+ "--send-copies",
759
+ metavar="N",
760
+ type=int,
761
+ default=1,
762
+ help="Number of copies to print (default: 1)",
763
+ )
764
+ parser.add_argument(
765
+ "--send-queue",
766
+ metavar="QUEUE",
767
+ default="lp",
768
+ help="LPD queue name for --send-job with --send-proto lpd (default: lp)",
769
+ )
770
+ # ── Interactive mode ───────────────────────────────────────────────────────
771
+ parser.add_argument(
772
+ "--interactive", "-I",
773
+ action="store_true",
774
+ help="Launch guided interactive menu (default when run with no arguments)",
775
+ )
776
+ return parser
777
+
778
+
779
+ def get_args() -> argparse.Namespace:
780
+ """Return parsed CLI arguments."""
781
+ parser = build_parser()
782
+ parser.add_argument(
783
+ "--version",
784
+ action="version",
785
+ version=f"%(prog)s {get_version_string()}",
786
+ help="Show program version and exit",
787
+ )
788
+ return parser.parse_args()
789
+
790
+
791
+ from itertools import zip_longest
792
+
793
+
794
+ # --------------------------------------------------------------------------- #
795
+ # Scan / recon mode
796
+ # --------------------------------------------------------------------------- #
797
+ def _ui_section(step: str, title: str, target: str = '') -> None:
798
+ """Print a clean section header for multi-step operations."""
799
+ _CYN = '\033[1;36m'
800
+ _DIM = '\033[2;37m'
801
+ _RST = '\033[0m'
802
+ _BLD = '\033[1m'
803
+ tgt = f' — {_DIM}{target}{_RST}' if target else ''
804
+ print(f"\n {_CYN}┌── [{step}] {_BLD}{title}{_RST}{tgt}")
805
+ print(f" {_CYN}│{_RST}")
806
+
807
+
808
+ def _run_auto_exploit(args) -> None:
809
+ """
810
+ Automatic exploit pipeline:
811
+ 1. Quick fingerprint (banner grab)
812
+ 2. Match + verify exploits with check()
813
+ 3. Pre-fill parameters and run() on top confirmed vulnerable exploit
814
+ """
815
+ from utils.banner_grabber import grab_all, print_fingerprint
816
+ from utils.exploit_manager import auto_exploit, print_auto_exploit_summary
817
+
818
+ target = args.target
819
+ timeout = getattr(args, 'timeout', 8)
820
+ dry_run = not getattr(args, 'no_dry', False)
821
+
822
+ output().green(f"\n>> Auto Exploit — {target}")
823
+
824
+ # Step 1: fingerprint
825
+ fp = {}
826
+ try:
827
+ fp = grab_all(target, timeout=timeout, quiet=True)
828
+ if not args.quiet:
829
+ print_fingerprint(fp)
830
+ except Exception as exc:
831
+ output().warning(f"Fingerprint failed: {exc} — proceeding with empty fingerprint")
832
+
833
+ make = fp.get('make', '') or ''
834
+ model = fp.get('model', '') or ''
835
+ firmware = fp.get('firmware', '') or ''
836
+ ports = fp.get('open_ports', []) or []
837
+ langs = fp.get('langs', []) or []
838
+ cves = fp.get('cves', []) or []
839
+ serial = getattr(args, 'bf_serial', '') or ''
840
+ mac = getattr(args, 'bf_mac', '') or ''
841
+
842
+ results = auto_exploit(
843
+ target,
844
+ make = make,
845
+ model = model,
846
+ firmware = firmware,
847
+ open_ports = ports,
848
+ langs = langs,
849
+ cves = cves,
850
+ serial = serial,
851
+ mac = mac,
852
+ source_filter = getattr(args, 'xpl_source', None),
853
+ custom_xpl_path= getattr(args, 'auto_exploit_file', None),
854
+ dry_run = dry_run,
855
+ check_limit = getattr(args, 'auto_exploit_limit', 8),
856
+ run_top_n = getattr(args, 'auto_exploit_run', 1),
857
+ timeout = float(timeout),
858
+ verbose = not getattr(args, 'quiet', False),
859
+ )
860
+
861
+ print_auto_exploit_summary(results)
862
+
863
+
864
+ def _run_scan(args) -> None:
865
+ """
866
+ Run banner grabbing + CVE scan + optional ML analysis on args.target.
867
+ No payloads are sent — this is pure reconnaissance.
868
+ """
869
+ from utils.config import load_config, nvd_key, feature_available, warn_missing
870
+ from utils.banner_grabber import grab_all, print_fingerprint
871
+ from utils.vuln_scanner import scan as vuln_scan, print_report
872
+
873
+ load_config(path=getattr(args, 'config', None))
874
+ target = args.target
875
+ use_nvd = not getattr(args, 'no_nvd', False)
876
+ use_ml = getattr(args, 'scan_ml', False)
877
+
878
+ # Inform user about optional NVD key (works without, just rate-limited)
879
+ if use_nvd and not feature_available('nvd_lookup'):
880
+ output().warning(
881
+ "NVD API key not configured — using public rate limit (5 req/30s). "
882
+ "Add nvd.api_key to config.json for higher limits."
883
+ )
884
+ timeout = 5.0
885
+
886
+ _ui_section('1/3', 'Fingerprint & Banner Grab', target)
887
+
888
+ # 1. Banner grab (with spinner)
889
+ try:
890
+ from ui.spinner import Spinner
891
+ sp = Spinner(f'Probing {target} ...').start()
892
+ try:
893
+ fp = grab_all(target, timeout=timeout, verbose=False)
894
+ finally:
895
+ sp.stop(True, f'Fingerprint complete — {fp.make or "?"} {fp.model or ""}')
896
+ except Exception:
897
+ fp = grab_all(target, timeout=timeout, verbose=True)
898
+ print_fingerprint(fp)
899
+
900
+ # 2. CVE / vuln scan
901
+ _ui_section('2/3', 'Vulnerability Assessment', target)
902
+ report = vuln_scan(
903
+ host = target,
904
+ make = fp.make,
905
+ model = fp.model,
906
+ firmware = fp.firmware,
907
+ open_ports = fp.open_ports,
908
+ printer_langs = fp.printer_langs,
909
+ snmp_descr = fp.snmp_descr,
910
+ doc_formats = fp.doc_formats,
911
+ nvd_api_key = nvd_key(),
912
+ use_nvd = use_nvd,
913
+ verbose = True,
914
+ )
915
+ print_report(report)
916
+
917
+ # 3. Exploit matching (always shown if exploits available; --xpl forces it)
918
+ _ui_section('3/3', 'Exploit Matching & Recommendations', target)
919
+ xpl_active = getattr(args, 'xpl', False) or True # always show on scan
920
+ try:
921
+ from utils.exploit_manager import get_matched_for_target, print_matched_exploits
922
+ all_cve_entries = report.specific_cves + report.vendor_cves + report.generic_cves
923
+ vuln_cves = []
924
+ for c in all_cve_entries:
925
+ if hasattr(c, 'cve_id'):
926
+ vuln_cves.append(c.cve_id)
927
+ elif hasattr(c, 'id'):
928
+ vuln_cves.append(c.id)
929
+ elif isinstance(c, dict):
930
+ vuln_cves.append(c.get('id', c.get('cve_id', '')))
931
+ matched_xpls = get_matched_for_target(
932
+ make=fp.make, model=fp.model, firmware=getattr(fp, 'firmware', '') or getattr(fp, 'firmware_version', ''),
933
+ open_ports=fp.open_ports, langs=fp.printer_langs,
934
+ cves=vuln_cves,
935
+ )
936
+ if matched_xpls:
937
+ print_matched_exploits(matched_xpls, target)
938
+ else:
939
+ output().message(f" [xpl] No specific exploits matched for {fp.make} {fp.model}")
940
+ except Exception as exc:
941
+ output().warning(f"Exploit matching error: {exc}")
942
+
943
+ # 4. Brute-force hint + next-steps summary
944
+ _CYN = '\033[1;36m'; _DIM = '\033[2;37m'; _RST = '\033[0m'
945
+ _GRN = '\033[1;32m'; _YEL = '\033[1;33m'
946
+ try:
947
+ from utils.default_creds import get_creds_for_vendor
948
+ bf_vendor_hint = (fp.make or '').lower().split()[0] if fp.make else 'generic'
949
+ vendor_creds = get_creds_for_vendor(bf_vendor_hint)
950
+ serial_hint = fp.serial or '<SERIAL>'
951
+ print(f"\n {_CYN}┌── Next Steps ──────────────────────────────────────────────{_RST}")
952
+ print(f" {_CYN}│{_RST}")
953
+ print(f" {_CYN}│{_RST} {_GRN}Brute-force{_RST} ({len(vendor_creds)} default creds for {bf_vendor_hint}):")
954
+ print(f" {_CYN}│{_RST} {_DIM}python src/main.py {target} --bruteforce "
955
+ f"--bf-vendor {bf_vendor_hint} --bf-serial {serial_hint}{_RST}")
956
+ print(f" {_CYN}│{_RST}")
957
+ print(f" {_CYN}│{_RST} {_GRN}Attack matrix{_RST} (BlackHat 2017 + CVEs, dry-run):")
958
+ print(f" {_CYN}│{_RST} {_DIM}python src/main.py {target} --attack-matrix{_RST}")
959
+ print(f" {_CYN}│{_RST}")
960
+ print(f" {_CYN}│{_RST} {_GRN}Network mapping{_RST} (subnet scan, pivot paths):")
961
+ print(f" {_CYN}│{_RST} {_DIM}python src/main.py {target} --network-map{_RST}")
962
+ print(f" {_CYN}│{_RST}")
963
+ print(f" {_CYN}│{_RST} {_YEL}Interactive guided menu:{_RST}")
964
+ print(f" {_CYN}│{_RST} {_DIM}python src/main.py (no args){_RST}")
965
+ print(f" {_CYN}└─────────────────────────────────────────────────────────────{_RST}")
966
+ except Exception:
967
+ pass
968
+
969
+ # 5. Optional ML analysis
970
+ if use_ml:
971
+ output().green(">> ML-Assisted Analysis:")
972
+ try:
973
+ from utils.ml_engine import quick_analyze
974
+ all_banners = ' '.join(str(v) for v in fp.raw_banners.values())
975
+ quick_analyze(
976
+ banner_text = all_banners,
977
+ open_ports = fp.open_ports,
978
+ verbose = True,
979
+ )
980
+ except Exception as exc:
981
+ output().warning(f"ML engine error: {exc}")
982
+
983
+ # 4. Auto-mode recommendation
984
+ print()
985
+ output().green(">> Attack Mode Recommendation:")
986
+ langs = [l.upper() for l in fp.printer_langs]
987
+ if 'PJL' in langs:
988
+ output().message(" Recommended: python src/main.py {target} pjl --safe")
989
+ elif 'PS' in langs or 'POSTSCRIPT' in langs:
990
+ output().message(f" Recommended: python src/main.py {target} ps --safe")
991
+ elif 'PCL' in langs:
992
+ output().message(f" Recommended: python src/main.py {target} pcl --safe")
993
+ elif fp.doc_formats:
994
+ output().warning(
995
+ f" Printer uses {fp.printer_langs} — not a PJL/PS/PCL laser printer.\n"
996
+ f" Attack surface: IPP job submission, web interface, LPD flooding."
997
+ )
998
+ else:
999
+ output().warning(" Could not determine printer language. Try: auto mode")
1000
+
1001
+ # --------------------------------------------------------------------------- #
1002
+ # --------------------------------------------------------------------------- #
1003
+ # Send job dispatcher
1004
+ # --------------------------------------------------------------------------- #
1005
+ def _run_install_printer(args) -> None:
1006
+ """Install the target printer on the current host OS."""
1007
+ from modules.install_printer import install_printer
1008
+
1009
+ _CYN = '\033[1;36m'; _GRN = '\033[1;32m'; _YEL = '\033[1;33m'
1010
+ _RED = '\033[1;31m'; _DIM = '\033[2;37m'; _RST = '\033[0m'; _BLD = '\033[1m'
1011
+
1012
+ target = args.target
1013
+ driver_mode = getattr(args, 'install_driver', 'auto')
1014
+ name = getattr(args, 'install_name', None)
1015
+
1016
+ print(f"\n {_CYN}{_BLD}[ Install Printer ]{_RST}")
1017
+ print(f" {_DIM}{'─' * 48}{_RST}")
1018
+ print(f" {_DIM}Target : {target}{_RST}")
1019
+ print(f" {_DIM}Driver : {driver_mode}{_RST}")
1020
+ if name:
1021
+ print(f" {_DIM}Name : {name}{_RST}")
1022
+ print()
1023
+
1024
+ import platform
1025
+ print(f" {_CYN}[*] Installing on {platform.system()}…{_RST}")
1026
+
1027
+ result = install_printer(host=target, name=name, driver_mode=driver_mode)
1028
+ print()
1029
+
1030
+ if result.success:
1031
+ print(f" {_GRN}[+] Printer installed successfully!{_RST}")
1032
+ print(f" {_DIM} Name : {result.printer_name}{_RST}")
1033
+ print(f" {_DIM} Protocol : {result.protocol}{_RST}")
1034
+ print(f" {_DIM} OS : {result.os_type}{_RST}")
1035
+ if result.message:
1036
+ print(f" {_DIM} Detail : {result.message}{_RST}")
1037
+ if result.hint:
1038
+ print()
1039
+ print(f" {_GRN}[*] Next step:{_RST}")
1040
+ print(f" {_DIM} {result.hint}{_RST}")
1041
+ else:
1042
+ print(f" {_RED}[-] Installation failed.{_RST}")
1043
+ print(f" {_DIM} Error : {result.error}{_RST}")
1044
+ if result.hint:
1045
+ print()
1046
+ print(f" {_YEL}[!] Suggestion: {result.hint}{_RST}")
1047
+ if result.commands:
1048
+ print()
1049
+ print(f" {_DIM} Manual command:{_RST}")
1050
+ for cmd in result.commands:
1051
+ print(f" {_DIM} $ {cmd}{_RST}")
1052
+
1053
+
1054
+ def _run_send_job(args) -> None:
1055
+ """Send a file/text to the target printer for printing."""
1056
+ from modules.print_job import (
1057
+ send_print_job, probe_printer,
1058
+ _SNMP_STATUS_LABEL, PrinterCapabilities,
1059
+ )
1060
+
1061
+ _CYN = '\033[1;36m'; _GRN = '\033[1;32m'; _YEL = '\033[1;33m'
1062
+ _RED = '\033[1;31m'; _DIM = '\033[2;37m'; _RST = '\033[0m'
1063
+ _BLD = '\033[1m'
1064
+
1065
+ target = args.target
1066
+ filepath = args.send_job
1067
+ proto = getattr(args, 'send_proto', 'auto')
1068
+ copies = getattr(args, 'send_copies', 1)
1069
+ queue = getattr(args, 'send_queue', 'lp')
1070
+ port_ovr = getattr(args, 'port', 0) or 0
1071
+
1072
+ print(f"\n {_CYN}{_BLD}[ Send Job ]{_RST}")
1073
+ print(f" {_DIM}{'─' * 48}{_RST}")
1074
+ print(f" {_DIM}File : {filepath}{_RST}")
1075
+ print(f" {_DIM}Protocol : {proto.upper()}{_RST}")
1076
+ print(f" {_DIM}Copies : {copies}{_RST}")
1077
+ print()
1078
+
1079
+ # ── Step 1: probe printer capabilities ───────────────────────────────────
1080
+ print(f" {_CYN}[*] Probing {target}…{_RST}")
1081
+ caps = probe_printer(target, timeout=6.0)
1082
+
1083
+ snmp_label = _SNMP_STATUS_LABEL.get(caps.snmp_status, f'code {caps.snmp_status}')
1084
+ proto_info = []
1085
+ if caps.ipp_available:
1086
+ proto_info.append(f"IPP{'S' if caps.ipp_requires_tls else ''}")
1087
+ if caps.lpd_available:
1088
+ proto_info.append("LPD")
1089
+ if caps.raw_available:
1090
+ proto_info.append("RAW")
1091
+
1092
+ avail_str = ', '.join(proto_info) if proto_info else 'none detected'
1093
+ print(f" {_DIM}Printer : {target}{_RST}")
1094
+ print(f" {_DIM}Protocols: {avail_str}{_RST}")
1095
+ print(f" {_DIM}Status : {snmp_label}{_RST}")
1096
+ print()
1097
+
1098
+ # Warn if printer is busy
1099
+ if caps.printer_busy:
1100
+ print(f" {_YEL}[!] Printer is currently BUSY (printing another job).{_RST}")
1101
+ print(f" {_YEL} Wait for it to finish, then retry.{_RST}")
1102
+ print()
1103
+
1104
+ # Warn if nothing is available
1105
+ if not proto_info:
1106
+ print(f" {_RED}[!] No compatible print protocols detected on {target}.{_RST}")
1107
+ print(f" {_RED} The printer may be hardened or not reachable.{_RST}")
1108
+ print(f" {_DIM} Suggestion: try --install-printer to add via OS driver,{_RST}")
1109
+ print(f" {_DIM} or verify the IP and that the printer is powered on.{_RST}")
1110
+ return
1111
+
1112
+ # ── Step 2: send the job ─────────────────────────────────────────────────
1113
+ best = caps.best_protocol if proto == 'auto' else proto
1114
+ port = port_ovr or {'raw': 9100, 'ipp': 631, 'lpd': 515}.get(best, 631)
1115
+ tls_note = ' (IPPS/TLS)' if (best == 'ipp' and caps.ipp_requires_tls) else ''
1116
+ print(f" {_CYN}[*] Sending via {best.upper()}{tls_note} → {target}:{port}{_RST}")
1117
+
1118
+ result = send_print_job(
1119
+ host=target, path=filepath,
1120
+ protocol=proto, port=port_ovr,
1121
+ copies=copies, queue=queue,
1122
+ caps=caps,
1123
+ )
1124
+
1125
+ print()
1126
+ if result.success:
1127
+ print(f" {_GRN}[+] Print job accepted!{_RST}")
1128
+ print(f" {_DIM} Protocol : {result.protocol.upper()}{_RST}")
1129
+ print(f" {_DIM} Payload : {result.file_size:,} bytes{_RST}")
1130
+ print(f" {_DIM} Elapsed : {result.elapsed_ms:.0f} ms{_RST}")
1131
+ if result.message:
1132
+ print(f" {_DIM} Detail : {result.message}{_RST}")
1133
+ print()
1134
+ print(f" {_GRN} Check the printer — the job should print shortly.{_RST}")
1135
+ else:
1136
+ print(f" {_RED}[-] Print job failed.{_RST}")
1137
+ print(f" {_DIM} Error : {result.error}{_RST}")
1138
+ if result.hint:
1139
+ print()
1140
+ print(f" {_YEL}[!] Suggestion:{_RST}")
1141
+ for line in result.hint.split('. '):
1142
+ line = line.strip()
1143
+ if line:
1144
+ print(f" {_DIM} {line}.{_RST}")
1145
+ print()
1146
+ print(f" {_DIM} Tip: use --install-printer to add this printer via OS driver{_RST}")
1147
+ print(f" {_DIM} and print through the system spooler instead.{_RST}")
1148
+
1149
+
1150
+ # Attack module dispatcher
1151
+ # --------------------------------------------------------------------------- #
1152
+ def _run_attack_modules(args) -> None:
1153
+ """
1154
+ Dispatch to the appropriate attack/audit module based on CLI flags.
1155
+
1156
+ Supports: --ipp, --ipp-submit, --pivot, --pivot-scan, --storage,
1157
+ --firmware, --firmware-reset, --payload, --implant.
1158
+ """
1159
+ from utils.config import load_config
1160
+ load_config(path=getattr(args, 'config', None))
1161
+
1162
+ target = args.target
1163
+ timeout = 10.0
1164
+
1165
+ # ── IPP audit ─────────────────────────────────────────────────────────────
1166
+ if getattr(args, 'ipp', False):
1167
+ output().green(f"\n>> IPP Security Audit: {target}")
1168
+ try:
1169
+ from protocols.ipp_attacks import audit
1170
+ results = audit(target, timeout=timeout, verbose=True)
1171
+ if results['risk']:
1172
+ output().errmsg(f"[!] Risks found: {', '.join(results['risk'])}")
1173
+ else:
1174
+ output().green("[OK] No critical IPP vulnerabilities detected.")
1175
+ except Exception as exc:
1176
+ output().errmsg(f"IPP audit error: {exc}")
1177
+
1178
+ # ── IPP job submission ─────────────────────────────────────────────────────
1179
+ if getattr(args, 'ipp_submit', False):
1180
+ dry = not getattr(args, 'no_dry', False)
1181
+ output().green(f"\n>> IPP Job Submission: {target} "
1182
+ f"({'dry-run' if dry else 'LIVE — actual print'})")
1183
+ try:
1184
+ from protocols.ipp_attacks import discover_endpoints, submit_job
1185
+ eps = discover_endpoints(target, timeout)
1186
+ if not eps:
1187
+ output().errmsg("No IPP endpoint found")
1188
+ else:
1189
+ ep = eps[0]
1190
+ res = submit_job(
1191
+ target, ep['port'], ep['path'], ep['scheme'],
1192
+ doc_fmt='image/pwg-raster', job_name='pentest-job',
1193
+ dry_run=dry, timeout=timeout,
1194
+ )
1195
+ if res['accepted']:
1196
+ output().errmsg(f"[!] Anonymous print ACCEPTED: {res['message']}")
1197
+ elif res['auth_required']:
1198
+ output().green(f"[OK] Authentication required: {res['message']}")
1199
+ else:
1200
+ output().warning(f"Result: {res['message']}")
1201
+ except Exception as exc:
1202
+ output().errmsg(f"IPP submit error: {exc}")
1203
+
1204
+ # ── Pivot / lateral movement ───────────────────────────────────────────────
1205
+ if getattr(args, 'pivot', False):
1206
+ output().green(f"\n>> Lateral Movement / SSRF Pivot Audit: {target}")
1207
+ try:
1208
+ from protocols.ssrf_pivot import pivot_audit
1209
+ from protocols.ipp_attacks import discover_endpoints
1210
+ eps = discover_endpoints(target, timeout)
1211
+ port = eps[0]['port'] if eps else 631
1212
+ path = eps[0]['path'] if eps else '/ipp/print'
1213
+ scheme = eps[0]['scheme'] if eps else 'https'
1214
+ results = pivot_audit(target, port, path, scheme, timeout, verbose=True)
1215
+ if results['risk']:
1216
+ output().errmsg(f"[!] Pivot risks: {', '.join(results['risk'])}")
1217
+ if results['internal_hosts']:
1218
+ output().errmsg(
1219
+ f"[!] Internal hosts reachable via SSRF: "
1220
+ f"{', '.join(results['internal_hosts'])}"
1221
+ )
1222
+ else:
1223
+ output().green("[OK] No SSRF pivot vectors confirmed.")
1224
+ except Exception as exc:
1225
+ output().errmsg(f"Pivot audit error: {exc}")
1226
+
1227
+ # ── Pivot port scan ────────────────────────────────────────────────────────
1228
+ if getattr(args, 'pivot_scan', None):
1229
+ internal = args.pivot_scan
1230
+ output().green(f"\n>> SSRF Port Scan: {internal} via printer {target}")
1231
+ try:
1232
+ from protocols.ipp_attacks import discover_endpoints
1233
+ from protocols.ssrf_pivot import ssrf_port_scan
1234
+ eps = discover_endpoints(target, timeout)
1235
+ port = eps[0]['port'] if eps else 631
1236
+ path = eps[0]['path'] if eps else '/ipp/print'
1237
+ scheme = eps[0]['scheme'] if eps else 'https'
1238
+ scan_results = ssrf_port_scan(
1239
+ target, port, path, internal,
1240
+ scheme=scheme, timeout=6, verbose=True,
1241
+ )
1242
+ open_ports = [p for p, s in scan_results.items() if s == 'open']
1243
+ if open_ports:
1244
+ output().errmsg(f"[!] Open ports on {internal}: {open_ports}")
1245
+ else:
1246
+ output().green(f"[OK] No open ports detected on {internal} via SSRF.")
1247
+ except Exception as exc:
1248
+ output().errmsg(f"Pivot scan error: {exc}")
1249
+
1250
+ # ── Storage audit ─────────────────────────────────────────────────────────
1251
+ if getattr(args, 'storage', False):
1252
+ output().green(f"\n>> Printer Storage Audit: {target}")
1253
+ try:
1254
+ from protocols.storage import storage_audit
1255
+ results = storage_audit(target, timeout=timeout, verbose=True)
1256
+ if results['risk']:
1257
+ output().errmsg(f"[!] Storage risks: {'; '.join(results['risk'])}")
1258
+ else:
1259
+ output().green("[OK] No storage vulnerabilities found.")
1260
+ except Exception as exc:
1261
+ output().errmsg(f"Storage audit error: {exc}")
1262
+
1263
+ # ── Firmware audit ────────────────────────────────────────────────────────
1264
+ if getattr(args, 'firmware', False):
1265
+ output().green(f"\n>> Firmware Security Audit: {target}")
1266
+ try:
1267
+ from protocols.firmware import firmware_audit
1268
+ results = firmware_audit(target, timeout=timeout, verbose=True)
1269
+ if results['risk']:
1270
+ output().errmsg(f"[!] Firmware risks: {', '.join(results['risk'])}")
1271
+ else:
1272
+ output().green("[OK] No firmware vulnerabilities found.")
1273
+ except Exception as exc:
1274
+ output().errmsg(f"Firmware audit error: {exc}")
1275
+
1276
+ # ── Factory reset ─────────────────────────────────────────────────────────
1277
+ if getattr(args, 'firmware_reset', None):
1278
+ method = args.firmware_reset
1279
+ output().warning(f"\n[!] Factory reset via {method} on {target} — AUTHORIZED TARGET ONLY")
1280
+ try:
1281
+ from protocols.firmware import factory_reset
1282
+ ok = factory_reset(target, timeout=timeout, method=method, verbose=True)
1283
+ if ok:
1284
+ output().errmsg(f"[!] Factory reset command accepted via {method}")
1285
+ else:
1286
+ output().green(f"[OK] Reset command rejected or not supported")
1287
+ except Exception as exc:
1288
+ output().errmsg(f"Firmware reset error: {exc}")
1289
+
1290
+ # ── Payload injection ─────────────────────────────────────────────────────
1291
+ if getattr(args, 'payload', None):
1292
+ spec = args.payload
1293
+ custom_data = getattr(args, 'payload_data', '')
1294
+ try:
1295
+ lang, kind = spec.split(':', 1)
1296
+ except ValueError:
1297
+ lang, kind = spec, 'info'
1298
+ output().green(f"\n>> Payload Injection: lang={lang} type={kind} target={target}")
1299
+ try:
1300
+ from protocols.firmware import make_payload
1301
+ payload = make_payload(lang, kind, custom_data)
1302
+ if not payload:
1303
+ output().warning(f"No payload generated for {lang}:{kind}")
1304
+ else:
1305
+ from utils.ports import PortConfig as _PC
1306
+ import socket as _sock
1307
+ _raw_port = _PC.resolve('raw')
1308
+ s = _sock.create_connection((target, _raw_port), timeout=timeout)
1309
+ s.sendall(payload)
1310
+ _sock.setdefaulttimeout(2)
1311
+ resp = b''
1312
+ try:
1313
+ resp = s.recv(4096)
1314
+ except Exception:
1315
+ pass
1316
+ s.close()
1317
+ output().green(f"[+] Payload sent ({len(payload)} bytes)")
1318
+ if resp:
1319
+ output().message(f" Response: {resp[:200]}")
1320
+ except Exception as exc:
1321
+ output().errmsg(f"Payload error: {exc}")
1322
+
1323
+ # ── Persistent implant ────────────────────────────────────────────────────
1324
+ if getattr(args, 'implant', None):
1325
+ raw = args.implant
1326
+ output().warning(f"\n[!] Persistent implant on {target}: {raw}")
1327
+ try:
1328
+ pairs = dict(kv.split('=', 1) for kv in raw.split(',') if '=' in kv)
1329
+ from protocols.firmware import implant_config
1330
+ results = implant_config(
1331
+ target,
1332
+ smtp_host = pairs.get('smtp_host', ''),
1333
+ smtp_email = pairs.get('smtp_email', ''),
1334
+ ntp_host = pairs.get('ntp_host', ''),
1335
+ dns_server = pairs.get('dns', ''),
1336
+ snmp_community = pairs.get('snmp_community', ''),
1337
+ timeout=timeout, verbose=True,
1338
+ )
1339
+ for k, ok in results.items():
1340
+ status = '\033[1;31m[IMPLANTED]\033[0m' if ok else '[failed]'
1341
+ output().message(f" {status} {k}")
1342
+ except Exception as exc:
1343
+ output().errmsg(f"Implant error: {exc}")
1344
+
1345
+ # ── Exploit module handlers ────────────────────────────────────────────────
1346
+
1347
+ # ── Destructive attack audit ───────────────────────────────────────────────
1348
+ if getattr(args, 'destructive_audit', False):
1349
+ try:
1350
+ from core.destructive_audit import main_destructive_audit
1351
+ main_destructive_audit(args)
1352
+ except SystemExit:
1353
+ raise
1354
+ except Exception as exc:
1355
+ output().errmsg(f"destructive-audit error: {exc}")
1356
+ return # destructive audit exits on its own
1357
+
1358
+ # --xpl-list: list all exploits
1359
+ if getattr(args, 'xpl_list', False):
1360
+ try:
1361
+ from utils.exploit_manager import load_all_exploits, print_exploit_list
1362
+ src_filter = getattr(args, 'xpl_source', None)
1363
+ xpls = load_all_exploits(source_filter=src_filter)
1364
+ src_label = f' [{src_filter}]' if src_filter else ''
1365
+ print_exploit_list(
1366
+ xpls,
1367
+ title=f'PrinterXPL-Forge Exploit Library{src_label} ({len(xpls)} exploits)'
1368
+ )
1369
+ except Exception as exc:
1370
+ output().errmsg(f"xpl-list error: {exc}")
1371
+
1372
+ # --xpl-update: rebuild index
1373
+ if getattr(args, 'xpl_update', False):
1374
+ try:
1375
+ from utils.exploit_manager import load_all_exploits, update_index
1376
+ xpls = load_all_exploits()
1377
+ update_index(xpls)
1378
+ output().green(f"[+] Exploit index updated ({len(xpls)} exploits)")
1379
+ except Exception as exc:
1380
+ output().errmsg(f"xpl-update error: {exc}")
1381
+
1382
+ # --xpl-fetch: download raw from ExploitDB
1383
+ if getattr(args, 'xpl_fetch', None):
1384
+ edb_id = args.xpl_fetch
1385
+ output().green(f"\n>> Fetching EDB-{edb_id} from exploit-db.com ...")
1386
+ try:
1387
+ from utils.exploit_manager import fetch_exploit_db_raw
1388
+ path = fetch_exploit_db_raw(edb_id)
1389
+ if path:
1390
+ output().green(f"[+] Saved to {path}")
1391
+ else:
1392
+ output().warning("Download failed — check EDB ID or connection")
1393
+ except Exception as exc:
1394
+ output().errmsg(f"xpl-fetch error: {exc}")
1395
+
1396
+ # --xpl-check: check if target is vulnerable to specific exploit
1397
+ if getattr(args, 'xpl_check', None):
1398
+ xpl_id = args.xpl_check
1399
+ if not args.target:
1400
+ output().errmsg("--xpl-check requires a target IP/host")
1401
+ else:
1402
+ output().green(f"\n>> Exploit Check: {xpl_id} against {target}")
1403
+ try:
1404
+ from utils.exploit_manager import load_all_exploits
1405
+ xpls = {x.id.upper(): x for x in load_all_exploits()}
1406
+ xpl = xpls.get(xpl_id.upper())
1407
+ if not xpl:
1408
+ output().errmsg(f"Exploit '{xpl_id}' not found in xpl/. Run --xpl-list.")
1409
+ else:
1410
+ output().message(f" [{xpl.severity.upper()}] {xpl.title}")
1411
+ vuln = xpl.check(target, timeout=timeout)
1412
+ if vuln:
1413
+ output().errmsg(f"[VULNERABLE] Target appears vulnerable to {xpl_id}")
1414
+ output().message(f" Run with --xpl-run {xpl_id} to exploit")
1415
+ else:
1416
+ output().green(f"[OK] Target does not appear vulnerable to {xpl_id}")
1417
+ except Exception as exc:
1418
+ output().errmsg(f"xpl-check error: {exc}")
1419
+
1420
+ # --xpl-run: execute specific exploit
1421
+ if getattr(args, 'xpl_run', None):
1422
+ xpl_id = args.xpl_run
1423
+ dry = not getattr(args, 'no_dry', False)
1424
+ if not args.target:
1425
+ output().errmsg("--xpl-run requires a target IP/host")
1426
+ else:
1427
+ output().green(
1428
+ f"\n>> Running Exploit: {xpl_id} against {target} "
1429
+ f"[{'DRY-RUN' if dry else 'LIVE EXPLOIT'}]"
1430
+ )
1431
+ if not dry:
1432
+ output().warning(
1433
+ "[!] LIVE mode — ensure explicit written authorization before proceeding."
1434
+ )
1435
+ try:
1436
+ from utils.exploit_manager import load_all_exploits, print_run_result
1437
+ xpls = {x.id.upper(): x for x in load_all_exploits()}
1438
+ xpl = xpls.get(xpl_id.upper())
1439
+ if not xpl:
1440
+ output().errmsg(f"Exploit '{xpl_id}' not found. Run --xpl-list.")
1441
+ else:
1442
+ output().message(f" Title : {xpl.title}")
1443
+ output().message(f" CVE : {xpl.cve or 'N/A'}")
1444
+ output().message(f" Severity : {xpl.severity.upper()} (CVSS {xpl.cvss})")
1445
+ output().message(f" Protocol : {xpl.protocol} port {xpl.port}")
1446
+ result = xpl.run(target, timeout=timeout, dry_run=dry)
1447
+ print_run_result(result, xpl_id)
1448
+ except Exception as exc:
1449
+ output().errmsg(f"xpl-run error: {exc}")
1450
+
1451
+ # ── Brute-force login ─────────────────────────────────────────────────────
1452
+ if getattr(args, 'bruteforce', False):
1453
+ from modules.login_bruteforce import bruteforce as bf_run, print_report as bf_print
1454
+ from utils.wordlist_loader import get_default_wordlist_path, wordlist_stats
1455
+
1456
+ # Resolve vendor: CLI override > auto-detect from scan
1457
+ bf_vendor = getattr(args, 'bf_vendor', None) or ''
1458
+ bf_serial = getattr(args, 'bf_serial', None) or ''
1459
+ bf_mac = getattr(args, 'bf_mac', None) or ''
1460
+ bf_delay = getattr(args, 'bf_delay', 0.3)
1461
+ bf_novary = getattr(args, 'bf_no_variations', False)
1462
+
1463
+ # Auto-detect vendor from fingerprint if not overridden
1464
+ if not bf_vendor:
1465
+ try:
1466
+ from utils.banner_grabber import grab_all
1467
+ fp = grab_all(target, timeout=5, verbose=False)
1468
+ bf_vendor = (fp.make or '').lower().split()[0]
1469
+ if not bf_serial and fp.serial:
1470
+ bf_serial = fp.serial
1471
+ output().message(f" [bf] Auto-detected vendor: {bf_vendor or 'unknown'}")
1472
+ if bf_serial:
1473
+ output().message(f" [bf] Serial from scan: {bf_serial}")
1474
+ except Exception:
1475
+ pass
1476
+
1477
+ if not bf_vendor:
1478
+ bf_vendor = 'generic'
1479
+
1480
+ # --bf-wordlist: use as the credential source (replaces default wordlist)
1481
+ # --bf-cred: additional credentials prepended (highest priority)
1482
+ bf_wordlist = getattr(args, 'bf_wordlist', None)
1483
+
1484
+ # Validate custom wordlist path
1485
+ if bf_wordlist:
1486
+ import os
1487
+ if not os.path.exists(bf_wordlist):
1488
+ output().warning(f" [bf] Wordlist not found: {bf_wordlist}")
1489
+ bf_wordlist = None
1490
+ else:
1491
+ stats = wordlist_stats(bf_wordlist)
1492
+ total_entries = sum(stats.values())
1493
+ output().message(f" [bf] Custom wordlist: {bf_wordlist} ({total_entries} entries)")
1494
+ else:
1495
+ # Show info about default wordlist
1496
+ default_wl = get_default_wordlist_path()
1497
+ if default_wl:
1498
+ stats = wordlist_stats(default_wl)
1499
+ total_entries = sum(stats.values())
1500
+ output().message(f" [bf] Default wordlist: {default_wl} ({total_entries} entries)")
1501
+ else:
1502
+ output().warning(" [bf] No wordlist found — place printer_default_creds.txt in wordlists/")
1503
+
1504
+ # Parse extra credentials from --bf-cred USER:PASS (prepended, highest priority)
1505
+ extra_creds = []
1506
+ for cred_str in getattr(args, 'bf_cred', []) or []:
1507
+ if ':' in cred_str:
1508
+ u, p = cred_str.split(':', 1)
1509
+ extra_creds.append((u, p if p else None))
1510
+ else:
1511
+ extra_creds.append((cred_str, None))
1512
+
1513
+ if extra_creds:
1514
+ output().message(f" [bf] Extra credentials (--bf-cred): {len(extra_creds)} entries")
1515
+
1516
+ output().green(
1517
+ f"\n>> Brute Force Login: {target} | vendor={bf_vendor} | "
1518
+ f"serial={bf_serial or '?'} | variations={'off' if bf_novary else 'on'}"
1519
+ )
1520
+
1521
+ report = bf_run(
1522
+ host = target,
1523
+ vendor = bf_vendor,
1524
+ serial = bf_serial,
1525
+ mac = bf_mac,
1526
+ open_ports = None,
1527
+ delay = bf_delay,
1528
+ enable_variations = not bf_novary,
1529
+ stop_on_first = True,
1530
+ extra_creds = extra_creds,
1531
+ wordlist_path = bf_wordlist, # None → use default wordlist automatically
1532
+ verbose = True,
1533
+ )
1534
+ bf_print(report)
1535
+
1536
+ # Write to log
1537
+ try:
1538
+ import pathlib, datetime
1539
+ log_dir = pathlib.Path('.log')
1540
+ log_dir.mkdir(exist_ok=True)
1541
+ with open(log_dir / 'terminal-output.log', 'a', encoding='utf-8') as f:
1542
+ f.write(f"\n[{datetime.datetime.now().isoformat()}] "
1543
+ f"bruteforce {target} vendor={bf_vendor} "
1544
+ f"serial={bf_serial} found={len(report.found)}\n")
1545
+ for r in report.found:
1546
+ f.write(f" FOUND {r.protocol.upper()} {r.username!r}/{r.password_display()!r}\n")
1547
+ except Exception:
1548
+ pass
1549
+
1550
+ # ── Full attack matrix campaign ────────────────────────────────────────────
1551
+ if getattr(args, 'attack_matrix', False):
1552
+ dry = not getattr(args, 'no_dry', False)
1553
+ nm = getattr(args, 'network_map', False)
1554
+ output().green(
1555
+ f"\n>> Attack Matrix Campaign: {target} "
1556
+ f"[{'DRY-RUN' if dry else 'LIVE EXPLOIT'}]"
1557
+ )
1558
+ if not dry:
1559
+ output().warning(
1560
+ "[!] LIVE EXPLOIT MODE — destructive actions WILL be executed. "
1561
+ "Ensure you have explicit written authorization."
1562
+ )
1563
+ try:
1564
+ # Quick banner grab to get printer context
1565
+ langs: list = []
1566
+ ports: list = []
1567
+ make_: str = ''
1568
+ model_: str = ''
1569
+ fw_: str = ''
1570
+ try:
1571
+ from utils.banner_grabber import grab_all
1572
+ fp = grab_all(target, timeout=timeout)
1573
+ langs = fp.printer_langs
1574
+ ports = fp.open_ports
1575
+ make_ = fp.make
1576
+ model_ = fp.model
1577
+ fw_ = fp.firmware_version
1578
+ except Exception:
1579
+ pass
1580
+
1581
+ from core.attack_orchestrator import run_campaign, print_campaign_report
1582
+ report = run_campaign(
1583
+ host=target, make=make_, model=model_, firmware=fw_,
1584
+ printer_langs=langs, open_ports=ports,
1585
+ dry_run=dry, timeout=timeout,
1586
+ run_netmap=nm, verbose=True,
1587
+ )
1588
+ print_campaign_report(report)
1589
+ except Exception as exc:
1590
+ output().errmsg(f"Attack matrix error: {exc}")
1591
+
1592
+ # ── Network map ────────────────────────────────────────────────────────────
1593
+ if getattr(args, 'network_map', False) and not getattr(args, 'attack_matrix', False):
1594
+ output().green(f"\n>> Network Map from Printer Perspective: {target}")
1595
+ try:
1596
+ from protocols.network_map import build_network_map, print_network_map
1597
+ nm = build_network_map(target, timeout=timeout, verbose=True)
1598
+ print_network_map(nm)
1599
+ except Exception as exc:
1600
+ output().errmsg(f"Network map error: {exc}")
1601
+
1602
+ # ── XSP payload generation ────────────────────────────────────────────────
1603
+ if getattr(args, 'xsp', None):
1604
+ attack = args.xsp
1605
+ cb = getattr(args, 'xsp_callback', '')
1606
+ output().green(
1607
+ f"\n>> Cross-Site Printing (XSP) Payload Generator: {target} [{attack}]"
1608
+ )
1609
+ try:
1610
+ from protocols.network_map import generate_xsp_payload
1611
+ from utils.ports import PortConfig as _PC2
1612
+ payloads = generate_xsp_payload(
1613
+ printer_ip=target, printer_port=_PC2.resolve('raw'),
1614
+ attack_type=attack, callback_url=cb, exfil_url=cb,
1615
+ )
1616
+ # Save HTML to .log/
1617
+ import os as _os
1618
+ log_dir = _os.path.join(
1619
+ _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))), '.log'
1620
+ )
1621
+ _os.makedirs(log_dir, exist_ok=True)
1622
+ html_path = _os.path.join(log_dir, f'xsp_{attack}_{target}.html')
1623
+ with open(html_path, 'w', encoding='utf-8') as fh:
1624
+ fh.write(payloads['html'])
1625
+ js_path = _os.path.join(log_dir, f'xsp_{attack}_{target}.js')
1626
+ with open(js_path, 'w', encoding='utf-8') as fh:
1627
+ fh.write(payloads['javascript'])
1628
+ output().green(f"[+] XSP HTML payload → {html_path}")
1629
+ output().green(f"[+] XSP JS payload → {js_path}")
1630
+ output().message("\n[PostScript payload preview]")
1631
+ print(payloads['postscript'][:300])
1632
+ except Exception as exc:
1633
+ output().errmsg(f"XSP error: {exc}")
1634
+
1635
+
1636
+ # --------------------------------------------------------------------------- #
1637
+ # Banner
1638
+ # --------------------------------------------------------------------------- #
1639
+ def intro(quiet: bool) -> None:
1640
+ """Print the PrinterXPL-Forge banner (ASCII art on the left, project info on the right)."""
1641
+ if quiet:
1642
+ return
1643
+
1644
+ # ASCII art for an MFP-style printer (left column)
1645
+ art = [
1646
+ " _____________________________________________________________ ",
1647
+ " /___________________________________________________________/| ",
1648
+ " | |=========================================================| | ",
1649
+ " | | | | ",
1650
+ " | | ____________ __________ ________________________ | | ",
1651
+ " | | | [] [] [] | | ________ | | . . . . . . . . . | | | ",
1652
+ " | | |___________| | | ____ || |________________________| | | ",
1653
+ " | |---------------| | |____| || |-------------------------- | | ",
1654
+ " | | ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ | | ",
1655
+ " | | |___|___|___|___|___|___|___|___|___|___|___|___|___| | | ",
1656
+ " | |_________________________________________________________| | ",
1657
+ " | |------------------- OUTPUT TRAY ---------------------- | | ",
1658
+ " | |_________________________________________________________|/| ",
1659
+ " | ______________________ ___________________________ | ",
1660
+ " | | | | | | ",
1661
+ " | | PAPER BIN | | SUPPLY DRAWER | | ",
1662
+ " | |_____________________| |__________________________| | ",
1663
+ " |___________________________________________________________|/ ",
1664
+ "|___________________[==== PAPER ====]___________________/ ",
1665
+ "",
1666
+ ]
1667
+
1668
+ # Project information (right column)
1669
+ info = [
1670
+ "",
1671
+ "",
1672
+ "",
1673
+ "",
1674
+ "",
1675
+ f"{APP_NAME} :: Advanced Printer Penetration Testing Toolkit",
1676
+ VERSION,
1677
+ "Author : Andre Henrique",
1678
+ "Contact: X / LinkedIn @mrhenrike",
1679
+ "",
1680
+ "feast on paper, harvest vulnerabilities",
1681
+ "",
1682
+ "(ASCII art by ChatGPT)",
1683
+ ]
1684
+
1685
+ gap = 4 # spaces between the two columns
1686
+ art_width = max(len(line) for line in art)
1687
+
1688
+ for left, right in zip_longest(art, info, fillvalue=""):
1689
+ print(f"{left:<{art_width}}{' ' * gap}{right}")
1690
+
1691
+ # --------------------------------------------------------------------------- #
1692
+ # Main logic
1693
+ # --------------------------------------------------------------------------- #
1694
+ def _require_target(args: argparse.Namespace, flag: str = 'this operation') -> None:
1695
+ """Prompt the user for a target IP/hostname if not already set in the session.
1696
+
1697
+ Args:
1698
+ args: Parsed argument namespace.
1699
+ flag: Name of the CLI flag or action requiring the target (for display).
1700
+ """
1701
+ if args.target:
1702
+ return
1703
+ try:
1704
+ print()
1705
+ val = input(f" ? Target IP or hostname ({flag}): ").strip()
1706
+ print()
1707
+ except (EOFError, KeyboardInterrupt):
1708
+ print()
1709
+ sys.exit(0)
1710
+ if not val:
1711
+ output().errmsg("Target IP/hostname is required to proceed.")
1712
+ sys.exit(1)
1713
+ args.target = val
1714
+
1715
+
1716
+ def main() -> None:
1717
+ """Main program flow."""
1718
+ # If called without any arguments → interactive guided menu
1719
+ if len(sys.argv) == 1:
1720
+ try:
1721
+ from ui.interactive import run_interactive
1722
+ run_interactive()
1723
+ except KeyboardInterrupt:
1724
+ print()
1725
+ sys.exit(0)
1726
+
1727
+ args = get_args()
1728
+
1729
+ # Handle discovery shortcuts that do not require positionals
1730
+ if args.discover_local:
1731
+ # 1. Show locally installed printers on this host
1732
+ try:
1733
+ from utils.local_printers import discover_local_installed, print_local_printers
1734
+ output().green("\n>> Locally Installed Printers (this host):")
1735
+ local_printers = discover_local_installed()
1736
+ print_local_printers(local_printers)
1737
+ except Exception as exc:
1738
+ output().errmsg(f"Local printer enumeration failed: {exc}")
1739
+
1740
+ # 2. SNMP network scan for printers on reachable subnets
1741
+ output().green(">> Network Discovery (SNMP scan):")
1742
+ discovery(usage=True)
1743
+ sys.exit(0)
1744
+
1745
+ # ── Load config first (honors --config flag) ─────────────────────────────
1746
+ try:
1747
+ from utils.config import load_config, check_all_features, require_feature
1748
+ load_config(path=getattr(args, 'config', None))
1749
+ except Exception as _cfg_err:
1750
+ pass # config is optional — tool runs without it
1751
+
1752
+ # ── Apply custom port overrides globally (must happen before any module connects)
1753
+ try:
1754
+ from utils.ports import PortConfig
1755
+ PortConfig.configure_from_args(args)
1756
+ except Exception:
1757
+ pass
1758
+
1759
+ # ── --check-config: print feature availability and exit ───────────────────
1760
+ if getattr(args, 'check_config', False):
1761
+ try:
1762
+ from utils.config import check_all_features
1763
+ check_all_features(print_report=True)
1764
+ except Exception as exc:
1765
+ print(f"[!] {exc}")
1766
+ sys.exit(0)
1767
+
1768
+ if args.discover_online:
1769
+ try:
1770
+ from utils.discovery_online import OnlineDiscoveryManager, DiscoveryParams
1771
+ from utils.config import (shodan_key as _sk, censys_credentials as _cc,
1772
+ fofa_key as _fk, zoomeye_key as _zk,
1773
+ netlas_key as _nk)
1774
+
1775
+ # ── Expand CSV values in all list-type dork flags ─────────────────
1776
+ # Each flag accepts either repeated usage OR comma-separated values.
1777
+ # _expand_csv normalises both forms into a flat, deduplicated list.
1778
+ _vendors = _expand_csv(getattr(args, 'dork_vendors', []) or [])
1779
+ _countries = _expand_csv(getattr(args, 'dork_countries', []) or [])
1780
+ _cities = _expand_csv(getattr(args, 'dork_cities', []) or [])
1781
+ _regions = _expand_csv(getattr(args, 'dork_regions', []) or [])
1782
+ _ports = _expand_csv_int(getattr(args, 'dork_ports', []) or [])
1783
+
1784
+ # Deduplicate while preserving order
1785
+ _vendors = list(dict.fromkeys(_vendors))
1786
+ _countries = list(dict.fromkeys(_countries))
1787
+ _cities = list(dict.fromkeys(_cities))
1788
+ _regions = list(dict.fromkeys(_regions))
1789
+ _ports = list(dict.fromkeys(_ports))
1790
+
1791
+ # ── Validate --dork-city: only when exactly ONE country is given ──
1792
+ if _cities:
1793
+ _n_countries = len(_countries)
1794
+ if _n_countries == 0:
1795
+ output().errmsg(
1796
+ "--dork-city requires exactly one --dork-country to be specified.\n"
1797
+ "Example: --dork-country BR --dork-city 'Sao Paulo'\n"
1798
+ "Without a country, city filtering is ambiguous and not supported."
1799
+ )
1800
+ sys.exit(1)
1801
+ if _n_countries > 1:
1802
+ output().errmsg(
1803
+ f"--dork-city cannot be used with multiple countries "
1804
+ f"({', '.join(_countries)}).\n"
1805
+ "Specify exactly ONE --dork-country when using --dork-city.\n"
1806
+ "To search multiple countries, omit --dork-city or run separate searches."
1807
+ )
1808
+ sys.exit(1)
1809
+
1810
+ # Build DiscoveryParams from --dork-* CLI flags
1811
+ dork_params = DiscoveryParams(
1812
+ vendors = _vendors,
1813
+ model = getattr(args, 'dork_model', None),
1814
+ countries = _countries,
1815
+ cities = _cities,
1816
+ regions = _regions,
1817
+ ports = _ports,
1818
+ org = getattr(args, 'dork_org', None),
1819
+ cpe = getattr(args, 'dork_cpe', None),
1820
+ limit = getattr(args, 'dork_limit', 100),
1821
+ )
1822
+
1823
+ # Enforce: at least one dork filter required when no IP target given
1824
+ if not dork_params.has_filters() and not getattr(args, 'target', None):
1825
+ output().errmsg(
1826
+ "--discover-online requires at least one filter:\n"
1827
+ " --dork-vendor VENDOR[,VENDOR...] e.g. hp | hp,canon,epson\n"
1828
+ " --dork-country COUNTRY[,COUNTRY...] e.g. BR | BR,AR,US\n"
1829
+ " --dork-port PORT[,PORT...] e.g. 9100 | 9100,515,631\n"
1830
+ " --dork-region REGION[,REGION...] e.g. latin_america | latin_america,europe\n"
1831
+ " --dork-city CITY[,CITY...] requires exactly ONE --dork-country\n"
1832
+ " --dork-org ORG e.g. 'Telefonica'\n"
1833
+ " --dork-cpe CPE Censys/Netlas only\n"
1834
+ " --dork-model MODEL e.g. 'deskjet pro 5500'\n\n"
1835
+ "Or provide a direct IP target: python printerxpl-forge.py <IP> --scan"
1836
+ )
1837
+ sys.exit(1)
1838
+
1839
+ # ── Resolve engine whitelist ──────────────────────────────────────
1840
+ # Rules:
1841
+ # • Individual flags (--shodan, --censys, …) → select exactly ONE engine.
1842
+ # • --dork-engine A,B,C → the ONLY way to query multiple engines.
1843
+ # • Mixing individual flags with --dork-engine is an error.
1844
+ # • Using more than one individual flag simultaneously is an error.
1845
+ # • No flag → all engines with configured API keys.
1846
+ _VALID_ENGINES = {'shodan', 'censys', 'fofa', 'zoomeye', 'netlas'}
1847
+ _flag_engines: list = [
1848
+ _eng for _eng in _VALID_ENGINES
1849
+ if getattr(args, f'engine_{_eng}', False)
1850
+ ]
1851
+
1852
+ _dork_engine_arg = getattr(args, 'dork_engine', None)
1853
+
1854
+ # Mutual exclusion: individual flags + --dork-engine cannot coexist
1855
+ if _flag_engines and _dork_engine_arg:
1856
+ output().errmsg(
1857
+ f"Cannot combine --{_flag_engines[0]} with --dork-engine.\n"
1858
+ f"Use exactly ONE of:\n"
1859
+ f" Individual flag : --{_flag_engines[0]}\n"
1860
+ f" Multi-engine flag: --dork-engine {_flag_engines[0]},<engine2>,..."
1861
+ )
1862
+ sys.exit(1)
1863
+
1864
+ # Mutual exclusion: only one individual engine flag allowed
1865
+ if len(_flag_engines) > 1:
1866
+ output().errmsg(
1867
+ f"Cannot combine individual engine flags: "
1868
+ f"{', '.join('--'+e for e in _flag_engines)}\n"
1869
+ f"To query multiple engines at once use --dork-engine instead:\n"
1870
+ f" --dork-engine {','.join(_flag_engines)}"
1871
+ )
1872
+ sys.exit(1)
1873
+
1874
+ _dork_engines: list = []
1875
+ if _dork_engine_arg:
1876
+ _dork_engines = [e.strip().lower() for e in _dork_engine_arg.split(',') if e.strip()]
1877
+ _bad = [e for e in _dork_engines if e not in _VALID_ENGINES]
1878
+ if _bad:
1879
+ output().errmsg(
1880
+ f"Unknown engine(s) in --dork-engine: {', '.join(_bad)}\n"
1881
+ f"Valid choices: {', '.join(sorted(_VALID_ENGINES))}\n"
1882
+ f"Example: --dork-engine shodan,censys,fofa"
1883
+ )
1884
+ sys.exit(1)
1885
+
1886
+ # Resolve final list: single flag → [engine]; --dork-engine → list; none → None (all)
1887
+ if _flag_engines:
1888
+ _engines: list | None = _flag_engines # exactly one engine
1889
+ elif _dork_engines:
1890
+ _engines = _dork_engines # explicit multi-engine
1891
+ else:
1892
+ _engines = None # all configured engines
1893
+
1894
+ # Load credentials for all engines
1895
+ try:
1896
+ _shodan_key = _sk()
1897
+ except Exception:
1898
+ _shodan_key = None
1899
+ try:
1900
+ _cid, _csec = _cc()
1901
+ except Exception:
1902
+ _cid, _csec = None, None
1903
+ try:
1904
+ _fkey = _fk()
1905
+ except Exception:
1906
+ _fkey = None
1907
+ try:
1908
+ _zykey = _zk()
1909
+ except Exception:
1910
+ _zykey = None
1911
+ try:
1912
+ _nlkey = _nk()
1913
+ except Exception:
1914
+ _nlkey = None
1915
+
1916
+ mgr = OnlineDiscoveryManager(
1917
+ shodan_key = _shodan_key,
1918
+ censys_id = _cid,
1919
+ censys_secret = _csec,
1920
+ fofa_key = _fkey,
1921
+ zoomeye_key = _zykey,
1922
+ netlas_key = _nlkey,
1923
+ )
1924
+ hits = mgr.targeted_search(dork_params, engines=_engines)
1925
+ mgr.print_results(hits)
1926
+ saved = mgr.export_results(hits)
1927
+ if saved:
1928
+ output().green(f"[+] Next: python printerxpl-forge.py <IP> --scan (test an individual target)")
1929
+
1930
+ except SystemExit:
1931
+ pass
1932
+ except Exception as e:
1933
+ output().errmsg(f"Online discovery failed: {e}")
1934
+ finally:
1935
+ sys.exit(0)
1936
+
1937
+ # ── --interactive: always launch guided menu ──────────────────────────────
1938
+ if getattr(args, 'interactive', False):
1939
+ try:
1940
+ from ui.interactive import run_interactive
1941
+ run_interactive()
1942
+ except KeyboardInterrupt:
1943
+ print()
1944
+ sys.exit(0)
1945
+
1946
+ # ── --xpl-list / --xpl-update / --xpl-fetch (no target needed) ─────────
1947
+ if getattr(args, 'xpl_list', False):
1948
+ try:
1949
+ from utils.exploit_manager import load_all_exploits, print_exploit_list
1950
+ src_filter = getattr(args, 'xpl_source', None)
1951
+ xpls = load_all_exploits(source_filter=src_filter)
1952
+ src_label = f' [{src_filter}]' if src_filter else ''
1953
+ print_exploit_list(
1954
+ xpls,
1955
+ title=f'PrinterXPL-Forge Exploit Library{src_label} ({len(xpls)} exploits)'
1956
+ )
1957
+ except Exception as exc:
1958
+ output().errmsg(f"xpl-list error: {exc}")
1959
+ sys.exit(0)
1960
+
1961
+ if getattr(args, 'xpl_update', False):
1962
+ try:
1963
+ from utils.exploit_manager import load_all_exploits, update_index
1964
+ xpls = load_all_exploits()
1965
+ update_index(xpls)
1966
+ output().green(f"[+] Exploit index updated ({len(xpls)} exploits)")
1967
+ except Exception as exc:
1968
+ output().errmsg(f"xpl-update error: {exc}")
1969
+ sys.exit(0)
1970
+
1971
+ if getattr(args, 'xpl_fetch', None):
1972
+ edb_id = args.xpl_fetch
1973
+ output().green(f"\n>> Fetching EDB-{edb_id} from exploit-db.com ...")
1974
+ try:
1975
+ from utils.exploit_manager import fetch_exploit_db_raw
1976
+ path = fetch_exploit_db_raw(edb_id)
1977
+ if path:
1978
+ output().green(f"[+] Saved to {path}")
1979
+ else:
1980
+ output().warning("Download failed — check EDB ID or connection")
1981
+ except Exception as exc:
1982
+ output().errmsg(f"xpl-fetch error: {exc}")
1983
+ sys.exit(0)
1984
+
1985
+ # ── --auto-exploit: automatic exploit selection, verification & execution ──
1986
+ if getattr(args, 'auto_exploit', False):
1987
+ _require_target(args, '--auto-exploit')
1988
+ _run_auto_exploit(args)
1989
+ sys.exit(0)
1990
+
1991
+ # ── --install-printer: install printer on host OS ────────────────────────
1992
+ if getattr(args, 'install_printer', False):
1993
+ _require_target(args, '--install-printer')
1994
+ _run_install_printer(args)
1995
+ sys.exit(0)
1996
+
1997
+ # ── --send-job: send file/text to printer ────────────────────────────────
1998
+ if getattr(args, 'send_job', None):
1999
+ _require_target(args, '--send-job')
2000
+ _run_send_job(args)
2001
+ sys.exit(0)
2002
+
2003
+ # ── --scan / --scan-ml: reconnaissance without payloads ─────────────────
2004
+ scan_requested = getattr(args, 'scan', False) or getattr(args, 'scan_ml', False)
2005
+ if scan_requested:
2006
+ _require_target(args, '--scan')
2007
+ _run_scan(args)
2008
+ sys.exit(0)
2009
+
2010
+ # ── Attack / audit dispatchers ────────────────────────────────────────────
2011
+ _needs_target = ('ipp', 'ipp_submit', 'pivot', 'storage', 'firmware',
2012
+ 'firmware_reset', 'payload', 'implant',
2013
+ 'attack_matrix', 'network_map', 'xsp',
2014
+ 'xpl_check', 'xpl_run', 'bruteforce', 'auto_exploit')
2015
+ _any_attack = any(getattr(args, a.replace('-', '_'), None)
2016
+ for a in _needs_target)
2017
+ if _any_attack:
2018
+ _require_target(args, 'attack/audit flag')
2019
+ _run_attack_modules(args)
2020
+ sys.exit(0)
2021
+
2022
+ # Show banner first (respects --quiet).
2023
+ intro(args.quiet)
2024
+
2025
+ # Verify host OS compatibility early.
2026
+ os_type = get_os()
2027
+ supported_os = ("linux", "windows", "wsl", "darwin", "bsd", "android")
2028
+ if os_type not in supported_os:
2029
+ output().errmsg(f"[!] Unsupported OS: {os_type!r}.")
2030
+ output().message(" This tool supports Linux, WSL, Windows, macOS, BSD, and Android (Termux).")
2031
+ sys.exit(1)
2032
+
2033
+ # Show OS detection result in non-quiet mode
2034
+ if not args.quiet:
2035
+ os_names = {
2036
+ "linux": "Linux",
2037
+ "wsl": "Windows Subsystem for Linux (WSL)",
2038
+ "windows": "Windows",
2039
+ "darwin": "macOS",
2040
+ "bsd": "BSD",
2041
+ "android": "Android (Termux)",
2042
+ }
2043
+ output().message(f">> Detected OS: {os_names.get(os_type, os_type)}")
2044
+
2045
+ # Basic startup message
2046
+ if not args.quiet:
2047
+ print()
2048
+ output().green(f">> Starting {APP_NAME} (Advanced Printer Penetration Testing)")
2049
+ print()
2050
+
2051
+ # ── No meaningful args or --interactive flag → guided menu ───────────────
2052
+ # Also catches: only --quiet or other non-action flags with no target/mode
2053
+ _has_action = (
2054
+ args.target or args.mode
2055
+ or getattr(args, 'discover_local', False)
2056
+ or getattr(args, 'discover_online', False)
2057
+ or getattr(args, 'check_config', False)
2058
+ or getattr(args, 'xpl_list', False)
2059
+ or getattr(args, 'xpl_update', False)
2060
+ or getattr(args, 'xpl_fetch', None)
2061
+ or getattr(args, 'scan', False)
2062
+ or getattr(args, 'scan_ml', False)
2063
+ or getattr(args, 'send_job', None)
2064
+ or getattr(args, 'destructive_audit', False)
2065
+ or getattr(args, 'install_printer', False)
2066
+ or getattr(args, 'bruteforce', False)
2067
+ or getattr(args, 'ipp', False)
2068
+ or getattr(args, 'auto_exploit', False)
2069
+ )
2070
+ if not _has_action or getattr(args, 'interactive', False):
2071
+ try:
2072
+ from ui.interactive import run_interactive
2073
+ run_interactive()
2074
+ except KeyboardInterrupt:
2075
+ print()
2076
+ sys.exit(0)
2077
+
2078
+ # Default to auto-detect when no mode is specified
2079
+ args.mode = args.mode or 'auto'
2080
+
2081
+ # Auto-detect printer language support if mode is 'auto'
2082
+ if args.mode == 'auto':
2083
+ output().info("Auto-detecting printer language support...")
2084
+ # Try to detect via capabilities
2085
+ cap = capabilities(args)
2086
+
2087
+ # Priority: PJL > PostScript > PCL
2088
+ if cap.support:
2089
+ if 'PJL' in str(cap.support):
2090
+ args.mode = 'pjl'
2091
+ output().info("✅ PJL support detected. Using PJL mode")
2092
+ elif 'PostScript' in str(cap.support) or 'PS' in str(cap.support):
2093
+ args.mode = 'ps'
2094
+ output().info("✅ PostScript support detected. Using PS mode")
2095
+ elif 'PCL' in str(cap.support):
2096
+ args.mode = 'pcl'
2097
+ output().info("✅ PCL support detected. Using PCL mode")
2098
+ else:
2099
+ output().warning("⚠️ Unknown language detected. Defaulting to PJL")
2100
+ args.mode = 'pjl'
2101
+ else:
2102
+ # Fallback to PJL
2103
+ output().warning("⚠️ Could not detect language. Defaulting to PJL")
2104
+ args.mode = 'pjl'
2105
+
2106
+ # Capability auto-detection (e.g., SNMP, USB IDs, PJL INFO, etc.)
2107
+ capabilities(args)
2108
+
2109
+ # Map language option to the corresponding interactive shell class.
2110
+ shell_map: Dict[str, Callable[[argparse.Namespace], object]] = {
2111
+ "pjl": pjl,
2112
+ "ps": ps,
2113
+ "pcl": pcl,
2114
+ }
2115
+
2116
+ # Instantiate and run the chosen shell.
2117
+ shell_class = shell_map[args.mode]
2118
+ shell = shell_class(args)
2119
+
2120
+ # Only enter interactive loop if not exiting from loaded commands
2121
+ if not shell.should_exit:
2122
+ shell.cmdloop()
2123
+
2124
+ # --------------------------------------------------------------------------- #
2125
+ # Entrypoint
2126
+ # --------------------------------------------------------------------------- #
2127
+ if __name__ == "__main__":
2128
+ try:
2129
+ main()
2130
+ except KeyboardInterrupt:
2131
+ print()
2132
+ output().warning("[!] Execution interrupted by user.")
2133
+ print()
2134
+ sys.exit(0)