An OpenSSH user enumeration vulnerability (CVE-2018-15473) became public via a GitHub commit.
This vulnerability does not produce a list of valid usernames, but it does allow guessing of usernames.
In this blog post, we take a closer look at this vulnerability and propose mitigation and monitoring actions.
This vulnerability manifests itself in several authentication functions of OpenSSH. We take a close look at this vulnerability in public key authentication of Ubuntu’s OpenSSH implementation.
By sending a malformed public key authentication message to an OpenSSH server, the existence of a particular username can be ascertained. If the user does not exist, an authentication failure message will be sent to the client. In case the user exists, failure to parse the message will abort the communication: the connection will be closed without sending back any message. This exploit is implemented in a Python PoC script.
The vulnerability exists because communication about the non-existence of a username occurs before fully parsing the message. Fixing the vulnerability is in essence simple: reverse the logic. First fully parse the message, then communicate.
One way to test the PoC, is to start the OpenSSH server in debug mode:
After, run the PoC script with an existing username:
On the server side, an error will occur:
This error can also be found in /var/log/auth.log:
Failure to parse the message causes the closing of the connection between client and server without message from the server:
Notice that the last packet is pink (i.e. client packet), there is no subsequent blue packet (i.e. server packet).
When the PoC script is executed with a non-existing username:
No “incomplete message” error occurs:
And the server sends back a message to the client:
Notice the blue server packet at the end of the communication.
This is how the vulnerability in public key authentication can be exploited to disclose the validity of a user name.
The behavior of OpenSSH is, of course, defined in the source code. Function userauth_pubkey is one of the implemented authentication functions, specific for authentication via public keys. It returns 0 when authentication fails, and 1 when authentication succeeds. It is called when message SSH2_MSG_USERAUTH_REQUEST (type publickey) is received, after which the result is used to send back message SSH2_MSG_USERAUTH_FAILURE or SSH2_MSG_USERAUTH_SUCCESS to the client.
The logic of this function is the following:
- if unknown username -> 0
- if known username with incorrect key -> 0
- if known username with correct key -> 1
What clever persons figured out, is that it is possible to stop the execution of function userauth_pubkey between step 1 and 2. After step 1, function userauth_pubkey retrieves strings from the message sent by the client. If this fails, because of malformed strings, the process will stop and close the connection, without sending back any message.
This can happen because of function packet_get_string:
If a username exists, step 1 will be followed by the extraction of fields from the message.
The first field to be extracted is a boolean (1 byte), with function packet_get_char(). This field is equal to 1 when the authentication type is publickey. Next it is followed by 2 strings: the algorithm and the key. In SSH messages, strings are encoded as a length-value pair. A string is composed of 4 bytes (the length of the string), followed by a variable number of bytes (equal to the length) containing the string. The length is encoded as bigendian, i.e. the most significant byte of the 4-byte integer is placed first, followed by the less significant bytes.
Function packet_get_string extracts a string from a message, while validating it, i.e. checking that the specified length can be correct. This function relies on other functions:
First there is a define to function ssh_packet_get_string:
Function ssh_packet_get_string calls function sshpkt_get_string, and if its return value is not 0, it calls function fatal. Function fatal logs a fatal error event, and then terminates the spawned OpenSSH process, without sending back any message.
Now follows another chain of functions: function sshpkt_get_string calls sshbuf_get_string:
sshbuf_get_string calls sshbuf_get_string_direct:
sshbuf_get_string_direct calls sshbuf_peek_string_direct:
And finally, sshbuf_peek_string_direct does the string validation:
Error SSH_ERR_MESSAGE_INCOMPLETE (the message found in the log) is returned if the remaining data in the message is smaller than 4 bytes (and can thus not contain the length of the string) or if the remaining data in the message is smaller than the length of the string.
To summarize this chain of functions: when packet_get_string is used to extract a string from a message, a fatal exception can occur if the string is malformed, resulting in the termination of the OpenSSH process.
This is exactly what the PoC Python script triggers. First it establishes an encrypted connection with the OpenSSH server, and then it sends a malformed SSH2_MSG_USERAUTH_REQUEST (type publickey) message. The script redefines Paramiko’s add_boolean function to a NULL function. Paramiko is a Python module for SSH communication. By redefining the add_boolean function, the boolean field (just before the algorithm and key string fields) is omitted from the message.
When this malformed message is parsed by function userauth_pubkey, the boolean field is read first. Since this field is actually missing, the first byte of the next field is read (function packet_get_char): the most significant byte of the 4-byte length of the algorithm string. Next function packet_get_string is called to read (and validate) the algorithm string. Because of the missing boolean field, this will fail.
Here is what the parsing of a well-formed message looks like:
With the malformed message, the boolean value is missing. The parsing function does not know this of course, and thus it parses the first byte of the string as a boolean field: it looks like the message is shifted one byte to the left:
The result is that a string length of 1907 bytes is parsed (0x00000773 hexadecimal), which is longer than the message itself. As such, function ssh_packet_get_string will call function fatal to cause the termination of the OpenSSH process.
This is a subtle bug. It’s not about buffer overflows leading to remote code execution, or missing input validation.
There are no buffer overflows, and all input is validated before it is used. The problem here is that input validation occurs after some of the functional processing already took place: probably for performance reasons, the username is checked first to see if it exists. If it doesn’t exist, then no further input validation and processing has to take place.
With an existing username, input validation takes place, and can cause the connection to be closed without sending a message. This can be used to derive the existence of a username.
The solution to this problem is rather simple: switch the order and first do all input validation, before any functional processing.
It’s likely that the same error was made in other authentication functions. A crude, incomplete way to check this, is to check for expression “!authctc->valid”, like this:
The same mistake was indeed made in host-based authentication (as can be witnessed in the GitHub commit):
And Kerberos authentication:
And potentially SSH1 RSA authentication (we have not checked this further, as it is no longer present in implementations like OpenBSD):
Notice that the comment even warns about this risk!
Depending on your use of OpenSSH, this vulnerability can be mitigated. The vulnerable authentication mechanisms can be disabled until a patch is available and deployed. For example, by disabling public key authentication, the PoC script no longer works, as the malformed authentication request is rejected.
Of course, we only recommend to disable public key authentication if you don’t use it. If you use it, don’t switch to password authentication, but keep using public key authentication! This is not a remote code execution vulnerability, it is an information disclosure vulnerability.
You can also check your logs for signs of exploitation of this vulnerability. A fatal error can be an indication. With this PoC on Ubuntu, the fatal error is “incomplete message”. However, this message can be slightly different, depending on your OpenSSH version, and there are also other ways to generate a malformed message that can potentially result in another fatal error. For example, one could craft an authentication request with a string length longer than the maximum allowed value.
In a default configuration, you will only get this fatal error. The IP address of the client, for example, will not be logged. You can include this information by increasing the log level (LogLevel) from INFO to VERBOSE: this will create extra log entries containing, among others, IP addresses of the clients. Be aware that this will generate larger logs and that you should monitor that your logs do not exceed your capacity.
About the authors
Didier Stevens is a malware expert working for NVISO. Didier is a SANS Internet Storm Center senior handler and Microsoft MVP, and has developed numerous popular tools to assist with malware analysis. You can find Didier on Twitter and LinkedIn.
12 thoughts on “OpenSSH User Enumeration Vulnerability: a Close Look”