Talos Vulnerability Report

TALOS-2019-0893

OpenWrt ustream-ssl certificate verification information leak vulnerability

November 15, 2019
CVE Number

CVE-2019-5101,CVE-2019-5102

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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)

PRODUCT URLS

OpenWRT - https://openwrt.org/

CVSSv3 SCORE

4.0 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N

CWE

CWE-295 - Improper Certificate Validation

DETAILS

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.

CVE-2019-5101 - OpenSSL (libustream-openssl)

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].

CVE-2019-5102 - mbed TLS (libustream-mbedtls)

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;
}
TIMELINE

2019-09-11 - Vendor disclosure
2019-11-13 - Vendor patched
2019-11-15 - Public release

Credit

Discovered by Claudio Bozzato of Cisco Talos.