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
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) |
noisgate verdict.
The single decisive factor is that this is a client-side flaw in ssh that requires an unquoted ProxyCommand %r in the victim's config plus victim interaction with an attacker-controlled repo — the vast majority of installs have no ProxyCommand and the bug cannot touch sshd. Blast radius is capped at the invoking user's UID with no priv-esc primitive.
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.
What to do — in priority order.
- Quote
%rin everyProxyCommand— ChangeProxyCommand ssh %r@%h ...toProxyCommand ssh '%r'@%h ...in/etc/ssh/ssh_configand user configs. This neutralizes both CVEs by preventing shell interpretation of control characters. No mitigation SLA at LOW severity — apply during routine config hygiene. - Enforce
protocol.ssh.allow=useron CI runners and dev workstations — Setgit config --global protocol.ssh.allow user(orGIT_ALLOW_PROTOCOL=file:git:http:https) so submodule URLs cannot force thesshtransport. Cuts off the primary delivery vector regardless of OpenSSH version. Deploy via config management within the 365-day remediation window. - Roll distro OpenSSH package updates through normal patching — RHEL, Debian, Ubuntu, Alpine, Rocky, and Alma have all backported 10.1 fixes. Include in the next monthly package baseline — no emergency change window needed.
- Audit
~/.ssh/configsprawl on developer laptops — Run a one-shot inventory forProxyCommanddirectives across your MDM-managed endpoints. Any hit gets remediated with the quoted-%rfix 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.
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
If you remember one thing.
protocol.ssh.allow=user to all CI runners and developer endpoints, and (2) grep your standard ssh_config templates for unquoted %r in ProxyCommand and quote them. If your CI plane clones untrusted third-party repos with submodule recursion, prioritize those runners first — they are the only place this CVE pair has a realistic weaponization story.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.