CitrixPot: NetScaler / Citrix ADC Gateway honeypot (initial public release)
This commit is contained in:
commit
4491b42714
54 files changed
+4532
No files matched your search
+122
@@ -0,0 +1,122 @@
|
||||
# Security model and threat model
|
||||
|
||||
This is a deliberately attacked system. Everything here is designed so that an
|
||||
attacker who "exploits" the decoy can **upload files, run commands and receive
|
||||
plausible output without doing anything real** to the collector or the host.
|
||||
|
||||
## Two execution modes
|
||||
|
||||
| Mode | Default | What happens to attacker commands |
|
||||
|------|---------|-----------------------------------|
|
||||
| `simulation` | yes | Parsed, logged and answered with canned output. **Never executed. No shell, no syscalls, no sockets.** |
|
||||
| `disposable_vm` | no | Real `bash` runs inside disposable VM223. Requires both `--vm-config` and `--allow-real-execution`. |
|
||||
|
||||
The simulator is pure Python dict manipulation (`simulate()`); it has no path to
|
||||
`os.system`, `subprocess`, `eval` or a real filesystem path. Uploaded bodies are
|
||||
stored as inert `0600` content-addressed blobs. This is the safe mode for an
|
||||
internet-facing listener and is what `deploy/decoy.service` runs.
|
||||
|
||||
The VM mode exists for private labs that want to observe real payload behaviour.
|
||||
It must **not** be enabled on a public listener. The two-flag gate above makes
|
||||
accidental activation impossible, and the process logs a loud warning plus
|
||||
`real_execution: true` in the start event.
|
||||
|
||||
## Isolation of the disposable execution guest (VM223)
|
||||
|
||||
- QEMU user networking with `restrict=on`; **no bridge NIC**, so the guest
|
||||
cannot reach the LAN, the management network or the internet directly.
|
||||
- **No host mounts, no shared folders, no 9p, no virtiofs, no SPICE.** The only
|
||||
block device is the guest's own discardable disk.
|
||||
- Guest TCP is redirected (iptables `REDIRECT`) to a local relay that speaks only
|
||||
to the outside recording broker. A separate guestfwd path reaches the broker;
|
||||
the broker is the sole policy point for every outbound connection.
|
||||
- The broker allows public IPv4 `80/443` unconditionally and other ports only to
|
||||
recently observed attacker peers. Private, loopback, metadata and LAN
|
||||
destinations are denied. DNS returns public A records only.
|
||||
- The guest is reset every hour: disk is archived to a validated qcow2 and
|
||||
rolled back to `decoy-clean`; unflushed RAM is discarded.
|
||||
- A host watchdog stops VM223 on pcap quota, disk/thin-pool reserve, collector
|
||||
service failure or a STOP latch. STOP is never auto-cleared.
|
||||
- Collector→guest SSH uses a dedicated key whose `authorized_keys` entry is
|
||||
restricted with `no-agent-forwarding,no-port-forwarding,no-X11-forwarding`.
|
||||
- Packet capture lives on the host (`execution.pcap`, 512-byte prefixes), so it
|
||||
survives guest tampering.
|
||||
|
||||
Residual risk in VM mode is a QEMU/KVM or guest-kernel escape. Keep the
|
||||
hypervisor patched, never enable VM mode on a public listener, and treat the
|
||||
guest disk and pcap as untrusted binary evidence.
|
||||
|
||||
## Collector isolation
|
||||
|
||||
`decoy.service` runs under systemd with `ProtectSystem=strict`,
|
||||
`NoNewPrivileges`, `PrivateTmp`, `RestrictAddressFamilies`, `MemoryMax` and
|
||||
`TasksMax`, writing only under `/var/lib/decoy` (root-only, `0700`). The ingress
|
||||
proxy runs as a `DynamicUser` with no capabilities. The local viewer is
|
||||
loopback-only and read-only.
|
||||
|
||||
## Real client IP behind the edge
|
||||
|
||||
The decoy never trusts attacker-supplied `X-Forwarded-For`. Forwarded client
|
||||
headers (`CF-Connecting-IP`, `True-Client-IP`, `X-Real-IP`, then the nearest
|
||||
`X-Forwarded-For` hop) are honoured **only** when the connecting hop is inside a
|
||||
configured trusted network (`--trusted-forwarded-from`, e.g. Cloudflare's
|
||||
published ranges plus loopback). The real client IP is recorded as `peer`; the
|
||||
proxy hop is retained separately as `proxy_peer`. Spoofed private/reserved
|
||||
addresses are rejected.
|
||||
|
||||
## Logging guarantee (capture everything)
|
||||
|
||||
Every request is journaled in full and its bytes are stored, so nothing depends on
|
||||
the decoy recognising it:
|
||||
|
||||
- `request` records method, raw URI and every raw header (including `User-Agent`).
|
||||
- `request_body` stores the exact raw body bytes as a content-addressed artifact for
|
||||
every request; multipart parts become `upload` events, other bodies
|
||||
`upload_or_raw_payload`, form posts `form_fields`. Filenames are metadata only.
|
||||
- `response` / `response_sent` store the bytes returned.
|
||||
- `request_end` stores the exact HTTP bytes the parser consumed, even for malformed,
|
||||
partial or rejected requests.
|
||||
- Log-poisoning candidates become `injection_candidate`; each extracted command is
|
||||
then `simulated_command`/`unsupported` (with argv, cwd and output) or, in VM mode,
|
||||
`execution_*` plus `guest_stdout`/`guest_syscalls_stderr`/`guest_file_*`.
|
||||
- Known Citrix exploit indicators are additionally labelled `exploit_attempt`
|
||||
(`CVE-2019-19781`, `path-traversal`, `config-read`, `management-probe`). This is a
|
||||
convenience label on top of the full capture, not a filter — unflagged requests
|
||||
are still stored.
|
||||
|
||||
Uploaded payloads are inert evidence on the collector and are never executed there.
|
||||
|
||||
## Evidence
|
||||
|
||||
The append-only JSONL journal and content-addressed blobs are `0600` under a
|
||||
`0700` directory. The service fails closed and exits non-zero when the evidence
|
||||
quota or disk fails, rather than dropping or overwriting records. Uploaded files
|
||||
are never executed by the collector and are never copied into a guest.
|
||||
|
||||
## Reporting boundary
|
||||
|
||||
No reporting UI is served on the attacker listener. The evidence viewer is a
|
||||
separate loopback-only process reached over SSH port-forwarding. Do not add
|
||||
reporting routes to the decoy listener.
|
||||
|
||||
## Public exposure checklist
|
||||
|
||||
1. Keep the listener in `simulation` mode.
|
||||
2. Put an edge proxy (Cloudflare) in front; pass the original client IP via
|
||||
`CF-Connecting-IP` and configure `--trusted-forwarded-from` with the edge
|
||||
ranges (`deploy/cloudflare-ips.txt`).
|
||||
3. Origin must only accept traffic from the edge; do not expose the origin
|
||||
directly.
|
||||
4. Terminate TLS at the edge so the decoy sees HTTP and can read headers.
|
||||
5. Keep the STOP latch and watchdog enabled; archive evidence before clearing it.
|
||||
|
||||
## Audit notes (2026-09-28)
|
||||
|
||||
- Verified the simulator contains no command-execution path.
|
||||
- Found the deployed collector running `--vm-config` (real execution). Hardened:
|
||||
`--vm-config` now also requires `--allow-real-execution`; the deployed unit was
|
||||
switched to simulation-only; a loud startup warning is emitted in VM mode.
|
||||
- Verified VM223 has no host mounts, no bridge, `restrict=on`, and a locked-down
|
||||
management key.
|
||||
- Added trusted-edge client-IP extraction with spoof rejection.
|
||||
- Added the loopback-only read-only viewer and its hardened systemd unit.
|
||||
Reference in new issue
Block a user