A Burp Suite extension (Montoya API) that adds an explicit-proxy TCP/UDP relay with TLS MITM, full intercept-and-edit, match-and-replace, and a pluggable dissector layer — the non-HTTP proxy capability Burp itself does not provide.
Burp's proxy is scoped to HTTP(S)/WebSocket. This embeds its own Netty-based proxy engine and surfaces it through a Burp suite tab, so you can intercept and rewrite arbitrary TCP and UDP traffic from inside Burp.
Proxy engine
- TCP, UDP, and SOCKS5 listeners — explicit-proxy (bind → fixed upstream), or a SOCKS5 listener that relays to each client-negotiated destination.
- Upstream proxy chaining — route a listener's upstream leg through an outbound SOCKS5 proxy.
- TLS MITM & STARTTLS — terminate client TLS with per-host leaf certs, or detect and upgrade a plaintext→TLS negotiation (SMTP / IMAP / POP3 / PostgreSQL). Client-certificate (mutual-TLS) support for upstreams that require one.
- Intercept and edit — hold, edit (hex / string / decoded view), forward, or drop live.
- Match and replace — literal-bytes / literal-string / regex rules, applied to the raw bytes or to a dissector's decoded view (re-encoded with length/framing fixed).
Protocol dissectors — pluggable SPI, auto-selected per message: hex/string, line/text, Redis (RESP), Protobuf, HTTP/2 (gRPC frame-level), STUN/TURN (RFC 5389), QUIC (Initial/Handshake SNI extraction), DNS, MQTT, PostgreSQL, MySQL, MongoDB, WebSocket, Modbus/TCP, DNP3.
Workflow
- History — connection/message log with connection replay, flow tagging, and JSON session save / restore / export.
- Fuzzer — replay a captured payload with mutations over TCP/UDP.
- Control API — a loopback JSON REST API (
:18112) to drive listeners, history, the Repeater, intercept, and match-and-replace from automation (see below); exposed in Praetor as thetcp_proxy_*MCP tools.
Requires a JDK 17 or newer on PATH (JDK 21 is fine — the build targets JVM 17
bytecode regardless, so it loads in Burp's bundled JRE). No separate Gradle install is
needed; the wrapper fetches everything.
# macOS / Linux
./gradlew shadowJar
# Windows (PowerShell / cmd)
gradlew.bat shadowJarProduces a self-contained extension at build/libs/burp-expedition-0.1.0.jar. Bundled
dependencies (Netty, Bouncy Castle) are relocated under expedition.shaded.*; the Montoya
API is compileOnly and supplied by Burp at runtime. Pure-JVM (Nio transport), so the same
jar runs on macOS, Linux, and Windows.
- Build the jar (above), or copy a prebuilt
burp-expedition-0.1.0.jar. - In Burp: Extensions → Add → Extension type: Java → select the jar.
- The extension loads as "Burp Expedition v0.1.0" and adds an Expedition suite tab with four sub-tabs: Listeners, Intercept, History, Match & Replace. TLS works out of the box via an auto-generated CA (see below).
To update: rebuild, then in Extensions untick and re-tick the extension (or remove and re-add) to reload the new jar.
By default it just works: on first load the extension generates a unique CA under
~/.expedition/ (EXPEDITION_CA_DIR to relocate, EXPEDITION_AUTOCA_DISABLE=1 to turn
off) and uses it for TLS MITM / STARTTLS. Install ~/.expedition/expedition-ca.crt in
whatever client you're testing so it trusts the minted leaf certs. The CA private key
never leaves that folder and is never committed.
To reuse Burp's own CA instead (so devices already trusting Burp need no new cert) — the Montoya API can't read Burp's CA key, so export it once:
- Burp → Proxy → Proxy settings → Import / export CA certificate → export as PKCS#12 (certificate and private key).
- In the Listeners tab, Configure CA Keystore…, select that
.p12and enter its password (this replaces the auto-generated CA). - Add a listener with TLS mode MITM. Clients that already trust Burp's CA need no new certificate.
The extension serves a small JSON REST API on 127.0.0.1:18112 so an external
client — the Praetor MCP server — can drive
the proxy programmatically: manage listeners, read captured connections/messages
as evidence, run the Repeater, and manage match-and-replace rules.
- Security: loopback-only by default; every request's
Hostheader is checked against literal loopback (or the configured bind host) — DNS-rebinding safe, never resolves a hostname. The API can start proxies and send arbitrary bytes, so it must never be exposed to an untrusted network. - Env:
EXPEDITION_API_PORT(default18112);EXPEDITION_API_DISABLE=1to turn it off;EXPEDITION_API_HOSTto bind a non-loopback interface only when the client is off-host (NAT-mode WSL reaching Burp on the Windows IP) — set it to the reachable IP, not0.0.0.0; a warning is logged when bound beyond loopback.
| Method + path | Purpose |
|---|---|
GET /status |
version, running listeners, connection/message counts |
GET /listeners · POST /listeners · DELETE /listeners/{name} |
list / add+start / stop a listener¹ |
GET /connections |
captured connection summaries |
GET /connections/{id}/messages · GET /messages?limit=N |
captured messages (hex + ASCII preview) |
POST /repeat |
Repeater — send a raw TCP/UDP payload, read the response |
POST /fuzz |
Mutation fuzzer — send N mutated payloads, collect responses + anomalies |
GET /matchreplace · POST /matchreplace · DELETE /matchreplace/{id} |
list / add / remove rules |
GET /intercept · POST /intercept/enable |
held-message queue + toggle live intercept |
POST /intercept/{id}/forward · POST /intercept/{id}/drop |
forward (edited or unchanged) / drop a held message |
Example:
curl -s 127.0.0.1:18112/status
curl -s -XPOST 127.0.0.1:18112/listeners \
-d '{"name":"redis","protocol":"tcp","bind_port":16379,"upstream_host":"10.0.0.5","upstream_port":6379}'
curl -s -XPOST 127.0.0.1:18112/repeat \
-d '{"protocol":"tcp","host":"10.0.0.5","port":6379,"text":"PING\r\n"}'¹ POST /listeners also accepts "protocol":"socks5", "tls":"mitm"|"starttls",
"upstream_proxy":"host:port", and "client_cert":"/path.p12" + "client_cert_password".
In Praetor these are the tcp_proxy_*, tcp_repeat, and tcp_match_replace_* MCP tools.
Kotlin (JVM 17), Gradle Kotlin DSL, Netty, Bouncy Castle, Montoya API,
JUnit 5. Exact versions are in build.gradle.kts.
Explicit-proxy only — clients are pointed at a listener directly or routed via SOCKS5; no transparent/OS-level redirection. UDP listeners are plaintext (no TLS/STARTTLS). The dissectors decode the common cases per protocol; anything beyond their documented editable fields falls back to hex. Pure-JVM (Nio transport only), so it runs the same on Linux, macOS, and Windows.
Your network has a TLS-intercepting proxy (common on corporate/VPN networks): it re-signs HTTPS with a company root CA that the JDK's truststore doesn't trust, so Gradle can't download the Kotlin/Shadow plugins. The error looks like:
Could not download kotlin-compiler-embeddable-...jar
Got SSL handshake exception ... PKIX path building failed:
unable to find valid certification path to requested target
Fix: import your corporate root CA into the JDK truststore (it's not about this project — it affects any Gradle/Java build on that network). First confirm which JDK Gradle uses:
./gradlew --version # note the "JVM: ..." lineGet the corporate root CA — export it from your OS trust store, or:
openssl s_client -showcerts -connect plugins.gradle.org:443 </dev/null 2>/dev/null \
| awk '/BEGIN CERT/,/END CERT/' > corp-root.pem # last block = the proxy's root CAImport it into that JDK (-cacerts targets the running keytool's JDK — export
JAVA_HOME to the one from --version first; needs admin/sudo):
# macOS
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
sudo keytool -importcert -trustcacerts -cacerts -storepass changeit -alias corp-root -noprompt -file corp-root.pem
# Linux
sudo keytool -importcert -trustcacerts -cacerts -storepass changeit -alias corp-root -noprompt -file corp-root.pem
# Windows (admin PowerShell) — adjust to your JDK path
& "$env:JAVA_HOME\bin\keytool.exe" -importcert -trustcacerts -cacerts -storepass changeit -alias corp-root -noprompt -file corp-root.pemThen rebuild. macOS shortcut: since the CA is already in your keychain, you can instead add
systemProp.javax.net.ssl.trustStoreType=KeychainStore to ~/.gradle/gradle.properties.
If your network also requires an explicit proxy, set systemProp.https.proxyHost /
systemProp.https.proxyPort there too. Never disable TLS verification to work around this.
If the machine is too locked down to fetch dependencies at all, build on an unrestricted
machine and copy the self-contained build/libs/burp-expedition-0.1.0.jar over.
An older build pinned a JDK 17 toolchain. The current build compiles with whatever JDK runs Gradle (≥17) and targets JVM 17, so just use a JDK 17+ — no separate JDK 17 install needed.