CVE-2023-22308
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.
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.
SoftEther VPN - https://www.softether.org/
7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-191 - Integer Underflow (Wrap or Wraparound)
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.
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 pull request on Github: https://github.com/SoftEtherVPN/SoftEtherVPN/pull/1824
2023-04-03 - Vendor Disclosure
2023-04-03 - Initial Vendor Contact
2023-04-17 - Vendor Patch Release
2023-10-12 - Public Release
Discovered by Lilith >_> of Cisco Talos.