Share
## https://sploitus.com/exploit?id=F270C3C0-56DB-5475-95EF-8F99331BB1C7
# http-bait

An HTTP honeypot that measures **exploitation**, not discovery.

Most honeypot writeups tell you who connected. This one is built to tell you who *used*
what they found. It serves plausible leaked secrets (`.env`, `.git/config`,
`.aws/credentials`, โ€ฆ), admin login panels, and fake vulnerable-version banners to the
mass scanners that reach any new public IP within hours โ€” and every "secret" it serves is
a unique, attributable honeytoken. When one of those values comes back in a later request,
that is proof a specific scrape led to a specific reuse, not an inference from timing.

## What it found

Numbers below cover a 12.66-day window (2026-07-10 to 2026-07-22): 3,820 requests from
159 distinct IPs, 810 honeytokens issued.

- **Real exploitation, fast.** Genuine exploit attempts for Log4Shell (CVE-2021-44228,
  a JNDI callback sprayed into headers), Spring4Shell (CVE-2022-22965, a POST trying to
  plant a JSP webshell via `Runtime.exec()`), React2Shell (CVE-2025-55182), PHPUnit
  (CVE-2017-9841), and WP-Automatic SQLi (CVE-2024-27956) โ€” 41 hits across 5 CVEs by
  retroactive scan, from 24 IPs.
- **One confirmed honeytoken replay.** An IP scraped a fake GitHub PAT from
  `/.git/config` and replayed it as a live `Authorization: Bearer` header against a batch
  of API-secret probe paths roughly two minutes later.
- **Nothing touched the credential-stuffing bait.** No `/admin/login` password spraying
  in the whole window, despite the panel being served and indexed.

Detail, caveats, and the numbers you should *not* quote without reading the caveats are in
[FINDINGS.md](FINDINGS.md). Two matter most: the live and retroactive CVE counts differ
(17 vs 41) because two detector signatures shipped *after* the traffic they match arrived,
and both are lower bounds because request bodies are capped.

## Run it

```bash
docker compose up -d --build
curl -s http://localhost:8100/.env          # a lure, via the app directly
python analyze.py data/logs/http_events.jsonl
```

Tests:

```bash
docker run --rm -v "$PWD:/src" -w /src python:3.12-slim sh -c "pip install -q -r app/requirements.txt pytest && cd app && HB_LOG_DIR=/tmp/l python -m pytest -q"
```

To put it on the internet โ€” which is the only way it collects anything interesting โ€” see
[DEPLOY.md](DEPLOY.md). You need a host with Docker, a DNS name pointing at it, and ports
80/443 free. A $4โ€“6/month VPS is enough; the service is idle-cheap and there are no API
costs unless you opt into live canary tokens (below).

## Configuration

| Variable | Default | Effect |
|---|---|---|
| `HB_LOG_DIR` | `/data/logs` | Where `http_events.jsonl` is written. |
| `HB_CANARY_BASE` | `https://http-bait.example` | Public base URL embedded in canary URLs. Must be the hostname you actually serve, or callbacks won't reach you. |
| `HB_LOG_MAX_BYTES` | `209715200` (200MB) | Rotate the live log at this size. |
| `HB_LOG_KEEP_ROTATED` | `10` | Rotated files to keep. `0` keeps everything โ€” only safe if something else prunes, or the disk will fill and collection will stop. |
| `HB_HEALTH_MIN_FREE_BYTES` | `536870912` (512MB) | Below this free space, `/healthz` reports unhealthy. |
| `HB_CANARYTOKENS_LIVE` | unset (off) | Opt in to minting **real** AWS canary keys via canarytokens.org. See below. |
| `HB_CANARYTOKENS_REFRESH_SECS` | `86400` | Max age of a minted live key before re-minting. |
| `HB_CANARYTOKENS_MAX_SERVINGS` | `30` | Max times one live key is served before re-minting. |
| `HB_CANARYTOKENS_RETRY_COOLDOWN_SECS` | `300` | Floor between mint attempts against canarytokens.org. |

## The one thing that isn't fake

Everything served is synthetic by default. With `HB_CANARYTOKENS_LIVE=1`, the AWS
credential lure is backed by a **real, functioning** AWS canary key minted through
canarytokens.org's public API, so a genuine third-party use fires their alerting as well
as ours. The key grants no permissions โ€” canarytokens.org's AWS canaries exist to trigger
detection, not access.

If you enable it, leave the three caps alone. `MAX_SERVINGS` is not a tuning knob: it
bounds how many visitors could have been responsible for any single real-world trigger,
which is what makes an alert attributable at all. `RETRY_COOLDOWN_SECS` bounds the request
volume this can generate against canarytokens.org, a free service run by someone else.

## Ethics and scope

Read [SPEC.md ยง2](SPEC.md) before deploying. The short version, all of it load-bearing:

- **Nothing executes.** No route runs a command, queries a database, or makes an outbound
  request on a caller's behalf. No SSRF amplification, ever.
- **Not a DoS amplifier.** Per-IP rate limiting, hard body caps, no reflection.
- **Don't entrap.** It offers plausible exposed files and observes. It never solicits
  illegal action or walks a visitor toward one.
- **Visitor credentials are never retained.** Anything submitted to a login lure is
  recorded as presence and shape only โ€” credentials sprayed at honeypots are frequently
  real credentials stolen from someone else.
- **Coordinate disclosure.** If you catch a *named* vendor or tool doing something
  disclosure-worthy, tell them before you publish specifics.

Running this points a deliberately attackable surface at the internet under your name.
Give it its own host and a hostname with no association to anything else you run.

## Layout

```
app/main.py            FastAPI honeypot: routes, telemetry middleware, honeytoken minting
app/formatters.py      Per-kind fake-secret generators
app/test_main.py       Behaviour tests (redaction, caps, non-retention)
app/test_regressions.py Regression tests for previously-broken detectors
analyze.py             Offline JSONL analyzer (stdlib only)
Caddyfile              TLS reverse proxy with JA4/JA4H fingerprinting
docker-compose.yml     caddy + app
deploy.sh              Push to a host and bring the stack up
pull-telemetry.sh      Fetch logs back for offline analysis
SPEC.md                Design spec: what each piece is for, which constraints are load-bearing
AGENTS.md              Machine-readable setup, commands, and hard constraints
```

## License

MIT. See [LICENSE](LICENSE).