A malicious .gitmodules file whispers shell metacharacters into your SSH client's ear
This Tenable plugin bundles CVE-2025-61984 and CVE-2025-61985 — two closely related client-side flaws in the OpenSSH ssh binary, fixed in 10.1 / 10.1p1 (released 6 October 2025). The first allows control characters (including newlines) in usernames sourced from the command line or %r expansion inside ProxyCommand. The second allows a NUL byte inside an ssh:// URI. Both funnel unsanitized bytes into a shell that ProxyCommand invokes, giving an attacker code execution *on the machine running ssh* — not on the remote host. The realistic delivery vector, per finder David Leadbeater, is a poisoned .gitmodules file with a hostile username; a git clone --recurse-submodules or git submodule update then triggers the injection.
Tenable's LOW rating is *correct* and matches upstream. The bug is client-side, requires the victim to interact with attacker-controlled content, and only weaponizes when ProxyCommand is configured with an unquoted %r — the default OpenSSH build does not set ProxyCommand. CVSS 3.6 (AV:L/AC:H/PR:L/UI:N/C:L/I:L/A:N) reflects reality: local attack vector, high complexity, low impact. Nothing here threatens a production SSH server. Do not treat this like a sshd RCE — the plugin title 'Multiple Vulnerabilities' is misleading if you skim it.
4 steps from start to impact.
Attacker publishes a poisoned repository
.gitmodules entry whose submodule URL uses an ssh:// scheme with a username containing control characters (CVE-2025-61984) or an embedded \0 (CVE-2025-61985). The malicious payload is a shell fragment that will land in argv when ProxyCommand expands %r. Reference PoC: David Leadbeater's disclosure writeup and the openssh-unix-announce thread.- Attacker can host or contribute to a repo the victim will clone
- Victim's
~/.ssh/configsetsProxyCommandwith unquoted%r
- Most enterprise SSH clients do not use
ProxyCommandat all - Modern secure configs quote
%ras'%r'per OpenSSH 8.x hardening guidance
ProxyCommand use.Victim clones or updates submodules
git clone --recurse-submodules <repo> or CI runs git submodule update --init. Git resolves the ssh:// URL and hands it to ssh for the transport, passing the crafted username through argv.- Victim runs git with submodule recursion enabled
- Git is configured to allow the
sshprotocol for submodules (default in git < 2.38 and in many CI runners)
protocol.ssh.allow=userupstream default since git 2.38 blocks user-controlled submodule URLs from using ssh unless the top-level clone did- CI runners that pin
GIT_ALLOW_PROTOCOLblock this outright
.gitmodules alone won't flag it; look for ssh process launches with argv containing control bytes.ssh expands ProxyCommand %r into a shell
ssh binary substitutes the attacker-controlled username into the ProxyCommand template and forks /bin/sh -c. Control characters or the embedded NUL let the payload break out of the intended token and inject arbitrary commands. Weaponized handler: any interactive shell reachable via sh -c.ProxyCommandset with%runquoted- Non-interactive shell available
- Default OpenSSH install has no
ProxyCommand - Bastion-style configs typically use
%h/%pand quote%r
execve where parent is ssh and child argv contains \x00 or control chars is effective; add a Sigma-style rule.Code executes as the developer / CI runner user
- Injected payload avoids interactive prompts
- No priv-esc primitive included; blast radius is the invoking UID
- MacOS/Linux EDRs (CrowdStrike, SentinelOne, Defender for Endpoint) will flag
sh -cspawned fromsshwith anomalous argv
%r in every ProxyCommand — Change ProxyCommand ssh %r@%h ... to ProxyCommand ssh '%r'@%h ... in /etc/ssh/ssh_config and user configs. This neutralizes both CVEs by preventing shell interpretation of control characters. No mitigation SLA at LOW severity — apply during routine config hygiene.protocol.ssh.allow=user on CI runners and dev workstations — Set git config --global protocol.ssh.allow user (or GIT_ALLOW_PROTOCOL=file:git:http:https) so submodule URLs cannot force the ssh transport. Cuts off the primary delivery vector regardless of OpenSSH version. Deploy via config management within the 365-day remediation window.~/.ssh/config sprawl on developer laptops — Run a one-shot inventory for ProxyCommand directives across your MDM-managed endpoints. Any hit gets remediated with the quoted-%r fix regardless of OpenSSH version.- Blocking port 22 at the firewall — this is a client-side flaw, not a
sshdbug. Perimeter controls are irrelevant. - Disabling password auth / enforcing keys — auth mode has no bearing; the injection happens before any protocol handshake.
- Rotating host keys — the bug does not touch host keys or MITM primitives (that was CVE-2025-26465 earlier in 2025, a separate issue).
- SSH bastion enforcement — bastions may in fact be MORE exposed if they use
ProxyCommand; enforcement without quoting%ris neutral at best.
The supporting signals.
| CVEs bundled | CVE-2025-61984, CVE-2025-61985 |
|---|---|
| In-the-wild exploitation | None observed. No CISA KEV listing; no threat actor campaigns tied to this CVE pair as of July 2026. |
| Public PoC | Yes — David Leadbeater's disclosure includes reproduction steps via .gitmodules. No Metasploit/Nuclei module. |
| EPSS | 0.20% / 11th percentile — well below action threshold |
| CVSS 3.1 | 3.6 LOW — AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N (local vector, high complexity, low impact) |
| Affected versions | All OpenSSH prior to 10.1 / 10.1p1; distro backports available for RHEL (RHSA-2025:23481), Debian, Ubuntu, Alpine, Rocky, Alma |
| Fixed versions | OpenSSH 10.1 / 10.1p1 (released 2025-10-06); vendor backports across major distros |
| Exposure telemetry | Client-side only — Shodan/Censys/GreyNoise don't index ssh client installs; scanner surfaces every host with the binary regardless of risk |
| Disclosure | 6 October 2025 via openssh-unix-announce |
| Credit | David Leadbeater (independent researcher) |
Sources.
Why this verdict
- Client-side, not server-side: The bug is in
ssh, notsshd. Your 10,000-host SSH server fleet is not exposed by these CVEs. The Nessus plugin flags every host with the OpenSSH package, which vastly overstates real risk. - Requires opt-in config:
ProxyCommandwith unquoted%ris the trigger. Default OpenSSH deployments do not setProxyCommand, so the chain terminates at step 3 for most of the fleet. - User interaction required: Victim must clone or update a hostile git repo with submodule recursion, or paste a malicious
ssh://URI. This is a social-engineering / supply-chain chain, not a scanner-and-pop primitive. - No priv-esc: Execution lands as the invoking UID with no included escalation. On a dev workstation that means user-level RCE; on a CI runner, secrets exposure — but neither is fleet-scale.
- Role multiplier: Bastion / jump-host role — even if a jump host uses
ProxyCommand, the attacker still needs to induce an admin to runsshwith a hostile username. Chain succeeds but blast radius = one admin session, not fleet compromise. Not a floor-raising role. - Role multiplier: CI/CD runner cloning untrusted repos — chain succeeds with more automation, and secrets in the runner environment can leak. This is the one non-trivial scenario, but it's mitigated by
protocol.ssh.allow=user(git ≥ 2.38 default) and by pinning submodule sources. Blast radius = one runner's secrets, not the CI plane. - EPSS and KEV agree: 0.2% EPSS, no KEV, no in-the-wild activity — the exploit economy has correctly priced this as low-value.
Why not higher?
There is no path to MEDIUM without an unquoted ProxyCommand in your standard build AND a workflow that clones untrusted repos with submodule recursion. Even then the outcome is single-user RCE, not lateral movement or fleet impact. Nothing here threatens sshd, key material, or Kerberos/GSSAPI trust.
Why not lower?
Not IGNORE because CI/CD runners that clone third-party code with submodules ARE a plausible target, and the fix is trivially bundled into distro package updates you're already shipping. Leaving unpatched OpenSSH clients on build infrastructure is unnecessary residual risk when the remediation is free.
Crowdsourced verification payload.
Run on any Linux/macOS host to check both the OpenSSH client version and whether a risky ProxyCommand pattern is present. Invoke as sudo bash check-openssh-10.1.sh on the target host — root is needed only to read /etc/ssh/ssh_config and per-user configs.
#!/usr/bin/env bash
# noisgate verification for CVE-2025-61984 / CVE-2025-61985 (OpenSSH < 10.1)
# Exit 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
FIXED_MAJOR=10
FIXED_MINOR=1
if ! command -v ssh >/dev/null 2>&1; then
echo "UNKNOWN: ssh client not found"
exit 2
fi
VER_RAW=$(ssh -V 2>&1)
# Parse OpenSSH_X.Y[pN]
VER=$(printf '%s' "$VER_RAW" | sed -n 's/^OpenSSH_\([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1 \2/p')
if [ -z "$VER" ]; then
echo "UNKNOWN: could not parse version from: $VER_RAW"
exit 2
fi
MAJ=$(echo $VER | awk '{print $1}')
MIN=$(echo $VER | awk '{print $2}')
VULN=0
if [ "$MAJ" -lt "$FIXED_MAJOR" ]; then
VULN=1
elif [ "$MAJ" -eq "$FIXED_MAJOR" ] && [ "$MIN" -lt "$FIXED_MINOR" ]; then
VULN=1
fi
# Config exposure check: unquoted %r in any ProxyCommand
CFG_HIT=0
for f in /etc/ssh/ssh_config /etc/ssh/ssh_config.d/*.conf $HOME/.ssh/config; do
[ -r "$f" ] || continue
# Look for ProxyCommand lines containing %r that is NOT already single-quoted as '%r'
if grep -Ei '^[[:space:]]*ProxyCommand.*%r' "$f" 2>/dev/null | grep -Ev "'%r'" >/dev/null; then
CFG_HIT=1
echo "CONFIG-RISK: unquoted %r in ProxyCommand -> $f"
fi
done
if [ $VULN -eq 1 ] && [ $CFG_HIT -eq 1 ]; then
echo "VULNERABLE: OpenSSH $MAJ.$MIN with unquoted ProxyCommand %r"
exit 1
elif [ $VULN -eq 1 ]; then
echo "VULNERABLE: OpenSSH $MAJ.$MIN (< $FIXED_MAJOR.$FIXED_MINOR) — no risky ProxyCommand detected; still patch"
exit 1
else
echo "PATCHED: OpenSSH $MAJ.$MIN"
exit 0
fi