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