diff --git a/documentation/modules/auxiliary/scanner/msf/handler_detect.md b/documentation/modules/auxiliary/scanner/msf/handler_detect.md new file mode 100644 index 0000000000000..4043b5d4e9ec5 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/msf/handler_detect.md @@ -0,0 +1,499 @@ +## Vulnerable Application + +This module detects Metasploit `exploit/multi/handler` listeners (and any other +**staged** `reverse_tcp` payload handler) on a network by abusing the staging +protocol. + +When a victim connects to a staged `reverse_tcp` handler, the handler +immediately transmits the second stage **without waiting for any client input** +- it "talks first". Almost no legitimate TCP service sends a large binary or +base64/zlib blob immediately on connect, which makes this behavior a reliable +fingerprint. + +The module recognizes several "handler talks first" behaviors: + +* **Windows native** stagers send a little-endian (`pack('V')`) length followed + by a binary stage (for meterpreter this is the `metsrv` reflective DLL, + ~200 KB+ for x64; for shells a small <1 KB stage). +* **Python** stagers send a big-endian (`pack('N')`) length followed by a + base64+zlib text stage. +* **PHP / Java** stagers send a big-endian (`pack('N')`) length followed by a + PHP-source / JAR stage. +* **Linux / OSX native** meterpreter stagers send the raw machine-code stage + with no length prefix at all. +* **Unix staged shells** send a tiny raw `execve("/bin/sh")` shellcode stage + (detected via the embedded `/bin/sh` string). +* **Reverse command shells** (`shell_reverse_tcp`, etc.): the + `Msf::Sessions::CommandShell` handler verifies the shell by sending + `echo ` on connect, which is a reliable Metasploit signature. +* **Java / Android (Dalvik)** stagers send a big-endian length prefix followed + by a jar/dex stage (detected via the `PK`/`META-INF` markers). + +When a port does not volunteer a stage (the handler waits for the client), the +module additionally sends an HTTP request to fingerprint Metasploit's +**HTTP-based handlers** (both plaintext and, optionally, over TLS): + +* **`reverse_http` / `reverse_https` Meterpreter handlers** answer `200 OK` with + the default `It works!` body to *any* unknown URI (a real server would return + `404`), with `Server: Apache`. +* **Rex HTTP servers** (`web_delivery`, the `fetch` payload servers, and most + exploit-module HTTP servers) return a distinctive Rex `404` page + (`

Not found

The requested URL ... was not found on this server.`). + +### Capturing operator AutoRunScript / follow-up commands + +When `ECHO_BACK` is enabled (default) and a reverse command-shell handler is +found, the module echoes the verification token back. This marks the shell +"valid" on the operator's console, after which the handler runs any configured +`InitialAutoRunScript` / `AutoRunScript` against what it believes is a live +session - and those commands are written down the socket to us. The module +captures and reports them, which can leak the operator's post-exploitation +automation. With no AutoRunScript configured, the handler simply stops after +verification (nothing further is sent). + +### Detectability summary by transport + +These figures come from spinning up one representative listener for every +distinct `(os/language x staged? x type x transport)` combination (203 of them, +collapsing arch variants that share a wire fingerprint) and scanning them. The +counts are the number of the 1090 total reverse payloads that map to each +transport class, and how many fall into a detectable class. + +| Transport class | Example payloads | Total | Detected | How | +|---|---|---:|---:|---| +| tcp | reverse_tcp (+dns/uuid-less variants) | 544 | 318 | talk-first stage / echo / binary burst | +| http | reverse_http / reverse_winhttp | 124 | 96 | HTTP probe -> It works! | +| https | reverse_https / reverse_winhttps | 108 | 86 | HTTPS probe -> It works! | +| tcp_rc4 | reverse_tcp_rc4 | 102 | 44 | RC4-encrypted stage (partial) | +| tcp_uuid | reverse_tcp_uuid | 81 | 47 | stage after 16-byte UUID | +| cmdshell | interpreter reverse shells | 42 | 23 | echo probe (timing) | +| tcp_ssl | reverse_tcp_ssl | 31 | 19 | SSL probe reads stage/echo | +| named_pipe | reverse_named_pipe (SMB) | 30 | 0 | SMB named pipe - not probed | +| udp | reverse_udp | 15 | 6 | UDP datagram -> stage | +| sctp | reverse_sctp | 13 | 0 | SCTP - not probed | + + +Notes on the partials/zeros: + +* **tcp_rc4 / tcp_uuid**: the RC4-encrypted stage looks like a high-entropy + binary burst (still flagged, lower confidence); UUID payloads prepend 16 + bytes before the length field. Both are caught when they fall back to the + generic "unsolicited binary burst" rule. +* **tcp_ssl**: caught by the SSL probe, which sends an HTTP request and then + classifies the reply, so it reads `reverse_https` (`It works!`), + `shell_reverse_tcp_ssl` (`echo ` over TLS) and the staged + `reverse_tcp_ssl` burst with a single round-trip. +* **named_pipe (SMB)** and **sctp**: not TCP or UDP, so this scanner does not + probe them - listed for completeness only. +* **silent stageless**: stageless payloads that wait for the client and are not + HTTP handlers (some meterpreter and shells) cannot be elicited and look like a + plain open port. + +The full per-type matrix (sorted by os/language, staged, type, transport) is in +the [Full payload matrix](#full-payload-matrix) appendix at the end of this +document. + +### Empirical validation (723 live listeners) + +The detector was validated end-to-end by starting a real `multi/handler` +listener for **every one of the 723 reverse payloads** that bind under +`multi/handler` (LPORT 10000+, scanned from a *separate*, lightly-loaded +console) and confirming what each is classified as: + +| Result | Count | | +|---|---:|---| +| Detected (passive probes off) | **673 / 723** | 93.1% | +| + `DEEP_PROBE` (double handlers + pingback) | **~698 / 723** | 96.5% | +| Not detected - *expected* (passive handlers) | 24 | see below | + +Confidence of the base 673: ~460 high, ~78 medium, ~135 low. Transports covered +include tcp, http(s), tcp_ssl, tcp_rc4, tcp_uuid, fetch (http/https/tftp) and +udp. + +`DEEP_PROBE` (on by default) actively provokes two families that are silent on a +single connection: + +* **`cmd/unix/reverse`, `reverse_openssl`, `reverse_ssl_double_telnet` (+ their + `php/unix/cmd` mirrors) (6)** use `Msf::Handler::ReverseTcpDouble` - they wait + for *two* connections, then write `echo ;` to both to pair them. The + scanner opens a second connection (plaintext and TLS) and reads that probe - + **high confidence**. +* **`pingback_*` (19)** read a 16-byte UUID then close, sending nothing. The + scanner writes 16 bytes and confirms the handler closes near-instantly with no + reply - **low confidence** (behavioral; any service that drops the connection + on 16 bytes of junk looks similar). + +The remaining 24 are genuinely passive and not elicitable with a simple probe: + +* **`powershell_reverse_tcp` / `_ssl` (20)** - the handler waits for the live + PowerShell to speak first; it stays silent for >22s to a UUID, a fake banner + and a null byte alike, so there is nothing to fingerprint. +* **mainframe z/OS shell (2)**, **`osx/aarch64/shell_reverse_tcp` (1)**, and a + stageless `cmd/unix/php/meterpreter_reverse_tcp` (1) - silent on a bare + connect. + +> Detection of reverse *shells* (`echo `) is timing-sensitive: the probe +> only fires once the handler treats the connection as a live shell. Under heavy +> load (many handlers in one console plus the scanner) that bootstrap is delayed +> and shells are missed. Run the scanner from a **separate console** and/or +> raise `FIRST_BYTE_WAIT` and they detect reliably - this accounted for the bulk +> of the gap between a first pass and the 673 total above. + +### Limitations + +* **Some passive handlers cannot be detected.** When the handler volunteers + nothing, is *not* an HTTP handler, and cannot be provoked by `DEEP_PROBE`, the + port just looks open. With `DEEP_PROBE` on, the `ReverseTcpDouble`/double-SSL + shells (`cmd/unix/reverse`, `reverse_openssl`, `reverse_ssl_double_telnet`) and + `pingback_*` are recovered; what is left is `powershell_reverse_tcp(_ssl)` + (waits for the live PowerShell to talk first) and a few stageless/exotic shells + - ~24 of 723. +* **Reverse-shell `echo` detection is timing-dependent.** The handler only + emits its `echo` probe once it treats the connection as a live shell, which is + delayed when the operator's console is heavily loaded. Scan from a *separate* + console and/or increase `FIRST_BYTE_WAIT` if shells are being missed. +* **Encrypted stages are low-confidence.** RC4 (`reverse_tcp_rc4`) and other + encrypted/stageless streams have no parseable framing, so they are only + flagged generically as an "unsolicited binary burst." +* The technique fingerprints the **transport/staging behavior**, not the exact + module. Several payloads share a family fingerprint (e.g. linux vs osx native + stages, or the legacy `reverse_nonx`/`reverse_ord` stager shellcode, are + indistinguishable on the wire), so the reported payload is a best-effort + family guess. + +### WARNING + +Connecting to a live handler causes it to send a stage and attempt to create a +session. On the operator's `msfconsole` this prints a `Sending stage (...)` +message and leaves a failed/dead session. This is an intentional, observable +side effect (`IOC_IN_LOGS`). + +### Setting up handlers for testing + +Start an `exploit/multi/handler` (or use `to_handler` / web_delivery) with a +staged payload, for example: + +``` +use exploit/multi/handler +set payload python/meterpreter/reverse_tcp +set lhost 127.0.0.1 +set lport 4445 +run -j + +set payload linux/x64/meterpreter/reverse_tcp +set lport 4446 +run -j + +set payload windows/x64/meterpreter/reverse_tcp +set lport 4448 +run -j +``` + +## Verification Steps + +1. Start one or more staged `reverse_tcp` handlers as shown above. +2. Start `msfconsole` +3. `use auxiliary/scanner/msf/handler_detect` +4. `set RHOSTS 127.0.0.1` +5. `set PORTS 4444-4464` +6. `run` +7. You should see `[+]` lines identifying each detected handler and the + fingerprinted payload family. + +## Options + +### PORTS + +The list of TCP ports to probe on each host, in the usual Metasploit portspec +format (e.g. `4444-4460,5555`). Defaults to `4444-4464`. + +### TIMEOUT + +The socket connect timeout in seconds. Default `1`. + +### FIRST_BYTE_WAIT + +How long (seconds) to wait for the handler to send the first stage/probe +bytes after connecting. Default `5`. Reverse-shell handlers emit their +`echo` verification probe a little late, so allow some slack; increase further +on slow/high-latency links. + +### IDLE_TIMEOUT + +How long (seconds) to wait for additional stage data before assuming the +stage is fully received. Default `0.75`. + +### MAX_STAGE_SIZE + +Maximum number of stage bytes to read while fingerprinting. Reading the full +stage allows the length-prefix to be matched exactly (high confidence). Default +`2097152` (2 MB). + +### HTTP_PROBE + +When a port does not volunteer a stage, send an HTTP request to fingerprint +Metasploit's HTTP-based handlers (`reverse_http`, `web_delivery`, `fetch`). +Default `true`. + +### HTTP_SSL_PROBE + +In addition to the plaintext HTTP probe, attempt an SSL/TLS request to catch +`reverse_https` / HTTPS `fetch` servers, and to read the stage/echo of +`reverse_tcp_ssl` handlers through the TLS handshake. Default `true`. + +### SCAN_UDP + +Also probe each port over UDP. A `reverse_udp` handler waits for any inbound +datagram and then sends the stage back, so a single probe datagram elicits the +same staging fingerprint. Default `false`. + +### ECHO_BACK + +When a command-shell `echo ` verification probe is seen, echo the token +back to mark the shell valid and capture any follow-up commands the handler +sends (for example an operator's `AutoRunScript`). Default `true`. + +### ECHO_FOLLOWUP_WAIT + +How long (seconds) to keep reading after echoing the token back, to capture +`AutoRunScript` / operator commands. Default `8`. + +### CONCURRENCY + +The number of concurrent ports to check per host. Default `10`. + +### DEEP_PROBE + +For ports that stay silent on the first connection, actively try to provoke +passive handlers before giving up (default `true`): + +* open a **second connection** (plaintext and, if `HTTP_SSL_PROBE`, TLS) to make + a `ReverseTcpDouble` handler pair them and emit its `echo` probe + (`cmd/unix/reverse`, `reverse_openssl`, `reverse_ssl_double_telnet`); +* write a **16-byte UUID** and watch for the immediate, reply-less close that + identifies a `pingback_*` handler. + +This opens a few extra connections per silent port. Set to `false` for the +quietest possible scan. + +## Scenarios + +### Example run against a host running several staged handlers + +``` +msf6 auxiliary(scanner/msf/handler_detect) > run +``` + +## Full payload matrix + +One row per distinct payload type (arch variants collapsed; the *Variants* column +is how many of the 1090 reverse payloads map to that row). Detection results are +empirical from live representative listeners. `n/t (no listener)` means that +representative did not start a TCP/UDP listener under `multi/handler` (needs +extra options, is SMB/SCTP, or is an exotic/unsupported OS). + +| OS / Language | Staged | Type | Transport | Variants | Detected | Method / Notes | +|---|---|---|---|---:|---|---| +| aix | single | shell | tcp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| android | staged | meterpreter | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| android | staged | meterpreter | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| android | staged | meterpreter | tcp | 1 | YES (tcp) | BE length prefix (jar/dex) | +| android | staged | shell | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| android | staged | shell | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| android | staged | shell | tcp | 1 | YES (tcp) | BE length prefix (jar/dex) | +| android | single | meterpreter | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| android | single | meterpreter | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| android | single | meterpreter | tcp | 1 | YES (tcp) | binary burst, no prefix | +| apple_ios | single | meterpreter | http | 2 | YES (tcp) | HTTP probe ("It works!") | +| apple_ios | single | meterpreter | https | 2 | YES (tcp) | HTTPS probe ("It works!") | +| apple_ios | single | meterpreter | tcp | 2 | YES (tcp) | binary burst, no prefix | +| apple_ios | single | shell | tcp | 1 | YES (tcp) | echo probe | +| bsd | staged | shell | tcp | 2 | YES (tcp) | raw /bin/sh shellcode | +| bsd | single | other | tcp | 1 | YES (tcp) | binary burst, no prefix | +| bsd | single | shell | tcp | 7 | YES (tcp) | echo probe | +| bsdi | staged | shell | tcp | 1 | YES (tcp) | raw /bin/sh shellcode | +| bsdi | single | shell | tcp | 1 | YES (tcp) | echo probe | +| cmd/linux | staged | meterpreter | sctp | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/linux | staged | meterpreter | tcp | 24 | YES (tcp) | LE length prefix (shell stage) | +| cmd/linux | staged | meterpreter | tcp_uuid | 3 | YES (tcp) | binary burst, no prefix | +| cmd/linux | staged | shell | sctp | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/linux | staged | shell | tcp | 24 | YES (tcp) | LE length prefix (shell stage) | +| cmd/linux | staged | shell | tcp_uuid | 3 | YES (tcp) | raw /bin/sh shellcode | +| cmd/linux | single | meterpreter | http | 24 | YES (tcp) | HTTP probe ("It works!") | +| cmd/linux | single | meterpreter | https | 24 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/linux | single | meterpreter | tcp | 24 | YES (tcp) | binary burst, no prefix | +| cmd/linux | single | other | tcp | 6 | no (silent) | handler waits for client (stageless) | +| cmd/linux | single | shell | tcp | 36 | YES (tcp) | echo probe | +| cmd/mainframe | single | shell | tcp | 1 | no (silent) | handler waits for client (stageless) | +| cmd/unix | staged | meterpreter | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| cmd/unix | staged | meterpreter | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/unix | staged | meterpreter | tcp | 2 | YES (tcp) | BE length prefix (php) | +| cmd/unix | staged | meterpreter | tcp_ssl | 1 | YES (tcp) | BE length prefix (base64/zlib) | +| cmd/unix | staged | meterpreter | tcp_uuid | 2 | YES (tcp) | BE length prefix (php) | +| cmd/unix | single | meterpreter | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| cmd/unix | single | meterpreter | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/unix | single | meterpreter | tcp | 2 | no (silent) | handler waits for client (stageless) | +| cmd/unix | single | other | cmdshell | 19 | no (silent) | handler waits for client (stageless) | +| cmd/unix | single | other | sctp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/unix | single | other | tcp | 6 | no (silent) | handler waits for client (stageless) | +| cmd/unix | single | other | tcp_ssl | 6 | YES (tcp) | echo probe | +| cmd/unix | single | shell | sctp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/unix | single | shell | tcp | 1 | no (silent) | handler waits for client (stageless) | +| cmd/unix | single | shell | tcp_ssl | 1 | YES (tcp) | echo probe | +| cmd/unix | single | shell | udp | 1 | no (silent) | handler waits for client (stageless) | +| cmd/windows | staged | custom | http | 16 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | custom | https | 16 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | custom | named_pipe | 8 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | custom | tcp | 26 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | custom | tcp_rc4 | 11 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | custom | tcp_uuid | 8 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | custom | udp | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | dllinject | http | 6 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | dllinject | tcp | 21 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | dllinject | tcp_rc4 | 6 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | dllinject | tcp_uuid | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | meterpreter | http | 17 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/windows | staged | meterpreter | https | 17 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/windows | staged | meterpreter | named_pipe | 8 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | meterpreter | tcp | 27 | YES (tcp) | LE length prefix (metsrv) | +| cmd/windows | staged | meterpreter | tcp_rc4 | 11 | YES (tcp) | binary burst, no prefix | +| cmd/windows | staged | meterpreter | tcp_ssl | 1 | YES (tcp) | BE length prefix (base64/zlib) | +| cmd/windows | staged | meterpreter | tcp_uuid | 9 | YES (tcp) | LE length prefix (metsrv) | +| cmd/windows | staged | patchupdllinject | tcp | 18 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | patchupdllinject | tcp_rc4 | 6 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | patchupdllinject | tcp_uuid | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | patchupmeterpreter | tcp | 18 | YES (tcp) | LE length prefix (shell stage) | +| cmd/windows | staged | patchupmeterpreter | tcp_rc4 | 6 | YES (tcp) | binary burst, no prefix | +| cmd/windows | staged | patchupmeterpreter | tcp_uuid | 3 | YES (tcp) | LE length prefix (shell stage) | +| cmd/windows | staged | shell | tcp | 23 | YES (tcp) | LE length prefix (shell stage) | +| cmd/windows | staged | shell | tcp_rc4 | 11 | YES (tcp) | binary burst, no prefix | +| cmd/windows | staged | shell | tcp_uuid | 8 | YES (tcp) | LE length prefix (shell stage) | +| cmd/windows | staged | shell | udp | 3 | YES (udp) | LE length prefix (shell stage) | +| cmd/windows | staged | upexec | tcp | 18 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | upexec | tcp_rc4 | 6 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | upexec | tcp_uuid | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | upexec | udp | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | staged | vncinject | http | 16 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/windows | staged | vncinject | https | 10 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/windows | staged | vncinject | tcp | 26 | YES (tcp) | binary burst, no prefix | +| cmd/windows | staged | vncinject | tcp_rc4 | 11 | YES (tcp) | binary burst, no prefix | +| cmd/windows | staged | vncinject | tcp_uuid | 8 | YES (tcp) | binary burst, no prefix | +| cmd/windows | single | meterpreter | http | 7 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/windows | single | meterpreter | https | 7 | YES (tcp) | HTTPS probe ("It works!") | +| cmd/windows | single | meterpreter | tcp | 13 | YES (tcp) | binary burst, no prefix | +| cmd/windows | single | other | cmdshell | 3 | YES (tcp) | echo probe | +| cmd/windows | single | other | named_pipe | 6 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | single | other | tcp | 26 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | single | other | tcp_rc4 | 8 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | single | other | tcp_uuid | 6 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | single | shell | cmdshell | 1 | YES (tcp) | echo probe | +| cmd/windows | single | shell | named_pipe | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | single | shell | sctp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | single | shell | tcp | 28 | no (silent) | handler waits for client (stageless) | +| cmd/windows | single | shell | tcp_rc4 | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | single | shell | tcp_ssl | 10 | no (silent) | handler waits for client (stageless) | +| cmd/windows | single | shell | tcp_uuid | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| cmd/windows | single | shell | udp | 1 | YES (udp) | echo probe | +| firefox | single | shell | tcp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| generic | single | shell | tcp | 1 | YES (tcp) | echo probe | +| java | staged | meterpreter | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| java | staged | meterpreter | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| java | staged | meterpreter | tcp | 1 | YES (tcp) | BE length prefix (jar/dex) | +| java | staged | shell | tcp | 1 | YES (tcp) | BE length prefix | +| java | single | shell | tcp | 2 | YES (tcp) | echo probe | +| linux | staged | meterpreter | sctp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| linux | staged | meterpreter | tcp | 8 | YES (tcp) | LE length prefix (shell stage) | +| linux | staged | meterpreter | tcp_uuid | 1 | YES (tcp) | binary burst, no prefix | +| linux | staged | shell | sctp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| linux | staged | shell | tcp | 8 | YES (tcp) | LE length prefix (shell stage) | +| linux | staged | shell | tcp_uuid | 1 | YES (tcp) | raw /bin/sh shellcode | +| linux | single | meterpreter | http | 9 | YES (tcp) | HTTP probe ("It works!") | +| linux | single | meterpreter | https | 9 | YES (tcp) | HTTPS probe ("It works!") | +| linux | single | meterpreter | tcp | 9 | no (silent) | handler waits for client (stageless) | +| linux | single | other | tcp | 2 | no (silent) | handler waits for client (stageless) | +| linux | single | shell | tcp | 12 | no (silent) | handler waits for client (stageless) | +| mainframe | single | shell | tcp | 1 | no (silent) | handler waits for client (stageless) | +| multi | staged | meterpreter | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| multi | staged | meterpreter | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| netware | staged | shell | tcp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| nodejs | single | shell | tcp | 1 | YES (tcp) | echo probe | +| nodejs | single | shell | tcp_ssl | 1 | YES (tcp) | echo probe | +| osx | staged | meterpreter | tcp | 2 | YES (tcp) | binary burst, no prefix | +| osx | staged | meterpreter | tcp_uuid | 1 | YES (tcp) | binary burst, no prefix | +| osx | staged | shell | tcp | 2 | YES (tcp) | LE length prefix (shell stage) | +| osx | single | meterpreter | http | 2 | YES (tcp) | HTTP probe ("It works!") | +| osx | single | meterpreter | https | 2 | YES (tcp) | HTTPS probe ("It works!") | +| osx | single | meterpreter | tcp | 2 | YES (tcp) | binary burst, no prefix | +| osx | single | other | tcp | 4 | YES (tcp) | LE length prefix (metsrv) | +| osx | single | other | tcp_uuid | 1 | YES (tcp) | raw /bin/sh shellcode | +| osx | single | shell | tcp | 7 | no (silent) | handler waits for client (stageless) | +| php | staged | meterpreter | tcp | 1 | YES (tcp) | BE length prefix (php) | +| php | staged | meterpreter | tcp_uuid | 1 | YES (tcp) | BE length prefix (php) | +| php | single | meterpreter | tcp | 1 | YES (tcp) | binary burst, no prefix | +| php | single | other | cmdshell | 19 | YES (tcp) | echo probe | +| php | single | other | sctp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| php | single | other | tcp | 5 | no (silent) | handler waits for client (stageless) | +| php | single | other | tcp_ssl | 6 | YES (tcp) | echo probe | +| python | staged | meterpreter | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| python | staged | meterpreter | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| python | staged | meterpreter | tcp | 1 | YES (tcp) | BE length prefix (base64/zlib) | +| python | staged | meterpreter | tcp_ssl | 1 | YES (tcp) | BE length prefix (base64/zlib) | +| python | staged | meterpreter | tcp_uuid | 1 | YES (tcp) | BE length prefix (base64/zlib) | +| python | single | meterpreter | http | 1 | YES (tcp) | HTTP probe ("It works!") | +| python | single | meterpreter | https | 1 | YES (tcp) | HTTPS probe ("It works!") | +| python | single | meterpreter | tcp | 1 | YES (tcp) | binary burst, no prefix | +| python | single | other | tcp | 1 | no (silent) | handler waits for client (stageless) | +| python | single | shell | sctp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| python | single | shell | tcp | 1 | YES (tcp) | echo probe | +| python | single | shell | tcp_ssl | 1 | YES (tcp) | echo probe | +| python | single | shell | udp | 1 | YES (udp) | echo probe | +| r | single | shell | tcp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| ruby | single | other | tcp | 1 | no (silent) | handler waits for client (stageless) | +| ruby | single | shell | tcp | 1 | YES (tcp) | echo probe | +| ruby | single | shell | tcp_ssl | 1 | YES (tcp) | echo probe | +| solaris | single | shell | tcp | 2 | YES (tcp) | echo probe | +| windows | staged | custom | http | 4 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | custom | https | 4 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | custom | named_pipe | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | custom | tcp | 8 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | custom | tcp_rc4 | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | custom | tcp_uuid | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | custom | udp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | dllinject | http | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | dllinject | tcp | 7 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | dllinject | tcp_rc4 | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | dllinject | tcp_uuid | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | meterpreter | http | 4 | YES (tcp) | HTTP probe ("It works!") | +| windows | staged | meterpreter | https | 4 | YES (tcp) | HTTPS probe ("It works!") | +| windows | staged | meterpreter | named_pipe | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | meterpreter | tcp | 8 | YES (tcp) | HTTP probe ("It works!") | +| windows | staged | meterpreter | tcp_rc4 | 3 | YES (tcp) | binary burst, no prefix | +| windows | staged | meterpreter | tcp_uuid | 2 | YES (tcp) | LE length prefix (metsrv) | +| windows | staged | patchupdllinject | tcp | 6 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | patchupdllinject | tcp_rc4 | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | patchupdllinject | tcp_uuid | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | patchupmeterpreter | tcp | 6 | YES (tcp) | LE length prefix (shell stage) | +| windows | staged | patchupmeterpreter | tcp_rc4 | 2 | YES (tcp) | binary burst, no prefix | +| windows | staged | patchupmeterpreter | tcp_uuid | 1 | YES (tcp) | LE length prefix (shell stage) | +| windows | staged | shell | tcp | 7 | YES (tcp) | LE length prefix (shell stage) | +| windows | staged | shell | tcp_rc4 | 3 | no (silent) | handler waits for client (stageless) | +| windows | staged | shell | tcp_uuid | 2 | YES (tcp) | LE length prefix (shell stage) | +| windows | staged | shell | udp | 1 | YES (udp) | LE length prefix (shell stage) | +| windows | staged | upexec | tcp | 6 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | upexec | tcp_rc4 | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | upexec | tcp_uuid | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | upexec | udp | 1 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | staged | vncinject | http | 4 | YES (tcp) | HTTP probe ("It works!") | +| windows | staged | vncinject | https | 2 | no (silent) | handler waits for client (stageless) | +| windows | staged | vncinject | tcp | 8 | YES (tcp) | HTTP probe ("It works!") | +| windows | staged | vncinject | tcp_rc4 | 3 | no (silent) | handler waits for client (stageless) | +| windows | staged | vncinject | tcp_uuid | 2 | no (silent) | handler waits for client (stageless) | +| windows | single | meterpreter | http | 2 | YES (tcp) | HTTP probe ("It works!") | +| windows | single | meterpreter | https | 2 | YES (tcp) | HTTPS probe ("It works!") | +| windows | single | meterpreter | tcp | 4 | YES (tcp) | binary burst, no prefix | +| windows | single | other | named_pipe | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | single | other | tcp | 10 | YES (tcp) | binary burst, no prefix | +| windows | single | other | tcp_rc4 | 3 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | single | other | tcp_uuid | 2 | n/t (no listener) | did not bind (needs opts / non-TCP / exotic OS) | +| windows | single | shell | tcp | 4 | no (silent) | handler waits for client (stageless) | +| windows | single | shell | tcp_ssl | 2 | no (silent) | handler waits for client (stageless) | diff --git a/modules/auxiliary/scanner/msf/handler_detect.rb b/modules/auxiliary/scanner/msf/handler_detect.rb new file mode 100644 index 0000000000000..aaeff1e284fa1 --- /dev/null +++ b/modules/auxiliary/scanner/msf/handler_detect.rb @@ -0,0 +1,683 @@ +# frozen_string_literal: true + +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::Tcp + include Msf::Auxiliary::Report + include Msf::Auxiliary::Scanner + + def initialize + super( + 'Name' => 'Metasploit Payload Handler Detection (TCP/UDP/HTTP/HTTPS)', + 'Description' => %q{ + Detect Metasploit exploit/multi/handler listeners and other reverse + payload handlers by fingerprinting their wire behavior. Several + techniques are combined: + + * Staged reverse handlers "talk first": on connect they transmit the + stage with a 4-byte length prefix whose endianness identifies the + family + - little-endian (pack 'V') for Windows native (metsrv) + - big-endian (pack 'N') for Python/PHP/Java/Android + - Linux/OSX native stagers send the raw machine-code stage with + no length prefix, and unix staged shells send a tiny execve("/bin/sh") + shellcode. + * Reverse command shells "talk first" with an "echo " probe. + Echoing the token back marks the shell valid and can capture an + operator's AutoRunScript / follow-up commands. + * reverse_http(s) Meterpreter handlers answer any unknown URI with the + default "It works!" body and Server: Apache. HTTP servers + (web_delivery, fetch handlers, exploit module servers) return a + distinctive 404 page. + * reverse_tcp_ssl handlers stage over TLS; an SSL probe reads the + stage/echo through the handshake. + * reverse_udp handlers send the stage in response to any datagram, so + an optional UDP probe (SCAN_UDP) catches them too. + + Transports that are not TCP/UDP (reverse_sctp, reverse_named_pipe/SMB) + and silent stageless payloads that wait for the client cannot be + fingerprinted and appear as a silent open port. + }, + 'Author' => [ 'h00die' ], + 'License' => MSF_LICENSE, + 'Notes' => { + 'Stability' => [ CRASH_SAFE ], + 'SideEffects' => [ IOC_IN_LOGS ], # msf console will report sending stage or meterp connection, but then report its invalid + 'Reliability' => [] + } + ) + + register_options( + [ + OptString.new('PORTS', [ true, 'Ports to scan (e.g. 4444-4460,5555)', '4444-4464' ]), + OptFloat.new('TIMEOUT', [ true, 'The socket connect timeout in seconds', 1 ]), + OptFloat.new('FIRST_BYTE_WAIT', [ true, 'How long to wait (seconds) for the handler to send the first stage/probe bytes (shell handlers emit their "echo" probe a little later, so allow some slack)', 5 ]), + OptFloat.new('IDLE_TIMEOUT', [ true, 'How long to wait (seconds) for more stage data before giving up', 0.75 ]), + OptInt.new('MAX_STAGE_SIZE', [ true, 'Maximum number of stage bytes to read while fingerprinting', 2 * 1024 * 1024 ]), + # different types + OptBool.new('HTTP_PROBE', [ true, 'If a port waits for the client, send an HTTP request to fingerprint MSF HTTP handlers (reverse_http, web_delivery, fetch)', true ]), + OptBool.new('HTTP_SSL_PROBE', [ true, 'Also attempt an SSL/TLS HTTP probe (catches reverse_https and HTTPS fetch servers)', true ]), + OptBool.new('SCAN_UDP', [ true, 'Also probe each port over UDP (catches reverse_udp handlers)', false ]), + OptBool.new('ECHO_BACK', [ true, 'When a command-shell "echo " probe is seen, echo the token back to verify the shell and capture any follow-up commands', true ]), + OptFloat.new('ECHO_FOLLOWUP_WAIT', [ true, 'How long (seconds) to keep reading after echoing the token back, to capture AutoRunScript/operator commands', 8 ]), + OptInt.new('CONCURRENCY', [ true, 'The number of concurrent ports to check per host', 10 ]), + OptBool.new('DEEP_PROBE', [ true, 'For ports that stay silent, actively elicit passive handlers: open a 2nd connection (ReverseTcpDouble: cmd/unix/reverse, reverse_openssl, reverse_ssl_double_telnet) and send a 16-byte UUID (pingback) to provoke a response', true ]) + ] + ) + + deregister_options('RPORT') # in favor of ports so we can do bulk ones + end + + def timeout + datastore['TIMEOUT'].to_f + end + + def validate_ports + ports = Rex::Socket.portspec_crack(datastore['PORTS']) + raise Msf::OptionValidateError, ['PORTS'] if ports.empty? + + ports + end + + def run_host(ip) + ports = validate_ports + + # Hard per-port deadline (seconds) so one wedged port - e.g. a black-hole TLS + # handshake - can never stall its whole concurrency batch. It sums the + # worst-case blocking waits a single port can incur back-to-back: + # timeout -> the initial TCP connect() + # 3 * FIRST_BYTE_WAIT -> up to three sequential "wait for the first byte" + # reads on one port: the stage drain, the silent-port + # HTTP/double-handler fallback, and the UDP probe + # ECHO_FOLLOWUP_WAIT -> reading a shell's AutoRunScript output after echo-back + # + 5 -> fixed slack for TLS setup, idle-read tails, scheduling + # It is deliberately a loose over-estimate (those paths rarely all hit the same + # port), so the Timeout below only fires for genuinely stuck ports, not merely + # slow ones. With defaults: 1 + (3 * 5) + 8 + 5 = 29s. + port_deadline = timeout + (3 * datastore['FIRST_BYTE_WAIT'].to_f) + datastore['ECHO_FOLLOWUP_WAIT'].to_f + 5 + + until ports.empty? + threads = [] + begin + 1.upto(datastore['CONCURRENCY']) do + this_port = ports.shift + break unless this_port + + threads << framework.threads.spawn("Module(#{refname})-#{ip}:#{this_port}", false, this_port) do |port| + ::Timeout.timeout(port_deadline) do + check_port(ip, port, timeout) # with deep_prob on handles all protocols but udp. with it turned off only handles tcp + check_port_udp(ip, port, timeout) if datastore['SCAN_UDP'] + end + rescue ::Timeout::Error + vprint_warning("#{Rex::Socket.to_authority(ip, port)} - per-port timeout (#{port_deadline.round}s), skipping") + end + end + # Bounded join as a backstop in case Timeout cannot interrupt a blocked call. + threads.each { |t| t.join(port_deadline + 5) } + rescue ::Timeout::Error + ensure + threads.each do |x| + x.kill + rescue StandardError + nil + end + end + end + end + + def check_port(ip, port, timeout) + sock = nil + begin + sock = connect(false, + { + 'RHOST' => ip, + 'RPORT' => port, + 'ConnectTimeout' => timeout + }) + return unless sock + + buf = drain_stage(sock) + result = fingerprint(buf) + + if result[:match] + handle_match(ip, port, 'tcp', result, sock) + elsif buf.nil? || buf.empty? + # The server waited for us to speak first. That is the behavior of MSF's + # HTTP-based handlers (reverse_http/https, web_delivery, fetch servers), + # so try to fingerprint them over HTTP before giving up. + http = datastore['HTTP_PROBE'] ? http_fingerprint(sock, ip, port, timeout) : nil + if http + report_handler(ip, port, 'tcp', http) + elsif datastore['DEEP_PROBE'] && (deep = deep_probe(ip, port, sock)) + # deep_probe pairs a second connection (ReverseTcpDouble) / sends a + # UUID (pingback); our idle first socket was dropped inside it first. + sock = nil + report_handler(ip, port, 'tcp', deep) + else + vprint_status("#{Rex::Socket.to_authority(ip, port)} - Open, no unsolicited data and no Metasploit HTTP fingerprint (stageless reverse shell/meterpreter, or unrelated service)") + end + else + vprint_status("#{Rex::Socket.to_authority(ip, port)} - Talks first but does not look like a stage (banner: #{buf[0, 32].inspect})") + end + rescue ::Rex::ConnectionRefused + vprint_status("#{Rex::Socket.to_authority(ip, port)} - Connection refused") + rescue ::Rex::ConnectionError, ::IOError, ::Timeout::Error => e + vprint_status("#{Rex::Socket.to_authority(ip, port)} - Connection error (#{e.class})") + rescue ::Interrupt + raise $ERROR_INFO + rescue ::StandardError => e + vprint_error("#{Rex::Socket.to_authority(ip, port)} - #{e.class} #{e}") + ensure + begin + disconnect(sock) + rescue StandardError + nil + end + end + end + + # Actively provoke handlers that stay silent on a single plain connection. + # Returns a result hash or nil. `idle_sock` is our first (silent) connection, + # dropped up front so it does not interfere with a double handler's pairing. + def deep_probe(ip, port, idle_sock) + begin + disconnect(idle_sock) + rescue StandardError + nil + end + paired_connect_probe(ip, port, false) || + (datastore['HTTP_SSL_PROBE'] && ssl_paired_probe(ip, port)) || + pingback_probe(ip, port) || nil + end + + # Msf::Handler::ReverseTcpDouble (cmd/unix/reverse, reverse_openssl, + # reverse_ssl_double_telnet, ...) waits for TWO connections, then writes + # "echo ;" to both to pair them. Open a second connection and read + # both to elicit that probe. + def paired_connect_probe(ip, port, ssl) + socks = [] + 2.times do + socks << connect(false, { 'RHOST' => ip, 'RPORT' => port, 'SSL' => ssl, 'ConnectTimeout' => 3 }) + end + socks.compact.each do |s| + fp = fingerprint(drain_stage(s)) + next unless fp[:match] + + suffix = ssl ? ' over SSL/TLS (ReverseTcpDouble - paired connections)' : ' (ReverseTcpDouble - paired connections)' + return { payload: "#{fp[:payload]}#{suffix}", framing: fp[:framing], confidence: fp[:confidence], bytes: fp[:bytes] } + end + nil + rescue ::StandardError + nil + ensure + socks.each do |s| + disconnect(s) + rescue StandardError + nil + end + end + + # The double-SSL variants need the paired connections over TLS. Bound it with + # its own deadline (a Rex SSL socket can block on a silent peer). + def ssl_paired_probe(ip, port) + ::Timeout.timeout(12) { paired_connect_probe(ip, port, true) } + rescue ::StandardError + nil + end + + # A pingback handler (Msf::Sessions::Pingback) reads up to 16 bytes with a 1s + # timeout, then closes and sends nothing. Send 16 bytes and confirm the server + # closes promptly with no reply - a benign silent service would instead leave + # the read to time out. Low confidence by nature (any service that drops the + # connection on 16 bytes of junk looks similar). + def pingback_probe(ip, port) + s = connect(false, { 'RHOST' => ip, 'RPORT' => port, 'ConnectTimeout' => 3 }) + return nil unless s + + s.put("\x00" * 16) + t0 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + data = begin + s.get_once(-1, 2) + rescue ::IOError + nil + end + elapsed = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - t0 + return nil unless (data.nil? || data.empty?) && elapsed < 1.0 + + { + payload: 'likely pingback handler (pingback_reverse_tcp family)', + framing: "handler closed the connection #{elapsed.round(2)}s after a 16-byte write, returning no data (reads a 16-byte UUID then closes)", + confidence: 'low' + } + rescue ::StandardError + nil + ensure + begin + disconnect(s) + rescue StandardError + nil + end + end + + # Probe a port over UDP. A reverse_udp handler waits in recvfrom() for any + # inbound datagram, then sends the stage back, so a single probe datagram + # elicits the same staging fingerprint as the TCP path. + def check_port_udp(ip, port, _timeout) + udp = nil + begin + udp = Rex::Socket::Udp.create( + 'PeerHost' => ip, + 'PeerPort' => port, + 'Context' => { 'Msf' => framework, 'MsfExploit' => self } + ) + udp.put("\x00") + buf = udp_drain(udp) + result = fingerprint(buf) + handle_match(ip, port, 'udp', result, nil) if result[:match] + rescue ::Rex::ConnectionError, ::IOError, ::Timeout::Error + rescue ::Interrupt + raise $ERROR_INFO + rescue ::StandardError => e + vprint_error("#{Rex::Socket.to_authority(ip, port)} (udp) - #{e.class} #{e}") + ensure + begin + udp.close + rescue StandardError + nil + end + end + end + + # Report a detected handler and, for command-shell echo probes, optionally + # echo the token back to verify the shell and capture follow-up commands. + def handle_match(ip, port, proto, result, sock) + report_handler(ip, port, proto, result) + + return unless result[:echo_token] && datastore['ECHO_BACK'] && sock + + followup = echo_back_capture(sock, result[:echo_token]) + if followup && !followup.empty? + print_good("#{Rex::Socket.to_authority(ip, port)} - Captured follow-up after shell verification (likely AutoRunScript / operator commands):") + followup.each_line { |line| print_line(" #{line.chomp}") } + loot_path = store_loot( + 'metasploit.handler.autorunscript', + 'text/plain', + ip, + followup, + "handler_followup_#{port}.txt", + "Commands an operator's AutoRunScript/InitialAutoRunScript ran against the connecting shell on #{ip}:#{port}" + ) + print_good("#{Rex::Socket.to_authority(ip, port)} - Saved captured commands to loot: #{loot_path}") + report_note(host: ip, port: port, type: 'msf.handler.followup', data: { 'commands' => followup.split("\n").reject(&:empty?), 'loot' => loot_path }, update: :unique_data) + else + vprint_status("#{Rex::Socket.to_authority(ip, port)} - No follow-up commands after echo-back (no AutoRunScript configured)") + end + end + + def report_handler(ip, port, proto, result) + bytes = result[:bytes] ? ", #{result[:bytes]} bytes" : '' + print_good("#{Rex::Socket.to_authority(ip, port)} - Metasploit handler detected (#{proto}): #{result[:payload]} (#{result[:confidence]} confidence#{bytes}) [#{result[:framing]}]") + report_service(host: ip, port: port, proto: proto, name: 'metasploit-handler', info: "#{result[:payload]} | #{result[:framing]} (#{result[:confidence]} confidence)") + end + + # Echo the verification token back, then keep reading to capture whatever the + # handler sends next (e.g. an operator's AutoRunScript / InitialAutoRunScript). + def echo_back_capture(sock, token) + sock.put("#{token}\n") + wait = datastore['ECHO_FOLLOWUP_WAIT'].to_f + idle = datastore['IDLE_TIMEOUT'].to_f + + buf = +'' + chunk = begin + sock.get_once(-1, wait) + rescue StandardError + nil + end + return buf if chunk.nil? || chunk.empty? + + buf << chunk + while buf.length < 65_536 + chunk = begin + sock.get_once(-1, idle) + rescue StandardError + nil + end + break if chunk.nil? || chunk.empty? + + buf << chunk + end + buf + rescue ::StandardError + buf + end + + # UDP equivalent of drain_stage using recvfrom (which returns [data, host, port]). + def udp_drain(udp) + first_wait = datastore['FIRST_BYTE_WAIT'].to_f + idle_wait = datastore['IDLE_TIMEOUT'].to_f + max_bytes = datastore['MAX_STAGE_SIZE'].to_i + + buf = +'' + data, = udp.recvfrom(65_535, first_wait) + return buf if data.nil? || data.empty? + + buf << data + while buf.length < max_bytes + data, = udp.recvfrom(65_535, idle_wait) + break if data.nil? || data.empty? + + buf << data + end + buf + end + + # Read whatever the server sends unprompted, up to MAX_STAGE_SIZE. + def drain_stage(sock) + first_wait = datastore['FIRST_BYTE_WAIT'].to_f + idle_wait = datastore['IDLE_TIMEOUT'].to_f + max_bytes = datastore['MAX_STAGE_SIZE'].to_i + + buf = +'' + chunk = begin + sock.get_once(-1, first_wait) + rescue StandardError + nil + end + return buf if chunk.nil? || chunk.empty? + + buf << chunk + while buf.length < max_bytes + chunk = begin + sock.get_once(-1, idle_wait) + rescue StandardError + nil + end + break if chunk.nil? || chunk.empty? + + buf << chunk + end + buf + end + + # Fingerprint MSF's HTTP-based handlers. Tries a plaintext request on the + # already-open socket first, then (optionally) an SSL request on a fresh + # connection for reverse_https / HTTPS fetch servers. Returns a result hash + # or nil. + def http_fingerprint(sock, ip, port, _timeout) + result = http_classify(http_exchange(sock), scheme: 'http') + return result if result + + return nil unless datastore['HTTP_SSL_PROBE'] + + ssl_sock = nil + begin + # The Rex connect timeout only bounds the TCP connect, not the TLS + # handshake read - so wrap the whole SSL probe in its own short deadline + # to keep a stalled handshake from eating the per-port budget. + ::Timeout.timeout(9) do + ssl_sock = connect(false, + { + 'RHOST' => ip, + 'RPORT' => port, + 'SSL' => true, + 'ConnectTimeout' => 3 + }) + # A single GET-and-read handles every SSL-fronted MSF handler: + # * reverse_https answers our request with the default "It works!" page + # * shell_reverse_tcp_ssl ignores the request and sends its unsolicited + # "echo " shell-verification probe over TLS + # * reverse_tcp_ssl (staged) has already pushed its stage on connect + # so send the request, then classify the reply as HTTP, then as a raw + # stage/echo. (A Rex SSL socket's get_once does not honor its read timeout + # against a silent peer, so we must speak first rather than blind-peek.) + raw = http_exchange(ssl_sock) + hc = http_classify(raw, scheme: 'https') + if hc + hc + elsif raw && !raw.empty? && (fp = fingerprint(raw))[:match] + { payload: "#{fp[:payload]} over SSL/TLS", framing: fp[:framing], confidence: fp[:confidence], bytes: fp[:bytes], echo_token: fp[:echo_token] } + end + end + rescue ::StandardError => e + vprint_error("#{Rex::Socket.to_authority(ip, port)} - SSL probe failed: #{e.class} #{e}") + nil + ensure + begin + disconnect(ssl_sock) + rescue StandardError + nil + end + end + end + + # Send a GET for a random path and return the raw HTTP response, or nil. + def http_exchange(sock) + return nil unless sock + + path = '/' + Rex::Text.rand_text_alphanumeric(8..16) + host = begin + sock.peerhost + rescue StandardError + 'localhost' + end + req = "GET #{path} HTTP/1.1\r\nHost: #{host}\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\nConnection: close\r\n\r\n" + sock.put(req) + + # An HTTP/TLS server that is going to answer answers fast; cap the wait so a + # non-answer (e.g. a plaintext GET to a TLS port) fails in ~2s, not 5s+. + first_wait = [datastore['FIRST_BYTE_WAIT'].to_f, 2.0].min + idle_wait = datastore['IDLE_TIMEOUT'].to_f + buf = +'' + chunk = sock.get_once(-1, first_wait) + return nil if chunk.nil? || chunk.empty? + + buf << chunk + while buf.length < 65_536 + chunk = begin + sock.get_once(-1, idle_wait) + rescue StandardError + nil + end + break if chunk.nil? || chunk.empty? + + buf << chunk + end + buf + rescue ::StandardError + nil + end + + # Classify a raw HTTP response against known MSF HTTP-server signatures. + def http_classify(raw, scheme: 'http') + return nil if raw.nil? || raw.empty? + return nil unless raw =~ %r{\AHTTP/1\.[01]\s+(\d{3})} + + code = ::Regexp.last_match(1).to_i + server = raw[/^Server:[ \t]*(.+?)\r?$/i, 1].to_s.strip + body = raw.partition("\r\n\r\n").last + server_str = server.empty? ? '(none)' : server + + # reverse_http(s) Meterpreter handler answers 200 OK with the default + # "It works!" body to ANY unknown URI (real servers would 404). + if code == 200 && body.include?('It works!') + return { + payload: "Metasploit reverse_#{scheme} Meterpreter handler (HTTP transport)", + framing: %(200 OK + default "It works!" body on an unknown URI; Server: #{server_str}), + confidence: 'high' + } + end + + # Rex HTTP server (web_delivery, fetch handler, exploit module servers) + # returns a distinctive 404 page on unknown URIs. + if body.include?('was not found on this server') && body.include?('

Not found

') + return { + payload: 'Metasploit Rex HTTP server (web_delivery / fetch handler / exploit module server)', + framing: "Rex 404 page on unknown URI; Server: #{server_str}", + confidence: 'high' + } + end + + nil + end + + # Classify the unsolicited data using the staging-protocol framing. + # + # The strongest, most reliable signal is "the 4-byte length the server + # declared up front was actually delivered" (body.length >= declared length). + # A benign service that happens to talk first will not satisfy this: a short + # text banner produces an enormous bogus length, and a service streaming lots + # of data is astronomically unlikely to have its first 4 bytes equal the + # number of bytes that follow. Note metsrv appends a config block after the + # stage, so body.length is frequently *larger* than the declared length. + def fingerprint(buf) + return { match: false } if buf.nil? || buf.empty? + + bytes = buf.length + return { match: false } if bytes < 8 + + body = buf[4..] || +'' + l_le = buf[0, 4].unpack1('V') + l_be = buf[0, 4].unpack1('N') + + # 1. Command-shell handler echo probe. Msf::Sessions::CommandShell#bootstrap + # verifies a reverse shell by sending "echo ". + # Catches stageless/staged reverse *shell* handlers that talk first. + if (md = buf.match(/\Aecho ([A-Za-z0-9]{8,24});?\s*\z/)) + return { + match: true, + payload: 'Metasploit command shell handler (reverse shell - "echo" verification probe)', + framing: 'unsolicited "echo " shell-verification command', + confidence: 'high', + bytes: bytes, + echo_token: md[1] + } + end + + # 2. Python staged: big-endian length prefix + base64(zlib(...)) text stage. + if base64_stage?(body) && l_be.positive? + confidence = (l_be == body.length) ? 'high' : 'medium' + return { + match: true, + payload: 'python/meterpreter/reverse_tcp (base64/zlib staged)', + framing: "4-byte big-endian length prefix (declared #{l_be}); base64/zlib stage", + confidence: confidence, + bytes: bytes + } + end + + # 3. Native staged, little-endian length prefix (Windows family). + if plausible_stage_len?(l_le) && body.length >= l_le + return { + match: true, + payload: windows_payload_guess(l_le), + framing: "4-byte little-endian length prefix (declared #{l_le}, #{bytes} bytes received)", + confidence: 'high', + bytes: bytes + } + end + + # 4. Native staged, big-endian length prefix but not base64 (php/java/uncommon). + if plausible_stage_len?(l_be) && body.length >= l_be + return { + match: true, + payload: big_endian_payload_guess(body), + framing: "4-byte big-endian length prefix (declared #{l_be}, #{bytes} bytes received)", + confidence: 'medium', + bytes: bytes + } + end + + # 5. Small unix execve stage (linux/bsd staged shell). These send raw + # /bin/sh shellcode with no length prefix, below the binary-burst floor. + # Gated to small buffers so large meterpreter stages (which may embed + # "/bin/sh") fall through to the generic native-stage branch instead. + if bytes < 4096 && (buf.include?('/bin/sh') || buf.include?('//sh')) + return { + match: true, + payload: 'native staged unix shell (execve /bin/sh shellcode, e.g. linux/*/shell/reverse_tcp)', + framing: "raw execve shellcode stage, no length prefix (#{bytes} bytes)", + confidence: 'medium', + bytes: bytes + } + end + + # 5b. Raw x86 stager shellcode with no length prefix. Legacy Windows stagers + # (reverse_nonx_tcp, reverse_ord_tcp, and the *_nonx/_ord shell, vncinject + # and patchupmeterpreter variants) push the stage as bare shellcode of only + # ~200-260 bytes starting with the classic block_api prelude (0xFC = cld) + # rather than a 4-byte length prefix, so they sit just under the generic + # binary-burst floor below. A benign text service cannot start with 0xFC. + if bytes >= 48 && buf.b.start_with?("\xFC".b) && printable_ratio(buf) < 0.9 + return { + match: true, + payload: 'Windows staged payload - raw stager shellcode (e.g. reverse_nonx_tcp/reverse_ord_tcp/shell)', + framing: "raw x86 stager shellcode, no length prefix (#{bytes} bytes, 0xFC prelude)", + confidence: 'medium', + bytes: bytes + } + end + + # 6. Raw stage with no length prefix (Linux/OSX native), an RC4-encrypted + # stage, or an encrypted stageless handler stream. Either way the server + # volunteered a burst of non-text data unprompted, which is highly abnormal + # for a benign service. The 128-byte floor still clears short binary banners + # while catching the small (~240B) RC4 and legacy stager bursts. + if bytes >= 128 && printable_ratio(buf) < 0.75 + return { + match: true, + payload: 'native staged meterpreter (linux/osx) or encrypted/stageless handler', + framing: "no length prefix; #{bytes} bytes of unsolicited binary data", + confidence: 'low', + bytes: bytes + } + end + + { match: false } + end + + # Refine a big-endian length-prefixed stage into its source family by content. + def big_endian_payload_guess(body) + sample = body[0, 65_536].to_s + if sample.include?(' 200_000 + 'windows/x64/meterpreter/reverse_tcp (native staged)' + else + 'windows/meterpreter/reverse_tcp (x86 native staged)' + end + end + + def printable_ratio(str) + return 0.0 if str.nil? || str.empty? + + str.count("\x20-\x7e\t\r\n").to_f / str.length + end +end