Talos Vulnerability Report

TALOS-2017-0294

Randombit Botan Library X509 Certificate Validation Bypass Vulnerability

April 28, 2017
CVE Number

CVE-2017-2801

Summary

A programming error exists in a way Randombit Botan cryptographic library version 2.0.1 implements x500 string comparisons which could lead to certificate verification issues and abuse. A specially crafted X509 certificate would need to be delivered to the client or server application in order to trigger this vulnerability.

Tested Versions

Randombit Botan 2.0.1

Product URLs

https://botan.randombit.net/

CVSSv3 Score

6.5 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:L

CWE

CWE-125: Out-of-bounds Read

Details

Botan is a C++ cryptographic library that implements the basis for practical systems that require TLS, PKIX certificate handling, password hashing or other cryptographic primitives.

There exists a programming error in code related to x509 distinguished name parsing. Namely, an x509 DN comparison function can lead to out of bounds memory access leading to unexpected results, information disclosure or potential denial of service.

The vulnerability is located in the overloaded equality comparison function Botan::x500_name_cmp:

bool x500_name_cmp(const std::string& name1, const std::string& name2)
     {
     auto p1 = name1.begin();
     auto p2 = name2.begin();


     while((p1 != name1.end()) && Charset::is_space(*p1)) ++p1;        [1]
     while((p2 != name2.end()) && Charset::is_space(*p2)) ++p2;


     while(p1 != name1.end() && p2 != name2.end())
            {
            if(Charset::is_space(*p1))                                     [2]
                 {
                 if(!Charset::is_space(*p2))                                 [3]
                        return false;


                 while((p1 != name1.end()) && Charset::is_space(*p1)) ++p1;  [4]
                 while((p2 != name2.end()) && Charset::is_space(*p2)) ++p2;  [5]


                 if(p1 == name1.end() && p2 == name2.end())                  [6]
                        return true;
                 }


            if(!Charset::caseless_cmp(*p1, *p2))                           [7]
                 return false;
            ++p1;                                                          [8]
            ++p2;
            }


     while((p1 != name1.end()) && Charset::is_space(*p1)) ++p1;
     while((p2 != name2.end()) && Charset::is_space(*p2)) ++p2;


     if((p1 != name1.end()) || (p2 != name2.end()))
            return false;
     return true;
     }

First, at [1], initiall whitespaces are skipped. Then, strings are compared byte by byte in a loop while checking for whitespace at [2]. If a space occurs in the first string [2] and the second too [3], those are again skipped at [4] and [5]. Then, at [6], if both have reached an end, true is returned. If not, another comparison is made at [7] and if it passes, the pointers are increased at [8].

The vulnerability lies in the way whitespaces are handeled. If we are comparing two strings which are initially the same up to a space character, we would enter while loops at [4] and [5]. Now, if one string contains a NULL byte after that space, and the other has spaces until its end, the check at [6] won’t be true, because only the second string would point to its end. However, both are actually pointing at a NULL byte, which means the check at [7] will still hold true, and pointers are once again increased at [8]. Then when the loop rolls around, one of the pointers can point outside its allocated buffer, leading to unexpected behaviour.

A specially crafted x509 certificate with specific x509 DN strings for subject and issuer fields can be created. Example strings that satisfy the above conditions are:

String 1: AA\x20\x00AAAAAAAAAA
String 2:  AA\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20

Notice that both are the same length, begin with same characters up until space after which the first is terminated and the second has spaces till the end. Because of the way these pieces of certificate are copied from the x509 file to their memory buffers, the first string’s length won’t be 3, that is, it won’t be terminated at the first NULL.

With careful control over X509 distinguished names contents and depending on memory layout in the target application, it could be possible to craft a certificate where equality checks could pass or fail. Also, a discrepancy between a way these malformed strings are handled in Botan and other x509 libraries could lead to other types of abuse, possibly not unlike the famed CVE-2009-2408.

The vulnerability can be triggered with the supplied example x509 certificate.

Crash Information

Address sanitizer output:

botan/botan cert_info --ber cert1.der 2>&1|  asan_symbolize -d
=================================================================
==15015==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60300000dfa3 at pc 0x7f027ec92e85 bp 0x7ffdf452fe60 sp 0x7ffdf452fe58
READ of size 1 at 0x60300000dfa3 thread T0
        #0 0x7f027ec92e84 in Botan::x500_name_cmp(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) botan/./src/lib/utils/parsing.cpp:232
        #1 0x7f027ec92e84 in ?? ??:0
        #2 0x7f027e269f2a in Botan::operator==(Botan::X509_DN const&, Botan::X509_DN const&) botan/./src/lib/asn1/x509_dn.cpp:153
        #3 0x7f027e269f2a in ?? ??:0
        #4 0x7f027ed8b8f4 in Botan::X509_Certificate::force_decode() botan/./src/lib/x509/x509cert.cpp:149
        #5 0x7f027ed8b8f4 in ?? ??:0
        #6 0x7f027ed85263 in Botan::X509_Object::do_decode() botan/./src/lib/x509/x509_obj.cpp:235
        #7 0x7f027ed85263 in ?? ??:0
        #8 0x7f027ed877b1 in X509_Certificate botan/./src/lib/x509/x509cert.cpp:50
        #9 0x7f027ed877b1 in ?? ??:0
        #10 0x5fcc93 in Botan_CLI::Cert_Info::go() botan/./src/cli/x509.cpp:85
        #11 0x5fcc93 in ?? ??:0
        #12 0x520ed5 in Botan_CLI::Command::run(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > const&) botan/./src/cli/cli.h:229
        #13 0x520ed5 in ?? ??:0
        #14 0x51ca4f in main botan/./src/cli/main.cpp:60
        #15 0x51ca4f in ?? ??:0
        #16 0x7f027d16982f in __libc_start_main /build/glibc-Qz8a69/glibc-2.23/csu/../csu/libc-start.c:291
        #17 0x7f027d16982f in ?? ??:0
        #18 0x42e328 in _start ??:?
        #19 0x42e328 in ?? ??:0


0x60300000dfa3 is located 0 bytes to the right of 19-byte region [0x60300000df90,0x60300000dfa3)
allocated by thread T0 here:
        #0 0x4ce458 in __interceptor_malloc ??:?
        #1 0x4ce458 in ?? ??:0
        #2 0x7f027f296e77 in operator new(unsigned long) ??:?
        #3 0x7f027f296e77 in ?? ??:0
        #4 0x7f027e272283 in std::pair<std::__decay_and_strip<Botan::OID const&>::__type, std::__decay_and_strip<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>::__type> std::make_pair<Botan::OID const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>(Botan::OID const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/5.4.0/../../../../include/c++/5.4.0/bits/stl_pair.h:281 (discriminator 4)
        #5 0x7f027e272283 in void Botan::multimap_insert<Botan::OID, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::multimap<Botan::OID, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::less<Botan::OID>, std::allocator<std::pair<Botan::OID const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >&, Botan::OID const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) botan/build/include/botan/internal/stl_util.h:79 (discriminator 4)
        #6 0x7f027e272283 in ?? ??:0
        #7 0x7f027e2671eb in Botan::X509_DN::get_attributes[abi:cxx11]() const botan/./src/lib/asn1/x509_dn.cpp:78 (discriminator 1)
        #8 0x7f027e2671eb in ?? ??:0
        #9 0x7f027e269d49 in Botan::operator==(Botan::X509_DN const&, Botan::X509_DN const&) botan/./src/lib/asn1/x509_dn.cpp:138 (discriminator 1)
        #10 0x7f027e269d49 in ?? ??:0
        #11 0x7f027ed8b8f4 in Botan::X509_Certificate::force_decode() botan/./src/lib/x509/x509cert.cpp:149
        #12 0x7f027ed8b8f4 in ?? ??:0
        #13 0x7f027ed85263 in Botan::X509_Object::do_decode() botan/./src/lib/x509/x509_obj.cpp:235
        #14 0x7f027ed85263 in ?? ??:0
        #15 0x7f027ed877b1 in X509_Certificate botan/./src/lib/x509/x509cert.cpp:50
        #16 0x7f027ed877b1 in ?? ??:0
        #17 0x5fcc93 in Botan_CLI::Cert_Info::go() botan/./src/cli/x509.cpp:85
        #18 0x5fcc93 in ?? ??:0
        #19 0x520ed5 in Botan_CLI::Command::run(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > const&) botan/./src/cli/cli.h:229
        #20 0x520ed5 in ?? ??:0
        #21 0x51ca4f in main botan/./src/cli/main.cpp:60
        #22 0x51ca4f in ?? ??:0
        #23 0x7f027d16982f in __libc_start_main /build/glibc-Qz8a69/glibc-2.23/csu/../csu/libc-start.c:291
        #24 0x7f027d16982f in ?? ??:0


SUMMARY: AddressSanitizer: heap-buffer-overflow (botan/libbotan-2.so.0+0xc38e84)
Shadow bytes around the buggy address:
    0x0c067fff9ba0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x0c067fff9bb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x0c067fff9bc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x0c067fff9bd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x0c067fff9be0: fa fa fa fa fa fa 00 00 03 fa fa fa fd fd fd fa
=>0x0c067fff9bf0: fa fa 00 00[03]fa fa fa fd fd fd fa fa fa 00 00
    0x0c067fff9c00: 00 04 fa fa fd fd fd fd fa fa 00 00 00 03 fa fa
    0x0c067fff9c10: fd fd fd fd fa fa 00 00 00 03 fa fa fd fd fd fd
    0x0c067fff9c20: fa fa 00 00 05 fa fa fa fd fd fd fa fa fa 00 00
    0x0c067fff9c30: 07 fa fa fa fd fd fd fa fa fa 00 00 01 fa fa fa
    0x0c067fff9c40: 00 00 00 fa fa fa fd fd fd fa fa fa fd fd fd fa
Shadow byte legend (one shadow byte represents 8 application bytes):
    Addressable:           00
    Partially addressable: 01 02 03 04 05 06 07
    Heap left redzone:       fa
    Heap right redzone:      fb
    Freed heap region:       fd
    Stack left redzone:      f1
    Stack mid redzone:       f2
    Stack right redzone:     f3
    Stack partial redzone:   f4
    Stack after return:      f5
    Stack use after scope:   f8
    Global redzone:          f9
    Global init order:       f6
    Poisoned by user:        f7
    Container overflow:      fc
    Array cookie:            ac
    Intra object redzone:    bb
    ASan internal:           fe
    Left alloca redzone:     ca
    Right alloca redzone:    cb
==15015==ABORTING

Mitigation

Adding another check which tests if either string is at the end while the other is not, which would make them different, is enough to resolve this vulnerability:

diff --git a/src/lib/utils/parsing.cpp b/src/lib/utils/parsing.cpp
index 8fd2ccc..ce4b02f 100644
--- a/src/lib/utils/parsing.cpp
+++ b/src/lib/utils/parsing.cpp
@@ -240,6 +240,11 @@ bool x500_name_cmp(const std::string& name1, const std::string& name2)
                    if(p1 == name1.end() && p2 == name2.end())
                         return true;
+         if(p1 == name1.end() || p2 == name2.end())
+            return false;
                    }


             if(!Charset::caseless_cmp(*p1, *p2))
                    return false;

Timeline

2017-03-16 - Vendor Disclosure
2017-04-28 - Public Release

Credit

Discovered by Aleksandar Nikolic of Cisco Talos.