Backport patch to fix CVE-2021-36370. Upstream-Status: Backport [https://github.com/MidnightCommander/mc/commit/9235d3c] CVE: CVE-2021-36370 Signed-off-by: Kai Kang From 9235d3c232d13ad7f973346077c9cf2eaa77dc5f Mon Sep 17 00:00:00 2001 From: Andrew Borodin Date: Mon, 12 Jul 2021 08:48:18 +0300 Subject: [PATCH] SFTPFS: verify server fingerprint (fix CVE-2021-36370). Use ~/.ssh/known_hosts file to verify server fingerprint using ssh way: $ ssh localhost The authenticity of host 'localhost (127.0.0.1)' can't be established. ED25519 key fingerprint is SHA256:FzqKTNTroFuNUj1wUzSeV2x/1lpcESnT0ZRCmq5H6o8. Are you sure you want to continue connecting (yes/no)? no ssh: Host key verification failed. $ ssh localhost The authenticity of host 'localhost (127.0.0.1)' can't be established. ED25519 key fingerprint is SHA256:FzqKTNTroFuNUj1wUzSeV2x/1lpcESnT0ZRCmq5H6o8. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added 'localhost' (ED25519) to the list of known hosts. andrew@localhost's password: Thanks the Curl project for the used code. Signed-off-by: Andrew Borodin Signed-off-by: Yury V. Zaytsev --- doc/man/mc.1.in | 15 ++ doc/man/ru/mc.1.in | 14 ++ src/vfs/sftpfs/connection.c | 428 +++++++++++++++++++++++++++++++++++- src/vfs/sftpfs/internal.h | 5 +- 4 files changed, 452 insertions(+), 10 deletions(-) diff --git a/doc/man/mc.1.in b/doc/man/mc.1.in index c0c06e32f7..7a3d118384 100644 --- a/doc/man/mc.1.in +++ b/doc/man/mc.1.in @@ -3364,6 +3364,21 @@ Examples: sftp://joe@noncompressed.ssh.edu/private sftp://joe@somehost.ssh.edu:2222/private .fi +.PP +When establishing the connection, server key fingerprint is verified using +the ~/.ssh/known_hosts file. If the host/key pair is not found or the host is found, +but the key doesn't match, an appropriate message is shown. +There are three buttons in the message dialog: +.PP +.B [Yes] +add new host/key pair to the ~/.ssh/known_hosts file and continue. +.PP +.B [Ignore] +do not add new host/key pair to the ~/.ssh/known_hosts file, but continue +nevertheless (at you own risk). +.PP +.B [No] +abort connection. .\"NODE " Undelete File System" .SH " Undelete File System" On Linux systems, if you asked configure to use the ext2fs undelete diff --git a/doc/man/ru/mc.1.in b/doc/man/ru/mc.1.in index 7609da1127..bc0c1810a9 100644 --- a/doc/man/ru/mc.1.in +++ b/doc/man/ru/mc.1.in @@ -3874,6 +3874,20 @@ bash\-совместимая оболочка shell. sftp://joe@noncompressed.ssh.edu/private sftp://joe@somehost.ssh.edu:2222/private .fi +При установлении соединения происходит проверка ключа сервера с использованием +файла ~/.ssh/known_hosts file. Если пара сервер/ключ в этом файле не найдена +или сервер найден, но ключ не соответствует, пользователю показывается +окно с соответствующим сообщением, содержащее три кнопки: +.PP +.B [Да] +добавить новую пару сервер/ключ в файл ~/.ssh/known_hosts и продолжить соединение. +.PP +.B [Игнорировать] +не добавлять новую пару сервер/ключ в файл ~/.ssh/known_hosts и всё равно +продолжить соединение (на свой страх и риск). +.PP +.B [Нет] +прервать соединение. .\"NODE " Undelete File System" .SH " Файловая система UFS (Undelete File System)" В ОС Linux можно сконфигурировать файловую систему ext2fs, используемую diff --git a/src/vfs/sftpfs/connection.c b/src/vfs/sftpfs/connection.c index 9f8ea5633b..acd5026515 100644 --- a/src/vfs/sftpfs/connection.c +++ b/src/vfs/sftpfs/connection.c @@ -42,6 +42,8 @@ #include "lib/util.h" #include "lib/tty/tty.h" /* tty_enable_interrupt_key () */ #include "lib/vfs/utilvfs.h" +#include "lib/mcconfig.h" /* mc_config_get_home_dir () */ +#include "lib/widget.h" /* query_dialog () */ #include "internal.h" @@ -49,10 +51,37 @@ /*** file scope macro definitions ****************************************************************/ +#define SHA1_DIGEST_LENGTH 20 + /*** file scope type declarations ****************************************************************/ /*** file scope variables ************************************************************************/ +#ifdef LIBSSH2_KNOWNHOST_KEY_ED25519 +static const char *const hostkey_method_ssh_ed25519 = "ssh-ed25519"; +#endif +#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_521 +static const char *const hostkey_method_ssh_ecdsa_521 = "ecdsa-sha2-nistp521"; +#endif +#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_384 +static const char *const hostkey_method_ssh_ecdsa_384 = "ecdsa-sha2-nistp384"; +#endif +#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_256 +static const char *const hostkey_method_ssh_ecdsa_256 = "ecdsa-sha2-nistp256"; +#endif +static const char *const hostkey_method_ssh_rsa = "ssh-rsa"; +static const char *const hostkey_method_ssh_dss = "ssh-dss"; + +/** + * + * The current implementation of know host key checking has following limitations: + * + * - Only plain-text entries are supported (`HashKnownHosts no` OpenSSH option) + * - Only HEX-encoded SHA1 fingerprint display is supported (`FingerprintHash` OpenSSH option) + * - Resolved IP addresses are *not* saved/validated along with the hostnames + * + */ + static const char *kbi_passwd = NULL; static const struct vfs_s_super *kbi_super = NULL; @@ -70,9 +99,12 @@ static const struct vfs_s_super *kbi_super = NULL; static int sftpfs_open_socket (struct vfs_s_super *super, GError ** mcerror) { + sftpfs_super_t *sftpfs_super = SFTP_SUPER (super); struct addrinfo hints, *res = NULL, *curr_res; int my_socket = 0; char port[BUF_TINY]; + static char address_ipv4[INET_ADDRSTRLEN]; + static char address_ipv6[INET6_ADDRSTRLEN]; int e; mc_return_val_if_error (mcerror, LIBSSH2_INVALID_SOCKET); @@ -120,6 +152,30 @@ sftpfs_open_socket (struct vfs_s_super *super, GError ** mcerror) { int save_errno; + switch (curr_res->ai_addr->sa_family) + { + case AF_INET: + sftpfs_super->ip_address = + inet_ntop (AF_INET, &((struct sockaddr_in *) curr_res->ai_addr)->sin_addr, + address_ipv4, INET_ADDRSTRLEN); + break; + case AF_INET6: + sftpfs_super->ip_address = + inet_ntop (AF_INET6, &((struct sockaddr_in6 *) curr_res->ai_addr)->sin6_addr, + address_ipv6, INET6_ADDRSTRLEN); + break; + default: + sftpfs_super->ip_address = NULL; + } + + if (sftpfs_super->ip_address == NULL) + { + mc_propagate_error (mcerror, 0, "%s", + _("sftp: failed to convert remote host IP address into text form")); + my_socket = LIBSSH2_INVALID_SOCKET; + goto ret; + } + my_socket = socket (curr_res->ai_family, curr_res->ai_socktype, curr_res->ai_protocol); if (my_socket < 0) @@ -161,8 +217,358 @@ sftpfs_open_socket (struct vfs_s_super *super, GError ** mcerror) } /* --------------------------------------------------------------------------------------------- */ + +/** + * Read ~/.ssh/known_hosts file. + * + * @param super connection data + * @param mcerror pointer to the error handler + * @return TRUE on success, FALSE otherwise + * + * Thanks the Curl project for the code used in this function. + */ +static gboolean +sftpfs_read_known_hosts (struct vfs_s_super *super, GError ** mcerror) +{ + sftpfs_super_t *sftpfs_super = SFTP_SUPER (super); + struct libssh2_knownhost *store = NULL; + int rc; + gboolean found = FALSE; + + sftpfs_super->known_hosts = libssh2_knownhost_init (sftpfs_super->session); + if (sftpfs_super->known_hosts == NULL) + goto err; + + sftpfs_super->known_hosts_file = + mc_build_filename (mc_config_get_home_dir (), ".ssh", "known_hosts", (char *) NULL); + rc = libssh2_knownhost_readfile (sftpfs_super->known_hosts, sftpfs_super->known_hosts_file, + LIBSSH2_KNOWNHOST_FILE_OPENSSH); + if (rc > 0) + { + const char *kh_name_end = NULL; + + while (!found && libssh2_knownhost_get (sftpfs_super->known_hosts, &store, store) == 0) + { + /* For non-standard ports, the name will be enclosed in + * square brackets, followed by a colon and the port */ + if (store == NULL) + continue; + + if (store->name == NULL) + found = TRUE; + else if (store->name[0] != '[') + found = strcmp (store->name, super->path_element->host) == 0; + else + { + int port; + + kh_name_end = strstr (store->name, "]:"); + if (kh_name_end == NULL) + /* Invalid host pattern */ + continue; + + port = (int) g_ascii_strtoll (kh_name_end + 2, NULL, 10); + if (port == super->path_element->port) + { + size_t kh_name_size; + + kh_name_size = strlen (store->name) - 1 - strlen (kh_name_end); + found = strncmp (store->name + 1, super->path_element->host, kh_name_size) == 0; + } + } + } + } + + if (found) + { + int mask; + const char *hostkey_method = NULL; + + mask = store->typemask & LIBSSH2_KNOWNHOST_KEY_MASK; + + switch (mask) + { +#ifdef LIBSSH2_KNOWNHOST_KEY_ED25519 + case LIBSSH2_KNOWNHOST_KEY_ED25519: + hostkey_method = hostkey_method_ssh_ed25519; + break; +#endif +#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_521 + case LIBSSH2_KNOWNHOST_KEY_ECDSA_521: + hostkey_method = hostkey_method_ssh_ecdsa_521; + break; +#endif +#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_384 + case LIBSSH2_KNOWNHOST_KEY_ECDSA_384: + hostkey_method = hostkey_method_ssh_ecdsa_384; + break; +#endif +#ifdef LIBSSH2_KNOWNHOST_KEY_ECDSA_256 + case LIBSSH2_KNOWNHOST_KEY_ECDSA_256: + hostkey_method = hostkey_method_ssh_ecdsa_256; + break; +#endif + case LIBSSH2_KNOWNHOST_KEY_SSHRSA: + hostkey_method = hostkey_method_ssh_rsa; + break; + case LIBSSH2_KNOWNHOST_KEY_SSHDSS: + hostkey_method = hostkey_method_ssh_dss; + break; + case LIBSSH2_KNOWNHOST_KEY_RSA1: + mc_propagate_error (mcerror, 0, "%s", + _("sftp: found host key of unsupported type: RSA1")); + return FALSE; + default: + mc_propagate_error (mcerror, 0, "%s %d", _("sftp: unknown host key type:"), mask); + return FALSE; + } + + rc = libssh2_session_method_pref (sftpfs_super->session, LIBSSH2_METHOD_HOSTKEY, + hostkey_method); + if (rc < 0) + goto err; + } + + return TRUE; + + err: + { + int sftp_errno; + + sftp_errno = libssh2_session_last_errno (sftpfs_super->session); + sftpfs_ssherror_to_gliberror (sftpfs_super, sftp_errno, mcerror); + } + return FALSE; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Write new host + key pair to the ~/.ssh/known_hosts file. + * + * @param super connection data + * @param remote_key he key for the remote host + * @param remote_key_len length of @remote_key + * @param type_mask info about format of host name, key and key type + * @return 0 on success, regular libssh2 error code otherwise + * + * Thanks the Curl project for the code used in this function. + */ +static int +sftpfs_update_known_hosts (struct vfs_s_super *super, const char *remote_key, size_t remote_key_len, + int type_mask) +{ + sftpfs_super_t *sftpfs_super = SFTP_SUPER (super); + int rc; + + /* add this host + key pair */ + rc = libssh2_knownhost_addc (sftpfs_super->known_hosts, super->path_element->host, NULL, + remote_key, remote_key_len, NULL, 0, type_mask, NULL); + if (rc < 0) + return rc; + + /* write the entire in-memory list of known hosts to the known_hosts file */ + rc = libssh2_knownhost_writefile (sftpfs_super->known_hosts, sftpfs_super->known_hosts_file, + LIBSSH2_KNOWNHOST_FILE_OPENSSH); + + if (rc < 0) + return rc; + + (void) message (D_NORMAL, _("Information"), + _("Permanently added\n%s (%s)\nto the list of known hosts."), + super->path_element->host, sftpfs_super->ip_address); + + return 0; +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Compute and return readable host key fingerprint hash. + * + * @param session libssh2 session handle + * @return pointer to static buffer on success, NULL otherwise + */ +static const char * +sftpfs_compute_fingerprint_hash (LIBSSH2_SESSION * session) +{ + static char result[SHA1_DIGEST_LENGTH * 3 + 1]; /* "XX:" for each byte, and EOL */ + const char *fingerprint; + size_t i; + + /* The fingerprint points to static storage (!), don't free() it. */ + fingerprint = libssh2_hostkey_hash (session, LIBSSH2_HOSTKEY_HASH_SHA1); + if (fingerprint == NULL) + return NULL; + + for (i = 0; i < SHA1_DIGEST_LENGTH && i * 3 < sizeof (result) - 1; i++) + g_snprintf ((gchar *) (result + i * 3), 4, "%02x:", (guint8) fingerprint[i]); + + /* remove last ":" */ + result[i * 3 - 1] = '\0'; + + return result; +} + +/* --------------------------------------------------------------------------------------------- */ + /** - * Recognize authenticaion types supported by remote side and filling internal 'super' structure by + * Process host info found in ~/.ssh/known_hosts file. + * + * @param super connection data + * @param mcerror pointer to the error handler + * @return TRUE on success, FALSE otherwise + * + * Thanks the Curl project for the code used in this function. + */ +static gboolean +sftpfs_process_known_host (struct vfs_s_super *super, GError ** mcerror) +{ + sftpfs_super_t *sftpfs_super = SFTP_SUPER (super); + const char *remote_key; + const char *key_type; + const char *fingerprint_hash; + size_t remote_key_len = 0; + int remote_key_type = LIBSSH2_HOSTKEY_TYPE_UNKNOWN; + int keybit = 0; + struct libssh2_knownhost *host = NULL; + int rc; + char *msg = NULL; + gboolean handle_query = FALSE; + + remote_key = libssh2_session_hostkey (sftpfs_super->session, &remote_key_len, &remote_key_type); + if (remote_key == NULL || remote_key_len == 0 + || remote_key_type == LIBSSH2_HOSTKEY_TYPE_UNKNOWN) + { + mc_propagate_error (mcerror, 0, "%s", _("sftp: cannot get the remote host key")); + return FALSE; + } + + switch (remote_key_type) + { + case LIBSSH2_HOSTKEY_TYPE_RSA: + keybit = LIBSSH2_KNOWNHOST_KEY_SSHRSA; + key_type = "RSA"; + break; + case LIBSSH2_HOSTKEY_TYPE_DSS: + keybit = LIBSSH2_KNOWNHOST_KEY_SSHDSS; + key_type = "DSS"; + break; +#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_256 + case LIBSSH2_HOSTKEY_TYPE_ECDSA_256: + keybit = LIBSSH2_KNOWNHOST_KEY_ECDSA_256; + key_type = "ECDSA"; + break; +#endif +#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_384 + case LIBSSH2_HOSTKEY_TYPE_ECDSA_384: + keybit = LIBSSH2_KNOWNHOST_KEY_ECDSA_384; + key_type = "ECDSA"; + break; +#endif +#ifdef LIBSSH2_HOSTKEY_TYPE_ECDSA_521 + case LIBSSH2_HOSTKEY_TYPE_ECDSA_521: + keybit = LIBSSH2_KNOWNHOST_KEY_ECDSA_521; + key_type = "ECDSA"; + break; +#endif +#ifdef LIBSSH2_HOSTKEY_TYPE_ED25519 + case LIBSSH2_HOSTKEY_TYPE_ED25519: + keybit = LIBSSH2_KNOWNHOST_KEY_ED25519; + key_type = "ED25519"; + break; +#endif + default: + mc_propagate_error (mcerror, 0, "%s", + _("sftp: unsupported key type, can't check remote host key")); + return FALSE; + } + + fingerprint_hash = sftpfs_compute_fingerprint_hash (sftpfs_super->session); + if (fingerprint_hash == NULL) + { + mc_propagate_error (mcerror, 0, "%s", _("sftp: can't compute host key fingerprint hash")); + return FALSE; + } + + rc = libssh2_knownhost_checkp (sftpfs_super->known_hosts, super->path_element->host, + super->path_element->port, remote_key, remote_key_len, + LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW | + keybit, &host); + + switch (rc) + { + default: + case LIBSSH2_KNOWNHOST_CHECK_FAILURE: + /* something prevented the check to be made */ + goto err; + + case LIBSSH2_KNOWNHOST_CHECK_MATCH: + /* host + key pair matched -- OK */ + break; + + case LIBSSH2_KNOWNHOST_CHECK_NOTFOUND: + /* no host match was found -- add it to the known_hosts file */ + msg = g_strdup_printf (_("The authenticity of host\n%s (%s)\ncan't be established!\n" + "%s key fingerprint hash is\nSHA1:%s.\n" + "Do you want to add it to the list of known hosts and continue connecting?"), + super->path_element->host, sftpfs_super->ip_address, + key_type, fingerprint_hash); + /* Select "No" initially */ + query_set_sel (2); + rc = query_dialog (_("Warning"), msg, D_NORMAL, 3, _("&Yes"), _("&Ignore"), _("&No")); + g_free (msg); + handle_query = TRUE; + break; + + case LIBSSH2_KNOWNHOST_CHECK_MISMATCH: + msg = g_strdup_printf (_("%s (%s)\nis found in the list of known hosts but\n" + "KEYS DO NOT MATCH! THIS COULD BE A MITM ATTACK!\n" + "Are you sure you want to add it to the list of known hosts and continue connecting?"), + super->path_element->host, sftpfs_super->ip_address); + /* Select "No" initially */ + query_set_sel (2); + rc = query_dialog (MSG_ERROR, msg, D_ERROR, 3, _("&Yes"), _("&Ignore"), _("&No")); + g_free (msg); + handle_query = TRUE; + break; + } + + if (handle_query) + switch (rc) + { + case 0: + /* Yes: add this host + key pair, continue connecting */ + if (sftpfs_update_known_hosts (super, remote_key, remote_key_len, + LIBSSH2_KNOWNHOST_TYPE_PLAIN + | LIBSSH2_KNOWNHOST_KEYENC_RAW | keybit) < 0) + goto err; + break; + case 1: + /* Ignore: do not add this host + key pair, continue connecting anyway */ + break; + case 2: + default: + mc_propagate_error (mcerror, 0, "%s", _("sftp: host key verification failed")); + /* No: abort connection */ + goto err; + } + + return TRUE; + + err: + { + int sftp_errno; + + sftp_errno = libssh2_session_last_errno (sftpfs_super->session); + sftpfs_ssherror_to_gliberror (sftpfs_super, sftp_errno, mcerror); + } + + return FALSE; +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Recognize authentication types supported by remote side and filling internal 'super' structure by * proper enum's values. * * @param super connection data @@ -461,6 +867,9 @@ sftpfs_open_connection (struct vfs_s_super *super, GError ** mcerror) if (sftpfs_super->session == NULL) return (-1); + if (!sftpfs_read_known_hosts (super, mcerror)) + return (-1); + /* ... start it up. This will trade welcome banners, exchange keys, * and setup crypto, compression, and MAC layers */ @@ -475,13 +884,8 @@ sftpfs_open_connection (struct vfs_s_super *super, GError ** mcerror) return (-1); } - /* At this point we havn't yet authenticated. The first thing to do - * is check the hostkey's fingerprint against our known hosts Your app - * may have it hard coded, may go to a file, may present it to the - * user, that's your call - */ - sftpfs_super->fingerprint = - libssh2_hostkey_hash (sftpfs_super->session, LIBSSH2_HOSTKEY_HASH_SHA1); + if (!sftpfs_process_known_host (super, mcerror)) + return (-1); if (!sftpfs_recognize_auth_types (super)) { @@ -538,7 +942,13 @@ sftpfs_close_connection (struct vfs_s_super *super, const char *shutdown_message sftpfs_super->agent = NULL; } - sftpfs_super->fingerprint = NULL; + if (sftpfs_super->known_hosts != NULL) + { + libssh2_knownhost_free (sftpfs_super->known_hosts); + sftpfs_super->known_hosts = NULL; + } + + MC_PTR_FREE (sftpfs_super->known_hosts_file); if (sftpfs_super->session != NULL) { diff --git a/src/vfs/sftpfs/internal.h b/src/vfs/sftpfs/internal.h index 5616fb8990..643ce5e3cc 100644 --- a/src/vfs/sftpfs/internal.h +++ b/src/vfs/sftpfs/internal.h @@ -42,6 +42,9 @@ typedef struct sftpfs_auth_type_t auth_type; sftpfs_auth_type_t config_auth_type; + LIBSSH2_KNOWNHOSTS *known_hosts; + char *known_hosts_file; + LIBSSH2_SESSION *session; LIBSSH2_SFTP *sftp_session; @@ -51,7 +54,7 @@ typedef struct char *privkey; int socket_handle; - const char *fingerprint; + const char *ip_address; vfs_path_element_t *original_connection_info; } sftpfs_super_t;