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.
- nse/README.md +204 -0
- nse/__init__.py +6 -0
- nse/install_nse.py +412 -0
- nse/lib/printerxpl.lua +238 -0
- nse/scripts/cups-info.nse +74 -0
- nse/scripts/cups-queue-info.nse +43 -0
- nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
- nse/scripts/http-device-mac.nse +107 -0
- nse/scripts/http-hp-ilo-info.nse +121 -0
- nse/scripts/http-info-xerox-enum.nse +101 -0
- nse/scripts/http-vuln-cve2022-1026.nse +158 -0
- nse/scripts/lexmark-config.nse +89 -0
- nse/scripts/pjl-ready-message.nse +106 -0
- nse/scripts/printer-banner.nse +217 -0
- nse/scripts/printer-cups-rce.nse +189 -0
- nse/scripts/printer-cve-detect.nse +279 -0
- nse/scripts/printer-discover.nse +205 -0
- nse/scripts/printer-firmware-exposed.nse +219 -0
- nse/scripts/printer-hp-pjl.nse +192 -0
- nse/scripts/printer-http-ews.nse +293 -0
- nse/scripts/printer-ipp-info.nse +235 -0
- nse/scripts/printer-lexmark-ipp.nse +203 -0
- nse/scripts/printer-passback.nse +204 -0
- nse/scripts/printer-pjl-info.nse +146 -0
- nse/scripts/printer-printnightmare.nse +211 -0
- nse/scripts/printer-snmp-info.nse +176 -0
- nse/scripts/printer-vuln-check.nse +256 -0
- nse/scripts/snmp-device-mac.nse +93 -0
- nse/scripts/snmp-info.nse +146 -0
- nse/scripts/snmp-sysdescr.nse +70 -0
- printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
- printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
- printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
- printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
- printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
- printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
- src/assets/fonts/gunplay.pfa +1671 -0
- src/assets/fonts/kshandwrt.pfa +315 -0
- src/assets/fonts/laksoner.pfa +2402 -0
- src/assets/fonts/paintcans.pfa +9699 -0
- src/assets/fonts/stencilod.pfa +4076 -0
- src/assets/fonts/takecover.pfa +26138 -0
- src/assets/fonts/topsecret.pfa +6652 -0
- src/assets/fonts/whoa.pfa +773 -0
- src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
- src/assets/mibs/Printer-MIB +4389 -0
- src/assets/mibs/README.md +9 -0
- src/assets/mibs/SNMPv2-MIB +854 -0
- src/assets/overlays/hacker.eps +596 -0
- src/assets/overlays/smiley.eps +214 -0
- src/assets/overlays/smiley2.eps +240 -0
- src/core/attack_orchestrator.py +1025 -0
- src/core/capabilities.py +323 -0
- src/core/destructive_audit.py +430 -0
- src/core/discovery.py +488 -0
- src/core/osdetect.py +74 -0
- src/core/poly_runner.py +579 -0
- src/core/printer.py +1426 -0
- src/main.py +2134 -0
- src/modules/install_printer.py +318 -0
- src/modules/login_bruteforce.py +852 -0
- src/modules/pcl.py +506 -0
- src/modules/pjl.py +3575 -0
- src/modules/print_job.py +1290 -0
- src/modules/ps.py +1102 -0
- src/payloads/__init__.py +98 -0
- src/payloads/assets/overlays/notice.eps +9 -0
- src/protocols/__init__.py +19 -0
- src/protocols/firmware.py +738 -0
- src/protocols/ipp.py +216 -0
- src/protocols/ipp_attacks.py +609 -0
- src/protocols/lpd.py +141 -0
- src/protocols/network_map.py +1004 -0
- src/protocols/raw.py +173 -0
- src/protocols/smb.py +359 -0
- src/protocols/ssrf_pivot.py +427 -0
- src/protocols/storage.py +587 -0
- src/ui/__init__.py +6 -0
- src/ui/interactive.py +742 -0
- src/ui/spinner.py +112 -0
- src/ui/tables.py +132 -0
- src/utils/banner_grabber.py +852 -0
- src/utils/codebook.py +456 -0
- src/utils/config.py +522 -0
- src/utils/cve_loader.py +158 -0
- src/utils/default_creds.py +134 -0
- src/utils/discovery_online.py +1327 -0
- src/utils/exploit_manager.py +805 -0
- src/utils/fuzzer.py +220 -0
- src/utils/helper.py +732 -0
- src/utils/local_printers.py +307 -0
- src/utils/ml_engine.py +491 -0
- src/utils/operators.py +474 -0
- src/utils/ports.py +234 -0
- src/utils/vuln_scanner.py +823 -0
- src/utils/wordlist_loader.py +412 -0
- 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()
|