Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
OpenSSH – users enumeration – CVE-2018-15473
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:
- We filter the list of potential user names by querying the server for the correctness of the given name
- 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
Od kiedy na Sekuraku mamy angielskie wpisy?
Od 2013 co najmniej: https://sekurak.pl/more-information-about-tp-link-backdoor/
It’s worth noting that there’s already working PoC published on Matthew Daley on oss-sec mailing list.
http://seclists.org/oss-sec/2018/q3/125
Chyba coś się redakcja zawiesiła. 2 tygodnie bez żadnego tekstu…