One-Time Payload Tokens#

Payload tokens prevent replayed payload URLs. Each beacon IP is issued a single-use token when it is first promoted to the dynamic whitelist. The beacon must present this token to download any route with require_token: true. Token consumption is atomic — a second use of the same token fails.

How It Works#

1. Beacon completes N valid C2 checkins (N = dynamic_whitelist_threshold)
2. IP promoted to dynamic whitelist
3. InfraGuard issues token, returns in X-Payload-Token response header
4. Beacon stores token, presents in X-DL-Token header (or ?_t= query param) on payload download
5. InfraGuard validates: token exists + not expired + used_count < max_uses
6. Atomic UPDATE increments used_count; rowcount 0 = already consumed → 403

An analyst who captures a payload URL cannot use it — the token was already consumed by the beacon.

Config#

payload_tokens:
  enabled: true
  default_ttl_seconds: 3600       # token expires after 1 hour
  default_max_uses: 1             # single-use (set higher for multi-stage flows)
  token_header: "X-DL-Token"      # header beacon sends token in
  token_param: "_t"               # query param alternative: ?_t=<token>
  issuance_header: "X-Payload-Token"  # response header token is issued in

Enable token requirement on specific routes:

content_routes:
  - path: "/jquery-3.7.1.min.js"
    backend: ...
    require_token: true

Token Issuance Flow#

Token issuance is automatic — no operator action required. When payload_tokens.enabled: true and a beacon IP is dynamically whitelisted:

  1. Token generated (random 32-byte hex)
  2. Stored in payload_tokens SQLite table with issued_at, expires_at, max_uses=1
  3. Returned in X-Payload-Token header of the C2 checkin response that triggered whitelisting

The beacon must extract the header value from the response and cache it for later use on payload download requests.

Token Validation#

On a request to a route with require_token: true:

UPDATE payload_tokens
SET used_count = used_count + 1
WHERE token = ?
  AND used_count < max_uses
  AND expires_at > unixepoch()

If rowcount == 0: token is invalid, expired, or already consumed → 403.

This single-statement atomic update eliminates race conditions — no read-then-write.

Multi-Stage Payloads#

For implants that download multiple stages, increase default_max_uses:

payload_tokens:
  default_max_uses: 3    # allow 3 downloads per token (stage1 + stage2 + config)

Or set per-route TTL and max_uses in a future per-route token config block.

SQLite Schema#

CREATE TABLE payload_tokens (
    token       TEXT PRIMARY KEY,
    beacon_ip   TEXT NOT NULL,
    route_path  TEXT NOT NULL,
    issued_at   INTEGER NOT NULL,
    expires_at  INTEGER NOT NULL,
    max_uses    INTEGER NOT NULL DEFAULT 1,
    used_count  INTEGER NOT NULL DEFAULT 0
);

Debugging#

View issued tokens via the management API:

curl http://127.0.0.1:8080/api/tokens

Or query SQLite directly:

sqlite3 /data/infraguard.db \
  "SELECT token, beacon_ip, used_count, datetime(expires_at,'unixepoch') FROM payload_tokens;"