One CVE, Two Ecosystems: a Kopia RCE Check for Metasploit and Nuclei
CVE-2026-45695 is an argument-injection RCE in Kopia's SFTP ProxyCommand handling. I shipped a detection for it twice — a Metasploit exploit and a Nuclei template — and the two idioms disagree in instructive ways.
Same bug, two detection philosophies. I wrote a check for CVE-2026-45695 twice in one sitting — once as a Metasploit module, once as a Nuclei template — and the contrast is a cleaner lesson than either tool alone.
The bug: Kopia’s backup server exposes POST /api/v1/repo/exists without auth when it’s started --without-password and bound beyond loopback. The endpoint instantiates the storage backend before opening any repository, and for sftp with externalSSH it builds the ssh argument vector by splitting the caller’s sshArguments string on spaces — no tokenizer. So an -oProxyCommand=<cmd> token is handed straight to OpenSSH, which runs it through /bin/sh before it ever connects. Unauthenticated RCE as the server process (often root in a container). Affected <= 0.22.3, fixed in 0.23.0.
Both detections use the same time-based oracle — make ssh sleep, watch the clock. The one trick they share is ${IFS}, which supplies the space the splitter would otherwise eat:
-oProxyCommand=sleep${IFS}6
That’s where the two ecosystems part ways.
Nuclei is declarative. The whole detection is one request and a matcher — no code, no state:
matchers-condition: and
matchers:
- type: dsl
dsl:
- 'duration >= 6'
- type: word
part: body
words:
- 'unable to open SFTP storage'
Send, measure duration, confirm it’s Kopia by the error string. With a shodan-query in the metadata it fans out across the internet in one pass. It tells you this host is vulnerable and stops there — by design.
Metasploit is imperative, and it doesn’t stop. The module is a full Msf::Exploit::Remote (ExcellentRanking) with a payload session, not just a yes/no. Its check is the same sleep oracle, but exploit turns an arbitrary command into a single space-free token that survives the splitter intact:
def proxy_command_token(cmd)
b64 = Rex::Text.encode_base64(cmd).gsub(/\s+/, '')
"-oProxyCommand=echo${IFS}#{b64}|base64${IFS}-d|sh"
end
It runs blind — ssh is spawned only to fire the ProxyCommand and its output never comes back — so the default payload is cmd/unix/reverse_bash: call home rather than read a response.
The note worth keeping: declarative-and-fast vs. imperative-and-verified isn’t a quality gap, it’s a job difference. Reach for the Nuclei template when the question is how many of my hosts are exposed — breadth, speed, one matcher. Reach for the Metasploit module when the question is can this actually be driven to code execution — depth, a session, proof. Writing the same finding in both idioms back-to-back is the fastest way I’ve found to feel what each framework is actually built to optimize.
Newsletter
Liked this? Get the next one.
One essay or short note every other week — privacy-first software, AI, security, and the occasional dispatch from the trail. No filler.
More writing
My First Metasploit Module to Land: an Audiobookshelf Auth Bypass
CVE-2025-25205 is an unauthenticated auth bypass in Audiobookshelf caused by matching a whitelist regex against the full request URL — query string and all. Here's the bug, the crash-safe detection module I wrote for it, and what it took to get my first PR merged into the Metasploit Framework.
ReadWriting a Metasploit Module for a Pre-Auth SQLi in an LLM Gateway
How I turned CVE-2026-42208 — a time-based blind SQL injection in LiteLLM's proxy — into a benign, lab-verified Metasploit detection module, and what the Rapid7 review cycle taught me about shipping upstream.
ReadA Header You Were Never Supposed to Send: Detecting the Next.js Middleware Bypass
CVE-2025-29927 lets an external request skip Next.js middleware entirely — and with it every auth check built there — by sending one header the framework only ever meant to talk to itself. Here's the bug, and the Metasploit scanner I wrote to detect it without touching the data behind the gate.
Read