Talos Vulnerability Report

TALOS-2023-1754

SoftEther VPN CiRpcAccepted() authentication bypass vulnerability

October 12, 2023
CVE Number

CVE-2023-27516

SUMMARY

An authentication bypass vulnerability exists in the CiRpcAccepted() functionality of SoftEther VPN 4.41-9782-beta and 5.01.9674. A specially crafted network packet can lead to unauthorized access. An attacker can send a network request 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 4.41-9782-beta
SoftEther VPN 5.01.9674

PRODUCT URLS

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

CVSSv3 SCORE

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

CWE

CWE-453 - Insecure Default Variable Initialization

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.

Within the SoftEtherVPN client there exists some interesting architectural decisions, first and foremost being the fact that the SoftEtherVPN client itself consists of an RPC client and an RPC server. While the process differs on Linux and Windows, there are some core common elements. The VPN client’s RPC server binds to a given TCP port within the range of 9931-9936, on all interfaces, and then the VPN client’s RPC client connects to that port to send the VPN client commands. For ease of reference, we will be referring to these components as the RPC client and RPC server, but just know that these are both contained within the client-side portion of SoftEtherVPN.

The internal RPC process in the SoftEtherVPN client ends up occurring on localhost by default. There is still an authentication mechanism in order to protect against other users on the same machine, and also because the VPN client RPC server can be configured to allow non-localhost connections. We observe the RPC server code for authentication below:

// RPC acceptance code
void CiRpcAccepted(CLIENT *c, SOCK *s)
{
    UCHAR hashed_password[SHA1_SIZE];
    UINT rpc_mode;
    UINT retcode;
    RPC *rpc;
    // Validate arguments
    if (c == NULL || s == NULL)
    {
        return;
    }

    // Receive the RPC mode
    if (RecvAll(s, &rpc_mode, sizeof(UINT), false) == false) // [1]
    {
        return;
    }

    rpc_mode = Endian32(rpc_mode);

    if (rpc_mode == CLIENT_RPC_MODE_NOTIFY)
    {
        // [...]
    }
    else if (rpc_mode == CLIENT_RPC_MODE_SHORTCUT || rpc_mode == CLIENT_RPC_MODE_SHORTCUT_DISCONNECT)
    {
        // [...]
    }

    // Password reception
    if (RecvAll(s, hashed_password, SHA1_SIZE, false) == false) // [2]
    {
        return;
    }

    retcode = 0;

    // Password comparison
    if (Cmp(hashed_password, c->EncryptedPassword, SHA1_SIZE) != 0) // [3]
    {
        retcode = 1;
    }

    if (c->PasswordRemoteOnly && IsLocalHostIP(&s->RemoteIP))
    {
        // If in a mode that requires a password only remote,
        // the password sent from localhost is considered to be always correct
        retcode = 0;
    }

    Lock(c->lock);
    {
        if (c->Config.AllowRemoteConfig == false)  // Default false on linux.
        {
            // If the remote control is prohibited,
            // identify whether this connection is from remote
            if (IsLocalHostIP(&s->RemoteIP) == false)
            {
                retcode = 2;
            }
        }
    }
    Unlock(c->lock);

// #define CLIENT_RPC_MODE_MANAGEMENT          1

After our RPC client connects to 0.0.0.0:9931, we receive four bytes to indicate the message type [1]. Assuming this message is of type CLIENT_RPC_MODE_MANAGEMENT, we skip down to receiving 0x14 more bytes [2], which is subsequently compared with the RPC server’s configured password [3]. If we look inside of our default vpn_client.config, we see that the c->EncryptedPassword corresponds to the configuration’s EncryptedPassword:

# Software Configuration File
# ---------------------------
#
# You may edit this file when the VPN Server / Client / Bridge program is not running.
#
# In prior to edit this file manually by your text editor,
# shutdown the VPN Server / Client / Bridge background service.
# Otherwise, all changes will be lost.
#
declare root
{
    bool DontSavePassword false
    byte EncryptedPassword +WzqGYrR3VYXrAhKPZLGEHcIwO8=

And if we decode the RPC server password:

$ echo -en "+WzqGYrR3VYXrAhKPZLGEHcIwO8=" | base64 -d | hexdump -C
00000000  f9 6c ea 19 8a d1 dd 56  17 ac 08 4a 3d 92 c6 10  |.l.....V...J=...|
00000010  77 08 c0 ef                                       |w...|
00000014

Curiously, by default, the RPC client is able to connect to the RPC server without any configuration, which raises the question of where this password comes from. A quick browse through the source code quickly reveals the CiInitConfiguration function:

// Initialize the settings
void CiInitConfiguration(CLIENT *c)
{
    // [...] 

    if (CiLoadConfigurationFile(c) == false)
    {
        CLog(c, "LC_LOAD_CONFIG_3");
        // Do the initial setup because the configuration file does not exist
        // Clear the password
        Sha0(c->EncryptedPassword, "", 0);            // [4]
        // Initialize the client configuration
        // Disable remote management
        c->Config.AllowRemoteConfig = false;
        StrCpy(c->Config.KeepConnectHost, sizeof(c->Config.KeepConnectHost), CLIENT_DEFAULT_KEEPALIVE_HOST);
        c->Config.KeepConnectPort = CLIENT_DEFAULT_KEEPALIVE_PORT;
        c->Config.KeepConnectProtocol = CONNECTION_UDP;
        c->Config.KeepConnectInterval = CLIENT_DEFAULT_KEEPALIVE_INTERVAL;
        c->Config.UseKeepConnect = false;   // Don't use the connection maintenance function by default in the Client
        // Eraser
        c->Eraser = NewEraser(c->Logger, 0);
    }
    else
    {
        CLog(c, "LC_LOAD_CONFIG_2");
    }

At [4] we can clearly see that the c->EncryptedPassword comes from this Sha0 function, which is called without input parameters. Following the code-flow, we eventually get to MY_SHA0_hash:

/* Convenience function */
const UCHAR* MY_SHA0_hash(const void* data, int len, UCHAR* digest) {
    MY_SHA0_CTX ctx;
    MY_SHA0_init(&ctx);
    MY_SHA0_update(&ctx, data, len);
    memcpy(digest, MY_SHA0_final(&ctx), MY_SHA0_DIGEST_SIZE);
    return digest;
}

But since this hashing is somehow able to function without input, we have to look inside MY_SHA0_final:

const UCHAR* MY_SHA0_final(MY_SHA0_CTX* ctx) {
    UCHAR *p = ctx->buf;
    UINT64 cnt = ctx->count * 8;
    int i;
    MY_SHA0_update(ctx, (UCHAR*)"\x80", 1); // [5]
    while ((ctx->count & 63) != 56) {
        MY_SHA0_update(ctx, (UCHAR*)"\0", 1);
    }
    for (i = 0; i < 8; ++i) {
        UCHAR tmp = (UCHAR) (cnt >> ((7 - i) * 8));
        MY_SHA0_update(ctx, &tmp, 1);
    }
    for (i = 0; i < 5; i++) {
        UINT tmp = ctx->state[i];
        *p++ = tmp >> 24;
        *p++ = tmp >> 16;
        *p++ = tmp >> 8;
        *p++ = tmp >> 0;
    }
    return ctx->buf;
}

As we can see above, a “\x80” byte is added to the sha context [5], as well as a given amount of padding bytes after. Regardless of the exact implementation, it suffices to say that this resulting hash from the Sha0 function never changes and will always by default be “\xf9\x6c\xea\x19\x8a\xd1\xdd\x56\x17\xac\x08\x4a\x3d\x92\xc6\x10\x77\x08\xc0\xef”. Since it is not common for a client program to consist of an internal RPC client and RPC server, it would be less expected for users to change this default password. This could lead to other local users gaining access to the RPC server, or remote users, if it is configured for it. If access is gained, certificates can be installed to allow for man-in-the-middle attacks on VPN connections. VPN authentication settings can be dumped, potentially leading to further compromise of the VPN endpoint.

VENDOR RESPONSE

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

TIMELINE

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

Credit

Discovered by Lilith >_> of Cisco Talos.