Sploitus

Exploit for RamziRange10

githubexploit Β· 2026-09-14

Exploit Code

README474 lines
## https://sploitus.com/exploit?id=CDE855CC-2630-5BD9-A807-715B76B530BB
πŸ•ΆοΈ RamziRange10


  A deliberately vulnerable web application engineered so that every weakness class a scanner looks for gets its own, correctly-classified finding β€” and so the whole scan finishes in hours, not a day.



  
  
  
  


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 an entire wing 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 makes this one different

Most vulnerable-app projects give you a grab bag of bugs and call it a day. The
problem that motivates *this* repo is subtler and much more annoying:

> **A vulnerability can work perfectly and still not show up in your report.**

Its predecessor registered 28 weakness classes. A full authenticated-capable op
reported **19**. Nothing was broken β€” every one of the 28 primitives was verified
working by hand afterwards. The nine that vanished were lost to three specific,
reproducible failure modes, and this entire codebase is the fix list for them.

| | |
|---|---|
| πŸŽ–οΈ **28 weakness classes** | Injection, XSS Γ—3 flavours, template injection Γ—4, traversal, SSRF, XXE, CRLF, authz, and the whole disclosure family |
| πŸͺ“ **Split attack surface** | Credential leaks quarantined into one wing so they stop cannibalising other findings |
| 🎣 **GET-fuzzable stored sinks** | Stored XSS/SSTI/CSTI that don't depend on a specialised scanner module being scheduled |
| 🧬 **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** | 71 paths, self-describing, working example values for every parameter |
| 🐳 **4 containers, one command** | No setup, no seeding, no fixtures to load |

---

## ⚑ Deploy it

### Prerequisites

| Need | Why |
|---|---|
| 🐳 **Docker + Compose v2** | `docker compose version` should print v2.x. Compose v1 (`docker-compose`) won't parse the healthcheck conditions. |
| 🌐 **Internet for the first build only** | The images pull base layers, `pip install`, and vendor AngularJS. After that the range is fully self-contained and makes no outbound calls. |
| πŸ”Œ **Port 9700 free** | The only published port. Change the left-hand side of `"9700:9700"` in `docker-compose.yml` if it clashes. |
| 🧱 **An isolated network** | See the warning at the top. This is not optional. |

### Two commands

```bash
git clone https://github.com//.git
cd 
docker compose up -d --build --wait
```

That's it. `--wait` blocks until the healthchecks pass, so when the command
returns the range is genuinely ready. Measured cold-clone to serving: **53 seconds**.

> [!TIP]
> **If you omit `--wait`, give it ~40 seconds.** The app deliberately blocks
> until MariaDB has finished its first-boot initialisation and the schema is
> seeded, so nginx will answer **502** until then. That's expected, not a fault.

If your user isn't in the `docker` group, prefix with `sudo`.

### Confirm it's up

```bash
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:9700/     # expect 200
docker compose ps                                                    # all healthy
```

Then open **`http://:9700/`** β€” the landing page is a live catalog of every
endpoint, grouped by weakness ID, each one a working link.

### Prove it's actually vulnerable

```bash
# SQL injection - throws a real MySQL syntax error
curl -s "http://localhost:9700/sqli/query?id=1'" | grep -o 'error in your SQL'

# OS command injection - runs as root
curl -s --get --data-urlencode 'host=127.0.0.1;id' \
     http://localhost:9700/cmd/ping | grep -o 'uid=0(root)'

# SSRF into a container with no published port
curl -s --get --data-urlencode 'url=http://internal-admin:5000/status' \
     http://localhost:9700/net/fetch | grep -o 'rr10-prod-internal'

# XXE - the external entity really expands
curl -s --get --data-urlencode \
     'doc=]>&x;' \
     http://localhost:9700/xml/parse | grep -o '4f1c9ae7d0b3e28a'

# Stored XSS in two steps: plant via GET, then read it back somewhere else
curl -s -o /dev/null "http://localhost:9700/xss/store?author=t&body=alert(1)"
curl -s http://localhost:9700/xss/feed | grep -o 'alert(1)'

# CRLF injection - the injected header lands on the wire
curl -sD- -o /dev/null \
     "http://localhost:9700/prefs?lang=en%0d%0aX-Injected:%20yes" | grep -i x-injected

# The developer-only portal (nothing else gets in)
curl -si -d 'username=developer1&password=nodezero' \
     http://localhost:9700/dev/login | grep -i 'set-cookie: devsid'
```

### Shut it down

```bash
docker compose down -v      # -v also drops the database volume
```

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  rr10_   Catalog Login Secrets Config Files Vault  …    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                         β”‚
β”‚        β–“β–“  β–“β–“β–“      β–“β–“ β–“β–“  β–“β–“β–“   β–“β–“β–“β–“ β–“β–“  β–“β–“            β”‚
β”‚        β–“   β–“ β–“      β–“β–“β–“β–“β–“ β–“β–“β–“β–“β–“ β–“     β–“β–“β–“β–“              β”‚
β”‚        β–“β–“β–“ β–“β–“β–“      β–“β–“ β–“β–“ β–“β–“ β–“β–“  β–“β–“β–“β–“ β–“β–“  β–“β–“            β”‚
β”‚              G O   H A C K   Y O U R S E L F            β”‚
β”‚                                                         β”‚
β”‚   83 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.** This is not optional, and it is
> the single biggest lever on your results. On the run that motivated this rebuild,
> the op was unauthenticated, and *every* endpoint returning 401/403 was absent from
> the findings β€” a perfectly clean split. That alone cost the improper-authorization
> class and the entire developer portal.

---

## 🩻 The post-mortem this range is built from

Three failure modes, nine lost weakness IDs. Each one is worth understanding
because they generalise well beyond this app.

### 1. Classifier collision β€” cost 5 IDs

One detector, **Exposed Credentials in Web Response**, took **17 of 61 findings**
and swallowed the carrier endpoints belonging to five other weakness classes.

The cause was self-inflicted. The SSRF endpoint pointed at an internal service whose
response was a credential dump. So the SSRF *worked* β€” it reached a container with no
published port and read its contents β€” and then got filed as a credential leak,
because a credential in the body outranks everything else. Same story for the config
page. The XXE endpoints lost the same way to a different detector: they read
`/etc/passwd`, so **Internal System Data Exposure** claimed them instead.

> **The rule:** a credential anywhere in a response body wins. If a payload proves
> weakness X but the response also contains a secret, you get a finding for the
> secret and nothing for X.

**The fix β€” a split surface.** Every non-credential carrier is now credential-free:

| Carrier | Before | After |
|---|---|---|
| `/net/fetch`, `/net/preview` | fetched an internal `/creds` endpoint | fetch `/status`, `/metrics`, `/latest/meta-data/` β€” credential-free by design |
| `/config` | leaked an SSH deploy password | build SHA, upstream host map, `uid=0(root)`, feature flags |
| `/xml/parse`, `/xml/import` | entity read `/etc/passwd` | entity reads `/app/version.txt`, a benign build stamp |
| `app.log` | contained a plaintext admin password | internal topology and SQL statements only |

…and every credential in the application moved into a dedicated **`/vault/*` wing**
whose entire job is to score credential findings in volume, where winning a collision
costs nothing.

### 2. Module starvation β€” cost 3 IDs

Stored XSS, stored SSTI and stored CSTI all scored **zero**. Confirming a stored bug
requires the scanner's dedicated stored-injection module to run *and* correlate
inject-at-A with render-at-B. In an 18-hour op, that module ran for about four
seconds.

It definitely reached the endpoint, though β€” the proof was sitting in the database
afterwards, in the form of the scanner's own out-of-band callback probes rendered on
the feed page.

**The fix β€” stop depending on that module.** The submit endpoints now accept **GET as
well as POST**, which puts the sink in reach of the ordinary query-parameter fuzzer
that runs constantly. Two details make this work rather than backfire:

- The GET receipt **echoes nothing** β€” no reflection of the submitted value, no
  redirect. If it echoed the payload, the finding would be misfiled as *reflected*
  XSS and you'd be back where you started.
- The render pages are **nav items**, so they're re-crawled on every pass and any
  accumulated payload is visible in the HTML.

Every store is also pre-seeded, because an empty page gets deprioritised by crawlers.

### 3. Crawl reachability β€” cost 1 ID

The page carrying the exposed Google Maps browser key was linked *only* from a
catalog card. It never appeared in the results at all β€” not as a miss, but as a page
the crawler simply never visited. Card-only links have always been hit-or-miss.

**The fix:** anything that must be reached is in the nav. Boring, and it works.

---

## πŸŽ–οΈ Weakness coverage

Every row is verified working against a live deployment, 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` Β· `/reviews/add`β†’`/reviews` Β· `/dev/notes`β†’`/dev/board` β€” all GET **and** POST |
| 🌐 **DOM XSS** | `H3-2026-0047` | `/dom?msg=` (innerHTML) · `/dom/write?name=` · `/dom/eval?expr=` |
| 🧩 **SSTI β€” reflected** | `H3-2025-0072` | `/ssti/query?tpl=` Β· `/preview/render?tpl=` β€” `{{7*7}}` β†’ `49` |
| 🧩 **SSTI β€” stored** | `H3-2025-0078` | `/ssti/store` β†’ `/ssti/report` (evaluated per row on view) |
| πŸ–₯️ **CSTI β€” reflected** | `H3-2026-0029` | `/csti/query?bio=` Β· `/profile/preview?bio=` (AngularJS, vendored locally) |
| πŸ–₯️ **CSTI β€” stored** | `H3-2026-0030` | `/csti/store` β†’ `/csti/profile` |
| πŸ’₯ **OS Command Injection** | `H3-2025-0077` | `/cmd/ping?host=` Β· `/logs/view?log=` (cat sink) Β· `/admin/exec?cmd=` (root, admin session) |
| πŸ“‚ **Path Traversal** | `H3-2022-0015` | `/files/read?file=` Β· `/download?doc=` Β· `/logs/view?log=` Β· `/dev/files?path=` |
| πŸ›°οΈ **SSRF** | `H3-2025-0076` | `/net/fetch?url=` β†’ internal-only container `/status`, `/latest/meta-data/` |
| πŸ›°οΈ **SSRF β€” full read** | `H3-2026-0024` | `/net/preview?target=` β†’ internal `/metrics`, `/status` |
| πŸ“„ **XXE** | `H3-2025-0050` | `/xml/parse?doc=` Β· `/xml/import?feed=` Β· parameter-entity variant |
| β†ͺ️ **Open Redirect** | `H3-2025-0079` | `/go?url=` Β· `/out?next=` |
| β†ͺ️ **Redirect via header injection** | `H3-2026-0027` | `/go/hdr?url=` Β· `/out/hdr?next=` (raw `Location`, bypasses the framework) |
| βœ‚οΈ **CRLF / Response Splitting** | `H3-2026-0026` | `/prefs?lang=` Β· `/track?ref=` |
| πŸšͺ **Improper Authorization** | `H3-2026-0061` | `/hr/salaries` Β· `/audit/logs` (developer+) Β· `/finance/payroll` (admin only) Β· `/dev/login` |
| πŸ”“ **IDOR / BOLA** | `H3-2026-0013` | `/api/orders/{id}` Β· `/api/invoice?id=` |
| πŸ—οΈ **Exposed Credentials** | `H3-2026-0033` | `/vault/dump` Β· `/vault/env` Β· `/vault/ssh` Β· `/vault/backup.sql` Β· `/bac/users` |
| πŸ”‘ **Exposed API Keys** | `H3-2026-0034` | `/api/keys` (4 key formats) Β· `/vault/kube` Β· `/dev/env` Β· `/dev/pipeline` |
| πŸ—ΊοΈ **Exposed browser key** | `H3-2026-0037` | `/contact` Β· `/support` (data attribute + inline config + comment) |
| πŸ“‹ **Sensitive Info Disclosure** | `H3-2025-0080` | `/config` Β· `/status/internal` |
| πŸ‘€ **PII Disclosure** | `H3-2026-0035` | `/api/customers` Β· `/api/employees` |
| 🏒 **Internal System Data** | `H3-2026-0036` | `/logs/view?log=app.log` |
| 🧯 **Stack Trace Disclosure** | `H3-2026-0038` | `/debug/lookup?key=zz` · `/export?rows=abc` |
| πŸ“ **Directory Listing** | `H3-2026-0039` | `/files/` Β· `/files/logs/` Β· `/files/backups/` |
| πŸ“— **Swagger Spec Exposed** | `H3-2026-0051` | `/openapi.json` Β· `/swagger.json` |
| 🧾 **.env Exposure** | `H3-2025-0032` | `/.env` |

**2–3 carriers per class.** One is too few β€” a single missed endpoint costs the whole
weakness class. Many more is worse: it inflates the fuzz surface without adding a
single distinct ID, which is exactly how an earlier range in this series took 18 hours.

---

## πŸ” The developer portal

A second, independent authentication boundary at `/dev/login`. Its check is
hardcoded and accepts exactly one account. The **super admin cannot get in** β€”
that asymmetry is itself the finding.

Behind it:

| Endpoint | What it gives up |
|---|---|
| `/dev/secrets` | The full CI/CD secret store, grouped by provider |
| `/dev/env` | The same store as a raw `.env` (`text/plain` β€” the shape secret scanners like most) |
| `/dev/pipeline` | A CI pipeline YAML with tokens inline |
| `/dev/files?path=` | A source browser with path traversal |
| `/dev/notes` β†’ `/dev/board` | Stored XSS inside the authenticated area |
| `/dev/deploy?token=` | A deploy endpoint gated by a token found in the store |

AWS, GitHub (classic + fine-grained + SSH deploy key), GitLab, Docker Hub,
Kubernetes, Terraform, Vault, npm, PyPI, Slack, Stripe, SendGrid, Twilio, Datadog,
OpenAI, Azure and GCP. All fake β€” see [SECURITY.md](SECURITY.md).

---

## πŸšͺ The authorization matrix

Three tiers, verified live. The *difference* between rows is the finding, which is
why the op needs more than one credential:

| | `/hr/salaries` | `/audit/logs` | `/finance/payroll` | `/dev/*` |
|---|:---:|:---:|:---:|:---:|
| unauthenticated | 403 | 403 | 403 | 401 |
| `ramzi` (user) | 403 | 403 | 403 | 401 |
| `developer1` (developer) | **200** | **200** | 403 | **200** |
| `dade.murphy` (admin) | **200** | **200** | **200** | 401 |

---

## 🧬 Attack chains

```
  CHAIN 1 β€” disclosure becomes access
  /bac/users ──leaks──> internal_api_key ──unlocks──> /api/v1/cloud/inventory
      └─> cloud inventory leaks a bastion credential and an admin IAM user

  CHAIN 2 β€” credential harvest becomes root
  /vault/dump ──leaks──> dade.murphy / nodezero ──login──> admin session
      └─> /admin/exec?cmd=id ──> uid=0(root) ──> host compromise

  CHAIN 3 β€” the developer track
  /dev/login (developer1 only) ──> /dev/secrets ──> CI/CD secret store
      └─> /dev/deploy?token=… ──> infra access
      └─> /dev/files?path=../../etc/passwd ──> traversal inside the authed area
```

---

## βš™οΈ Tuning notes β€” how to keep the scan short

Accumulated the hard way, mostly by watching an 18-hour op and reading its
action logs afterwards.

- **Zero external URLs.** An earlier range used a real hostname as an example
  parameter value. The fuzzer cross-mutated it into ~50 bogus hostnames and the
  open-redirect module alone burned **3.45 hours** at 40s per DNS timeout. The only
  "attacker" host here is `127.0.0.1:9` β€” the discard port, which refuses instantly
  with no lookup.
- **AngularJS is vendored at build time.** Pulling it from a CDN sends the crawler
  off-box and drags in a dozen third-party hosts.
- **Bound every blocking call.** `ping -W 1`, HTTP fetch timeout 2s,
  `SET SESSION max_statement_time=10`.
- **No time-based SQL injection.** Each probe costs ~10 seconds and carries the same
  weakness ID as the error-based one, which is free.
- **`LIMIT 25` on stored renders, plus an index on `(store, id)`.** Without it, a
  notes table grew to 26,000 rows mid-scan and the SSTI report re-rendered every row
  on every view β€” page cost climbed all scan long.
- **The theme is pure CSS.** A perpetual canvas animation costs real CPU on every one
  of thousands of page loads.
- **Werkzeug, not waitress.** Waitress validates header values and raises on CR/LF,
  which silently deletes the CRLF-injection finding. Werkzeug passes raw headers
  through a small WSGI shim. It also measured faster.

---

## πŸ—οΈ Architecture

```
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   you ───── :9700 ────▢│  rr10-proxy   (nginx 1.27)   β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                       β”‚ proxy_pass, Host: $http_host
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚  rr10-app     (Flask, root)  β”‚
                        β”‚  83 endpoints Β· 28 IDs       β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚              β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚ rr10-db          β”‚    β”‚ rr10-internal-admin         β”‚
                β”‚ MariaDB 10.5     β”‚    β”‚ NO published port           β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚ the SSRF payoff             β”‚
                                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

`internal-admin` has no host port at all. It is reachable *only* from inside the
Docker network, which is what makes `/net/fetch` a genuine SSRF finding rather than
a reflected URL. Its `/status`, `/metrics` and `/latest/meta-data/` endpoints are
deliberately credential-free so the SSRF finding doesn't get outranked.

> [!NOTE]
> `proxy_set_header Host $http_host` β€” **not** `$host`. `$host` drops the port, which
> makes the OpenAPI `servers[].url` resolve to port 80 and quietly breaks spec-driven
> scanning.

---

## πŸ“ Layout

```
.
β”œβ”€β”€ docker-compose.yml        healthchecks + depends_on so --wait is deterministic
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ app.py                the whole application, one file
β”‚   └── Dockerfile            vendors AngularJS at build time
β”œβ”€β”€ internal-admin/
β”‚   β”œβ”€β”€ app.py                internal-only SSRF target, credential-free
β”‚   └── Dockerfile
β”œβ”€β”€ proxy/
β”‚   └── nginx.conf            single entrypoint on :9700
β”œβ”€β”€ README.md
β”œβ”€β”€ SECURITY.md               why secret scanners will flag this repo
└── LICENSE
```

---

## πŸ”Œ Pointing a scanner at it

1. Scope: `http://:9700`
2. Load **all three** credentials (see the ⚠️ note above β€” this is the biggest lever).
3. Feed it the spec: `http://:9700/openapi.json` β€” 71 paths, valid 3.0.3.
4. Enable out-of-band / callback confirmation if your tooling supports it.
5. Expect a couple of hours, not a day.

---


🩺 Troubleshooting

**`502 Bad Gateway` right after starting.** The app blocks until MariaDB finishes
first-boot init. Use `--wait`, or wait ~40 seconds.

**`docker compose up --wait` exits 1.** Check `docker compose ps` for which service
is unhealthy. Note the proxy deliberately has *no* healthcheck β€” it raced its own
retry budget and reported a false `unhealthy`, and `depends_on: service_healthy` on
the app already sequences it correctly.

**Findings appear under the wrong weakness ID.** That's the whole subject of the
post-mortem section above. If you add an endpoint, keep credentials out of its
response body unless you *want* a credential finding.

**Stored payloads don't show up.** Check the render page, not the submit page β€”
they're deliberately different URLs. The submit receipt echoes nothing on purpose.

**A rebuilt backend 404s through the proxy.** nginx resolves the upstream hostname
once at startup, so after recreating the app container run
`docker compose restart proxy`.

**Port clash.** Change both sides of `"9700:9700"` in `docker-compose.yml` and the
`listen` directive in `proxy/nginx.conf`.



---

## 🧾 What's fake

Everything sensitive. No key in this repository authenticates anywhere, no hostname
resolves to infrastructure anyone owns, and no account exists on any real machine.
Only the key *prefixes* are genuine, so that credential scanners fingerprint them β€”
which is the entire point of a credential-exposure test target.

Read [SECURITY.md](SECURITY.md) before you file an issue about a leaked secret.

---

## πŸ“œ License

MIT β€” see [LICENSE](LICENSE). Use it to test things you are authorized to test.