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.
Files changed (97) hide show
  1. nse/README.md +204 -0
  2. nse/__init__.py +6 -0
  3. nse/install_nse.py +412 -0
  4. nse/lib/printerxpl.lua +238 -0
  5. nse/scripts/cups-info.nse +74 -0
  6. nse/scripts/cups-queue-info.nse +43 -0
  7. nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
  8. nse/scripts/http-device-mac.nse +107 -0
  9. nse/scripts/http-hp-ilo-info.nse +121 -0
  10. nse/scripts/http-info-xerox-enum.nse +101 -0
  11. nse/scripts/http-vuln-cve2022-1026.nse +158 -0
  12. nse/scripts/lexmark-config.nse +89 -0
  13. nse/scripts/pjl-ready-message.nse +106 -0
  14. nse/scripts/printer-banner.nse +217 -0
  15. nse/scripts/printer-cups-rce.nse +189 -0
  16. nse/scripts/printer-cve-detect.nse +279 -0
  17. nse/scripts/printer-discover.nse +205 -0
  18. nse/scripts/printer-firmware-exposed.nse +219 -0
  19. nse/scripts/printer-hp-pjl.nse +192 -0
  20. nse/scripts/printer-http-ews.nse +293 -0
  21. nse/scripts/printer-ipp-info.nse +235 -0
  22. nse/scripts/printer-lexmark-ipp.nse +203 -0
  23. nse/scripts/printer-passback.nse +204 -0
  24. nse/scripts/printer-pjl-info.nse +146 -0
  25. nse/scripts/printer-printnightmare.nse +211 -0
  26. nse/scripts/printer-snmp-info.nse +176 -0
  27. nse/scripts/printer-vuln-check.nse +256 -0
  28. nse/scripts/snmp-device-mac.nse +93 -0
  29. nse/scripts/snmp-info.nse +146 -0
  30. nse/scripts/snmp-sysdescr.nse +70 -0
  31. printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
  32. printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
  33. printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
  34. printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
  35. printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
  36. printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
  37. src/assets/fonts/gunplay.pfa +1671 -0
  38. src/assets/fonts/kshandwrt.pfa +315 -0
  39. src/assets/fonts/laksoner.pfa +2402 -0
  40. src/assets/fonts/paintcans.pfa +9699 -0
  41. src/assets/fonts/stencilod.pfa +4076 -0
  42. src/assets/fonts/takecover.pfa +26138 -0
  43. src/assets/fonts/topsecret.pfa +6652 -0
  44. src/assets/fonts/whoa.pfa +773 -0
  45. src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
  46. src/assets/mibs/Printer-MIB +4389 -0
  47. src/assets/mibs/README.md +9 -0
  48. src/assets/mibs/SNMPv2-MIB +854 -0
  49. src/assets/overlays/hacker.eps +596 -0
  50. src/assets/overlays/smiley.eps +214 -0
  51. src/assets/overlays/smiley2.eps +240 -0
  52. src/core/attack_orchestrator.py +1025 -0
  53. src/core/capabilities.py +323 -0
  54. src/core/destructive_audit.py +430 -0
  55. src/core/discovery.py +488 -0
  56. src/core/osdetect.py +74 -0
  57. src/core/poly_runner.py +579 -0
  58. src/core/printer.py +1426 -0
  59. src/main.py +2134 -0
  60. src/modules/install_printer.py +318 -0
  61. src/modules/login_bruteforce.py +852 -0
  62. src/modules/pcl.py +506 -0
  63. src/modules/pjl.py +3575 -0
  64. src/modules/print_job.py +1290 -0
  65. src/modules/ps.py +1102 -0
  66. src/payloads/__init__.py +98 -0
  67. src/payloads/assets/overlays/notice.eps +9 -0
  68. src/protocols/__init__.py +19 -0
  69. src/protocols/firmware.py +738 -0
  70. src/protocols/ipp.py +216 -0
  71. src/protocols/ipp_attacks.py +609 -0
  72. src/protocols/lpd.py +141 -0
  73. src/protocols/network_map.py +1004 -0
  74. src/protocols/raw.py +173 -0
  75. src/protocols/smb.py +359 -0
  76. src/protocols/ssrf_pivot.py +427 -0
  77. src/protocols/storage.py +587 -0
  78. src/ui/__init__.py +6 -0
  79. src/ui/interactive.py +742 -0
  80. src/ui/spinner.py +112 -0
  81. src/ui/tables.py +132 -0
  82. src/utils/banner_grabber.py +852 -0
  83. src/utils/codebook.py +456 -0
  84. src/utils/config.py +522 -0
  85. src/utils/cve_loader.py +158 -0
  86. src/utils/default_creds.py +134 -0
  87. src/utils/discovery_online.py +1327 -0
  88. src/utils/exploit_manager.py +805 -0
  89. src/utils/fuzzer.py +220 -0
  90. src/utils/helper.py +732 -0
  91. src/utils/local_printers.py +307 -0
  92. src/utils/ml_engine.py +491 -0
  93. src/utils/operators.py +474 -0
  94. src/utils/ports.py +234 -0
  95. src/utils/vuln_scanner.py +823 -0
  96. src/utils/wordlist_loader.py +412 -0
  97. 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