Sploitus

Exploit for RamziRange3

githubexploit Β· 2026-09-10

Exploit Code

README450 lines
## https://sploitus.com/exploit?id=BBF7559C-063B-5CEC-AF7A-96C54A368430
πŸ•ΆοΈ RamziRange9


  A deliberately vulnerable web application built to light up every single weakness class a modern web-app pentest looks for β€” and to do it fast.



  
  
  
  


GO HACK YOURSELF

---

## β›” Read this first

> [!CAUTION]
> **This application is intentionally, comprehensively insecure.** It ships remote code
> execution as root, SQL injection, path traversal, XXE, SSRF and a pile of exposed
> credentials β€” *on purpose*. It exists so that scanners and analysts have something
> real to find.
>
> - πŸ”’ **Run it on an isolated lab network only.** Never on a corporate LAN, never
>   internet-facing, never on a host you care about.
> - πŸ§ͺ **Authorized testing and training only.** Point tooling at it because you own it.
> - 🐳 **Treat the container as compromised by design.** `/admin/exec` gives `uid=0`.
> - 🚫 **Do not reuse a single line of this code** in anything real. Every pattern in
>   here is an anti-pattern, deliberately.

---

## 🎯 What this is

Most vulnerable-app projects give you a grab bag of bugs. This one is built backwards
from a **scanner's finding taxonomy**: every weakness ID gets a purpose-built carrier
endpoint, wired so it is *actually reachable, actually confirmable, and correctly
classified*.

That last part turns out to be the hard bit. A vulnerability that exists but never gets
crawled, gets shadowed by a different detector, or takes 40 seconds per probe might as
well not be there. A big chunk of this repo is the accumulated fix list for exactly
those problems β€” see [βš™οΈ Tuning notes](#️-tuning-notes-how-an-18-hour-scan-became-hours).

**What you get:**

| | |
|---|---|
| πŸŽ–οΈ **28 weakness classes** | Injection, XSS Γ—3 flavours, template injection Γ—4, traversal, SSRF, XXE, CRLF, authz, and the whole disclosure family |
| 🧬 **3 exploit chains** | Credential discovery that converts into super-admin and then into root |
| πŸ” **A gated developer portal** | A second login only one account can pass, hiding a full CI/CD secret store |
| πŸ“œ **A valid OpenAPI 3.0.3 spec** | Self-describing, with working example values for every parameter |
| 🎨 **A Matrix-themed UI** | Because a demo target should look like something |
| 🐳 **4 containers, one command** | No setup, no seeding, no fixtures to load |

---

## ⚑ Quick start

```bash
git clone  && cd RamziRange9
docker compose up -d --build
```

Then open **`http://:9600/`**. The landing page is a live catalog of every
endpoint, grouped by weakness ID, each one a working link.

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  rr9_                    Catalog Login Secrets Config … β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                         β”‚
β”‚        β–“β–“  β–“β–“β–“      β–“β–“ β–“β–“  β–“β–“β–“   β–“β–“β–“β–“ β–“β–“  β–“β–“            β”‚
β”‚        β–“   β–“ β–“      β–“β–“β–“β–“β–“ β–“β–“β–“β–“β–“ β–“     β–“β–“β–“β–“             β”‚
β”‚        β–“β–“β–“ β–“β–“β–“      β–“β–“ β–“β–“ β–“β–“ β–“β–“  β–“β–“β–“β–“ β–“β–“  β–“β–“            β”‚
β”‚              G O   H A C K   Y O U R S E L F            β”‚
β”‚                                                         β”‚
β”‚   60 endpoints Β· 28 weakness IDs                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

---

## πŸ”‘ Credentials

### Main application β€” `/login`

| Username | Password | Role | Notes |
|---|---|---|---|
| `ramzi` | `ramzi` | `user` | Normal user. Baseline for privilege comparison. |
| `dade.murphy` | `nodezero` | `admin` | πŸ‘‘ **Super admin.** Reaches root RCE. |
| `developer1` | `nodezero` | `developer` | πŸ› οΈ Privileged. The only account the dev portal accepts. |

Session is a plain unsigned cookie: `sid=` β€” trivially forgeable, which is
the point.

### Developer portal β€” `/dev/login`

A **completely separate login** with its own hardcoded check and its own cookie
(`devsid`). Only `developer1` / `nodezero` gets in β€” *the super admin cannot*.

> [!IMPORTANT]
> Supply **all three** credentials to your scanner. `developer1` is not optional: the
> entire developer portal, the CI/CD secret store, and two weakness classes live behind
> it and are invisible without that account.

---

## πŸŽ–οΈ Weakness coverage

Every row below is verified working, not aspirational.

| Class | ID | Carrier endpoints |
|---|---|---|
| πŸ’‰ **SQL Injection** | `H3-2025-0069` | `/sqli/query?id=` Β· `/search/orders?q=` (UNION) Β· `/login` (auth bypass) |
| πŸ” **Reflected XSS** | `H3-2025-0059` | `/xss/reflect?q=` Β· `/xss/attr?value=` (attribute ctx) Β· `/xss/jsvar?user=` (JS string ctx) |
| πŸ’Ύ **Stored XSS** | `H3-2025-0071` | `/xss/store` β†’ `/xss/feed` Β· `/guestbook` β†’ `/guestbook/wall` Β· `/dev/notes` β†’ `/dev/board` |
| 🌐 **DOM XSS** | `H3-2026-0047` | `/dom?msg=` (innerHTML) · `/dom/write?name=` · `/dom/eval?expr=` |
| 🧩 **SSTI β€” reflected** | `H3-2025-0072` | `/ssti/query?tpl=` β€” `{{7*7}}` β†’ `49` |
| 🧩 **SSTI β€” stored** | `H3-2025-0078` | `/ssti/store` β†’ `/ssti/report` |
| πŸ…°οΈ **CSTI β€” reflected** | `H3-2026-0029` | `/csti/query?bio=` β€” real AngularJS `ng-app` |
| πŸ…°οΈ **CSTI β€” stored** | `H3-2026-0030` | `/csti/store` β†’ `/csti/profile` |
| πŸ’€ **OS Command Injection** | `H3-2025-0077` | `/cmd/ping?host=` Β· `/logs/view?log=` Β· `/admin/exec?cmd=` (**root**) |
| πŸ“‚ **Path Traversal** | `H3-2022-0015` | `/files/read?file=` Β· `/download?doc=` Β· `/logs/view?log=` Β· `/dev/files?path=` |
| πŸ›°οΈ **SSRF (+ full read)** | `H3-2025-0076` `H3-2026-0024` | `/net/fetch?url=` Β· `/net/preview?target=` |
| β†ͺ️ **Open Redirect** | `H3-2025-0079` | `/go?url=` Β· `/out?next=` |
| β†ͺ️ **…via header injection** | `H3-2026-0027` | `/go/hdr?url=` Β· `/out/hdr` (honours `X-Forwarded-Host`) |
| βœ‚οΈ **CRLF / Response Splitting** | `H3-2026-0026` | `/prefs?lang=` Β· `/track?ref=` |
| πŸ“„ **XXE** | `H3-2025-0050` | `/xml/parse?doc=` Β· `/xml/import?feed=` |
| πŸšͺ **Improper Authorization** | `H3-2026-0061` | `/hr/salaries` Β· `/audit/logs` Β· `/finance/payroll` Β· `/admin/exec` |
| πŸ”’ **IDOR / BOLA** | `H3-2026-0013` | `/api/orders/` β€” walks three distinct owners |
| πŸ—ƒοΈ **Sensitive Info Disclosure** | `H3-2025-0080` | `/config` |
| πŸ”“ **Exposed Credentials** | `H3-2026-0033` | `/bac/users` Β· `/dev/secrets` |
| πŸ—οΈ **Exposed API Key** | `H3-2026-0034` | `/api/keys` Β· `/dev/env` Β· `/dev/pipeline` |
| πŸ—ΊοΈ **Google Maps API Key** | `H3-2026-0037` | `/contact` β€” key in 4 places, no external fetch |
| πŸ§‘β€πŸ’Ό **PII Disclosure** | `H3-2026-0035` | `/api/customers` β€” SSNs, card numbers, DOBs |
| 🏒 **Internal Data Disclosure** | `H3-2026-0036` | `/logs/view` β€” DSNs, service map, admin password |
| 🧨 **Stack Trace Disclosure** | `H3-2026-0038` | `/export?rows=` · `/debug/lookup?key=` |
| πŸ—‚οΈ **Directory Listing** | `H3-2026-0039` | `/files/` β€” authentic nginx autoindex markup |
| πŸ“˜ **Swagger Spec Exposed** | `H3-2026-0051` | `/openapi.json` Β· `/swagger.json` Β· `/api-docs` Β· `/v2/api-docs` |
| πŸ”‘ **Generic `.env` Exposure** | `H3-2025-0032` | `/.env` Β· `/.env.production` Β· `/.env.local` |

> [!NOTE]
> **SSRF needs out-of-band callbacks enabled** on your scanner. The endpoints do a
> genuine full-read server-side fetch and reach an internal-only container with no host
> port β€” but *confirming* SSRF requires an OOB channel. That is a scanner setting, not
> something the app can provide.

---

## 🧬 Attack chains

The interesting part isn't the individual bugs β€” it's that **discovery converts into
access**.

### πŸ₯‡ Chain 1 β€” Cloud credential reuse β†’ super admin β†’ root

```
GET /bac/users                      ← unauthenticated
  ↳ aws.console_user     = dade.murphy@queebler.test
    aws.console_password = nodezero
       ↳ POST /login  as dade.murphy               β†’ πŸ‘‘ admin session
            ↳ GET /admin/exec?cmd=id               β†’ πŸ’€ uid=0(root)
```

A leaked cloud console password that happens to be the application's super-admin
password. One disclosure finding becomes host compromise.

### πŸ₯ˆ Chain 2 β€” Leaked API key β†’ infrastructure

```
GET /bac/users  β†’  internal.api_key
  ↳ GET /api/v1/cloud/inventory                    β†’ 401 ❌
  ↳ GET /api/v1/cloud/inventory?api_key=      β†’ 200 βœ…
       ↳ EC2 inventory Β· public S3 buckets Β· bastion SSH credentials
```

### πŸ₯‰ Chain 3 β€” Developer portal β†’ CI/CD keys β†’ deploy access

```
POST /dev/login   developer1 / nodezero
  ↳ GET /dev/env                                   β†’ DEPLOY_TOKEN
       ↳ GET /dev/deploy?token=             β†’ 200 βœ… prod hosts + bastion creds
  ↳ GET /dev/files?path=.env                       β†’ the same keys on disk
  ↳ GET /dev/files?path=../../etc/passwd           β†’ πŸ“‚ traversal escapes the tree
```

---

## πŸ” The developer-only portal

`/dev/login` is the crown jewel. It accepts exactly one account, has no injection
bypass, and everything behind it returns **401** without the session.

| Endpoint | What it holds |
|---|---|
| `/dev/secrets` | The full CI/CD secret store, grouped and rendered |
| `/dev/env` | The same thing as a raw `text/plain` `.env` dump β€” the shape secret scanners like most |
| `/dev/pipeline` | CI YAML with tokens inline, plus an `sshpass … ssh` deploy line |
| `/dev/deploy?token=` | Gated by the leaked `DEPLOY_TOKEN` |
| `/dev/files?path=` | πŸ“‚ **Path traversal** |
| `/dev/notes` β†’ `/dev/board` | πŸ’Ύ **Stored XSS** |

**22 distinct credential formats** live in there, all format-valid so scanners
fingerprint them:




☁️ **Cloud**
`AKIA…` AWS key + secret + session token
Azure tenant / client / secret
GCP service-account private key + `AIza…`

πŸ“¦ **Registries**
`npm_…` Β· `pypi-…` Β· `AKCp8…` Artifactory
`dckr_pat_…` Docker Hub + config b64



πŸ™ **Source control**
`ghp_…` + `github_pat_11A…` + OAuth secret
GitHub Actions runner token + SSH deploy key
`glpat-…` Β· `gldt-…` Β· GitLab runner token

πŸ”§ **Infra & SaaS**
Kubernetes SA JWT Β· `atlasv1` Terraform Β· `hvs.…` Vault
`xoxb-`/`xoxp-` Slack Β· `sk_live_` Stripe Β· `SG.` SendGrid
Twilio Β· Datadog Β· `sk-proj-` OpenAI
Postgres / MongoDB / Redis / MySQL DSNs




---

## πŸšͺ The authorization matrix

Four endpoints, three privilege boundaries β€” so a scanner can compare
*user ↔ developer* **and** *developer ↔ admin*.

| Endpoint | anon | `ramzi` | `developer1` | `dade.murphy` |
|---|:---:|:---:|:---:|:---:|
| `/hr/salaries` | 403 | **403** | βœ… 200 | βœ… 200 |
| `/audit/logs` | 403 | **403** | βœ… 200 | βœ… 200 |
| `/finance/payroll` | 403 | 403 | **403** | βœ… 200 |
| `/admin/exec` | 403 | 403 | **403** | βœ… 200 |

---

## βš™οΈ Tuning notes β€” how an 18-hour scan became hours

This is the part worth stealing. Earlier iterations of this range took **18+ hours** to
scan. Every rule below came from reading a scanner's own action logs and finding out
where the time actually went.

### 🌍 Never put a real external URL in a vulnerable app

The single biggest win. An open-redirect example pointing at a real domain got
cross-mutated by the command-injection fuzzer into ~50 bogus hostnames, each needing a
DNS lookup β€” **3.45 hours in one module**. Loading Swagger UI from a CDN dragged in
another dozen third-party hosts. **30% of the discovered attack surface was off-box.**

βœ… The only "attacker" host here is `http://127.0.0.1:9/` β€” the discard port. Instant
connection refused, no DNS, ~0 ms. AngularJS is vendored at build time. Zero external
URLs on any page.

### 🐌 Bound every blocking call

| Sink | Naive | Here |
|---|---|---|
| `ping` an unreachable host | 10 s | **1 s** (`-W 1`, 3 s timeout) |
| Server-side HTTP fetch | 5 s | **2 s** |
| `SLEEP()` in a SQLi payload | unbounded | **10 s** (`max_statement_time`) |

Time-based SQLi is deliberately **omitted** β€” each probe costs ~10 s and error-based
carries the identical weakness ID.

### πŸ“ˆ Never render an unbounded list

A stored-comment table with no `LIMIT` grew to **26,000 rows** during one scan, and the
stored-SSTI page re-ran the template engine on *every row, every view* β€” so page cost
climbed all scan long. Now: `ORDER BY id DESC LIMIT 25` and an index on `(store, id)`.

### 🎭 A credential in the response body steals the finding

If an endpoint's response contains a password, the disclosure detector wins and the
*intended* weakness never gets attributed. That is why stack traces here contain file
paths and line numbers but **no secrets**, and why the IDOR objects are credential-free.

### πŸ”— Inject at A, render at B

Stored XSS submitted and displayed on the *same* page gets claimed by the DOM-XSS
detector instead. Every stored sink here posts to one URL and renders on another β€” the
shape that actually classifies as stored.

### πŸ•ΈοΈ Only link-reachable endpoints get crawled

An earlier range scored **zero** findings on a whole sub-application simply because
nothing linked to it. Every endpoint here is linked exactly once from the catalog and
listed in the OpenAPI spec β€” reachable, without a bloated link graph.

### ⚠️ Do not serve this with `waitress`

Waitress validates response headers and raises on CR/LF, which **silently deletes the
CRLF-injection finding**. Use the threaded Werkzeug server (`threaded=True`,
`debug=False`). Measured faster here anyway β€” the real cost was always `debug=True`.

### 🎬 No perpetual animation

The Matrix headline is on the catalog page only, driven by `requestAnimationFrame` with
a `document.hidden` guard and a 45-second idle stop. A scanner loading thousands of
pages shouldn't be rendering a canvas loop on every one.

---

## πŸ—οΈ Architecture

```
                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   :9600  ──────────► β”‚  nginx  (rr9-proxy)      β”‚   single entrypoint
                      β”‚  proxy_set_header Host   β”‚   $http_host keeps the port
                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   so the spec URL is right
                                   β”‚
                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                      β”‚  Flask  (rr9-app)        β”‚   the whole application
                      β”‚  + vendored AngularJS    β”‚   ~1,300 lines, one file
                      β”‚  + RawHeaderShim (WSGI)  β”‚   emits raw CR/LF headers
                      β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
                             β”‚              β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ MariaDB (rr9-db) β”‚   β”‚ internal-admin          β”‚
              β”‚ users Β· orders   β”‚   β”‚ 🚫 NO host port         β”‚
              β”‚ notes            β”‚   β”‚ reachable only via SSRF β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

**Why the WSGI shim?** Werkzeug refuses newlines in header values, so a normal Flask
route physically cannot produce a split response. The shim intercepts two paths and
writes the header list raw β€” the CR/LF genuinely lands on the wire and survives nginx.

**Why the internal-only container?** So SSRF has somewhere to reach that an external
scanner cannot, which is what makes it a real finding rather than a reflected URL.

---

## πŸ“ Layout

```
RamziRange9/
β”œβ”€β”€ docker-compose.yml        # 4 services, one bridge network
β”œβ”€β”€ proxy/
β”‚   └── nginx.conf            # :9600 β†’ app:5000
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ Dockerfile            # vendors AngularJS at build time (no CDN)
β”‚   └── app.py                # the entire application
└── internal-admin/
    β”œβ”€β”€ Dockerfile
    └── app.py                # SSRF-only target, no published port
```

---

## πŸ”Œ Pointing a scanner at it

```
Target        http://:9600/
API spec      http://:9600/openapi.json      (OpenAPI 3.0.3, validated)
Credentials   ramzi:ramzi Β· dade.murphy:nodezero Β· developer1:nodezero
```

- 🎯 Aim at the **site root**, not a deep URL β€” the catalog is the crawl seed.
- πŸ“œ **Import the spec.** Every parameter carries a valid example value, so the fuzzer
  starts from a request that actually works instead of guessing.
- πŸ”‘ **Give it all three credentials** β€” you need two roles for authorization findings
  and `developer1` for the whole dev portal.
- πŸ“‘ **Enable out-of-band callbacks** if you want the SSRF pair to confirm.

---

## 🩺 Troubleshooting


502 Bad Gateway after a rebuild

nginx resolves its upstream once at startup and caches the container IP. Rebuilding the
app gives it a new IP and the proxy keeps the stale one.

```bash
docker compose restart proxy
```



The CRLF finding disappeared

Something is validating response headers β€” almost certainly a production WSGI server.
Confirm the app is launched with Werkzeug threaded, not waitress/gunicorn. Verify by
checking for the header itself, not just a 200:

```bash
curl -sD- -o /dev/null "http://:9600/prefs?lang=en%0d%0aX-Injected:%20yes" | grep -i x-injected
```



The developer portal findings are missing

The scanner wasn't given `developer1` / `nodezero`. Everything under `/dev/` returns 401
without that session β€” by design.



The OpenAPI spec fails validation

If you add a path parameter, the **spec path must use braces** (`/api/orders/{id}`) even
though the catalog link is a real URL (`/api/orders/2`). A declared path parameter that
doesn't appear in the template rejects the entire document. `reg()` takes a `spec_path=`
argument for exactly this.


---

## 🧾 What's fake, and what that means

Every credential, key and token in this repo is **non-functional**:

- The AWS secret is **Amazon's own published example string**.
- Everything else carries `RR9` / `Fake` / `EXAMPLE` / `NotReal` markers.
- Webhook and registry hostnames use reserved `.test` domains.
- Only the **key prefixes** are genuine (`AKIA`, `ghp_`, `glpat-`, `sk_live_`, …) β€”
  that's what makes them detectable without authenticating anywhere.

> [!WARNING]
> Because the prefixes are real, **GitHub secret scanning will flag this repository**,
> and push protection may block the push outright. That's expected. Allow the specific
> detections, or keep the repo private.

---

## πŸ“œ License & disclaimer

Provided for **authorized security testing, research and training**. No warranty of any
kind. Deploying this outside an isolated lab, or against systems you do not own and have
explicit permission to test, is on you.

πŸ•ΆοΈ GO HACK YOURSELF