Add http-vuln-cve2026-63030: detect WordPress wp2shell RCE (CVE-2026-63030, CVE-2026-60137)

Fingerprints the WordPress core version from public resources (the generator
meta tag and the RSS feed generator) and reports via the vulns library. No
crafted requests are sent to the batch endpoint and no exploitation is
performed, so it is safe against production hosts.

Version boundaries verified against NVD: 6.9.0-6.9.4 and 7.0.0-7.0.1 get the
full RCE chain (VULNERABLE); 6.8.0-6.8.5 are affected by the CVE-2026-60137
SQLi only (LIKELY VULNERABLE); 6.8.6 / 6.9.5 / 7.0.2 are patched. Uses the
High risk_factor because nmap's vulns library has no Critical tier.
This commit is contained in:
iabdullah215 2026-07-27 12:02:30 +05:00
parent e2265fe97c
commit a22824d0bd
2 changed files with 174 additions and 0 deletions

View file

@ -0,0 +1,173 @@
local http = require "http"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local vulns = require "vulns"
description = [[
Detects WordPress core installations vulnerable to "wp2shell", the
pre-authentication remote code execution chain formed by CVE-2026-63030 (a REST
API batch-endpoint route-confusion weakness in
WP_REST_Server::serve_batch_request_v1()) and CVE-2026-60137 (a SQL injection in
the author__not_in parameter of WP_Query).
Detection is performed purely by fingerprinting the WordPress core version from
publicly served resources (the "generator" meta tag on the home page and the
RSS feed generator). The script does NOT send any crafted request
to the batch endpoint and does NOT attempt exploitation, so it is safe to run
against production hosts.
Affected versions:
* 6.9.0 - 6.9.4 and 7.0.0 - 7.0.1 are exposed to the full unauthenticated RCE
chain (both CVEs) and are reported as VULNERABLE.
* 6.8.0 - 6.8.5 are affected only by the SQL injection (CVE-2026-60137), not
the full RCE chain, and are reported as LIKELY VULNERABLE.
* 6.8.6, 6.9.5, 7.0.2 and later are patched.
* Versions before 6.8.0 are not affected.
References:
https://github.com/advisories/GHSA-ff9f-jf42-662q
https://wordpress.org/documentation/wordpress-version/version-7-0-2/
]]
---
-- @usage
-- nmap -p 80,443 --script http-vuln-cve2026-63030 <target>
-- nmap --script http-vuln-cve2026-63030 --script-args http-vuln-cve2026-63030.root=/blog/ <target>
--
-- @output
-- PORT STATE SERVICE
-- 80/tcp open http
-- | http-vuln-cve2026-63030:
-- | VULNERABLE:
-- | WordPress core pre-authentication RCE (wp2shell)
-- | State: VULNERABLE
-- | IDs: CVE:CVE-2026-63030
-- | Risk factor: High
-- | WordPress 7.0.1 is vulnerable to the wp2shell unauthenticated RCE
-- | chain (CVE-2026-63030 + CVE-2026-60137).
-- | Disclosure date: 2026-07-17
-- | Extra information:
-- | Detected WordPress 7.0.1 via generator meta tag (/).
-- | References:
-- | https://github.com/advisories/GHSA-ff9f-jf42-662q
-- |_ https://wordpress.org/documentation/wordpress-version/version-7-0-2/
--
-- @args http-vuln-cve2026-63030.root Base path of the WordPress install
-- (default "/"). Use this when WordPress is served from a subdirectory.
author = "hwatsauce <muhammadabdullah8040@gmail.com>"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"vuln", "safe"}
portrule = shortport.http
-- Turn a version string like "7.0.1" or "6.9" into a comparable integer so we
-- can do simple range checks (minor and patch numbers stay well under 100).
local function version_key(v)
local major, minor, patch = v:match("^(%d+)%.(%d+)%.?(%d*)")
if not major then
return nil
end
return tonumber(major) * 10000 + tonumber(minor) * 100 + (tonumber(patch) or 0)
end
-- Normalize the user-supplied base path so it starts and ends with "/".
local function normalize_root(root)
root = root or "/"
if root:sub(1, 1) ~= "/" then root = "/" .. root end
if root:sub(-1) ~= "/" then root = root .. "/" end
return root
end
-- Attempt to recover the WordPress core version from public resources.
-- Returns version_string, source_description (or nil if not found).
local function get_wp_version(host, port, root)
-- 1) "generator" meta tag on the home page (handles either attribute order).
local resp = http.get(host, port, root)
if resp and resp.body then
local v = resp.body:match('name="generator"%s+content="WordPress%s+([%d%.]+)"')
or resp.body:match('content="WordPress%s+([%d%.]+)"%s+name="generator"')
if v then
return v, "generator meta tag (" .. root .. ")"
end
end
-- 2) RSS feed generator, e.g. <generator>https://wordpress.org/?v=7.0.1</generator>
resp = http.get(host, port, root .. "feed/")
if resp and resp.body then
local v = resp.body:match("wordpress%.org/%?v=([%d%.]+)")
if v then
return v, "RSS feed generator (" .. root .. "feed/)"
end
end
return nil
end
action = function(host, port)
local root = normalize_root(stdnse.get_script_args("http-vuln-cve2026-63030.root"))
local vuln = {
title = "WordPress core pre-authentication RCE (wp2shell)",
state = vulns.STATE.NOT_VULN,
-- The vulns IDS table takes one CVE key; the second CVE is noted in the
-- description and extra_info below.
IDS = { CVE = "CVE-2026-63030" },
-- nmap's vulns library only accepts "High"/"Medium"/"Low"; "High" is the
-- most severe tier available (there is no "Critical").
risk_factor = "High",
description = [[
wp2shell is an unauthenticated remote code execution chain in WordPress core.
CVE-2026-63030 is a route-confusion flaw in the REST API batch endpoint
(/wp-json/batch/v1) that lets an unvalidated sub-request reach a handler it was
never checked against; chained with the CVE-2026-60137 SQL injection in
WP_Query's author__not_in parameter, an anonymous attacker can forge an
administrator account and execute code. Exploitable on a default install with no
plugins, no configuration, and no authentication.]],
references = {
"https://github.com/advisories/GHSA-ff9f-jf42-662q",
"https://wordpress.org/documentation/wordpress-version/version-7-0-2/",
},
dates = { disclosure = { year = 2026, month = 7, day = 17 } },
}
local report = vulns.Report:new(SCRIPT_NAME, host, port)
local version, source = get_wp_version(host, port, root)
if not version then
-- Could not confirm WordPress / determine a version: report nothing rather
-- than emit a misleading result.
stdnse.debug1("Could not determine WordPress version at %s", root)
return nil
end
local key = version_key(version)
if not key then
return nil
end
if (key >= 60900 and key <= 60904) or (key >= 70000 and key <= 70001) then
-- 6.9.0-6.9.4 and 7.0.0-7.0.1: full unauthenticated RCE chain.
vuln.state = vulns.STATE.VULN
vuln.extra_info = string.format(
"Detected WordPress %s via %s. Vulnerable to the full wp2shell RCE chain "
.. "(CVE-2026-63030 + CVE-2026-60137). Fixed in 6.9.5 / 7.0.2.",
version, source)
elseif key >= 60800 and key <= 60805 then
-- 6.8.0-6.8.5: SQL injection only, not the full RCE chain.
vuln.state = vulns.STATE.LIKELY_VULN
vuln.risk_factor = "High"
vuln.IDS = { CVE = "CVE-2026-60137" }
vuln.extra_info = string.format(
"Detected WordPress %s via %s. Affected by the CVE-2026-60137 SQL "
.. "injection only (not the full wp2shell RCE chain). Fixed in 6.8.6.",
version, source)
else
vuln.state = vulns.STATE.NOT_VULN
vuln.extra_info = string.format(
"Detected WordPress %s via %s. Not affected by wp2shell.", version, source)
end
return report:make_output(vuln)
end

View file

@ -277,6 +277,7 @@ Entry { filename = "http-vuln-cve2017-1001000.nse", categories = { "safe", "vuln
Entry { filename = "http-vuln-cve2017-5638.nse", categories = { "vuln", } }
Entry { filename = "http-vuln-cve2017-5689.nse", categories = { "auth", "exploit", "vuln", } }
Entry { filename = "http-vuln-cve2017-8917.nse", categories = { "intrusive", "vuln", } }
Entry { filename = "http-vuln-cve2026-63030.nse", categories = { "safe", "vuln", } }
Entry { filename = "http-vuln-misfortune-cookie.nse", categories = { "intrusive", "vuln", } }
Entry { filename = "http-vuln-wnr1000-creds.nse", categories = { "exploit", "intrusive", "vuln", } }
Entry { filename = "http-waf-detect.nse", categories = { "discovery", "intrusive", } }