-15% na nową książkę sekuraka: Wprowadzenie do bezpieczeństwa IT. Przy zamówieniu podaj kod: 10000

OpenSSH – users enumeration – CVE-2018-15473

22 sierpnia 2018, 17:57 | Teksty | komentarze 4
We planned to publish the following article after release fixed OpenSSH version. However, due to the fact that technical details with working exploit were published on the Internet, we decided to publish our research.

Introduction

When testing infrastructure security, we often face the task of testing the security of the SSH server. One of the basic tests is to check the resistance to brute-force attacks. When performing such a type of attacks, knowledge of correct user names significantly increases the probability of success – instead of checking all possible combinations of potential usernames and passwords, we check the validity of passwords only for existing users. One way to obtain a list of correct usernames is enumeration, which, in simple terms, boils down to using the authentication server error to verify that the username is correct. If the authentication server allows to enumerate user names, the brute-force attack can be divided into two stages:

  1. We filter the list of potential user names by querying the server for the correctness of the given name
  2. We use the list of existing user names to perform the main brute-force attack

Due to the popularity of the OpenSSH server, I decided to analyze the server code for the vulnerability to enumerating user names. The result of the analysis is presented below.

Technical details

The latest version (as of 2018.07.16) of the OpenSSH server has been analyzed.

The auth2-pubkey.c file contains the code implementing the key authentication:

static int
userauth_pubkey(struct ssh *ssh)
{
    Authctxt *authctxt = ssh->authctxt;
    struct passwd *pw = authctxt->pw;
    struct sshbuf *b;
    struct sshkey *key = NULL;
    char *pkalg, *userstyle = NULL, *key_s = NULL, *ca_s = NULL;
    u_char *pkblob, *sig, have_sig;
    size_t blen, slen;
    int r, pktype;
    int authenticated = 0;
    struct sshauthopt *authopts = NULL;

    if (!authctxt->valid) {
        debug2("%s: disabled because of invalid user", __func__);
        return 0;
    }
    if ((r = sshpkt_get_u8(ssh, &have_sig)) != 0 ||
        (r = sshpkt_get_cstring(ssh, &pkalg, NULL)) != 0 ||
        (r = sshpkt_get_string(ssh, &pkblob, &blen)) != 0)
        fatal("%s: parse request failed: %s", __func__, ssh_err(r));
    pktype = sshkey_type_from_name(pkalg);

In the case when the authentication attempt takes place for a non-existent user, the condition checked at line 101 is satisfied, as a result the server completes the authentication process (line 103). Otherwise, when the username is correct, the authentication process continues.

Lines 105-107 contain the code responsible for parsing the SSH packet. If a parsing error occurs, the process ends in an error (calling the „fatal” function on line 108). At this stage of the analysis, we can assume that „gentle” (line 103) and emergency (line 108) ending of the authentication process should be clearly distinguishable from the SSH client’s perspective.

In order to develop a method for the emergency completion of the process, let’s analyze the function „sshpkt_get_string” (packet.c):

int
sshpkt_get_string(struct ssh *ssh, u_char **valp, size_t *lenp)
{
  return sshbuf_get_string(ssh->state->incoming_packet, valp, lenp);
}

At line 2521, the „sshbuf_get_string” function is called (sshbuf-getput-basic.c):

int
sshbuf_get_string(struct sshbuf *buf, u_char **valp, size_t *lenp)
{
    const u_char *val;
    size_t len;
    int r;

    if (valp != NULL)
        *valp = NULL;
    if (lenp != NULL)
        *lenp = 0;
    if ((r = sshbuf_get_string_direct(buf, &val, &len)) < 0)
        return r;

At line 107, the „sshbuf_get_string_direct” function is called (sshbuf-getput-basic.c):

int
sshbuf_get_string_direct(struct sshbuf *buf, const u_char **valp, size_t *lenp)
{
   size_t len;
   const u_char *p;
   int r;

   if (valp != NULL)
       *valp = NULL;
   if (lenp != NULL)
       *lenp = 0;
   if ((r = sshbuf_peek_string_direct(buf, &p, &len)) < 0)
       return r;
   if (valp != NULL)

At line 134, the „sshbuf_peek_string_direct” function is called (sshbuf-getput-basic.c):

int
sshbuf_peek_string_direct(const struct sshbuf *buf, const u_char **valp,
   size_t *lenp)
{
   u_int32_t len;
   const u_char *p = sshbuf_ptr(buf);

   if (valp != NULL)
       *valp = NULL;
   if (lenp != NULL)
       *lenp = 0;
   if (sshbuf_len(buf) < 4) {
       SSHBUF_DBG(("SSH_ERR_MESSAGE_INCOMPLETE"));
       return SSH_ERR_MESSAGE_INCOMPLETE;
   }
   len = PEEK_U32(p);
   if (len > SSHBUF_SIZE_MAX - 4) {
       SSHBUF_DBG(("SSH_ERR_STRING_TOO_LARGE"));
       return SSH_ERR_STRING_TOO_LARGE;
   }
   if (sshbuf_len(buf) - 4 < len) {
       SSHBUF_DBG(("SSH_ERR_MESSAGE_INCOMPLETE"));
       return SSH_ERR_MESSAGE_INCOMPLETE;
   }

The function performs validation of the „string” value, among other things the data length is checked (line 165) – the length cannot be greater than SSHBUF_SIZE_MAX – 4.

The constant SSHBUF_SIZE_MAX is defined in the sshbuf.h file:

#define SSHBUF_SIZE_MAX     0x8000000 /* Hard maximum size */

At this point, we obtained a way to force the „sshpkt_get_string” function to return an error – sending a „string” value with a length greater than 0x8000000 – 4.

Let’s go back to the analysis of the function „userauth_pubkey” (auth2-pubkey.c):

static int
userauth_pubkey(struct ssh *ssh)
{
   Authctxt *authctxt = ssh->authctxt;
   struct passwd *pw = authctxt->pw;
   struct sshbuf *b;
   struct sshkey *key = NULL;
   char *pkalg, *userstyle = NULL, *key_s = NULL, *ca_s = NULL;
   u_char *pkblob, *sig, have_sig;
   size_t blen, slen;
   int r, pktype;
   int authenticated = 0;
   struct sshauthopt *authopts = NULL;

   if (!authctxt->valid) {
       debug2("%s: disabled because of invalid user", __func__);
       return 0;
   }
   if ((r = sshpkt_get_u8(ssh, &have_sig)) != 0 ||
       (r = sshpkt_get_cstring(ssh, &pkalg, NULL)) != 0 ||
       (r = sshpkt_get_string(ssh, &pkblob, &blen)) != 0)
       fatal("%s: parse request failed: %s", __func__, ssh_err(r));
   pktype = sshkey_type_from_name(pkalg);

There is an attempt to read the „string” value at line 107. Sending a value greater than 0x8000000 – 4 should cause the „sshpkt_get_string” function to return an error and result in an emergency termination of the process (line 108). It is worth to mention that the data sent does not have to have a length greater than 0x8000000 – 4, only the declared size (sent in a separate field) must exceed the allowed value.

In order to send a crafted SSH packet that contains a „string” with the appropriate length, I will modify the paramiko library. Paramiko is an implementation of SSHv2 protocol written in Python (https://github.com/paramiko/paramiko). I encourage you to modify the library on your own.

Based on the modified paramiko library, let’s write a simple script to verify the correctness of the user’s name:

import paramiko
import sys

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=sys.argv[1], port=int(sys.argv[2]), username=sys.argv[3], key_filename='./conf/id_rsa', password='', look_for_keys=False)
ssh.close()

Running the script for the name of an existing user (user) ends with the error „No existing session”:

python2 test.py debian 22 user
[...]
paramiko.ssh_exception.SSHException: No existing session

Running the script for the name of a non-existent user (asdf) ends with the error „Authentication failed.”:

python2 test.py debian 22 asdf
[...]
paramiko.ssh_exception.AuthenticationException: Authentication failed.

The difference in the server response allows to unambiguously determine whether a user with a given name exists in the tested system.

At the end it is worth mentioning that the presented enumeration error, in addition to the obvious use in the first phase of the brute-force attack, can also be used to detect software installed on the tested server, e.g. detection of the user „postgres” suggests that the PostgreSQL database is used. Going forward, the existence or absence of certain names may suggest the use of a given operating system.

Summary

Issue was reported to OpenSSH team. Due to the fact that fixed OpenSSH version has not been published yet, while on the Internet it is possible to find a working exploit, we recommend the following mitigations to protect against brute-force attacks:

  • if possible use key-based authentication instead a password-based one
  • secure the accounts with strong passwords
  • use mechanisms that help to block brute-force attacks, eg fail2ban

Timeline

16.07.2018 – bug was reported to OpenSSH team
18.07.2018 – bug was confirmed by OpenSSH team

31.07.2018 – fix was published on github (https://github.com/openbsd/src/commit/779974d35b4859c07bc3cb8a12c74b43b0a7d1e0)
15.08.2018 – technical details, based on publicly available fix, were published by independent researcher (http://seclists.org/oss-sec/2018/q3/124)
17.08.2018 – CVE-2018-15473 was assigned
22.08.2018 – our publication

Dariusz Tytko, Pentester in Securitum

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Emilian

    Od kiedy na Sekuraku mamy angielskie wpisy?

    Odpowiedz
  2. mszustak
    Odpowiedz
  3. Ja

    Chyba coś się redakcja zawiesiła. 2 tygodnie bez żadnego tekstu…

    Odpowiedz

Odpowiedz