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/core/discovery.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Author : Andre Henrique (@mrhenrike)
|
|
6
|
+
# GitHub : https://github.com/mrhenrike
|
|
7
|
+
# LinkedIn : https://linkedin.com/in/mrhenrike
|
|
8
|
+
# X/Twitter : https://x.com/mrhenrike
|
|
9
|
+
|
|
10
|
+
import socket
|
|
11
|
+
import subprocess
|
|
12
|
+
import ipaddress
|
|
13
|
+
import shutil
|
|
14
|
+
from utils.helper import output, conv
|
|
15
|
+
from core.osdetect import get_os
|
|
16
|
+
|
|
17
|
+
# SNMP OIDs we’ll query
|
|
18
|
+
OID_HRDEV_TYPE = '1.3.6.1.2.1.25.3.2.1.2.1'
|
|
19
|
+
OID_HRDEV_DESCR = '1.3.6.1.2.1.25.3.2.1.3.1'
|
|
20
|
+
OID_SYS_UPTIME = '1.3.6.1.2.1.1.3.0'
|
|
21
|
+
OID_PR_STATUS = '1.3.6.1.2.1.43.16.5.1.2.1.1'
|
|
22
|
+
OID_PR_INTERPRETER = '1.3.6.1.2.1.43.16.5.1.2.1.2'
|
|
23
|
+
|
|
24
|
+
# HOST-RESOURCES-MIB Printer Table
|
|
25
|
+
OID_HRPRINTER_STATUS = '1.3.6.1.2.1.25.3.5.1.1.1'
|
|
26
|
+
OID_HRPRINTER_ERROR_STATE = '1.3.6.1.2.1.25.3.5.1.2.1'
|
|
27
|
+
OID_HRPRINTER_JOB_COUNT = '1.3.6.1.2.1.25.3.5.1.3.1'
|
|
28
|
+
|
|
29
|
+
# Printer-MIB Supplies & Alerts
|
|
30
|
+
OID_PRT_MARKER_SUPPLY_DESC = '1.3.6.1.2.1.43.11.1.1.6.1.1'
|
|
31
|
+
OID_PRT_MARKER_SUPPLY_TYPE = '1.3.6.1.2.1.43.11.1.1.2.1.1'
|
|
32
|
+
OID_PRT_MARKER_SUPPLY_LEVEL = '1.3.6.1.2.1.43.11.1.1.9.1.1'
|
|
33
|
+
OID_PRT_INPUT_MEDIA_TYPE = '1.3.6.1.2.1.43.11.1.1.2.1.1'
|
|
34
|
+
OID_PRT_INPUT_STATUS = '1.3.6.1.2.1.43.11.1.1.8.1.1'
|
|
35
|
+
OID_PRT_ALERTS_VALUE = '1.3.6.1.2.1.43.18.1.1.8.1.1'
|
|
36
|
+
|
|
37
|
+
# ENTITY-MIB Physical Entities
|
|
38
|
+
OID_ENT_PHYS_DESCR = '1.3.6.1.2.1.47.1.1.1.1.2'
|
|
39
|
+
OID_ENT_PHYS_NAME = '1.3.6.1.2.1.47.1.1.1.1.7'
|
|
40
|
+
OID_ENT_PHYS_FIRMWARE_REV = '1.3.6.1.2.1.47.1.1.1.1.10'
|
|
41
|
+
OID_ENT_PHYS_SERIAL = '1.3.6.1.2.1.47.1.1.1.1.11'
|
|
42
|
+
OID_ENT_PHYS_MODEL_NAME = '1.3.6.1.2.1.47.1.1.1.1.13'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_selection(sel, max_index):
|
|
46
|
+
sel = sel.strip().lower()
|
|
47
|
+
if sel in ('all', 'a', ''):
|
|
48
|
+
return list(range(1, max_index + 1))
|
|
49
|
+
chosen = set()
|
|
50
|
+
for part in sel.split(','):
|
|
51
|
+
if '-' in part:
|
|
52
|
+
start, end = part.split('-', 1)
|
|
53
|
+
chosen.update(range(int(start), int(end) + 1))
|
|
54
|
+
else:
|
|
55
|
+
chosen.add(int(part))
|
|
56
|
+
return sorted(i for i in chosen if 1 <= i <= max_index)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_local_networks(os_type):
|
|
60
|
+
"""
|
|
61
|
+
Return a list of IPv4 /24 networks on UP, non-loopback,
|
|
62
|
+
non-link-local interfaces.
|
|
63
|
+
|
|
64
|
+
Supports: Linux, WSL, Windows, macOS (darwin), BSD, Android (Termux).
|
|
65
|
+
"""
|
|
66
|
+
raw = []
|
|
67
|
+
|
|
68
|
+
# ── Linux / WSL / Android (Termux) ───────────────────────────────────
|
|
69
|
+
if os_type in ('linux', 'wsl', 'android'):
|
|
70
|
+
# Try modern iproute2 'ip' command (Linux, Android)
|
|
71
|
+
if shutil.which('ip'):
|
|
72
|
+
try:
|
|
73
|
+
out = subprocess.check_output(
|
|
74
|
+
['ip', '-o', '-f', 'inet', 'addr', 'show'],
|
|
75
|
+
text=True, stderr=subprocess.DEVNULL
|
|
76
|
+
)
|
|
77
|
+
for line in out.splitlines():
|
|
78
|
+
parts = line.split()
|
|
79
|
+
if len(parts) < 4:
|
|
80
|
+
continue
|
|
81
|
+
iface = parts[1].rstrip(':')
|
|
82
|
+
cidr = next((p for p in parts if '/' in p), None)
|
|
83
|
+
if not cidr or iface in ('lo', 'lo0'):
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
raw.append(ipaddress.ip_network(cidr, strict=False))
|
|
87
|
+
except ValueError:
|
|
88
|
+
pass
|
|
89
|
+
except Exception as e:
|
|
90
|
+
output().warning(f"Could not list interfaces via 'ip': {e}")
|
|
91
|
+
|
|
92
|
+
# Fallback: ifconfig (older Linux distros, Android without ip)
|
|
93
|
+
if not raw and shutil.which('ifconfig'):
|
|
94
|
+
try:
|
|
95
|
+
out = subprocess.check_output(
|
|
96
|
+
['ifconfig'], text=True, stderr=subprocess.DEVNULL
|
|
97
|
+
)
|
|
98
|
+
for m in __import__('re').finditer(
|
|
99
|
+
r'inet (?:addr:)?(\d+\.\d+\.\d+\.\d+).*?(?:Mask:|netmask )(\S+)',
|
|
100
|
+
out, __import__('re').DOTALL
|
|
101
|
+
):
|
|
102
|
+
ip_str, mask_str = m.group(1), m.group(2)
|
|
103
|
+
try:
|
|
104
|
+
net = ipaddress.ip_network(f"{ip_str}/{mask_str}", strict=False)
|
|
105
|
+
raw.append(net)
|
|
106
|
+
except ValueError:
|
|
107
|
+
pass
|
|
108
|
+
except Exception as e:
|
|
109
|
+
output().warning(f"Could not list interfaces via 'ifconfig': {e}")
|
|
110
|
+
|
|
111
|
+
# ── macOS (darwin) ────────────────────────────────────────────────────
|
|
112
|
+
if os_type == 'darwin':
|
|
113
|
+
# networksetup gives cleaner output than ifconfig on macOS
|
|
114
|
+
if shutil.which('networksetup'):
|
|
115
|
+
try:
|
|
116
|
+
# Get list of network services
|
|
117
|
+
svcs = subprocess.check_output(
|
|
118
|
+
['networksetup', '-listallnetworkservices'],
|
|
119
|
+
text=True, stderr=subprocess.DEVNULL
|
|
120
|
+
).splitlines()[1:] # skip header line
|
|
121
|
+
|
|
122
|
+
for svc in svcs:
|
|
123
|
+
svc = svc.strip()
|
|
124
|
+
if not svc or svc.startswith('*'):
|
|
125
|
+
continue
|
|
126
|
+
try:
|
|
127
|
+
info = subprocess.check_output(
|
|
128
|
+
['networksetup', '-getinfo', svc],
|
|
129
|
+
text=True, stderr=subprocess.DEVNULL
|
|
130
|
+
)
|
|
131
|
+
ip_m = __import__('re').search(r'IP address: (\d+\.\d+\.\d+\.\d+)', info)
|
|
132
|
+
mask_m = __import__('re').search(r'Subnet mask: (\d+\.\d+\.\d+\.\d+)', info)
|
|
133
|
+
if ip_m and mask_m:
|
|
134
|
+
ip_str, mask_str = ip_m.group(1), mask_m.group(1)
|
|
135
|
+
net = ipaddress.ip_network(f"{ip_str}/{mask_str}", strict=False)
|
|
136
|
+
raw.append(net)
|
|
137
|
+
except Exception:
|
|
138
|
+
continue
|
|
139
|
+
except Exception as e:
|
|
140
|
+
output().warning(f"Could not list macOS network services: {e}")
|
|
141
|
+
|
|
142
|
+
# Fallback: ifconfig (always present on macOS)
|
|
143
|
+
if not raw and shutil.which('ifconfig'):
|
|
144
|
+
try:
|
|
145
|
+
import re as _re
|
|
146
|
+
out = subprocess.check_output(
|
|
147
|
+
['ifconfig'], text=True, stderr=subprocess.DEVNULL
|
|
148
|
+
)
|
|
149
|
+
for m in _re.finditer(
|
|
150
|
+
r'inet (\d+\.\d+\.\d+\.\d+) netmask (0x[0-9a-f]+|\d+\.\d+\.\d+\.\d+)',
|
|
151
|
+
out
|
|
152
|
+
):
|
|
153
|
+
ip_str, mask_str = m.group(1), m.group(2)
|
|
154
|
+
# macOS ifconfig uses hex netmasks (e.g. 0xffffff00)
|
|
155
|
+
if mask_str.startswith('0x'):
|
|
156
|
+
mask_int = int(mask_str, 16)
|
|
157
|
+
mask_str = str(ipaddress.IPv4Address(mask_int))
|
|
158
|
+
try:
|
|
159
|
+
net = ipaddress.ip_network(f"{ip_str}/{mask_str}", strict=False)
|
|
160
|
+
raw.append(net)
|
|
161
|
+
except ValueError:
|
|
162
|
+
pass
|
|
163
|
+
except Exception as e:
|
|
164
|
+
output().warning(f"Could not list macOS interfaces via ifconfig: {e}")
|
|
165
|
+
|
|
166
|
+
# ── Windows / WSL ─────────────────────────────────────────────────────
|
|
167
|
+
if os_type in ('windows', 'wsl'):
|
|
168
|
+
pwsh = shutil.which('powershell.exe') or shutil.which('pwsh.exe')
|
|
169
|
+
if pwsh:
|
|
170
|
+
try:
|
|
171
|
+
cmd = [
|
|
172
|
+
pwsh, '-NoProfile', '-Command',
|
|
173
|
+
"Get-NetIPAddress -AddressFamily IPv4 "
|
|
174
|
+
"| Where { $_.IPAddress -ne '127.0.0.1' } "
|
|
175
|
+
"| Select -ExpandProperty IPAddress"
|
|
176
|
+
]
|
|
177
|
+
out = subprocess.check_output(
|
|
178
|
+
cmd, text=True, stderr=subprocess.DEVNULL)
|
|
179
|
+
for ip in out.splitlines():
|
|
180
|
+
ip = ip.strip()
|
|
181
|
+
if not ip:
|
|
182
|
+
continue
|
|
183
|
+
try:
|
|
184
|
+
raw.append(ipaddress.ip_network(f"{ip}/24", strict=False))
|
|
185
|
+
except ValueError:
|
|
186
|
+
pass
|
|
187
|
+
except Exception as e:
|
|
188
|
+
output().warning(f"Could not list Windows interfaces: {e}")
|
|
189
|
+
else:
|
|
190
|
+
output().warning("PowerShell not found; skipping Windows IPs.")
|
|
191
|
+
|
|
192
|
+
# ── BSD ───────────────────────────────────────────────────────────────
|
|
193
|
+
if os_type == 'bsd':
|
|
194
|
+
if shutil.which('ifconfig'):
|
|
195
|
+
try:
|
|
196
|
+
import re as _re
|
|
197
|
+
out = subprocess.check_output(
|
|
198
|
+
['ifconfig'], text=True, stderr=subprocess.DEVNULL
|
|
199
|
+
)
|
|
200
|
+
for m in _re.finditer(
|
|
201
|
+
r'inet (\d+\.\d+\.\d+\.\d+) netmask (0x[0-9a-f]+|\d+\.\d+\.\d+\.\d+)',
|
|
202
|
+
out
|
|
203
|
+
):
|
|
204
|
+
ip_str, mask_str = m.group(1), m.group(2)
|
|
205
|
+
if mask_str.startswith('0x'):
|
|
206
|
+
mask_int = int(mask_str, 16)
|
|
207
|
+
mask_str = str(ipaddress.IPv4Address(mask_int))
|
|
208
|
+
try:
|
|
209
|
+
net = ipaddress.ip_network(f"{ip_str}/{mask_str}", strict=False)
|
|
210
|
+
raw.append(net)
|
|
211
|
+
except ValueError:
|
|
212
|
+
pass
|
|
213
|
+
except Exception as e:
|
|
214
|
+
output().warning(f"Could not list BSD interfaces: {e}")
|
|
215
|
+
|
|
216
|
+
# ── dedupe and filter loopback/link-local ────────────────────────────
|
|
217
|
+
uniq = []
|
|
218
|
+
for net in raw:
|
|
219
|
+
na = net.network_address
|
|
220
|
+
if na.is_loopback or na.is_link_local:
|
|
221
|
+
continue
|
|
222
|
+
if net not in uniq:
|
|
223
|
+
uniq.append(net)
|
|
224
|
+
return uniq
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _snmp_get(ip, oid):
|
|
228
|
+
"""
|
|
229
|
+
Run snmpget and return the value or None.
|
|
230
|
+
"""
|
|
231
|
+
cmd = shutil.which('snmpget')
|
|
232
|
+
if not cmd:
|
|
233
|
+
return None
|
|
234
|
+
try:
|
|
235
|
+
from utils.ports import PortConfig as _PC
|
|
236
|
+
_snmp_port = _PC.resolve('snmp')
|
|
237
|
+
return subprocess.check_output(
|
|
238
|
+
[cmd, '-v1', '-c', 'public', '-Oqv', '-t', '1', '-r', '1', f'{ip}:{_snmp_port}', oid],
|
|
239
|
+
stderr=subprocess.DEVNULL,
|
|
240
|
+
text=True
|
|
241
|
+
).strip()
|
|
242
|
+
except subprocess.CalledProcessError:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class discovery:
|
|
247
|
+
def __init__(self, usage=False):
|
|
248
|
+
os_type = get_os()
|
|
249
|
+
print(f"Detected OS: {os_type}")
|
|
250
|
+
if os_type == 'unsupported':
|
|
251
|
+
output().warning("This OS is not supported for SNMP-based discovery.")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# macOS and Android are supported – just ensure the user knows the requirements
|
|
255
|
+
if os_type == 'darwin':
|
|
256
|
+
output().chitchat("macOS detected. Discovery requires: brew install net-snmp")
|
|
257
|
+
if os_type == 'android':
|
|
258
|
+
output().chitchat("Android/Termux detected. Discovery requires: pkg install net-snmp")
|
|
259
|
+
|
|
260
|
+
if usage:
|
|
261
|
+
print("No target given — discovering printers on local network.")
|
|
262
|
+
print("Press CTRL+C at any time to cancel.\n")
|
|
263
|
+
|
|
264
|
+
if not shutil.which('snmpget'):
|
|
265
|
+
_install_hints = {
|
|
266
|
+
'linux': "apt install snmp OR yum install net-snmp-utils",
|
|
267
|
+
'wsl': "apt install snmp",
|
|
268
|
+
'darwin': "brew install net-snmp",
|
|
269
|
+
'bsd': "pkg install net-snmp",
|
|
270
|
+
'windows': "choco install net-snmp OR use WSL",
|
|
271
|
+
'android': "pkg install net-snmp (Termux)",
|
|
272
|
+
}
|
|
273
|
+
hint = _install_hints.get(os_type, "install net-snmp for your OS")
|
|
274
|
+
output().warning(f"'snmpget' not found. Install with: {hint}")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
networks = _get_local_networks(os_type)
|
|
278
|
+
if not networks:
|
|
279
|
+
output().warning("No eligible networks found to scan.")
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
print(f"Found {len(networks)} network(s) to consider:")
|
|
283
|
+
for idx, net in enumerate(networks, 1):
|
|
284
|
+
hosts = net.num_addresses - 2 if net.num_addresses > 2 else net.num_addresses
|
|
285
|
+
print(f" [{idx}] {net} ({hosts} hosts)")
|
|
286
|
+
|
|
287
|
+
sel = input("\nSelect networks to scan [e.g. 1,1-3,all]: ")
|
|
288
|
+
chosen = parse_selection(sel, len(networks))
|
|
289
|
+
if not chosen:
|
|
290
|
+
print("Nothing selected, exiting.")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
verb = input("Verbose probing? [y/N]: ").strip().lower()
|
|
294
|
+
verbose = verb in ('y', 'yes')
|
|
295
|
+
|
|
296
|
+
results = {}
|
|
297
|
+
total = 0
|
|
298
|
+
try:
|
|
299
|
+
for i in chosen:
|
|
300
|
+
net = networks[i - 1]
|
|
301
|
+
print(f"\nScanning {net} (Ctrl+C to cancel)...")
|
|
302
|
+
for host in net.hosts():
|
|
303
|
+
ip = str(host)
|
|
304
|
+
total += 1
|
|
305
|
+
|
|
306
|
+
typ = _snmp_get(ip, OID_HRDEV_TYPE)
|
|
307
|
+
if typ != '1.3.6.1.2.1.25.3.1.5':
|
|
308
|
+
if verbose:
|
|
309
|
+
print(f" {ip}: not a printer ({typ})")
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
# collect all SNMP fields
|
|
313
|
+
descr = _snmp_get(ip, OID_HRDEV_DESCR) or '?'
|
|
314
|
+
upv = _snmp_get(ip, OID_SYS_UPTIME)
|
|
315
|
+
uptime = conv().elapsed(int(upv), 100, True) if upv and upv.isdigit() else '?'
|
|
316
|
+
pr_status = _snmp_get(ip, OID_PR_STATUS) or '?'
|
|
317
|
+
interp = _snmp_get(ip, OID_PR_INTERPRETER) or '?'
|
|
318
|
+
hp_status = _snmp_get(ip, OID_HRPRINTER_STATUS) or '?'
|
|
319
|
+
err_state = _snmp_get(ip, OID_HRPRINTER_ERROR_STATE) or '?'
|
|
320
|
+
job_count = _snmp_get(ip, OID_HRPRINTER_JOB_COUNT) or '?'
|
|
321
|
+
m_desc = _snmp_get(ip, OID_PRT_MARKER_SUPPLY_DESC) or '?'
|
|
322
|
+
m_type = _snmp_get(ip, OID_PRT_MARKER_SUPPLY_TYPE) or '?'
|
|
323
|
+
m_level = _snmp_get(ip, OID_PRT_MARKER_SUPPLY_LEVEL) or '?'
|
|
324
|
+
in_media = _snmp_get(ip, OID_PRT_INPUT_MEDIA_TYPE) or '?'
|
|
325
|
+
in_status = _snmp_get(ip, OID_PRT_INPUT_STATUS) or '?'
|
|
326
|
+
alerts = _snmp_get(ip, OID_PRT_ALERTS_VALUE) or '?'
|
|
327
|
+
phys_descr = _snmp_get(ip, OID_ENT_PHYS_DESCR) or '?'
|
|
328
|
+
phys_name = _snmp_get(ip, OID_ENT_PHYS_NAME) or '?'
|
|
329
|
+
phys_fw = _snmp_get(ip, OID_ENT_PHYS_FIRMWARE_REV) or '?'
|
|
330
|
+
phys_serial = _snmp_get(ip, OID_ENT_PHYS_SERIAL) or '?'
|
|
331
|
+
phys_model = _snmp_get(ip, OID_ENT_PHYS_MODEL_NAME) or '?'
|
|
332
|
+
|
|
333
|
+
results[ip] = [
|
|
334
|
+
descr, uptime, pr_status, interp,
|
|
335
|
+
hp_status, err_state, job_count,
|
|
336
|
+
m_desc, m_type, m_level,
|
|
337
|
+
in_media, in_status, alerts,
|
|
338
|
+
phys_name, phys_model, phys_serial, phys_fw
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
if verbose:
|
|
342
|
+
print(f" {ip}:")
|
|
343
|
+
print(f" Description: {descr}")
|
|
344
|
+
print(f" Uptime: {uptime}")
|
|
345
|
+
print(f" PJL Status: {pr_status}")
|
|
346
|
+
print(f" Interpreter: {interp}")
|
|
347
|
+
print(f" hrStatus: {hp_status}")
|
|
348
|
+
print(f" Errors: {err_state}")
|
|
349
|
+
print(f" Jobs: {job_count}")
|
|
350
|
+
print(f" Supplies: {m_desc} / {m_type} @ {m_level}")
|
|
351
|
+
print(f" Input: {in_media} / {in_status}")
|
|
352
|
+
print(f" Alerts: {alerts}")
|
|
353
|
+
print(f" Entity: {phys_name} ({phys_model})")
|
|
354
|
+
print(f" Serial: {phys_serial}")
|
|
355
|
+
print(f" FW Rev: {phys_fw}")
|
|
356
|
+
else:
|
|
357
|
+
print(f" {ip}: Printer → {descr}, uptime={uptime}, status={pr_status}")
|
|
358
|
+
|
|
359
|
+
except KeyboardInterrupt:
|
|
360
|
+
print()
|
|
361
|
+
output().warning("[!] Discovery interrupted by user. Exiting...\n")
|
|
362
|
+
|
|
363
|
+
print(f"\nProbed {total} hosts in total.\n")
|
|
364
|
+
if results:
|
|
365
|
+
print("Discovered printers:")
|
|
366
|
+
hdr = (
|
|
367
|
+
'address',
|
|
368
|
+
('descr','uptime','pjl_status','interp','hr_status','errors',
|
|
369
|
+
'jobs','sup_desc','sup_type','sup_lvl',
|
|
370
|
+
'in_media','in_status','alerts',
|
|
371
|
+
'ent_name','ent_model','ent_serial','ent_fw')
|
|
372
|
+
)
|
|
373
|
+
output().discover(hdr)
|
|
374
|
+
output().hline(79)
|
|
375
|
+
for entry in sorted(results.items(), key=lambda i: socket.inet_aton(i[0])):
|
|
376
|
+
output().discover(entry)
|
|
377
|
+
print()
|
|
378
|
+
else:
|
|
379
|
+
output().info("No printers found via SNMP scan")
|
|
380
|
+
print()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ── Mirai-style concurrent TCP port scanner (batch-32 enhancement) ────────────
|
|
384
|
+
# Inspired by jgamblin/Mirai-Source-Code (scanner.c) concurrent scan approach
|
|
385
|
+
# and groinc (thau0x01) passive packet inspection patterns.
|
|
386
|
+
|
|
387
|
+
import concurrent.futures as _futures
|
|
388
|
+
import threading as _threading
|
|
389
|
+
|
|
390
|
+
_PRINTER_PROBE_PORTS = [9100, 80, 443, 631, 515, 8080, 23, 161]
|
|
391
|
+
|
|
392
|
+
def _tcp_open(host: str, port: int, timeout: float = 1.5) -> bool:
|
|
393
|
+
"""Non-blocking TCP probe."""
|
|
394
|
+
try:
|
|
395
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
396
|
+
return True
|
|
397
|
+
except Exception:
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def mirai_tcp_scan(cidr: str, ports: list = None, threads: int = 256,
|
|
402
|
+
timeout: float = 1.5) -> list:
|
|
403
|
+
"""
|
|
404
|
+
Concurrent Mirai-style TCP scanner.
|
|
405
|
+
Probes all hosts in `cidr` for printer-relevant ports in parallel.
|
|
406
|
+
Returns list of dicts: {host, open_ports}.
|
|
407
|
+
"""
|
|
408
|
+
if ports is None:
|
|
409
|
+
ports = _PRINTER_PROBE_PORTS
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
net = ipaddress.ip_network(cidr, strict=False)
|
|
413
|
+
targets = [str(ip) for ip in net.hosts()]
|
|
414
|
+
except ValueError as exc:
|
|
415
|
+
print(f"[!] Invalid CIDR {cidr}: {exc}")
|
|
416
|
+
return []
|
|
417
|
+
|
|
418
|
+
print(f"[*] Mirai-TCP scan: {len(targets)} hosts × {len(ports)} ports ({threads} threads)")
|
|
419
|
+
results = []
|
|
420
|
+
lock = _threading.Lock()
|
|
421
|
+
|
|
422
|
+
def _probe(host):
|
|
423
|
+
open_ports = [p for p in ports if _tcp_open(host, p, timeout)]
|
|
424
|
+
if open_ports:
|
|
425
|
+
with lock:
|
|
426
|
+
results.append({"host": host, "open_ports": open_ports})
|
|
427
|
+
|
|
428
|
+
with _futures.ThreadPoolExecutor(max_workers=threads) as pool:
|
|
429
|
+
list(pool.map(_probe, targets))
|
|
430
|
+
|
|
431
|
+
print(f"[*] {len(results)} host(s) with open printer ports.")
|
|
432
|
+
return results
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def groinc_passive_sniff(interface: str = None, timeout: int = 30,
|
|
436
|
+
printer_ports: list = None) -> list:
|
|
437
|
+
"""
|
|
438
|
+
Groinc-inspired passive packet capture to detect printer traffic.
|
|
439
|
+
Requires scapy (pip install scapy). Falls back gracefully if unavailable.
|
|
440
|
+
Detects: PJL (port 9100), IPP (port 631), LPD (port 515), SNMP (port 161).
|
|
441
|
+
"""
|
|
442
|
+
if printer_ports is None:
|
|
443
|
+
printer_ports = [9100, 631, 515, 161]
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
from scapy.all import sniff, TCP, UDP, IP
|
|
447
|
+
except ImportError:
|
|
448
|
+
print("[!] scapy not installed — passive sniffer unavailable.")
|
|
449
|
+
print(" Install with: pip install scapy")
|
|
450
|
+
return []
|
|
451
|
+
|
|
452
|
+
print(f"[*] Passive sniffer on {interface or 'default'} for {timeout}s "
|
|
453
|
+
f"(ports: {printer_ports}) ...")
|
|
454
|
+
|
|
455
|
+
seen = {}
|
|
456
|
+
port_filter = " or ".join(f"port {p}" for p in printer_ports)
|
|
457
|
+
bpf = f"tcp or udp and ({port_filter})"
|
|
458
|
+
|
|
459
|
+
def _pkt_handler(pkt):
|
|
460
|
+
if IP not in pkt:
|
|
461
|
+
return
|
|
462
|
+
src = pkt[IP].src
|
|
463
|
+
dst = pkt[IP].dst
|
|
464
|
+
if TCP in pkt:
|
|
465
|
+
dport = pkt[TCP].dport
|
|
466
|
+
sport = pkt[TCP].sport
|
|
467
|
+
elif UDP in pkt:
|
|
468
|
+
dport = pkt[UDP].dport
|
|
469
|
+
sport = pkt[UDP].sport
|
|
470
|
+
else:
|
|
471
|
+
return
|
|
472
|
+
for p in printer_ports:
|
|
473
|
+
if dport == p or sport == p:
|
|
474
|
+
key = (src, dst, p)
|
|
475
|
+
if key not in seen:
|
|
476
|
+
seen[key] = True
|
|
477
|
+
print(f" [sniff] {src}:{sport} → {dst}:{dport}")
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
sniff(iface=interface, filter=bpf, prn=_pkt_handler,
|
|
481
|
+
store=False, timeout=timeout)
|
|
482
|
+
except Exception as exc:
|
|
483
|
+
print(f"[!] Sniffer error: {exc}")
|
|
484
|
+
|
|
485
|
+
flows = [{"src": k[0], "dst": k[1], "port": k[2]} for k in seen]
|
|
486
|
+
print(f"[*] Captured {len(flows)} unique printer flows.")
|
|
487
|
+
return flows
|
|
488
|
+
|
src/core/osdetect.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
# Author : Andre Henrique (@mrhenrike)
|
|
3
|
+
# GitHub : https://github.com/mrhenrike
|
|
4
|
+
# LinkedIn : https://linkedin.com/in/mrhenrike
|
|
5
|
+
# X/Twitter : https://x.com/mrhenrike
|
|
6
|
+
|
|
7
|
+
_cached_os = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_os() -> str:
|
|
11
|
+
"""
|
|
12
|
+
Detect the host operating system.
|
|
13
|
+
|
|
14
|
+
Returns one of:
|
|
15
|
+
'linux' – Standard Linux
|
|
16
|
+
'wsl' – Windows Subsystem for Linux
|
|
17
|
+
'android' – Android via Termux (or similar)
|
|
18
|
+
'windows' – Native Windows
|
|
19
|
+
'darwin' – macOS
|
|
20
|
+
'bsd' – FreeBSD / OpenBSD / NetBSD
|
|
21
|
+
'unsupported' – anything else
|
|
22
|
+
|
|
23
|
+
Result is cached after the first call.
|
|
24
|
+
"""
|
|
25
|
+
global _cached_os
|
|
26
|
+
if _cached_os:
|
|
27
|
+
return _cached_os
|
|
28
|
+
|
|
29
|
+
import platform
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
sysname = platform.system().lower()
|
|
33
|
+
|
|
34
|
+
if "linux" in sysname:
|
|
35
|
+
# ── Android / Termux detection ──────────────────────────────────
|
|
36
|
+
# Android uses a Linux kernel but has a distinct runtime environment.
|
|
37
|
+
# Termux sets TERMUX_VERSION env var; alternatively check for /data/data.
|
|
38
|
+
if (
|
|
39
|
+
os.environ.get("TERMUX_VERSION")
|
|
40
|
+
or os.path.isdir("/data/data/com.termux")
|
|
41
|
+
or os.environ.get("PREFIX", "").startswith("/data/data/com.termux")
|
|
42
|
+
):
|
|
43
|
+
_cached_os = "android"
|
|
44
|
+
return _cached_os
|
|
45
|
+
|
|
46
|
+
# ── WSL detection ────────────────────────────────────────────────
|
|
47
|
+
osrelease = '/proc/sys/kernel/osrelease'
|
|
48
|
+
if os.path.exists(osrelease):
|
|
49
|
+
try:
|
|
50
|
+
with open(osrelease) as f:
|
|
51
|
+
content = f.read().lower()
|
|
52
|
+
if 'microsoft' in content or 'wsl' in content:
|
|
53
|
+
_cached_os = "wsl"
|
|
54
|
+
return _cached_os
|
|
55
|
+
except OSError:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
_cached_os = "linux"
|
|
59
|
+
return _cached_os
|
|
60
|
+
|
|
61
|
+
if "windows" in sysname:
|
|
62
|
+
_cached_os = "windows"
|
|
63
|
+
return _cached_os
|
|
64
|
+
|
|
65
|
+
if "darwin" in sysname:
|
|
66
|
+
_cached_os = "darwin" # macOS
|
|
67
|
+
return _cached_os
|
|
68
|
+
|
|
69
|
+
if any(bsd in sysname for bsd in ("freebsd", "openbsd", "netbsd")):
|
|
70
|
+
_cached_os = "bsd"
|
|
71
|
+
return _cached_os
|
|
72
|
+
|
|
73
|
+
_cached_os = "unsupported"
|
|
74
|
+
return _cached_os
|