Talos Vulnerability Report

TALOS-2023-1768

SoftEther VPN ClientConnect() information disclosure vulnerability

October 12, 2023
CVE Number

CVE-2023-31192

SUMMARY

An information disclosure vulnerability exists in the ClientConnect() functionality of SoftEther VPN 5.01.9674. A specially crafted network packet can lead to a disclosure of sensitive information. An attacker can perform a man-in-the-middle attack 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

PRODUCT URLS

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

CVSSv3 SCORE

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

CWE

CWE-457 - Use of Uninitialized Variable

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.

On the client-side of the SoftEther VPN, the initial codeflow for setting up a VPN connection to a SoftEther VPN server starts at ClientConnect():

// Client connects to the server
bool ClientConnect(CONNECTION *c)
{
    // [...]
    // Validate arguments
    if (c == NULL)
    {
        return false;
    }

    sess = c->Session;


REDIRECTED:

    // [Connecting]
    c->Status = CONNECTION_STATUS_CONNECTING;
    c->Session->ClientStatus = CLIENT_STATUS_CONNECTING;

    s = ClientConnectToServer(c);    // [1]

  if (s == NULL)
    {
        // Do not retry if untrusted or hostname mismatched
        if (c->Session->LinkModeClient == false && (c->Err == ERR_CERT_NOT_TRUSTED || c->Err == ERR_HOSTNAME_MISMATCH) // [2]
            && (c->Session->Account == NULL || ! c->Session->Account->RetryOnServerCert))
        {
            c->Session->ForceStopFlag = true;
        }
        PrintStatus(sess, L"free");
        return false;  // hit this branch...
    }

    PrintStatus(sess, _UU("STATUS_5"));

    // Prompt user whether to continue on verification errors
    if ((c->Err == ERR_CERT_NOT_TRUSTED || c->Err == ERR_HOSTNAME_MISMATCH || c->Err == ERR_SERVER_CERT_EXPIRES) && ClientCheckServerCert(c, &expired) == false)   // [3]
    {

The client creates an SSL connection to the destination VPN server at [1]. Assuming the server responds with a certificate that matches with the target hostname, [2] and either the certificate is trusted or the user does not care if the server cert is trusted [3], we end up continuing in ClientConnect:

// Check the certificate of the redirected VPN server
if (c->UseTicket && CompareX(s->RemoteX, c->ServerX) == false)  // [4]
{
    c->Err = ERR_CERT_NOT_TRUSTED;  
    goto CLEANUP;
}
// [...]

// Send a signature
Debug("Uploading Signature...\n");
if (ClientUploadSignature(s) == false) // [5]
{
    c->Err = ERR_DISCONNECTED;
    goto CLEANUP;
}

if (c->Halt)
{
    // Stop
    c->Err = ERR_USER_CANCEL;
    goto CLEANUP;
}

// Receive a Hello packet
Debug("Downloading Hello...\n");
if (ClientDownloadHello(c, s) == false) // [6]
{
    goto CLEANUP;
}

The branch at [4] is ignored for now and is only utilized if our client has been redirected to another port or server. Our client just needs to send a particular HTTPS request at [5] to continue the code flow; it essentially just needs to contain POST /vpnsvc/connect.cgi HTTP/1.1\r\n to be considered valid by the Softether VPN server. At this point the server responds with an HTTP 200 response, with version information inside the body of the response, which is parsed at [6]. Continuing on in ClientConnect:

// [...]

// Send the authentication data
if (ClientUploadAuth(c) == false)    // [7] 
{
    goto CLEANUP;
}

if (c->Halt)
{
    // Stop
    c->Err = ERR_USER_CANCEL;
    goto CLEANUP;
}

// Receive a Welcome packet
p = HttpClientRecv(s);              // [8] 
if (p == NULL)
{
    c->Err = ERR_DISCONNECTED;
    goto CLEANUP;
}

The next packet in the sequence occurs at [7], when the client authenticates and passes all of the parameters that it needs for the connection. The server responds with an HTTP 200 [8]. If the authentication was successful, the error parameter is passed in the body of the response instead. Assuming there is no error, the VPN connection is considered setup, and encrypted packets immediately start being forwarded through the tunnel. However, a few more options get parsed from the server response that can affect the code flow, key among them being the Redirect parameter:

if (PackGetInt(p, "Redirect") != 0)
{
    UINT i;
    UINT ip;
    UINT num_port;
    UINT *ports;
    UINT use_port = 0;
    UINT current_port = c->ServerPort;
    UCHAR ticket[SHA1_SIZE];
    X *server_cert = NULL;
    BUF *b;

    ip = PackGetIp32(p, "Ip");  // [9]
    // [...]

    if (PackGetDataSize(p, "Ticket") == SHA1_SIZE)
    {
        PackGetData(p, "Ticket", ticket);  // [10]
    }

    b = PackGetBuf(p, "Cert");  // [11]
    if (b != NULL)
    {
        server_cert = BufToX(b, false); 
        FreeBuf(b);
    }

    if (c->ServerX != NULL)
    {
        FreeX(c->ServerX);
    }
    c->ServerX = server_cert;

    IPToStr32(c->ServerName, sizeof(c->ServerName), ip);
    c->ServerPort = use_port;

    c->UseTicket = true;
    Copy(c->Ticket, ticket, SHA1_SIZE);  // [12] 

    FreePack(p);

    p = NewPack();
    HttpClientSend(s, p); // CreateDummyValue in here
    FreePack(p);

    p = NULL;

    c->FirstSock = NULL;
    Disconnect(s);
    ReleaseSock(s);
    s = NULL;

    goto REDIRECTED; // [13]
}

Assuming our server responds with the Redirect parameter, the client will attempt to redo the entire process, directed wherever the VPN server points it to. This is done via the IP and Port parameters (the latter’s code omitted for brevity). The subsequent authentication is changed with the Ticket [10] and Cert [11] parameters. The Cert parameter is used to verify the destination, whereas the Ticket is passed to the destination server as the authentication method (instead of the normally configured one). Curiously, if the VPN server provides all this information to the client except for the Ticket itself, there is no code failure, and the contents of the ticket variable are still copied into the client, to be passed to the new server [12]. Since we can see that the ticket stack variable is never zeroed, this ends up resulting in 20 uninitialized stack bytes being copied into c->Ticket at [12], which are then sent inside the packet generated in ClientUploadAuth. As such, even if a malicious server redirects a client to itself, it can do so while also gaining a 20-byte information leak.

VENDOR RESPONSE

The vendor issued an advisory: https://www.softether.org/9-about/News/904-SEVPN202301

TIMELINE

2023-06-22 - Vendor Disclosure
2023-06-30 - Vendor Patch Release
2023-10-12 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.