CVE-2019-5101,CVE-2019-5102
An exploitable information leak vulnerability exists in the ustream-ssl library of OpenWrt, versions 18.06.4 and 15.05.1. When connecting to a remote server, the server’s SSL certificate is checked but no action is taken when the certificate is invalid. An attacker could exploit this behavior by performing a man-in-the-middle attack, providing any certificate, leading to the theft of all the data sent by the client during the first request.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
OpenWRT OpenWrt 15.05.1, via wget (busybox)
OpenWRT OpenWrt 18.06.4, via wget (uclient-fetch)
OpenWRT - https://openwrt.org/
4.0 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N
CWE-295 - Improper Certificate Validation
OpenWrt is a Linux-based OS, primarily used on embedded devices to route network traffic. OpenWrt is highly customizable, and ships with a set of tools and libraries that have been optimized to run on hardware with limited resources.
Among these tools, OpenWrt uses wget
to allow scripts to download files from the web. In OpenWrt 18.06.4, wget
is a symbolic link to uclient-fetch
, while it’s a symbolic link to busybox
in OpenWrt 15.05.1. In both cases, the SSL support is provided by the ustream-ssl
library, which is an SSL wrapper for OpenSSL
, mbed TLS
, and wolfSSL
.
When the underlying SSL library used is OpenSSL
(package libustream-openssl
) or mbed TLS
(package libustream-mbedtls
), ustream-ssl
has an issue that could be exploited by an attacker to bypass the server’s certificate check and reveal the whole contents of the client’s request.
After an SSL connection is initialized via _ustream_ssl_init
, and after any data (e.g. the client’s HTTP request) is written to the stream using ustream_printf
, the code eventually enters the function __ustream_ssl_poll
, which is used to dispatch the read/write events.
static bool __ustream_ssl_poll(struct ustream *s)
{
struct ustream_ssl *us = container_of(s->next, struct ustream_ssl, stream);
char *buf;
int len, ret;
bool more = false;
ustream_ssl_check_conn(us); // [1]
if (!us->connected || us->error)
return false;
...
The first action taken is to check the SSL connection by calling ustream_ssl_check_conn
at [1]:
static void ustream_ssl_check_conn(struct ustream_ssl *us)
{
if (us->connected || us->error)
return;
if (__ustream_ssl_connect(us) == U_SSL_OK) { // [2]
us->connected = true; // [3] connected
if (us->notify_connected)
us->notify_connected(us);
ustream_write_pending(&us->stream); // [4] write to the stream
}
}
This function, in turn, calls __ustream_ssl_connect
[2], and if the return is U_SSL_OK
, then the connection is assumed to be established [3] and any pending write operations are executed [4].
Because of the write happening at [4], the function __ustream_ssl_connect
should only return U_SSL_OK
when the SSL connection has been checked and the server’s certificate is verified:
__hidden enum ssl_conn_status __ustream_ssl_connect(struct ustream_ssl *us)
{
void *ssl = us->ssl;
int r;
if (us->server)
r = SSL_accept(ssl);
else
r = SSL_connect(ssl);
if (r == 1) {
#ifndef CYASSL_OPENSSL_H_
ustream_ssl_verify_cert(us); // [5]
#endif
return U_SSL_OK; // [6]
}
r = SSL_get_error(ssl, r);
if (r == SSL_ERROR_WANT_READ || r == SSL_ERROR_WANT_WRITE)
return U_SSL_PENDING;
ustream_ssl_error(us, r);
return U_SSL_ERROR;
}
However, while the function will call ustream_ssl_verify_cert
at [5], U_SSL_OK
will be returned in any case [6].
Indeed, ustream_ssl_verify_cert
checks the connection and returns early [7], without setting us->valid_cert
(which will stay false
).
static void ustream_ssl_verify_cert(struct ustream_ssl *us)
{
void *ssl = us->ssl;
X509 *cert;
int res;
res = SSL_get_verify_result(ssl);
if (res != X509_V_OK) {
if (us->notify_verify_error) // [7]
us->notify_verify_error(us, res,
X509_verify_cert_error_string(res));
return;
}
cert = SSL_get_peer_certificate(ssl);
if (!cert)
return;
us->valid_cert = true; // [8]
us->valid_cn = ustream_ssl_verify_cn(us, cert);
X509_free(cert);
}
At this point, the SSL connection is established [3] (from the point of view of ustream-ssl
), and the pending writes are executed on the stream [4], allowing a man-in-the-middle attacker, by suppling any certificate, to read the data written into the stream.
Despite this, the code in __ustream_ssl_poll
will terminate a few calls later:
static bool __ustream_ssl_poll(struct ustream *s)
{
struct ustream_ssl *us = container_of(s->next, struct ustream_ssl, stream);
char *buf;
int len, ret;
bool more = false;
ustream_ssl_check_conn(us);
if (!us->connected || us->error)
return false;
do {
buf = ustream_reserve(&us->stream, 1, &len);
if (!len)
break;
ret = __ustream_ssl_read(us, buf, len); // [9]
switch (ret) {
case U_SSL_PENDING: // [10]
return more;
case U_SSL_ERROR:
return false;
case 0:
us->stream.eof = true;
ustream_state_change(&us->stream);
return false;
default:
ustream_fill_read(&us->stream, ret);
more = true;
continue;
}
} while (1);
return more;
}
At [9], the stream is read, but the underlying SSL_read
function (defined in the OpenSSL
library) will error out and the function will exit at [10].
The same issue exists in the libustream-mbedtls
package, the affected code [11] is similar:
__hidden enum ssl_conn_status __ustream_ssl_connect(struct ustream_ssl *us)
{
void *ssl = us->ssl;
int r;
r = mbedtls_ssl_handshake(ssl);
if (r == 0) {
ustream_ssl_verify_cert(us);
return U_SSL_OK; // [11]
}
if (ssl_do_wait(r))
return U_SSL_PENDING;
ustream_ssl_error(us, r);
return U_SSL_ERROR;
}
2019-09-11 - Vendor disclosure
2019-11-13 - Vendor patched
2019-11-15 - Public release
Discovered by Claudio Bozzato of Cisco Talos.