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 → 403An 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 inEnable token requirement on specific routes:
content_routes:
- path: "/jquery-3.7.1.min.js"
backend: ...
require_token: trueToken Issuance Flow#
Token issuance is automatic — no operator action required. When payload_tokens.enabled: true and a beacon IP is dynamically whitelisted:
- Token generated (random 32-byte hex)
- Stored in
payload_tokensSQLite table with issued_at, expires_at, max_uses=1 - Returned in
X-Payload-Tokenheader 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/tokensOr query SQLite directly:
sqlite3 /data/infraguard.db \
"SELECT token, beacon_ip, used_count, datetime(expires_at,'unixepoch') FROM payload_tokens;"