Metasploit Deep Dive Part 14: Closing the Loop
Thirteen parts ago I said this series would end with a full walkthrough tying everything together against the lab. I’ve gone back and forth on what that should actually mean, and landed somewhere I didn’t expect: no new commands. Everything hands-on that’s going to happen already happened, across Part 6, Part 7, and Part 8. What’s left is the bit I skipped in the moment because I was too busy getting a shell: going back through the internals I wrote up in Parts 2 through 5 and 9 through 13, and checking them against what actually happened on the wire, in the console, and on disk. Some of it held up exactly as described. Some of it turned out to matter in ways the theory posts didn’t anticipate. And some of it — quite a lot of it, if I’m honest — never got exercised at all, which is its own kind of finding.
Consider this the series’ own after-action review.
The datastore was doing more than I gave it credit for
Part 2 covered Msf::Module and its nineteen mixins in the abstract: options live in a datastore, check and run are just methods the mixins wire up differently depending on module type. It’s easy to read that as plumbing and move on. It stopped being plumbing the moment wp_file_manager_rce’s own check came back with a false negative against target-wordpress and I had to decide, in real time, whether to trust it or override with set AutoCheck false and just run the thing. That’s the datastore and the check/exploit split doing actual work: the module author’s confidence in their own detection logic is separable from the exploit itself, and Metasploit lets you say “I don’t trust your check, try anyway” without touching a line of Ruby. It’s a small thing. It’s also exactly the kind of small thing that’s invisible until you’re standing in front of it wondering if the box is patched or the fingerprint is just wrong.
The CVE here, for the record, is CVE-2020-25213 — unauthenticated arbitrary file upload in the WP File Manager plugin, which is a startlingly common way into a WordPress box even now. The exploit dropped its payload at /wp-content/plugins/wp-file-manager/lib/files/AoFWQh.php, ran it, and cleaned up after itself. All the module identification I fumbled through beforehand — there is no exploit/unix/webapp/wp_file_manager_rce, it’s exploit/multi/http/wp_file_manager_rce — is exactly the kind of namespace confusion Part 2’s module-type taxonomy exists to prevent, if you actually remember to search instead of guessing.
Stages, singles, and the number I got wrong
Part 3 walked through the difference between a single payload (self-contained, no second stage) and a staged one (small stager, then a fetched stage over the established channel). The WordPress exploit is the staged case in the wild: “Sending stage (72686 bytes) to 10.10.10.40” is msfvenom’s staged PHP meterpreter arriving over the connection the stager already opened. I’d originally written that line up with the wrong byte count in the Part 8 draft — a number I’d half-remembered instead of pulled from the actual transcript — and caught it while preparing this post, which is its own small lesson in why “real verbatim output only” is a rule worth having rather than a style preference.
The pivot exploitation later in Part 8 is the single-payload case, and it’s the more interesting one precisely because the choice was forced rather than stylistic. cmd/unix/bind_awk needs nothing but LPORT — no callback, no staging, nothing that depends on the target being able to reach back out. That’s not a preference, it’s a constraint, and it’s the same constraint Part 5 describes in the abstract as “no route back” scenarios. Theory met lab and theory was right, which doesn’t happen every time.
The session that died was never a protocol failure
Part 4 is the one about meterpreter’s actual wire format — the TLV packet structure, the XOR-obfuscated header, the AES layer underneath. It’s tempting, when a session dies unexpectedly, to go looking for a protocol-level explanation. The PHP meterpreter session against target-wordpress died a few minutes in, and the honest answer had nothing to do with TLV framing or crypto — it was PHP’s own max_execution_time catching up with a long-running script that was never designed to run forever. The fix, post/multi/manage/shell_to_meterpreter, doesn’t repair a broken protocol; it replaces a fragile execution environment (a PHP process with a ceiling on its own lifetime) with a durable one (a native ELF meterpreter with no such ceiling). Worth remembering: not every session death is a wire-protocol problem, and the TLV internals I spent a whole post on turned out to be the one thing that wasn’t at fault here.
Transports: reverse almost everywhere, until it couldn’t be
Part 5 catalogued the full list of handlers — reverse_tcp, bind_tcp, the SSH and SSM oddities, the lot. Every real session in this series used plain reverse_tcp, which is the boring, correct default when the target can reach the attacker and nothing’s in the way. The pivot changed that calculus entirely: once the route to hidden-target only existed through an existing meterpreter session and there was no NAT rule, no firewall exception, nothing bringing traffic back out to Kali1, a reverse payload was never going to complete a handshake. cmd/unix/bind_awk listening locally and being connected to through the pivot was the only shape that could work. Part 5 framed bind vs. reverse as a NAT/firewall question in the abstract; the lab version of that question was “is there a route back at all,” which is really the same question with the firewall replaced by network segmentation. Same answer either way.
Evasion: nothing needed, and that’s the finding
Part 9 argued that classic encoders are diminished rather than dead, and that reflective, memory-only loading still beats disk-scanning AV specifically — file-based detection, not behavioural. Nothing in this series’ actual runs needed any of that, and it’s worth being honest about why: there was no AV or EDR on any of these boxes to evade. The one place the theory actually bit was PHP meterpreter itself, which — unlike a native shellcode meterpreter loaded reflectively into memory — has to exist as an actual .php file on disk for PHP to execute it at all. That’s not an evasion failure, it’s a structural property of the payload type, and it’s the reason the exploit module deletes AoFWQh.php immediately after triggering it: the artifact has to touch disk, however briefly, so the cleanup step is doing the evasion work that reflective loading does for free on other platforms.
Detection: the wire pattern Part 10 described was there, nobody was watching for it
Part 10 is about network-level detection: the two-phase shape of a staged handshake, default ports and certs, JA3/JA3S fingerprinting, beaconing cadence. The staged handshake against target-wordpress has exactly that two-phase shape — small stager in, “Sending stage (72686 bytes)” as the second phase — and it happened on port 4446 rather than the famous default of 4444, which is a coincidence of what I typed into LPORT, not a deliberate evasion choice, but it’s a fair illustration of how little effort it takes to sidestep a signature built around a well-known default.
Here’s the honest gap, though: nothing in this lab ever tested Part 10’s claims against an actual network monitor. Wazuh, which caught the two findings I’ll get to below, is host-based — it’s reading OS-level and application logs on the endpoints themselves, not watching packets on the wire. A post about JA3 fingerprinting and beaconing cadence needs a Suricata or Zeek sensor sitting on the span port to actually validate any of it, and that sensor doesn’t exist in this lab yet. Consider that the most concrete unfinished business this series leaves behind.
RPC and MCP: theory that never left the page
Part 11 and Part 12 covered msfrpcd and msfmcpd respectively — MessagePack RPC, the rpc_* call families, and Rapid7’s own MCP server with its split of twelve read-only tools and four gated dangerous ones. None of it got touched. Every single command across Parts 6 through 8 was typed by hand at an interactive msfconsole prompt, including the moments where I typo’d a payload name and had to show payloads to find the real one. That’s not a failure of the lab, it’s just accurate: automation-layer tooling doesn’t get exercised by a human doing exploratory pentesting against three boxes over an evening, and there’s no reason to pretend otherwise. If there’s a natural next series here, it’s standing up msfrpcd or msfmcpd and driving the same three exploits through it instead of the console — a good excuse to actually validate Part 12’s tool-gating claims against something real instead of Rapid7’s own documentation.
The database that never got used
Part 13 is probably the one this retrospective embarrasses the most. Workspaces, hosts/services, vuln and note tracking, credential create/invalidate, loot metadata — all of it exists specifically so that findings from an engagement like this one persist somewhere queryable instead of scattered across a terminal scrollback. And that is, verbatim, what happened instead: target-win2022’s identity, target-wordpress’s exploitation path, hidden-target’s IP and the fact it’s running an unregistered Wazuh agent — all of it lives in this blog’s drafts and in a status document, never in an actual msfdb workspace. msfdb init was never even run against this lab. Three real hosts, three real findings, zero rows in the one piece of the framework purpose-built to hold them. If I’m being honest about what “closing the loop” should mean, this is the loop that’s still open: the next time I run anything against this lab, it goes through a workspace from the first command, not written up after the fact from memory and pasted terminal output.
The bit that actually mattered: getting the host wrong
None of the internals posts predicted this, because it isn’t really about Metasploit at all. The wrong-host detour in Part 8 — chasing .50, getting empty banner grabs, eventually finding hidden-target sitting on .10 instead — was basic reconnaissance discipline failing before any exploit code ran. No mixin, transport, or evasion technique fixes “I scanned the wrong IP.” The framework did exactly what it was told to do against a target that wasn’t the one I meant. .50 turned out to be target-win2022, a leftover from an entirely unrelated earlier post, dual-homed onto the same bridge — not a bug, just two demos sharing a subnet with no name tags on. ARP plus a MAC-address grep against the Proxmox configs sorted it out in about thirty seconds once I thought to check it that way, and it’s a good reminder that the most useful diagnostic tool in this entire series wasn’t in msfconsole at all.
What the Wazuh findings actually showed
Two things came out of pointing Wazuh at all this, and they cut in opposite directions. hidden-target never showed up in agent_control -l at all — no agent installed, no coverage, nothing. That’s the boring, expected gap: an unmonitored host is unmonitored, full stop. The more interesting finding was on target-wordpress, where the agent was healthy, reporting, and completely blind to the actual RCE — its alerts were routine SSH and PAM noise, nothing that so much as gestured at a malicious file being written into a plugin directory and executed. A working agent and zero relevant detection aren’t a contradiction; they’re a fair description of what host-based log shipping gets you without web-application-layer visibility or file-integrity monitoring pointed at the right directories. Wazuh doing its job correctly and still missing the actual incident is, if anything, the most useful single data point this whole series produced.
The agent list itself carries one more unused thread worth naming. DC01, a real Active Directory domain controller, sits registered on this same Wazuh manager and never got touched by anything in this series. That domain exists for exactly the kind of Kerberos and ACL-abuse techniques this site’s Impacket and BloodHound series already covered against a documented-only lab. Pointing Metasploit’s own post-exploitation modules at a live domain controller, with this same Wazuh manager watching, is the obvious next lab project once there’s a reason to open it.
What thirteen parts of theory bought me
Going through this after the fact, the honest scorecard is: the module/datastore/check split, the stage-vs-single distinction, and the bind-vs-reverse transport choice all showed up exactly where the theory posts said they would, and understanding them beforehand made the real-time decisions faster rather than mysterious. The TLV internals and the evasion techniques were mostly not load-bearing for this particular lab, because nothing in it needed to survive scrutiny from an EDR product or reconstruct a corrupted session. And the automation layer — RPC, MCP, the database — sat entirely unused, which says less about Metasploit and more about how a solo evening of hands-on pentesting differs from a sustained engagement with a team behind it.
That’s a fair place to leave a fourteen-part series: not “everything got used,” but a reasonably clear map of which parts of the framework earned their keep against a real target, and which ones are still waiting for a lab that actually needs them.
Sources:
- Part 6: CVE-2014-3120, a Real Module, and Three Things Windows Defender Got Right
- Part 7: A Different Door, the Same Wall
- Part 8: Pivoting Into the Blind Spot
- NVD — CVE-2020-25213 (WP File Manager)
- NVD — CVE-2011-2523 (vsftpd 2.3.4 backdoor)