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,211 @@
1
+ local description = [[
2
+ printer-printnightmare — Windows Print Spooler vulnerability detection.
3
+
4
+ Detects exposure of the Windows Print Spooler service (spoolsv.exe) and assesses
5
+ vulnerability to the PrintNightmare family of exploits:
6
+
7
+ - CVE-2021-1675: PrintNightmare RCE via RpcAddPrinterDriverEx (unauthenticated + network)
8
+ - CVE-2021-34527: PrintNightmare (official patch bypass) — still exploitable in some configs
9
+ - CVE-2020-1048: PrintDemon — persistent SYSTEM backdoor via printer port race condition
10
+ - CVE-2020-1337: PrintDemon hardlink bypass — arbitrary privileged file write
11
+ - CVE-2021-36958: Windows Print Spooler remote code execution
12
+ - CVE-2022-38028: GooseEgg — APT28 Print Spooler LPE (CISA KEV 2024, actively exploited)
13
+ - CVE-2022-21999: SpoolFool — Windows Print Spooler LPE
14
+
15
+ Probes: SMB (445), RPC (135), and WSD (3702) to confirm spooler exposure.
16
+
17
+ Author: André Henrique (@mrhenrike) | União Geek
18
+ ]]
19
+
20
+ ---
21
+ -- @usage
22
+ -- nmap -p 445,135,3702 --script printer-printnightmare <target>
23
+ -- @output
24
+ -- PORT STATE SERVICE
25
+ -- 445/tcp open microsoft-ds
26
+ -- | printer-printnightmare:
27
+ -- | SMB : Open (Windows detected)
28
+ -- | OS : Windows 10 / Server 2019
29
+ -- | Spooler : POSSIBLY ACTIVE (SMB open, historical exposure)
30
+ -- | [CRITICAL] CVE-2021-1675 (CVSS 9.8) — PrintNightmare unauthenticated network RCE
31
+ -- | [HIGH] CVE-2022-38028 (CVSS 7.8) — GooseEgg LPE (APT28, CISA KEV)
32
+ -- | [HIGH] CVE-2020-1048 (CVSS 7.8) — PrintDemon persistent SYSTEM backdoor
33
+ -- | Verdict : POSSIBLY VULNERABLE
34
+ -- |_ Suggest : printerxpl-forge run --module xpl/edb-50498 --target <IP>
35
+ ---
36
+
37
+ categories = { "vuln", "safe" }
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 smb = require "smb"
44
+ local smb2 = require "smb2"
45
+ local comm = require "comm"
46
+ local string = require "string"
47
+ local table = require "table"
48
+
49
+ portrule = shortport.port_or_service(
50
+ { 445, 135, 3702, 139 },
51
+ { "microsoft-ds", "msrpc", "wsd", "netbios-ssn" },
52
+ "tcp"
53
+ )
54
+
55
+ local SPOOLER_CVES = {
56
+ { id="CVE-2021-1675", cvss=9.8, sev="CRITICAL",
57
+ desc="PrintNightmare — Windows Print Spooler unauthenticated RCE via RpcAddPrinterDriverEx",
58
+ xpl="xpl/edb-50498" },
59
+ { id="CVE-2021-34527", cvss=8.8, sev="HIGH",
60
+ desc="PrintNightmare (official) — patch bypass via RpcAddPrinterDriverEx with Point&Print",
61
+ xpl="xpl/edb-50498" },
62
+ { id="CVE-2022-38028", cvss=7.8, sev="HIGH",
63
+ desc="GooseEgg — APT28 LPE via Windows Print Spooler (CISA KEV 2024, actively exploited)",
64
+ xpl="xpl/research/research-gooseegg-spooler" },
65
+ { id="CVE-2020-1048", cvss=7.8, sev="HIGH",
66
+ desc="PrintDemon — persistent SYSTEM backdoor via printer port + DLL side-loading",
67
+ xpl="xpl/msf-cve-2020-1048-printerdemon" },
68
+ { id="CVE-2020-1337", cvss=7.8, sev="HIGH",
69
+ desc="PrintDemon hardlink bypass — arbitrary privileged file write → SYSTEM",
70
+ xpl="xpl/edb-cve-2020-1337" },
71
+ { id="CVE-2021-36958", cvss=7.8, sev="HIGH",
72
+ desc="Windows Print Spooler remote code execution (out-of-band patch)",
73
+ xpl="xpl/edb-50498" },
74
+ { id="CVE-2022-21999", cvss=7.8, sev="HIGH",
75
+ desc="SpoolFool — Windows Print Spooler LPE via printer driver DLL planting",
76
+ xpl="xpl/edb-cve-2022-21999" },
77
+ { id="CVE-2023-21678", cvss=7.8, sev="HIGH",
78
+ desc="Windows Print Spooler elevation of privilege (Jan 2023 Patch Tuesday)",
79
+ xpl="xpl/research/research-spooler-eop-2023" },
80
+ }
81
+
82
+ -- SMB negotiation probe to detect Windows
83
+ local function smb_probe(host, port)
84
+ if port.number ~= 445 and port.number ~= 139 then return nil end
85
+ local smb_neg =
86
+ "\x00\x00\x00\x2f" .. -- NetBIOS length
87
+ "\xff\x53\x4d\x42" .. -- SMB magic
88
+ "\x72\x00\x00\x00\x00\x08\x01\x40\x00\x00\x00\x00\x00\x00\x00\x00" ..
89
+ "\x00\x00\x00\x00\xff\xff\xff\xfe\x00\x00\x00\x00" ..
90
+ "\x00\x0c\x00\x02NT LM 0.12\x00"
91
+
92
+ local ok, resp = comm.exchange(host, port, smb_neg,
93
+ { timeout = 5000, bytes = 256 })
94
+ if ok and resp then return resp end
95
+ return nil
96
+ end
97
+
98
+ -- RPC portmapper probe to check if spooler is registered
99
+ local function rpc_probe(host, port)
100
+ if port.number ~= 135 then return nil end
101
+ -- Minimal DCE/RPC bind to portmapper
102
+ local rpc_bind =
103
+ "\x05\x00\x0b\x03\x10\x00\x00\x00\x48\x00\x00\x00\x01\x00\x00\x00" ..
104
+ "\xd0\x16\xd0\x16\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00" ..
105
+ "\x08\x83\xaf\xe1\x1f\x5d\xc9\x11\x91\xa4\x08\x00\x2b\x14\xa0\xfa" ..
106
+ "\x03\x00\x00\x00\x33\x05\x71\x71\xba\xbe\x37\x49\x83\x19\xb5\xdb" ..
107
+ "\xef\x9c\xcc\x36\x01\x00\x00\x00"
108
+ local ok, resp = comm.exchange(host, port, rpc_bind,
109
+ { timeout = 4000, bytes = 256 })
110
+ if ok and resp then return resp end
111
+ return nil
112
+ end
113
+
114
+ -- WSD (Web Services for Devices) probe on 3702/UDP
115
+ local function wsd_probe(host, port)
116
+ if port.number ~= 3702 then return nil end
117
+ local wsd_probe_xml =
118
+ '<?xml version="1.0" encoding="utf-8"?>' ..
119
+ '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" ' ..
120
+ 'xmlns:wsd="http://schemas.xmlsoap.org/ws/2005/04/discovery">' ..
121
+ '<soap:Body><wsd:Probe><wsd:Types>wsdp:Printer</wsd:Types></wsd:Probe></soap:Body>' ..
122
+ '</soap:Envelope>'
123
+ local ok, resp = comm.exchange(host, { number = 3702, protocol = "udp" },
124
+ wsd_probe_xml, { timeout = 3000, bytes = 1024 })
125
+ if ok then return resp end
126
+ return nil
127
+ end
128
+
129
+ action = function(host, port)
130
+ local out = stdnse.output_table()
131
+ local windows_detected = false
132
+ local smb_open = false
133
+ local rpc_open = false
134
+ local wsd_open = false
135
+ local os_hint = nil
136
+
137
+ -- SMB probe
138
+ if port.number == 445 or port.number == 139 then
139
+ local smb_resp = smb_probe(host, port)
140
+ if smb_resp then
141
+ smb_open = true
142
+ -- Check for Windows response marker
143
+ if smb_resp:match("\xff\x53\x4d\x42") then
144
+ windows_detected = true
145
+ -- Try to extract OS info
146
+ local os_str = smb_resp:match("Windows[%w%s%.]+")
147
+ if os_str then os_hint = os_str:sub(1, 50) end
148
+ end
149
+ end
150
+ end
151
+
152
+ -- RPC probe
153
+ if port.number == 135 then
154
+ local rpc_resp = rpc_probe(host, port)
155
+ if rpc_resp then
156
+ rpc_open = true
157
+ windows_detected = true
158
+ end
159
+ end
160
+
161
+ -- WSD probe
162
+ if port.number == 3702 then
163
+ local wsd_resp = wsd_probe(host, port)
164
+ if wsd_resp then
165
+ wsd_open = true
166
+ if wsd_resp:lower():match("printer") then
167
+ windows_detected = true
168
+ end
169
+ end
170
+ end
171
+
172
+ if not smb_open and not rpc_open and not wsd_open then
173
+ return "No SMB/RPC/WSD response — Print Spooler not exposed or target is not Windows"
174
+ end
175
+
176
+ -- Build output
177
+ if smb_open then out["SMB"] = "Open" end
178
+ if rpc_open then out["RPC"] = "Open (DCE/RPC portmapper responding)" end
179
+ if wsd_open then out["WSD"] = "Open (WSD printer broadcast)" end
180
+ if os_hint then out["OS"] = os_hint end
181
+ out["Windows"] = windows_detected and "Detected" or "Possible (SMB open, no OS banner)"
182
+
183
+ -- Spooler estimation
184
+ if smb_open and windows_detected then
185
+ out["Spooler-Status"] = "LIKELY RUNNING — SMB open on Windows host"
186
+ elseif smb_open then
187
+ out["Spooler-Status"] = "POSSIBLY RUNNING — SMB open, OS unconfirmed"
188
+ end
189
+
190
+ -- CVE output
191
+ local cve_lines = {}
192
+ for _, c in ipairs(SPOOLER_CVES) do
193
+ table.insert(cve_lines, string.format("[%s] %s (CVSS %.1f) — %s\n module: %s",
194
+ c.sev, c.id, c.cvss, c.desc, c.xpl))
195
+ end
196
+ out["CVEs"] = cve_lines
197
+
198
+ -- Verdict
199
+ if windows_detected and smb_open then
200
+ out["Verdict"] = "POSSIBLY VULNERABLE — Windows + SMB confirmed; spooler status requires auth check"
201
+ out["Note"] = "Requires auth check: printerxpl-forge can enumerate spooler via NULL session or valid creds"
202
+ out["Suggest"] = "printerxpl-forge run --module xpl/edb-50498 --target " .. host.ip
203
+ else
204
+ out["Verdict"] = "POSSIBLY VULNERABLE — some exposure indicators present"
205
+ out["Suggest"] = "printerxpl-forge run --module xpl/edb-50498 --dry-run --target " .. host.ip
206
+ end
207
+
208
+ out["Full-Scan"] = "pip install printerxpl-forge | printerxpl-forge scan --target " .. host.ip
209
+
210
+ return out
211
+ end
@@ -0,0 +1,176 @@
1
+ local description = [[
2
+ printer-snmp-info — SNMP-based printer information extraction.
3
+
4
+ Queries standard Printer MIB (RFC 3805) and vendor-specific OIDs to retrieve:
5
+ device description, location, contact, serial number, firmware, page count,
6
+ ink/toner levels, and error log. Community strings tested: public, private,
7
+ internal, printer (common defaults).
8
+
9
+ CVE check: CVE-2022-1026 (Kyocera unauthenticated address book dump via SNMP).
10
+
11
+ Author: André Henrique (@mrhenrike) | União Geek
12
+ ]]
13
+
14
+ ---
15
+ -- @usage
16
+ -- nmap -sU -p 161 --script printer-snmp-info <target>
17
+ -- @output
18
+ -- PORT STATE SERVICE
19
+ -- 161/udp open snmp
20
+ -- | printer-snmp-info:
21
+ -- | sysDescr : HP LaserJet M606 FW FY5L.04
22
+ -- | sysLocation : Server Room B
23
+ -- | Serial : JPGD12345
24
+ -- | Page-Count : 127,493
25
+ -- | Community : public (readable — unauthenticated SNMP)
26
+ -- | Verdict : POSSIBLY VULNERABLE (public community writable?)
27
+ -- |_ Suggest : printerxpl-forge run --module xpl/edb-cve-2022-1026
28
+ ---
29
+
30
+ categories = { "discovery", "safe", "vuln" }
31
+ author = "André Henrique (@mrhenrike) | União Geek"
32
+ license = "Same as Nmap -- See https://nmap.org/book/man-legal.html"
33
+
34
+ local stdnse = require "stdnse"
35
+ local shortport = require "shortport"
36
+ local snmp = require "snmp"
37
+ local comm = require "comm"
38
+ local string = require "string"
39
+ local table = require "table"
40
+
41
+ portrule = shortport.port_or_service(161, "snmp", "udp")
42
+
43
+ -- Standard + Printer MIB OIDs
44
+ local OIDS = {
45
+ { oid = "1.3.6.1.2.1.1.1.0", key = "sysDescr" },
46
+ { oid = "1.3.6.1.2.1.1.5.0", key = "sysName" },
47
+ { oid = "1.3.6.1.2.1.1.6.0", key = "sysLocation" },
48
+ { oid = "1.3.6.1.2.1.1.4.0", key = "sysContact" },
49
+ { oid = "1.3.6.1.2.1.43.5.1.1.17.1", key = "Serial-Number" },
50
+ { oid = "1.3.6.1.2.1.43.10.2.1.4.1.1", key = "Page-Count" },
51
+ { oid = "1.3.6.1.2.1.43.11.1.1.9.1.1", key = "Toner-Level-%" },
52
+ { oid = "1.3.6.1.4.1.11.2.3.9.1.1.7.0", key = "HP-Firmware" },
53
+ { oid = "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.8.0", key = "Brother-Model" },
54
+ { oid = "1.3.6.1.4.1.18334.1.1.1.5.7.2.1.1.19.1", key = "Kyocera-Serial" },
55
+ }
56
+
57
+ local COMMUNITIES = { "public", "private", "internal", "printer", "snmpv1", "community", "admin" }
58
+
59
+ action = function(host, port)
60
+ local out = stdnse.output_table()
61
+ local community_used = nil
62
+ local results = {}
63
+
64
+ -- Try communities
65
+ for _, community in ipairs(COMMUNITIES) do
66
+ local req = snmp.buildPacket(snmp.PDU:new("GETNEXT", OIDS[1].oid), community)
67
+ local ok, resp = comm.exchange(host, port, req,
68
+ { proto = "udp", timeout = 3000 })
69
+ if ok and resp and not resp:match("noSuchName") then
70
+ community_used = community
71
+ break
72
+ end
73
+ end
74
+
75
+ if not community_used then
76
+ return "SNMP community not found — try: nmap -p 161 --script snmp-brute"
77
+ end
78
+
79
+ out["Community"] = community_used .. " (readable)"
80
+
81
+ -- Walk OIDs with found community
82
+ for _, entry in ipairs(OIDS) do
83
+ local req = snmp.buildPacket(snmp.PDU:new("GET", entry.oid), community_used)
84
+ local ok, resp = comm.exchange(host, port, req,
85
+ { proto = "udp", timeout = 2000 })
86
+ if ok and resp then
87
+ local val = snmp.parseResponse(resp)
88
+ if val and val ~= "" then
89
+ results[entry.key] = tostring(val):match("^%s*(.-)%s*$")
90
+ end
91
+ end
92
+ end
93
+
94
+ for k, v in pairs(results) do
95
+ out[k] = v:sub(1, 120)
96
+ end
97
+
98
+ -- Detect vendor from sysDescr
99
+ local desc_lower = (results["sysDescr"] or ""):lower()
100
+ local vendor = nil
101
+ if desc_lower:match("hp ") or desc_lower:match("laserjet") then
102
+ vendor = "HP"
103
+ elseif desc_lower:match("kyocera") then
104
+ vendor = "Kyocera"
105
+ elseif desc_lower:match("ricoh") or desc_lower:match("aficio") then
106
+ vendor = "Ricoh"
107
+ elseif desc_lower:match("lexmark") then
108
+ vendor = "Lexmark"
109
+ elseif desc_lower:match("canon") then
110
+ vendor = "Canon"
111
+ elseif desc_lower:match("xerox") then
112
+ vendor = "Xerox"
113
+ elseif desc_lower:match("brother") then
114
+ vendor = "Brother"
115
+ elseif desc_lower:match("konica") or desc_lower:match("bizhub") then
116
+ vendor = "Konica Minolta"
117
+ end
118
+
119
+ if vendor then out["Vendor"] = vendor end
120
+
121
+ -- SNMP write test (COMMUNITY "private")
122
+ local write_possible = false
123
+ if community_used == "public" then
124
+ -- Try write with private
125
+ local write_req = snmp.buildPacket(
126
+ snmp.PDU:new("SET", "1.3.6.1.2.1.1.6.0", "nmap-test"), "private")
127
+ local wok, wresp = comm.exchange(host, port, write_req,
128
+ { proto = "udp", timeout = 2000 })
129
+ if wok and wresp and not wresp:match("noAccess") and not wresp:match("readOnly") then
130
+ write_possible = true
131
+ out["SNMP-Write"] = "POSSIBLE with 'private' community — SNMP SET attacks feasible"
132
+ else
133
+ out["SNMP-Write"] = "Not available (read-only with public)"
134
+ end
135
+ end
136
+
137
+ -- Kyocera CVE-2022-1026 check
138
+ local cves_found = {}
139
+ if vendor == "Kyocera" or (results["Kyocera-Serial"] ~= nil) then
140
+ table.insert(cves_found, {
141
+ id = "CVE-2022-1026",
142
+ sev = "HIGH",
143
+ cvss = 7.5,
144
+ desc = "Kyocera ECOSYS unauthenticated address book dump via SNMP/PJL",
145
+ xpl = "xpl/edb-cve-2022-1026",
146
+ })
147
+ end
148
+
149
+ -- Build verdict
150
+ local is_vuln = write_possible or #cves_found > 0
151
+ local is_possible = community_used == "public"
152
+
153
+ if #cves_found > 0 then
154
+ local cve_lines = {}
155
+ for _, c in ipairs(cves_found) do
156
+ table.insert(cve_lines, string.format("[%s] %s (CVSS %.1f) — %s | module: %s",
157
+ c.sev, c.id, c.cvss, c.desc, c.xpl))
158
+ end
159
+ out["CVEs"] = table.concat(cve_lines, " | ")
160
+ end
161
+
162
+ if is_vuln then
163
+ out["Verdict"] = "POSSIBLY VULNERABLE — CVE match or SNMP write enabled"
164
+ if cves_found[1] then
165
+ out["Suggest"] = "printerxpl-forge run --module " .. cves_found[1].xpl .. " --target " .. host.ip
166
+ end
167
+ elseif is_possible then
168
+ out["Verdict"] = "POSSIBLY VULNERABLE — public community accessible (enumeration only)"
169
+ else
170
+ out["Verdict"] = "NOT VULNERABLE to detected CVEs (SNMP read-only, no CVE match)"
171
+ end
172
+
173
+ out["Full-Scan"] = "pip install printerxpl-forge | printerxpl-forge scan --target " .. host.ip
174
+
175
+ return out
176
+ end
@@ -0,0 +1,256 @@
1
+ local description = [[
2
+ printer-vuln-check — Active, non-destructive vulnerability validation.
3
+
4
+ Performs lightweight active probes to confirm or rule out specific CVEs detected
5
+ by printer-cve-detect. Each check sends a minimal proof-of-concept payload and
6
+ evaluates the response — no damage, no file writes, no persistence.
7
+
8
+ Active checks included:
9
+ 1. PJL NVRAM info-leak (DISPLAY/USTATUS abuse)
10
+ 2. HP PostScript RCE probe (print error dict exploit path)
11
+ 3. CUPS IPP newline injection marker (CVE-2026-34980)
12
+ 4. Lexmark IPP malformed attr overflow indicator (CVE-2023-50739)
13
+ 5. Canon SLP pre-auth probe (CVE-2022-24673)
14
+ 6. Xerox command injection surface check (CVE-2021-27508/CVE-2024-6333)
15
+ 7. Sharp unauthenticated command execution probe (CVE-2022-45796)
16
+ 8. PrintNightmare SMB print spooler exposure check
17
+
18
+ Author: André Henrique (@mrhenrike) | União Geek
19
+ ]]
20
+
21
+ ---
22
+ -- @usage
23
+ -- nmap -p 9100,631,80,427,445 --script printer-vuln-check <target>
24
+ -- @output
25
+ -- PORT STATE SERVICE
26
+ -- 9100/tcp open jetdirect
27
+ -- | printer-vuln-check:
28
+ -- | [CHECK 1] PJL NVRAM info-leak : PASS (firmware info disclosed)
29
+ -- | [CHECK 2] HP PostScript RCE path : POSSIBLY VULNERABLE (error dict accessible)
30
+ -- | [CHECK 3] CUPS PPD injection : NOT VULNERABLE (CUPS not detected)
31
+ -- | Overall Verdict : POSSIBLY VULNERABLE
32
+ -- |_ Suggest : printerxpl-forge run --module xpl/edb-cve-2025-26506 --target <IP>
33
+ ---
34
+
35
+ categories = { "vuln", "intrusive" }
36
+ author = "André Henrique (@mrhenrike) | União Geek"
37
+ license = "Same as Nmap -- See https://nmap.org/book/man-legal.html"
38
+
39
+ local stdnse = require "stdnse"
40
+ local shortport = require "shortport"
41
+ local comm = require "comm"
42
+ local http = require "http"
43
+ local string = require "string"
44
+ local table = require "table"
45
+
46
+ portrule = shortport.port_or_service(
47
+ { 9100, 631, 80, 443, 427, 445, 515 },
48
+ { "jetdirect", "ipp", "http", "https", "snmp", "printer" },
49
+ "tcp"
50
+ )
51
+
52
+ local UEL = "\x1b%-12345X"
53
+ local V = { VULN = "VULNERABLE", POSS = "POSSIBLY VULNERABLE", NOT = "NOT VULNERABLE", SKIP = "SKIP" }
54
+
55
+ -- ── Check helpers ────────────────────────────────────────────────────────────
56
+
57
+ local function check_pjl_nvram(host, port)
58
+ if port.number ~= 9100 then return V.SKIP, nil end
59
+ local cmd = UEL .. "@PJL INFO VARIABLES\r\nDISPLAY\r\n" .. UEL
60
+ local ok, r = comm.exchange(host, port, cmd, { timeout = 5000, bytes = 4096 })
61
+ if not ok then return V.SKIP, "no response" end
62
+ if r:match("VARIABLES") or r:match("DISPLAY") then
63
+ return V.POSS, "PJL VARIABLES/DISPLAY responded — NVRAM read-back possible"
64
+ end
65
+ return V.NOT, "PJL VARIABLES not readable"
66
+ end
67
+
68
+ local function check_hp_ps_rce(host, port)
69
+ if port.number ~= 9100 then return V.SKIP, nil end
70
+ -- Minimal PS: push serverdict begin and check for response
71
+ local ps_probe = UEL ..
72
+ "@PJL ENTER LANGUAGE=POSTSCRIPT\r\n" ..
73
+ "serverdict begin 0 exitserver\r\n" ..
74
+ "systemdict begin\r\n" ..
75
+ "/internaldict where { pop internaldict /setpassword known { (probe-xpl) } if } if\r\n" ..
76
+ "flush\r\n" ..
77
+ UEL
78
+ local ok, r = comm.exchange(host, port, ps_probe, { timeout = 6000, bytes = 2048 })
79
+ if not ok then return V.SKIP, "no PS response" end
80
+ if r:match("probe.xpl") or r:match("internaldict") or r:match("setpassword") then
81
+ return V.VULN, "PostScript serverdict/exitserver accepted — RCE path confirmed"
82
+ elseif r:match("exitserver") or r:match("[Ee]rror") then
83
+ return V.POSS, "PS interpreter responded (exitserver/error) — partial RCE surface"
84
+ end
85
+ return V.NOT, "PS probe not reflected"
86
+ end
87
+
88
+ local function check_cups_ppd_inject(host, port)
89
+ if port.number ~= 631 then return V.SKIP, nil end
90
+ local resp = http.get(host, port.number, "/", { timeout = 4000 })
91
+ if not resp or not resp.body then return V.SKIP, "IPP/HTTP not responding" end
92
+ local body = resp.body:lower()
93
+ if not body:match("cups") then return V.NOT, "CUPS not detected on port 631" end
94
+ local ver = resp.body:match("CUPS%s+([%d%.]+)")
95
+ if ver then
96
+ if ver == "2.4.16" then
97
+ return V.VULN, "CUPS 2.4.16 detected — CVE-2026-34980 unauthenticated RCE confirmed"
98
+ elseif ver >= "2.4.0" then
99
+ return V.POSS, "CUPS " .. ver .. " — possible CVE-2024-47176 (cups-browsed RCE chain)"
100
+ else
101
+ return V.NOT, "CUPS " .. ver .. " — not in vulnerable range"
102
+ end
103
+ end
104
+ return V.POSS, "CUPS detected but version unknown — manual check recommended"
105
+ end
106
+
107
+ local function check_lexmark_ipp_bof(host, port)
108
+ if port.number ~= 631 then return V.SKIP, nil end
109
+ -- Send IPP with oversized attribute name to test length validation
110
+ local big_attr = string.rep("A", 2048)
111
+ local payload =
112
+ "\x02\x00\x00\x0b\x00\x00\x00\x01\x01" ..
113
+ "\x47\x00\x12attributes-charset\x00\x05utf-8" ..
114
+ "\x48\x00\x1battributes-natural-language\x00\x05en-us" ..
115
+ "\x42" .. string.char(0x00, 0x08) .. "job-name" ..
116
+ string.char(0x08, 0x00) .. big_attr:sub(1, 2048) ..
117
+ "\x03"
118
+ local resp = http.post(
119
+ host, port.number, "/printers/",
120
+ { header = { ["Content-Type"] = "application/ipp" }, timeout = 5000 },
121
+ nil, payload
122
+ )
123
+ if not resp then return V.SKIP, "no response" end
124
+ if resp.status == 500 or resp.status == 400 then
125
+ local body = (resp.body or ""):lower()
126
+ if body:match("lexmark") or body:match("ipp") then
127
+ return V.POSS, "Lexmark IPP returned " .. resp.status .. " on oversized attribute — CVE-2023-50739 surface"
128
+ end
129
+ elseif resp.status == 200 then
130
+ return V.NOT, "IPP accepted oversized attr without error (sanitized)"
131
+ end
132
+ return V.SKIP, "indeterminate response: HTTP " .. (resp.status or "nil")
133
+ end
134
+
135
+ local function check_canon_slp(host, port)
136
+ if port.number ~= 427 then return V.SKIP, nil end
137
+ -- Minimal SLP service request for "printer" service type
138
+ local slp_req =
139
+ "\x02\x01" .. -- SLP v2, Service Request
140
+ "\x00\x00\x00\x30" ..
141
+ "\x00\x00\x00\x00\x00\x01" ..
142
+ "\x00\x05en\x00\x00" ..
143
+ "\x00\x07printer\x00\x00\x00"
144
+ local ok, r = comm.exchange(host, port, slp_req, { timeout = 4000, bytes = 512, proto = "udp" })
145
+ if not ok then
146
+ -- Try TCP SLP
147
+ ok, r = comm.exchange(host, port, slp_req, { timeout = 4000, bytes = 512 })
148
+ end
149
+ if ok and r then
150
+ local body = r:lower()
151
+ if body:match("canon") or body:match("imageclass") then
152
+ return V.VULN, "Canon SLP responding with device info — CVE-2022-24673 pre-auth BOF surface"
153
+ elseif #r > 4 then
154
+ return V.POSS, "SLP service responding on 427 — check for Canon imageCLASS (CVE-2022-24673)"
155
+ end
156
+ end
157
+ return V.NOT, "SLP not responding on port 427"
158
+ end
159
+
160
+ local function check_xerox_cmd_inject(host, port)
161
+ if port.number ~= 80 and port.number ~= 443 then return V.SKIP, nil end
162
+ -- Probe Xerox clone endpoint (CVE-2021-27508)
163
+ local r = http.get(host, port.number, "/cgi-bin/cloning", { timeout = 4000 })
164
+ if r then
165
+ if r.status == 200 then
166
+ return V.POSS, "Xerox /cgi-bin/cloning endpoint accessible (200) — CVE-2021-27508 surface"
167
+ elseif r.status == 302 or r.status == 401 then
168
+ return V.NOT, "Xerox cloning endpoint requires auth (HTTP " .. r.status .. ")"
169
+ end
170
+ end
171
+ -- Probe VersaLink panel (CVE-2024-6333)
172
+ local r2 = http.get(host, port.number, "/web/index.html", { timeout = 3000 })
173
+ if r2 and r2.status == 200 and r2.body then
174
+ if r2.body:lower():match("versalink") then
175
+ return V.POSS, "Xerox VersaLink panel detected — CVE-2024-6333 requires authentication"
176
+ end
177
+ end
178
+ return V.NOT, "Xerox endpoints not accessible"
179
+ end
180
+
181
+ local function check_sharp_rce(host, port)
182
+ if port.number ~= 80 then return V.SKIP, nil end
183
+ local r = http.get(host, port.number, "/cgi-bin/system_cmd.cgi", { timeout = 4000 })
184
+ if r then
185
+ if r.status == 200 then
186
+ return V.VULN, "Sharp /cgi-bin/system_cmd.cgi accessible (200) — CVE-2022-45796 unauthenticated RCE"
187
+ elseif r.status == 401 or r.status == 403 then
188
+ return V.NOT, "Sharp cmd endpoint requires auth (HTTP " .. r.status .. ")"
189
+ end
190
+ end
191
+ return V.NOT, "Sharp cmd endpoint not found"
192
+ end
193
+
194
+ local function check_print_spooler(host, port)
195
+ if port.number ~= 445 then return V.SKIP, nil end
196
+ -- Check if port 445 is open (SMB) — PrintNightmare requires SMB + Spooler
197
+ -- We just fingerprint — actual exploit is in xpl/edb-50498
198
+ local ok, r = comm.exchange(host, port,
199
+ "\x00\x00\x00\x2f\xff\x53\x4d\x42\x72\x00\x00\x00\x00\x08\x01\x40" ..
200
+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xfe" ..
201
+ "\x00\x00\x00\x00\x00\x0c\x00\x02NT LM 0.12\x00",
202
+ { timeout = 4000, bytes = 256 })
203
+ if ok and r and r:match("\xff\x53\x4d\x42") then
204
+ return V.POSS, "SMB port 445 open — PrintNightmare (CVE-2021-1675) requires Spooler + domain enum"
205
+ end
206
+ return V.NOT, "SMB not responding or not Windows"
207
+ end
208
+
209
+ -- ── Main ─────────────────────────────────────────────────────────────────────
210
+
211
+ action = function(host, port)
212
+ local out = stdnse.output_table()
213
+ local checks = {
214
+ { name = "PJL NVRAM info-leak", fn = check_pjl_nvram, xpl = "xpl/edb-cve-2017-2741" },
215
+ { name = "HP PostScript RCE path", fn = check_hp_ps_rce, xpl = "xpl/edb-cve-2025-26506" },
216
+ { name = "CUPS PPD injection", fn = check_cups_ppd_inject, xpl = "xpl/research/research-cups-chain-2026" },
217
+ { name = "Lexmark IPP BOF", fn = check_lexmark_ipp_bof, xpl = "xpl/edb-cve-2023-50739" },
218
+ { name = "Canon SLP pre-auth BOF", fn = check_canon_slp, xpl = "xpl/edb-cve-2022-24673" },
219
+ { name = "Xerox command injection surface", fn = check_xerox_cmd_inject, xpl = "xpl/edb-cve-2024-6333" },
220
+ { name = "Sharp unauthenticated RCE", fn = check_sharp_rce, xpl = "xpl/research/research-sharp-rce" },
221
+ { name = "PrintNightmare SMB exposure", fn = check_print_spooler, xpl = "xpl/edb-50498" },
222
+ }
223
+
224
+ local results = {}
225
+ local top_verdict = V.NOT
226
+ local top_xpl = nil
227
+
228
+ for i, chk in ipairs(checks) do
229
+ local verdict, detail = chk.fn(host, port)
230
+ local label = string.format("[CHECK %d] %-30s: %s", i, chk.name, verdict)
231
+ if detail and detail ~= "" then
232
+ label = label .. " (" .. detail .. ")"
233
+ end
234
+ table.insert(results, label)
235
+
236
+ -- Track overall verdict
237
+ if verdict == V.VULN and top_verdict ~= V.VULN then
238
+ top_verdict = V.VULN
239
+ top_xpl = chk.xpl
240
+ elseif verdict == V.POSS and top_verdict == V.NOT then
241
+ top_verdict = V.POSS
242
+ top_xpl = chk.xpl
243
+ end
244
+ end
245
+
246
+ out["Checks"] = results
247
+ out["Overall-Verdict"] = top_verdict
248
+
249
+ if top_xpl then
250
+ out["Suggest"] = "printerxpl-forge run --module " .. top_xpl .. " --target " .. host.ip
251
+ end
252
+
253
+ out["Full-Scan"] = "pip install printerxpl-forge | printerxpl-forge scan --target " .. host.ip
254
+
255
+ return out
256
+ end