Talos Vulnerability Report

TALOS-2023-1737

SoftEther VPN vpnserver OvsProcessData denial of service vulnerability

October 12, 2023
CVE Number

CVE-2023-22308

SUMMARY

An integer underflow vulnerability exists in the vpnserver OvsProcessData functionality of SoftEther VPN 5.01.9674 and 5.02. A specially crafted network packet can lead to denial of service. An attacker can send a malicious packet to trigger this vulnerability.

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-191 - Integer Underflow (Wrap or Wraparound)

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.

Among the various VPN protocols that SoftEther can talk in, by default, OpenVPN code can be reached via any of the open TCP ports. Based on the start of the input buffer, we can either hit the OpenVPN code or the Wireguard code, or even the normal HTTPS server for management of the VPN configuration itself:

bool ProtoHandleConnection(PROTO *proto, SOCK *sock, const char *protocol)
{
    const PROTO_IMPL *impl;
    void *impl_data = NULL;

    UCHAR *buf;
    TCP_RAW_DATA *recv_raw_data;
    FIFO *send_fifo;
    INTERRUPT_MANAGER *im;
    SOCK_EVENT *se;

    if (proto == NULL || sock == NULL)
    {
        return false;
    }

    {
        const PROTO_CONTAINER *container = NULL;
        wchar_t *proto_name;
        LIST *options;

        if (protocol != NULL)
        {
            UINT i;
            for (i = 0; i < LIST_NUM(proto->Containers); ++i)
            {
                const PROTO_CONTAINER *tmp = LIST_DATA(proto->Containers, i);
                if (StrCmp(tmp->Name, protocol) == 0)
                {
                    container = tmp;
                    break;
                }
            }
        }
        else
        {
            UCHAR tmp[PROTO_CHECK_BUFFER_SIZE]; // 2 bytes

            if (Peek(sock, tmp, sizeof(tmp)) == 0)
            {
                return false;
            }

            container = ProtoDetect(proto, PROTO_MODE_TCP, tmp, sizeof(tmp)); // [1] 
        }

Unless a protocol is explicitly passed to the ProtoHandleConnection function, we end up iterating over all the loaded proto->Containers and calling the container’s ProtoDetect() function at [1]:

const PROTO_CONTAINER *ProtoDetect(const PROTO *proto, const PROTO_MODE mode, const UCHAR *data, const UINT size)
{
    UINT i;

    if (proto == NULL || data == NULL || size == 0)
    {
        return NULL;
    }

    for (i = 0; i < LIST_NUM(proto->Containers); ++i)
    {
        const PROTO_CONTAINER *container = LIST_DATA(proto->Containers, i);
        const PROTO_IMPL *impl = container->Impl;

        if (ProtoEnabled(proto, container->Name) == false)
        {
            Debug("ProtoDetect(): skipping %s because it's disabled\n", container->Name);
            continue;
        }

        if (impl->IsPacketForMe != NULL && impl->IsPacketForMe(mode, data, size)) // [2]
        {
            Debug("ProtoDetect(): %s detected\n", container->Name);
            return container;
        }
    }

    Debug("ProtoDetect(): unrecognized protocol\n");
    return NULL;
}

ProtoDetect() boils down to answering the question: Is this packet designated for this specific protocol? And so each container calls its Impl->IsPacketForMe at [2]. To understand what functions underpin the IsPacketForMe function pointer, let us quickly look to see where protocols actually get loaded:

PROTO *ProtoNew(CEDAR *cedar)
{
    PROTO *proto;

    if (cedar == NULL)
    {
        return NULL;
    }

    proto = Malloc(sizeof(PROTO));
    proto->Cedar = cedar;
    proto->Containers = NewList(ProtoContainerCompare);
    proto->Sessions = NewHashList(ProtoSessionHash, ProtoSessionCompare, 0, true);

    AddRef(cedar->ref);

    // WireGuard
    Add(proto->Containers, ProtoContainerNew(WgsGetProtoImpl()));   // [3]
    // OpenVPN
    Add(proto->Containers, ProtoContainerNew(OvsGetProtoImpl()));   // [4]
    // SSTP
    Add(proto->Containers, ProtoContainerNew(SstpGetProtoImpl()));  // [5]

    proto->UdpListener = NewUdpListener(ProtoHandleDatagrams, proto, &cedar->Server->ListenIP);

    return proto;
}

Clearly written above, we can see that the only VPN protocols enabled by default in SoftEther are WireGuard [3], OpenVPN [4] and SSTP [5]. Since we only care about OpenVPN in this advisory, only the OvsGetProtoImpl function will follow:

const PROTO_IMPL *OvsGetProtoImpl()
{
    static const PROTO_IMPL impl =
    {
        OvsName,
        OvsOptions,
        NULL,
        OvsInit,
        OvsFree,
        OvsIsPacketForMe,
        OvsProcessData,
        OvsProcessDatagrams
    };

    return &impl;
}

Wrapped by the most simple function, OvsGetProtoImpl() returns a set of function pointers to our proto->Containers. As such, we must now look at the OvsIsPacketForMe function and subsequently OvsProcessData:

// Check whether it's an OpenVPN packet
bool OvsIsPacketForMe(const PROTO_MODE mode, const void *data, const UINT size)
{
    if (data == NULL || size < 2)
    {
        return false;
    }

    if (mode == PROTO_MODE_TCP)  // [6]
    {
        const UCHAR *raw = data;
        if (raw[0] == 0x00 && raw[1] == 0x0E)
        {
            return true;
        }
    }
    else if (mode == PROTO_MODE_UDP)
    {
        OPENVPN_PACKET *packet = OvsParsePacket(data, size);
        if (packet == NULL)
        {
            return false;
        }

        OvsFreePacket(packet);
        return true;
    }

    return false;
}

Since we’re talking TCP, we hit the branch at [6]. The only thing that determines whether or not a packet is treated as OpenVPN traffic is if it starts with “\x00\x0E”. Continuing on, we now examine where the actual processing of data occurs in OvsProcessData:

bool OvsProcessData(void *param, TCP_RAW_DATA *in, FIFO *out)
{
    bool ret = true;
    UINT i;
    OPENVPN_SERVER *server = param;
    UCHAR buf[OPENVPN_TCP_MAX_PACKET_SIZE];  // 2000, 0x7d0  

    if (server == NULL || in == NULL || out == NULL)
    {
        return false;
    }

    // Separate to a list of datagrams by interpreting the data received from the TCP socket
    while (true)
    {
        UDPPACKET *packet;
        USHORT payload_size, packet_size;
        FIFO *fifo = in->Data;
        const UINT fifo_size = FifoSize(fifo);

        if (fifo_size < sizeof(USHORT))
        {
            // Non-arrival
            break;
        }

        // The beginning of a packet contains the data size
        payload_size = READ_USHORT(FifoPtr(fifo));           // [7]
        packet_size = payload_size + sizeof(USHORT);         // [8] 

        if (payload_size == 0 || packet_size > sizeof(buf))
        {
            ret = false;
            Debug("OvsProcessData(): Invalid payload size: %u bytes\n", payload_size);
            break;
        }

        if (fifo_size < packet_size) // fifo_size ends up being actual size of bytes recvd...
        {
            // Non-arrival
            break;
        }


    if (ReadFifo(fifo, buf, packet_size) != packet_size)    // [9]
    {
        ret = false;
        Debug("OvsProcessData(): ReadFifo() failed to read the packet\n");
        break;
    }

    // Insert packet into the list  // only a dos since we're oob'ing into thread stack data
    packet = NewUdpPacket(&in->SrcIP, in->SrcPort, &in->DstIP, in->DstPort, Clone(buf + sizeof(USHORT), payload_size), payload_size);  // [10]
    Add(server->RecvPacketList, packet);
}

// Process the list of received datagrams
OvsRecvPacket(server, server->RecvPacketList, OPENVPN_PROTOCOL_TCP); // [11]

Starting out, because of our required “\x00\x0e” start to our packet, we hit the READ_USHORT at [7], and payload_size is naturally 0xe. The subsequent 0xe bytes get read in at [9], and then a UDP packet is created at [10] to pass the packet on for further processing at [11]. We need not go further, however, since the vulnerability lies plainly above. When hitting the next iteration of the while (true) loop, there is no requirement for our buffer to start with “\x00\x0e”. As such, if our next two bytes are, say, “\xFF\xFF”, then the payload_size gets set to 0xFFFF at [7], while the packet_size ushort gets set to 0x1 at [8]. Thus we read in a single byte at [9], which passes the return value check, and a new UDP packet is created with a length of payload_size at [10] (not packet_size).

While normally this would just read in out-of-bounds data from the buf stack buffer and pass it into further processing, since we’re in a linux thread, there’s usually a guard page at the bottom of the thread stack like so:

  0x7f5d00000000     0x7f5d00063000    0x63000        0x0  rw-p   // buf @ 0x7f5d00045abe
  0x7f5d486c6000     0x7f5d486c7000     0x1000        0x0  ---p   // guard pages
  0x7f5d486c7000     0x7f5d486f8000    0x31000        0x0  rw-p   // next thread 

Apparently the OvsProcessData function is not far up enough in the backtrace, and so our 0xFFFF read on the stack ends up hitting the guard page and causing a crash. This might behave differently if we are on a Windows SoftEther server; it has not been tested.

Crash Information

Thread 22 "vpnserver" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f5d486c5380 (LWP 1214911)]
__memmove_evex_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:708
708     ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S: No such file or directory.
<(^.^)>#bt
#0  __memmove_evex_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:708
#1  0x00007f5d49959e1c in Clone (addr=addr@entry=0x7f5d486c3e52, size=size@entry=65535) at /softether/SoftEtherVPN_orig/src/Mayaqua/Memory.c:3790
#2  0x00007f5d49ae7253 in OvsProcessData (param=0x7f5d0001d710, in=0x7f5d0000fa40, out=0x7f5d0000bbe0) at /softether/SoftEtherVPN_orig/src/Cedar/Proto_OpenVPN.c:171
#3  0x00007f5d49acfc0a in ProtoHandleConnection (proto=0x558a14d696d0, sock=sock@entry=0x7f5d1c007080, protocol=protocol@entry=0x0) at /softether/SoftEtherVPN_orig/src/Cedar/Proto.c:590
#4  0x00007f5d49aa6213 in ConnectionAccept (c=c@entry=0x7f5d0000df40) at /softether/SoftEtherVPN_orig/src/Cedar/Connection.c:3027
#5  0x00007f5d49ac3142 in TCPAcceptedThread (param=<optimized out>, t=<optimized out>) at /softether/SoftEtherVPN_orig/src/Cedar/Listener.c:181
#6  TCPAcceptedThread (t=<optimized out>, param=<optimized out>) at /softether/SoftEtherVPN_orig/src/Cedar/Listener.c:140
#7  0x00007f5d4995443d in ThreadPoolProc (param=0x7f5cfc00c730, t=0x7f5cfc00c500) at /softether/SoftEtherVPN_orig/src/Mayaqua/Kernel.c:872
#8  ThreadPoolProc (t=0x7f5cfc00c500, param=0x7f5cfc00c730) at /softether/SoftEtherVPN_orig/src/Mayaqua/Kernel.c:827
#9  0x00007f5d499907d1 in UnixDefaultThreadProc (param=0x7f5cfc0072e0) at /softether/SoftEtherVPN_orig/src/Mayaqua/Unix.c:1594
#10 0x00007f5d49759b43 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442
#11 0x00007f5d497eba00 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81
VENDOR RESPONSE

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

TIMELINE

2023-04-03 - Vendor Disclosure
2023-04-03 - Initial Vendor Contact
2023-04-17 - Vendor Patch Release
2023-10-12 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.