Talos Vulnerability Report

TALOS-2023-1741

SoftEther VPN vpnserver EnSafeHttpHeaderValueStr denial of service vulnerability

October 12, 2023
CVE Number

CVE-2023-23581

SUMMARY

A denial-of-service vulnerability exists in the vpnserver EnSafeHttpHeaderValueStr functionality of SoftEther VPN 5.01.9674 and 5.02. A specially crafted network packet can lead to denial of service.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

SoftEther VPN 5.01.9674
SoftEther VPN 5.02
While 5.01.9674 is a development version, it is distributed at the time of writing by Ubuntu and other Debian-based distributions.

PRODUCT URLS

SoftEther VPN - https://www.softether.org/

CVSSv3 SCORE

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

CWE

CWE-125 - Out-of-bounds Read

DETAILS

SoftEther is a multi-platform VPN project that provides both server and client code to connect over a variety of VPN protocols, including Wireguard, PPTP, SSTP, L2TP, etc. SoftEther has a variety of features for both enterprise and personal use, and enables Nat Traversal out-of-the-box for remote-access setups behind firewalls.

To various ends, SoftEther VPN server accepts and parses HTTP connections by default on all of its configured TCP ports. Out of the box this means TCP ports 443, 992, 1194 and 5555. Assuming that our incoming packet does not look like any of the other supported TCP protocols, we eventually get to the ServerAccept function:

// Server accepts a connection from client
bool ServerAccept(CONNECTION *c)
{
    bool ret = false;
    UINT err;
    PACK *p;
    char username_real[MAX_SIZE];
    char method[MAX_SIZE]; // 512
    char hubname[MAX_SIZE];
    char username[MAX_SIZE];
    char groupname[MAX_SIZE];
    UCHAR session_key[SHA1_SIZE];
    UCHAR ticket[SHA1_SIZE];

    // [...]

    // Validate arguments
    if (c == NULL)
    {
        return false;
    }

    // [...]

    // Receive the signature
    Debug("Downloading Signature...\n");
    error_detail_2 = NULL;
    if (ServerDownloadSignature(c, &error_detail_2) == false){  // [1] 
        //[...]
        goto CLEANUP;
    }

Assorted initialization and an unruly amount of stack variables are skipped just so we can quickly see the first branching piece of code we hit at [1] in ServerDownloadSignature:

// Download the signature
bool ServerDownloadSignature(CONNECTION *c, char **error_detail_str)
{
    HTTP_HEADER *h;
    UCHAR *data;
    UINT data_size;
    SOCK *s;
    UINT num = 0, max = 19;
    SERVER *server;
    char *vpn_http_target = HTTP_VPN_TARGET2;
    // Validate arguments
    if (c == NULL)
    {
        return false;
    }

    server = c->Cedar->Server;

    s = c->FirstSock;          // [2]

    while (true)
    {
        bool not_found_error = false;

        num++;
        if (num > max)
        {
            // Disconnect
            Disconnect(s);
            c->Err = ERR_CLIENT_IS_NOT_VPN;

            *error_detail_str = "HTTP_TOO_MANY_REQUEST";
            return false;
        }
        // Receive a header
        h = RecvHttpHeader(s);  // [3]
        if (h == NULL)
        {

    // [...]

The SOCK *s variable is our client’s socket [2], which we then pass into the RecvHttpHeader function at [3] to actually read bytes and process the data:

// Receive an HTTP header
HTTP_HEADER *RecvHttpHeader(SOCK *s)
{
    TOKEN_LIST *token = NULL;
    char *str = NULL;
    HTTP_HEADER *header = NULL;
    // Validate arguments
    if (s == NULL)
    {
        return NULL;
    }

    // Get the first line
    str = RecvLine(s, HTTP_HEADER_LINE_MAX_SIZE); // [4]
    if (str == NULL)
    {
        return NULL;
    }

    // Split into tokens
    token = ParseToken(str, " "); 

    FreeSafe(PTR_TO_PTR(str));

    if (token->NumTokens < 3)
    {
        FreeToken(token);
        return NULL;
    }

    // Creating a header object
    header = NewHttpHeader(token->Token[0], token->Token[1], token->Token[2]);
    FreeToken(token);

    if (StrCmpi(header->Version, "HTTP/0.9") == 0)
    {
        // The header ends with this line
        return header;
    }

    // Get the subsequent lines
    while (true)   
    {
        str = RecvLine(s, HTTP_HEADER_LINE_MAX_SIZE);  // [5]
        Trim(str);
        if (IsEmptyStr(str))
        {
            // End of header
            FreeSafe(PTR_TO_PTR(str)); 
            break;
        }

        if (AddHttpValueStr(header, str) == false) // [6] 
        {
            FreeSafe(PTR_TO_PTR(str)); 
            FreeHttpHeader(header);
            header = NULL;
            break;
        }

        FreeSafe(PTR_TO_PTR(str));
    }

    return header;
}

To start, at [4], RecvLine reads in at max 0x1000 bytes or until it reads a “\r\n”. This line is split into the basic HTTP headers, and, assuming that’s all well and good, we enter the loop at [5] to continue reading the rest of the data. Again reading until 0x1000 bytes or “\r\n”, we take each line of the HTTP header and pass it into the AddHttpValueStr function at [6]. A quick digression before we delve into AddHttpValueStr: All of the parsed data is added into the HTTP_HEADER struct, which is just three pointers for the Method, Target and Version, and then a LIST * object which holds all of the actual HTTP headers:

// HTTP header
struct HTTP_HEADER
{
    char *Method;                   // Method
    char *Target;                   // Target
    char *Version;                  // Version
    LIST *ValueList;                // Value list
};

Continuing into AddHttpValueStr:

// Adds the HTTP value contained in the string to the HTTP header
bool AddHttpValueStr(HTTP_HEADER* header, char *string)
{
    HTTP_VALUE *value = NULL;
    UINT pos = 0;
    char *value_name = NULL;
    char *value_data = NULL;

    // Validate arguments
    if (header == NULL || IsEmptyStr(string))
    {
        return false;
    }

    // Sanitize string
    EnSafeHttpHeaderValueStr(string, ' '); // [7]

    // Get the position of the colon
    pos = SearchStr(string, ":", 0);  // [8]
    if (pos == INFINITE)
    {
        // The colon does not exist
        return false;
    }

    if ((pos + 1) >= StrLen(string))
    {
        // There is no data
        return false;
    }

    // Divide into the name and the data
    value_name = Malloc(pos + 1);
    Copy(value_name, string, pos);
    value_name[pos] = 0;
    value_data = &string[pos + 1]; 

    value = NewHttpValue(value_name, value_data); // [9] 
    if (value == NULL)
    {
        Free(value_name);
        return false;
    }

    Free(value_name);

    AddHttpValue(header, value); // [10] 

    return true;
}

At [7], there is some escaping and processing done on our char *string parameter that was passed in. Soon after, the string is separated at the colon [8], allocations are made to contain copies of the split string [9] and we finally add a new entry to the HTTP_HEADER->ValueList at [10]. We need not go much further, so let us examine the EnSafeHttpHeaderValueStr function to see how the input HTTP header string gets processed:

// Replace '\r' and '\n' with the specified character.
// If the specified character is a space (unsafe), the original character is removed.
void EnSafeHttpHeaderValueStr(char *str, char replace)
{
    UINT length = 0;
    UINT index = 0;

    // Validate arguments
    if (str == NULL)
    {
        return;
    }

    length = StrLen(str);
    while (index < length)
    {
        if (str[index] == '\r' || str[index] == '\n')
        {
            if (replace == ' ')
            {
                Move(&str[index], &str[index + 1], length - index); // [11]
            }
            else
            {
                str[index] = replace;
            }
        }
        else if (str[index] == '\\')
        {
            if (str[index + 1] == 'r' || str[index + 1] == 'n') 
            {
                if (replace == ' ')
                {
                    Move(&str[index], &str[index + 2], length - index); // [12]
                    index--;   
                }             
                else
                {
                    str[index] = str[index + 1] = replace;
                    index++;
                }
            }
        }
        index++;
    }
}

To sum up this function, each character is checked to see if there’s a “\r”, “\n”, “\\r” or “\\n”. If there is, it is replaced with the char replace, or the entire string is shifted backwards by one or two bytes. While the code at [11] is valid because the char *str parameter is null terminated, and as such can never read out of bounds, the line at [12] is a different story. Since our Move function copies from [index + 2] and the length is length - index, every copy this line makes will hit the null terminator and the byte immediately after. As such, any HTTP header passed into this function that contains a “\\r” or “\\n” will cause a one-byte out-of-bounds read to happen. And at least for the code path we have followed above, our input char *str is allocated on the heap, which means that with very careful heap manipulation, we can cause this single byte to be the first byte of an unmapped page or non-readable page in memory, resulting in a server crash and denial of service.

Crash Information

SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /softether/SoftEtherVPN/src/Mayaqua/Encrypt.c:4725:20 in 
=================================================================
==89041==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6040001a58bb at pc 0x565370d9394f bp 0x7f2598180410 sp 0x7f259817fbe0
READ of size 26 at 0x6040001a58bb thread T30
    #0 0x565370d9394e in __asan_memmove (/softether/asan_build/vpnserver+0xa094e) (BuildId: 78a21100df0def7d4dc91631945f35b3269dac3a)
    #1 0x7f259c6e1099 in Move /softether/SoftEtherVPN/src/Mayaqua/Memory.c:3822:2
    #2 0x7f259c7e4218 in EnSafeHttpHeaderValueStr /softether/SoftEtherVPN/src/Mayaqua/Str.c:2082:6
    #3 0x7f259c689be1 in AddHttpValueStr /softether/SoftEtherVPN/src/Mayaqua/HTTP.c:929:2
    #4 0x7f259c68b890 in RecvHttpHeader /softether/SoftEtherVPN/src/Mayaqua/HTTP.c:1145:7
    #5 0x7f259d94168b in ServerDownloadSignature /softether/SoftEtherVPN/src/Cedar/Protocol.c:5791:7
    #6 0x7f259d91dc91 in ServerAccept /softether/SoftEtherVPN/src/Cedar/Protocol.c:1228:6
    #7 0x7f259d677def in ConnectionAccept /softether/SoftEtherVPN/src/Cedar/Connection.c:3077:6
    #8 0x7f259d760478 in TCPAcceptedThread /softether/SoftEtherVPN/src/Cedar/Listener.c:181:2
    #9 0x7f259c6ac632 in ThreadPoolProc /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:872:3
    #10 0x7f259c844b63 in UnixDefaultThreadProc /softether/SoftEtherVPN/src/Mayaqua/Unix.c:1594:2
    #11 0x7f259c094b42 in start_thread nptl/./nptl/pthread_create.c:442:8
    #12 0x7f259c1269ff  misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S:81

0x6040001a58bb is located 0 bytes to the right of 43-byte region [0x6040001a5890,0x6040001a58bb)
allocated by thread T30 here:
    #0 0x565370d941ce in malloc (/softether/asan_build/vpnserver+0xa11ce) (BuildId: 78a21100df0def7d4dc91631945f35b3269dac3a)
    #1 0x7f259c83b386 in UnixMemoryAlloc /softether/SoftEtherVPN/src/Mayaqua/Unix.c:2043:6
    #2 0x7f259c6dfb9e in InternalMalloc /softether/SoftEtherVPN/src/Mayaqua/Memory.c:3692:10
    #3 0x7f259c6df2da in MallocEx /softether/SoftEtherVPN/src/Mayaqua/Memory.c:3523:8
    #4 0x7f259c793df1 in RecvLine /softether/SoftEtherVPN/src/Mayaqua/Network.c:19506:11
    #5 0x7f259c68b8aa in RecvHttpHeader /softether/SoftEtherVPN/src/Mayaqua/HTTP.c:1136:9
    #6 0x7f259d94168b in ServerDownloadSignature /softether/SoftEtherVPN/src/Cedar/Protocol.c:5791:7
    #7 0x7f259d91dc91 in ServerAccept /softether/SoftEtherVPN/src/Cedar/Protocol.c:1228:6
    #8 0x7f259d677def in ConnectionAccept /softether/SoftEtherVPN/src/Cedar/Connection.c:3077:6
    #9 0x7f259d760478 in TCPAcceptedThread /softether/SoftEtherVPN/src/Cedar/Listener.c:181:2
    #10 0x7f259c6ac632 in ThreadPoolProc /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:872:3
    #11 0x7f259c844b63 in UnixDefaultThreadProc /softether/SoftEtherVPN/src/Mayaqua/Unix.c:1594:2
    #12 0x7f259c094b42 in start_thread nptl/./nptl/pthread_create.c:442:8

Thread T30 created by T27 here:
    #0 0x565370d7d64c in __interceptor_pthread_create (/softether/asan_build/vpnserver+0x8a64c) (BuildId: 78a21100df0def7d4dc91631945f35b3269dac3a)
    #1 0x7f259c83d677 in UnixInitThread /softether/SoftEtherVPN/src/Mayaqua/Unix.c:1673:6
    #2 0x7f259c6ae087 in NewThreadInternal /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:1200:7
    #3 0x7f259c6ad525 in NewThreadNamed /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:997:10
    #4 0x7f259c770bcb in ConnectEx5 /softether/SoftEtherVPN/src/Mayaqua/Network.c:14692:8
    #5 0x7f259c7279e7 in ConnectEx4 /softether/SoftEtherVPN/src/Mayaqua/Network.c:14556:9
    #6 0x7f259c7279e7 in ConnectEx3 /softether/SoftEtherVPN/src/Mayaqua/Network.c:14552:9
    #7 0x7f259c7279e7 in ConnectEx2 /softether/SoftEtherVPN/src/Mayaqua/Network.c:14548:9
    #8 0x7f259c7279e7 in ConnectEx /softether/SoftEtherVPN/src/Mayaqua/Network.c:14544:9
    #9 0x7f259c7279e7 in GetMyPrivateIP /softether/SoftEtherVPN/src/Mayaqua/Network.c:4405:6
    #10 0x7f259c726e32 in RUDPIpQueryThread /softether/SoftEtherVPN/src/Mayaqua/Network.c:4345:8
    #11 0x7f259c6ac632 in ThreadPoolProc /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:872:3
    #12 0x7f259c844b63 in UnixDefaultThreadProc /softether/SoftEtherVPN/src/Mayaqua/Unix.c:1594:2
    #13 0x7f259c094b42 in start_thread nptl/./nptl/pthread_create.c:442:8

Thread T27 created by T25 here:
    #0 0x565370d7d64c in __interceptor_pthread_create (/softether/asan_build/vpnserver+0x8a64c) (BuildId: 78a21100df0def7d4dc91631945f35b3269dac3a)
    #1 0x7f259c83d677 in UnixInitThread /softether/SoftEtherVPN/src/Mayaqua/Unix.c:1673:6
    #2 0x7f259c6ae087 in NewThreadInternal /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:1200:7
    #3 0x7f259c6ad525 in NewThreadNamed /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:997:10
    #4 0x7f259c72f950 in NewRUDP /softether/SoftEtherVPN/src/Mayaqua/Network.c:5351:22
    #5 0x7f259c71bdf2 in NewRUDPServer /softether/SoftEtherVPN/src/Mayaqua/Network.c:5178:6
    #6 0x7f259c71bdf2 in ListenRUDPEx /softether/SoftEtherVPN/src/Mayaqua/Network.c:2504:6
    #7 0x7f259d7632cb in ListenerTCPMainLoop /softether/SoftEtherVPN/src/Cedar/Listener.c:405:9
    #8 0x7f259d765924 in ListenerThread /softether/SoftEtherVPN/src/Cedar/Listener.c:563:3
    #9 0x7f259c6ac632 in ThreadPoolProc /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:872:3
    #10 0x7f259c844b63 in UnixDefaultThreadProc /softether/SoftEtherVPN/src/Mayaqua/Unix.c:1594:2
    #11 0x7f259c094b42 in start_thread nptl/./nptl/pthread_create.c:442:8

Thread T25 created by T0 here:
    #0 0x565370d7d64c in __interceptor_pthread_create (/softether/asan_build/vpnserver+0x8a64c) (BuildId: 78a21100df0def7d4dc91631945f35b3269dac3a)
    #1 0x7f259c83d677 in UnixInitThread /softether/SoftEtherVPN/src/Mayaqua/Unix.c:1673:6
    #2 0x7f259c6ae087 in NewThreadInternal /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:1200:7
    #3 0x7f259c6ad525 in NewThreadNamed /softether/SoftEtherVPN/src/Mayaqua/Kernel.c:997:10
    #4 0x7f259d768069 in NewListenerEx5 /softether/SoftEtherVPN/src/Cedar/Listener.c:821:6
    #5 0x7f259d76799e in NewListenerEx4 /softether/SoftEtherVPN/src/Cedar/Listener.c:772:9
    #6 0x7f259da20f4b in SiNewServerEx /softether/SoftEtherVPN/src/Cedar/Server.c:10934:10
    #7 0x7f259d9e8e51 in SiNewServer /softether/SoftEtherVPN/src/Cedar/Server.c:10795:9
    #8 0x7f259d9e8e51 in StStartServer /softether/SoftEtherVPN/src/Cedar/Server.c:6696:12
    #9 0x7f259c848901 in UnixExecService /softether/SoftEtherVPN/src/Mayaqua/Unix.c:2560:3
    #10 0x7f259c849566 in UnixServiceMain /softether/SoftEtherVPN/src/Mayaqua/Unix.c:2765:3
    #11 0x7f259c84917a in UnixService /softether/SoftEtherVPN/src/Mayaqua/Unix.c:2679:5
    #12 0x7f259c029d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow (/softether/asan_build/vpnserver+0xa094e) (BuildId: 78a21100df0def7d4dc91631945f35b3269dac3a) in __asan_memmove
Shadow bytes around the buggy address:
  0x0c088002cac0: fa fa fd fd fd fd fd fa fa fa 00 00 00 00 00 00
  0x0c088002cad0: fa fa fd fd fd fd fd fa fa fa 00 00 00 00 00 02
  0x0c088002cae0: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fa
  0x0c088002caf0: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fa
  0x0c088002cb00: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fa
=>0x0c088002cb10: fa fa 00 00 00 00 00[03]fa fa fd fd fd fd fd fd
  0x0c088002cb20: fa fa fd fd fd fd fd fd fa fa fa fa fa fa fa fa
  0x0c088002cb30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c088002cb40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c088002cb50: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c088002cb60: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 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
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  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
==89041==ABORTING
VENDOR RESPONSE

Vendor pull request on Github: https://github.com/SoftEtherVPN/SoftEtherVPN/pull/1829

TIMELINE

2023-04-11 - Vendor Disclosure
2023-04-22 - Vendor Patch Release
2023-10-12 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.