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,293 @@
|
|
|
1
|
+
local description = [[
|
|
2
|
+
printer-http-ews — HTTP Embedded Web Server (EWS) fingerprinting and vulnerability check.
|
|
3
|
+
|
|
4
|
+
Probes common printer EWS paths to identify: vendor, model, firmware version,
|
|
5
|
+
admin interface presence, default credential indicators, and exposed management endpoints.
|
|
6
|
+
|
|
7
|
+
Active checks:
|
|
8
|
+
- Firmware update endpoint exposure (unauthenticated access)
|
|
9
|
+
- Default credentials probe (admin/admin, admin/blank, etc.)
|
|
10
|
+
- LDAP/SMB passback surface detection (scan-to-email config pages)
|
|
11
|
+
- Session fixation indicators
|
|
12
|
+
|
|
13
|
+
CVE coverage: CVE-2023-6018 (HP FW bypass), CVE-2024-21911 (Toshiba TopAccess),
|
|
14
|
+
CVE-2021-27508 (Xerox WorkCentre), CVE-2024-6333 (Xerox VersaLink),
|
|
15
|
+
CVE-2022-29943 (Ricoh web cmd inject), FIRMWARE-RICOH/KONICA/BROTHER-001
|
|
16
|
+
|
|
17
|
+
Author: André Henrique (@mrhenrike) | União Geek
|
|
18
|
+
]]
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
-- @usage
|
|
22
|
+
-- nmap -p 80,443,8080 --script printer-http-ews <target>
|
|
23
|
+
-- @output
|
|
24
|
+
-- PORT STATE SERVICE
|
|
25
|
+
-- 80/tcp open http
|
|
26
|
+
-- | printer-http-ews:
|
|
27
|
+
-- | Vendor : Xerox
|
|
28
|
+
-- | Model : VersaLink C405
|
|
29
|
+
-- | Firmware : 57.66.91 (vulnerable range)
|
|
30
|
+
-- | Admin-Interface : /web/index.html (200 OK — no auth)
|
|
31
|
+
-- | Firmware-Update : /cgi-bin/fw_update (200 OK — exposed)
|
|
32
|
+
-- | [HIGH] CVE-2024-6333 — Xerox VersaLink authenticated OS command injection
|
|
33
|
+
-- | Verdict : POSSIBLY VULNERABLE
|
|
34
|
+
-- |_ Suggest : printerxpl-forge run --module xpl/edb-cve-2024-6333 --target <IP>
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
categories = { "discovery", "auth", "vuln" }
|
|
38
|
+
author = "André Henrique (@mrhenrike) | União Geek"
|
|
39
|
+
license = "Same as Nmap -- See https://nmap.org/book/man-legal.html"
|
|
40
|
+
|
|
41
|
+
local stdnse = require "stdnse"
|
|
42
|
+
local shortport = require "shortport"
|
|
43
|
+
local http = require "http"
|
|
44
|
+
local base64 = require "base64"
|
|
45
|
+
local string = require "string"
|
|
46
|
+
local table = require "table"
|
|
47
|
+
|
|
48
|
+
portrule = shortport.http
|
|
49
|
+
|
|
50
|
+
-- Vendor-specific EWS admin paths
|
|
51
|
+
local VENDOR_PATHS = {
|
|
52
|
+
{ vendor = "HP", path = "/hp/device/DeviceMgmt.htm", fw_path = "/hp/device/FirmwareUpdate" },
|
|
53
|
+
{ vendor = "HP", path = "/hp/device/info_deviceStatus.htm", fw_path = nil },
|
|
54
|
+
{ vendor = "Xerox", path = "/web/index.html", fw_path = nil },
|
|
55
|
+
{ vendor = "Xerox", path = "/cgi-bin/cloning", fw_path = nil },
|
|
56
|
+
{ vendor = "Ricoh", path = "/web/entry.cgi", fw_path = "/cgi-bin/fw_update" },
|
|
57
|
+
{ vendor = "Ricoh", path = "/", fw_path = "/cgi-bin/fw_update" },
|
|
58
|
+
{ vendor = "Konica", path = "/wcd/index.html", fw_path = "/wcd/fwdl.cgi" },
|
|
59
|
+
{ vendor = "Brother", path = "/general/status.html", fw_path = "/firmware" },
|
|
60
|
+
{ vendor = "Toshiba", path = "/TopAccess/", fw_path = nil },
|
|
61
|
+
{ vendor = "Lexmark", path = "/cgi-bin/dynamic/", fw_path = nil },
|
|
62
|
+
{ vendor = "Canon", path = "/portal_top.html", fw_path = nil },
|
|
63
|
+
{ vendor = "Epson", path = "/PRESENTATION/ADVANCED/TOP", fw_path = "/PRESENTATION/ADVANCED/FIRMWARE_UPDATE/TOP" },
|
|
64
|
+
{ vendor = "Sharp", path = "/", fw_path = nil },
|
|
65
|
+
{ vendor = "Kyocera", path = "/js/jssrc/model/system/SystemStatus.js", fw_path = nil },
|
|
66
|
+
{ vendor = "Generic", path = "/", fw_path = nil },
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
local DEFAULT_CREDS = {
|
|
70
|
+
{ user = "admin", pass = "" },
|
|
71
|
+
{ user = "admin", pass = "admin" },
|
|
72
|
+
{ user = "admin", pass = "1111" },
|
|
73
|
+
{ user = "admin", pass = "password" },
|
|
74
|
+
{ user = "supervisor", pass = "supervisor" },
|
|
75
|
+
{ user = "root", pass = "" },
|
|
76
|
+
{ user = "user", pass = "" },
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
local CVE_RULES = {
|
|
80
|
+
{ vendor = "hp", fw_path_exposed = true, cve = "CVE-2023-6018", sev = "CRITICAL", cvss = 9.8,
|
|
81
|
+
desc = "HP firmware auth bypass — arbitrary firmware upload",
|
|
82
|
+
xpl = "xpl/research/research-hp-fw-bypass" },
|
|
83
|
+
{ vendor = "xerox", path = "/cgi-bin/cloning", cve = "CVE-2021-27508", sev = "HIGH", cvss = 8.0,
|
|
84
|
+
desc = "Xerox WorkCentre OS command injection via clone_group",
|
|
85
|
+
xpl = "xpl/research/research-xerox-workcentre-cmdinject" },
|
|
86
|
+
{ vendor = "xerox", default_creds = true, cve = "CVE-2024-6333", sev = "HIGH", cvss = 8.1,
|
|
87
|
+
desc = "Xerox VersaLink OS command injection (authenticated)",
|
|
88
|
+
xpl = "xpl/edb-cve-2024-6333" },
|
|
89
|
+
{ vendor = "ricoh", fw_path_exposed = true, cve = "FIRMWARE-RICOH-001", sev = "HIGH", cvss = 8.0,
|
|
90
|
+
desc = "Ricoh Aficio unsigned firmware upload without sig check",
|
|
91
|
+
xpl = "xpl/research/research-ricoh-fw-unsigned" },
|
|
92
|
+
{ vendor = "ricoh", default_creds = true, cve = "CVE-2022-29943", sev = "HIGH", cvss = 8.8,
|
|
93
|
+
desc = "Ricoh MP web admin command injection",
|
|
94
|
+
xpl = "xpl/research/research-ricoh-web-cmdinject" },
|
|
95
|
+
{ vendor = "konica", fw_path_exposed = true, cve = "FIRMWARE-KONICA-001", sev = "HIGH", cvss = 8.5,
|
|
96
|
+
desc = "Konica bizhub unsigned firmware upload via /wcd/fwdl.cgi",
|
|
97
|
+
xpl = "xpl/research/research-konica-fw-upload" },
|
|
98
|
+
{ vendor = "brother", fw_path_exposed = true, cve = "FIRMWARE-BROTHER-001", sev = "HIGH", cvss = 8.0,
|
|
99
|
+
desc = "Brother MFC firmware upload via HTTP PUT (chain with CVE-2024-51978)",
|
|
100
|
+
xpl = "xpl/research/research-brother-fw-upload" },
|
|
101
|
+
{ vendor = "toshiba", default_creds = true, cve = "CVE-2024-21911", sev = "HIGH", cvss = 8.8,
|
|
102
|
+
desc = "Toshiba e-STUDIO TopAccess authentication bypass",
|
|
103
|
+
xpl = "xpl/research/research-toshiba-auth-bypass" },
|
|
104
|
+
{ vendor = "lexmark", default_creds = true, cve = "CVE-2023-50733", sev = "MEDIUM", cvss = 6.5,
|
|
105
|
+
desc = "Lexmark EWS SSRF — printer as lateral movement pivot",
|
|
106
|
+
xpl = "xpl/edb-cve-2023-50733" },
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
-- Basic auth header helper
|
|
110
|
+
local function basic_auth(user, pass)
|
|
111
|
+
return "Basic " .. base64.enc(user .. ":" .. pass)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
-- Probe a path (with optional auth)
|
|
115
|
+
local function probe(host, port, path, auth_header)
|
|
116
|
+
local opts = { timeout = 5000 }
|
|
117
|
+
if auth_header then
|
|
118
|
+
opts.header = { ["Authorization"] = auth_header }
|
|
119
|
+
end
|
|
120
|
+
local resp = http.get(host, port.number, path, opts)
|
|
121
|
+
if resp then
|
|
122
|
+
return resp.status, (resp.body or ""):sub(1, 4096)
|
|
123
|
+
end
|
|
124
|
+
return nil, ""
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
action = function(host, port)
|
|
128
|
+
local out = stdnse.output_table()
|
|
129
|
+
|
|
130
|
+
-- Step 1: probe root and collect banner
|
|
131
|
+
local root_status, root_body = probe(host, port, "/")
|
|
132
|
+
if not root_status then
|
|
133
|
+
return "HTTP EWS not responding"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
local body_lower = root_body:lower()
|
|
137
|
+
|
|
138
|
+
-- Detect vendor
|
|
139
|
+
local vendor_name = nil
|
|
140
|
+
local vendor_key = nil
|
|
141
|
+
local vmap = {
|
|
142
|
+
{ pat = "hp laserjet|hewlett.packard|jetdirect|futuresmart", name = "HP", key = "hp" },
|
|
143
|
+
{ pat = "xerox versalink|xerox workcentre|xerox altalink", name = "Xerox", key = "xerox" },
|
|
144
|
+
{ pat = "ricoh|aficio|mp c", name = "Ricoh", key = "ricoh" },
|
|
145
|
+
{ pat = "konica|bizhub", name = "Konica Minolta", key = "konica" },
|
|
146
|
+
{ pat = "brother mfc|brother dcp|brother hl", name = "Brother", key = "brother" },
|
|
147
|
+
{ pat = "toshiba e.studio|topaccess", name = "Toshiba", key = "toshiba" },
|
|
148
|
+
{ pat = "lexmark", name = "Lexmark", key = "lexmark" },
|
|
149
|
+
{ pat = "canon imagerunner|canon imageclass|canon lbp", name = "Canon", key = "canon" },
|
|
150
|
+
{ pat = "epson workforce|epson ecotank", name = "Epson", key = "epson" },
|
|
151
|
+
{ pat = "sharp mx|sharp ar", name = "Sharp", key = "sharp" },
|
|
152
|
+
{ pat = "kyocera ecosys|kyocera taskalfa", name = "Kyocera", key = "kyocera" },
|
|
153
|
+
{ pat = "samsung scx|samsung clx|samsung ml", name = "Samsung", key = "samsung" },
|
|
154
|
+
}
|
|
155
|
+
for _, v in ipairs(vmap) do
|
|
156
|
+
if body_lower:match(v.pat) then
|
|
157
|
+
vendor_name = v.name
|
|
158
|
+
vendor_key = v.key
|
|
159
|
+
break
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
-- Server header hint
|
|
164
|
+
local server_hdr = ""
|
|
165
|
+
-- (Nmap doesn't expose raw headers directly, but http.get returns header table)
|
|
166
|
+
|
|
167
|
+
if vendor_name then
|
|
168
|
+
out["Vendor"] = vendor_name
|
|
169
|
+
else
|
|
170
|
+
out["Vendor"] = "Unknown (generic EWS detected)"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
-- Extract model from title or meta
|
|
174
|
+
local model = root_body:match("<title>([^<]+)</title>")
|
|
175
|
+
or root_body:match('content="([^"]+)"')
|
|
176
|
+
if model then
|
|
177
|
+
out["Model"] = model:match("^%s*(.-)%s*$"):sub(1, 80)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
-- Extract firmware hint
|
|
181
|
+
local fw = root_body:match("[Ff]irmware[Vv]ersion[^:]*[:%s]+([%w%.%-]+)")
|
|
182
|
+
or root_body:match("[Vv]ersion[^:]*[:%s]+([%w%.%-]+)")
|
|
183
|
+
if fw then out["Firmware"] = fw end
|
|
184
|
+
|
|
185
|
+
-- Step 2: check specific vendor paths
|
|
186
|
+
local fw_exposed = false
|
|
187
|
+
local admin_open = false
|
|
188
|
+
local passback_exposed = false
|
|
189
|
+
|
|
190
|
+
for _, vp in ipairs(VENDOR_PATHS) do
|
|
191
|
+
local vl = vp.vendor:lower()
|
|
192
|
+
if vl == "generic" or vl == (vendor_key or "") then
|
|
193
|
+
local st, bd = probe(host, port, vp.path)
|
|
194
|
+
if st == 200 and #bd > 100 then
|
|
195
|
+
admin_open = true
|
|
196
|
+
out["Admin-Interface"] = vp.path .. " (HTTP 200 — accessible)"
|
|
197
|
+
end
|
|
198
|
+
if vp.fw_path then
|
|
199
|
+
local fst, _ = probe(host, port, vp.fw_path)
|
|
200
|
+
if fst == 200 then
|
|
201
|
+
fw_exposed = true
|
|
202
|
+
out["Firmware-Update-Endpoint"] = vp.fw_path .. " (HTTP 200 — EXPOSED)"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
break
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
-- Check LDAP/SMB passback surface
|
|
210
|
+
local passback_paths = {
|
|
211
|
+
"/hp/device/LdapConfiguration.htm",
|
|
212
|
+
"/cgi-bin/dynamic/config/ldapconfig.html",
|
|
213
|
+
"/TopAccess/ADMIN/Setting/E-MAIL/LDAPSetting.htm",
|
|
214
|
+
"/web/entry.cgi?id=ldap",
|
|
215
|
+
}
|
|
216
|
+
for _, pp in ipairs(passback_paths) do
|
|
217
|
+
local pst, _ = probe(host, port, pp)
|
|
218
|
+
if pst == 200 then
|
|
219
|
+
passback_exposed = true
|
|
220
|
+
out["LDAP-Passback-Surface"] = pp .. " (accessible — SMB/LDAP passback possible)"
|
|
221
|
+
break
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
-- Step 3: default credential test
|
|
226
|
+
local creds_found = nil
|
|
227
|
+
if admin_open and vendor_name then
|
|
228
|
+
for _, cr in ipairs(DEFAULT_CREDS) do
|
|
229
|
+
local ah = basic_auth(cr.user, cr.pass)
|
|
230
|
+
local st, bd = probe(host, port, "/", ah)
|
|
231
|
+
if st == 200 and bd:lower():match("logout") then
|
|
232
|
+
creds_found = cr.user .. ":" .. (cr.pass == "" and "(blank)" or cr.pass)
|
|
233
|
+
out["Default-Creds"] = "FOUND — " .. creds_found
|
|
234
|
+
break
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
if not creds_found then
|
|
238
|
+
out["Default-Creds"] = "Not found (or auth not required)"
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
-- Step 4: CVE matching
|
|
243
|
+
local cves_found = {}
|
|
244
|
+
if vendor_key then
|
|
245
|
+
for _, rule in ipairs(CVE_RULES) do
|
|
246
|
+
local match = (rule.vendor == vendor_key)
|
|
247
|
+
if match then
|
|
248
|
+
local triggers = false
|
|
249
|
+
if rule.fw_path_exposed and fw_exposed then triggers = true end
|
|
250
|
+
if rule.default_creds and (creds_found ~= nil or admin_open) then triggers = true end
|
|
251
|
+
if rule.path then
|
|
252
|
+
local pst, _ = probe(host, port, rule.path)
|
|
253
|
+
if pst == 200 then triggers = true end
|
|
254
|
+
end
|
|
255
|
+
if triggers then
|
|
256
|
+
table.insert(cves_found, rule)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
-- Build CVE output
|
|
263
|
+
if #cves_found > 0 then
|
|
264
|
+
local cve_lines = {}
|
|
265
|
+
for _, c in ipairs(cves_found) do
|
|
266
|
+
table.insert(cve_lines, string.format(
|
|
267
|
+
"[%s] %s (CVSS %.1f) — %s", c.sev, c.cve, c.cvss, c.desc))
|
|
268
|
+
end
|
|
269
|
+
out["CVEs"] = table.concat(cve_lines, " | ")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
-- Verdict
|
|
273
|
+
local is_vuln = (#cves_found > 0 and fw_exposed) or (creds_found ~= nil)
|
|
274
|
+
local is_possible = #cves_found > 0 or admin_open or passback_exposed
|
|
275
|
+
|
|
276
|
+
if is_vuln then
|
|
277
|
+
out["Verdict"] = "VULNERABLE"
|
|
278
|
+
if cves_found[1] then
|
|
279
|
+
out["Suggest"] = "printerxpl-forge run --module " .. cves_found[1].xpl .. " --target " .. host.ip
|
|
280
|
+
end
|
|
281
|
+
elseif is_possible then
|
|
282
|
+
out["Verdict"] = "POSSIBLY VULNERABLE — run printer-vuln-check for confirmation"
|
|
283
|
+
if cves_found[1] then
|
|
284
|
+
out["Suggest"] = "printerxpl-forge run --module " .. cves_found[1].xpl .. " --dry-run --target " .. host.ip
|
|
285
|
+
end
|
|
286
|
+
else
|
|
287
|
+
out["Verdict"] = "NOT VULNERABLE to detected CVEs (manual review recommended)"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
out["Full-Scan"] = "pip install printerxpl-forge | printerxpl-forge scan --target " .. host.ip
|
|
291
|
+
|
|
292
|
+
return out
|
|
293
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
local description = [[
|
|
2
|
+
printer-ipp-info — IPP (Internet Printing Protocol) enumeration on port 631.
|
|
3
|
+
|
|
4
|
+
Sends IPP Get-Printer-Attributes to enumerate: printer make/model, firmware version,
|
|
5
|
+
supported document formats, color capabilities, duplex support, media types, and
|
|
6
|
+
printer state. Performs secondary CVE check for CUPS and Lexmark IPP vulnerabilities.
|
|
7
|
+
|
|
8
|
+
Checks for:
|
|
9
|
+
- CUPS CVE-2024-47176 / CVE-2026-34980 (unauthenticated RCE)
|
|
10
|
+
- Lexmark CVE-2023-50739 (IPP heap buffer overflow)
|
|
11
|
+
- Anonymous print job acceptance (no auth required)
|
|
12
|
+
|
|
13
|
+
Author: André Henrique (@mrhenrike) | União Geek
|
|
14
|
+
]]
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
-- @usage
|
|
18
|
+
-- nmap -p 631 --script printer-ipp-info <target>
|
|
19
|
+
-- @output
|
|
20
|
+
-- PORT STATE SERVICE
|
|
21
|
+
-- 631/tcp open ipp
|
|
22
|
+
-- | printer-ipp-info:
|
|
23
|
+
-- | Make-Model : CUPS 2.4.16 (Linux)
|
|
24
|
+
-- | State : idle
|
|
25
|
+
-- | Auth-Required : NO (anonymous print accepted)
|
|
26
|
+
-- | Formats : application/pdf, image/jpeg, application/postscript
|
|
27
|
+
-- | [CRITICAL] CVE-2026-34980 — CUPS 2.4.16 unauthenticated RCE via PPD injection
|
|
28
|
+
-- | Verdict : POSSIBLY VULNERABLE
|
|
29
|
+
-- |_ Suggest : printerxpl-forge run --module xpl/research/research-cups-chain-2026
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
categories = { "discovery", "safe", "vuln" }
|
|
33
|
+
author = "André Henrique (@mrhenrike) | União Geek"
|
|
34
|
+
license = "Same as Nmap -- See https://nmap.org/book/man-legal.html"
|
|
35
|
+
|
|
36
|
+
local stdnse = require "stdnse"
|
|
37
|
+
local shortport = require "shortport"
|
|
38
|
+
local http = require "http"
|
|
39
|
+
local string = require "string"
|
|
40
|
+
local table = require "table"
|
|
41
|
+
|
|
42
|
+
portrule = shortport.port_or_service(631, "ipp", "tcp")
|
|
43
|
+
|
|
44
|
+
-- IPP attribute tag constants
|
|
45
|
+
local IPP_TAG_OPERATION = "\x01"
|
|
46
|
+
local IPP_TAG_END = "\x03"
|
|
47
|
+
local IPP_TAG_CHARSET = "\x47"
|
|
48
|
+
local IPP_TAG_LANG = "\x48"
|
|
49
|
+
local IPP_TAG_URI = "\x45"
|
|
50
|
+
local IPP_TAG_KEYWORD = "\x44"
|
|
51
|
+
local IPP_TAG_ENUM = "\x23"
|
|
52
|
+
local IPP_TAG_NAME = "\x42"
|
|
53
|
+
|
|
54
|
+
-- Build IPP Get-Printer-Attributes request
|
|
55
|
+
local function build_get_printer_attrs(host, port)
|
|
56
|
+
local printer_uri = string.format("ipp://%s:%d/printers/", host.ip, port.number)
|
|
57
|
+
local uri_bytes = printer_uri
|
|
58
|
+
|
|
59
|
+
return
|
|
60
|
+
"\x02\x00" .. -- IPP 2.0
|
|
61
|
+
"\x00\x0b" .. -- Get-Printer-Attributes op
|
|
62
|
+
"\x00\x00\x00\x01" .. -- request-id = 1
|
|
63
|
+
IPP_TAG_OPERATION ..
|
|
64
|
+
IPP_TAG_CHARSET .. "\x00\x12" .. "attributes-charset" .. "\x00\x05" .. "utf-8" ..
|
|
65
|
+
IPP_TAG_LANG .. "\x00\x1b" .. "attributes-natural-language" .. "\x00\x05" .. "en-us" ..
|
|
66
|
+
IPP_TAG_URI .. "\x00\x0b" .. "printer-uri" ..
|
|
67
|
+
string.char(0x00, #uri_bytes >> 8, #uri_bytes & 0xff) .. uri_bytes ..
|
|
68
|
+
IPP_TAG_KEYWORD .. "\x00\x14" .. "requested-attributes" .. "\x00\x03" .. "all" ..
|
|
69
|
+
IPP_TAG_END
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
-- Extract string value from IPP binary response after a keyword
|
|
73
|
+
local function ipp_extract(resp, keyword)
|
|
74
|
+
local pos = resp:find(keyword, 1, true)
|
|
75
|
+
if not pos then return nil end
|
|
76
|
+
-- Skip keyword + length, grab value
|
|
77
|
+
local val_start = pos + #keyword + 2
|
|
78
|
+
if val_start > #resp then return nil end
|
|
79
|
+
local val_len = string.byte(resp, val_start - 1) * 256 + string.byte(resp, val_start)
|
|
80
|
+
if val_start + val_len > #resp + 1 then return nil end
|
|
81
|
+
return resp:sub(val_start + 1, val_start + val_len)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
-- Try anonymous print job to test auth
|
|
85
|
+
local function test_anon_print(host, port)
|
|
86
|
+
local printer_uri = string.format("ipp://%s:%d/printers/default", host.ip, port.number)
|
|
87
|
+
local jn = "nmap-probe"
|
|
88
|
+
local jn_bytes = jn
|
|
89
|
+
local payload =
|
|
90
|
+
"\x02\x00\x00\x02\x00\x00\x00\x02" .. -- Print-Job
|
|
91
|
+
IPP_TAG_OPERATION ..
|
|
92
|
+
IPP_TAG_CHARSET .. "\x00\x12" .. "attributes-charset" .. "\x00\x05" .. "utf-8" ..
|
|
93
|
+
IPP_TAG_LANG .. "\x00\x1b" .. "attributes-natural-language" .. "\x00\x05" .. "en-us" ..
|
|
94
|
+
IPP_TAG_URI .. "\x00\x0b" .. "printer-uri" ..
|
|
95
|
+
string.char(0x00, #printer_uri >> 8, #printer_uri & 0xff) .. printer_uri ..
|
|
96
|
+
IPP_TAG_NAME .. "\x00\x08" .. "job-name" ..
|
|
97
|
+
string.char(0x00, #jn_bytes >> 8, #jn_bytes & 0xff) .. jn_bytes ..
|
|
98
|
+
IPP_TAG_END .. "%!PS\n" -- tiny PostScript body
|
|
99
|
+
|
|
100
|
+
local resp = http.post(
|
|
101
|
+
host, port.number, "/printers/default",
|
|
102
|
+
{ header = { ["Content-Type"] = "application/ipp" } },
|
|
103
|
+
nil, payload
|
|
104
|
+
)
|
|
105
|
+
if resp then
|
|
106
|
+
-- 200 = accepted, 401/403 = auth required, 426 = client error (no job but auth ok)
|
|
107
|
+
return resp.status, resp.body or ""
|
|
108
|
+
end
|
|
109
|
+
return nil, ""
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
action = function(host, port)
|
|
113
|
+
local attrs_req = build_get_printer_attrs(host, port)
|
|
114
|
+
local resp = http.post(
|
|
115
|
+
host, port.number, "/printers/",
|
|
116
|
+
{ header = { ["Content-Type"] = "application/ipp" } },
|
|
117
|
+
nil, attrs_req
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if not resp or not resp.body or resp.status ~= 200 then
|
|
121
|
+
-- Try /ipp/print as fallback
|
|
122
|
+
resp = http.post(
|
|
123
|
+
host, port.number, "/ipp/print",
|
|
124
|
+
{ header = { ["Content-Type"] = "application/ipp" } },
|
|
125
|
+
nil, attrs_req
|
|
126
|
+
)
|
|
127
|
+
if not resp or not resp.body then
|
|
128
|
+
return "No IPP response — server may not support IPP or requires TLS"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
local body = resp.body
|
|
133
|
+
local out = stdnse.output_table()
|
|
134
|
+
|
|
135
|
+
-- Extract key attributes
|
|
136
|
+
local model = ipp_extract(body, "printer-make-and-model")
|
|
137
|
+
if model then out["Make-Model"] = model end
|
|
138
|
+
|
|
139
|
+
local state = ipp_extract(body, "printer-state")
|
|
140
|
+
if state then
|
|
141
|
+
local states = { ["\x03"] = "idle", ["\x04"] = "processing", ["\x05"] = "stopped" }
|
|
142
|
+
out["State"] = states[state] or ("state-" .. state:byte(1))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
local formats = ipp_extract(body, "document-format-supported")
|
|
146
|
+
if formats then out["Formats"] = formats:gsub("[%c]", ","):sub(1, 120) end
|
|
147
|
+
|
|
148
|
+
local uri_security = ipp_extract(body, "uri-security-supported")
|
|
149
|
+
out["TLS"] = (uri_security and uri_security ~= "none") and "Supported" or "None/Unknown"
|
|
150
|
+
|
|
151
|
+
-- Test anonymous print
|
|
152
|
+
local auth_status, _ = test_anon_print(host, port)
|
|
153
|
+
local anon_print = false
|
|
154
|
+
if auth_status and (auth_status == 200 or auth_status == 426) then
|
|
155
|
+
out["Auth-Required"] = "NO — anonymous print jobs accepted"
|
|
156
|
+
anon_print = true
|
|
157
|
+
elseif auth_status == 401 or auth_status == 403 then
|
|
158
|
+
out["Auth-Required"] = "YES"
|
|
159
|
+
else
|
|
160
|
+
out["Auth-Required"] = "UNKNOWN"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
-- CVE checks
|
|
164
|
+
local cves_found = {}
|
|
165
|
+
local body_lower = body:lower()
|
|
166
|
+
local snippet = body:gsub("[%c]", " ")
|
|
167
|
+
|
|
168
|
+
-- CUPS version check
|
|
169
|
+
local cups_ver = model and model:match("CUPS%s+([%d%.]+)")
|
|
170
|
+
if cups_ver then
|
|
171
|
+
out["CUPS-Version"] = cups_ver
|
|
172
|
+
if cups_ver == "2.4.16" then
|
|
173
|
+
table.insert(cves_found, {
|
|
174
|
+
id = "CVE-2026-34980",
|
|
175
|
+
sev = "CRITICAL",
|
|
176
|
+
cvss = 9.1,
|
|
177
|
+
desc = "CUPS 2.4.16 unauthenticated RCE via PPD newline injection",
|
|
178
|
+
xpl = "xpl/research/research-cups-chain-2026",
|
|
179
|
+
})
|
|
180
|
+
end
|
|
181
|
+
if cups_ver >= "2.4.0" and cups_ver <= "2.4.9" then
|
|
182
|
+
table.insert(cves_found, {
|
|
183
|
+
id = "CVE-2024-47176",
|
|
184
|
+
sev = "CRITICAL",
|
|
185
|
+
cvss = 9.9,
|
|
186
|
+
desc = "CUPS cups-browsed unauthenticated RCE chain",
|
|
187
|
+
xpl = "xpl/edb-cve-2024-47176",
|
|
188
|
+
})
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
-- Lexmark check
|
|
193
|
+
if snippet:lower():match("lexmark") then
|
|
194
|
+
table.insert(cves_found, {
|
|
195
|
+
id = "CVE-2023-50739",
|
|
196
|
+
sev = "HIGH",
|
|
197
|
+
cvss = 8.8,
|
|
198
|
+
desc = "Lexmark IPP heap buffer overflow → RCE (100+ models)",
|
|
199
|
+
xpl = "xpl/edb-cve-2023-50739",
|
|
200
|
+
})
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
-- Output CVEs
|
|
204
|
+
if #cves_found > 0 then
|
|
205
|
+
local cve_lines = {}
|
|
206
|
+
for _, c in ipairs(cves_found) do
|
|
207
|
+
table.insert(cve_lines, string.format(
|
|
208
|
+
"[%s] %s (CVSS %.1f) — %s | module: %s",
|
|
209
|
+
c.sev, c.id, c.cvss, c.desc, c.xpl))
|
|
210
|
+
end
|
|
211
|
+
out["CVEs"] = table.concat(cve_lines, " | ")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
-- Verdict
|
|
215
|
+
local is_vuln = #cves_found > 0 and anon_print
|
|
216
|
+
local is_possible = #cves_found > 0 or anon_print
|
|
217
|
+
|
|
218
|
+
if is_vuln then
|
|
219
|
+
out["Verdict"] = "VULNERABLE — CVE match + anonymous print accepted"
|
|
220
|
+
if cves_found[1] then
|
|
221
|
+
out["Suggest"] = "printerxpl-forge run --module " .. cves_found[1].xpl .. " --target " .. host.ip
|
|
222
|
+
end
|
|
223
|
+
elseif is_possible then
|
|
224
|
+
out["Verdict"] = "POSSIBLY VULNERABLE — CVE match or anon print (verify with printer-vuln-check)"
|
|
225
|
+
if cves_found[1] then
|
|
226
|
+
out["Suggest"] = "printerxpl-forge run --module " .. cves_found[1].xpl .. " --dry-run --target " .. host.ip
|
|
227
|
+
end
|
|
228
|
+
else
|
|
229
|
+
out["Verdict"] = "NOT VULNERABLE (no matching CVEs detected on IPP)"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
out["Full-Scan"] = "pip install printerxpl-forge | printerxpl-forge scan --target " .. host.ip
|
|
233
|
+
|
|
234
|
+
return out
|
|
235
|
+
end
|