Talos Vulnerability Report

TALOS-2023-1753

SoftEther VPN CtEnumCa() information disclosure vulnerability

October 12, 2023
CVE Number

CVE-2023-32275

SUMMARY

An information disclosure vulnerability exists in the CtEnumCa() functionality of SoftEther VPN 4.41-9782-beta and 5.01.9674. Specially crafted network packets can lead to a disclosure of sensitive information. An attacker can send packets 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

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

CWE

CWE-201 - Information Exposure Through Sent Data

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.

Upon connection, the RPC client immediately authenticates to the RPC server with a message formed like “\x00\x00\x00\x01” for the message type and then a 20-byte SHA0 hash of the RPC server password. Assuming this authentication is bypassed in one way or another, legitimately or not, our RCP client then has access to the following RPC messages:

    if (StrCmpi(name, "GetClientVersion") == 0)
    else if (StrCmpi(name, "GetCmSetting") == 0)
    else if (StrCmpi(name, "SetCmSetting") == 0)
    else if (StrCmpi(name, "SetPassword") == 0) 
    else if (StrCmpi(name, "GetPasswordSetting") == 0)
    else if (StrCmpi(name, "EnumCa") == 0)    // [1]
    else if (StrCmpi(name, "AddCa") == 0) 
    else if (StrCmpi(name, "DeleteCa") == 0)
    else if (StrCmpi(name, "GetCa") == 0) 
    else if (StrCmpi(name, "EnumSecure") == 0)
    else if (StrCmpi(name, "UseSecure") == 0)
    else if (StrCmpi(name, "GetUseSecure") == 0) 
    else if (StrCmpi(name, "EnumObjectInSecure") == 0)
    else if (StrCmpi(name, "CreateVLan") == 0)
    else if (StrCmpi(name, "UpgradeVLan") == 0)
    else if (StrCmpi(name, "GetVLan") == 0) 
    else if (StrCmpi(name, "SetVLan") == 0) 
    else if (StrCmpi(name, "EnumVLan") == 0) 
    else if (StrCmpi(name, "DeleteVLan") == 0)
    else if (StrCmpi(name, "EnableVLan") == 0)
    else if (StrCmpi(name, "DisableVLan") == 0)
    else if (StrCmpi(name, "CreateAccount") == 0)
    else if (StrCmpi(name, "EnumAccount") == 0)
    else if (StrCmpi(name, "DeleteAccount") == 0)
    else if (StrCmpi(name, "SetStartupAccount") == 0)
    else if (StrCmpi(name, "RemoveStartupAccount") == 0)
    else if (StrCmpi(name, "GetIssuer") == 0)
    else if (StrCmpi(name, "GetCommonProxySetting") == 0)
    else if (StrCmpi(name, "SetCommonProxySetting") == 0)
    else if (StrCmpi(name, "SetAccount") == 0)
    else if (StrCmpi(name, "GetAccount") == 0)
    else if (StrCmpi(name, "RenameAccount") == 0)
    else if (StrCmpi(name, "SetClientConfig") == 0)
    else if (StrCmpi(name, "GetClientConfig") == 0)
    else if (StrCmpi(name, "Connect") == 0)
    else if (StrCmpi(name, "Disconnect") == 0)
    else if (StrCmpi(name, "GetAccountStatus") == 0)

All these RPC commands encompass the functionality of what the normal VPN client is capable of from the GUI or commandline, so there’s not really much to be gained if an attacker already has access to the user account of whomever is using this SoftetherVPN client. Since the RPC server binds to the network stack, another user on the same computer who is able to bypass the authentication mechanism has a lot of interesting things to play with. In this advisory we’re just going to be looking at the EnumCa command up at [1], whose code flow we examine below:

else if (StrCmpi(name, "EnumCa") == 0)
{
    RPC_CLIENT_ENUM_CA a;
    if (CtEnumCa(c, &a) == false)
    {
        RpcError(ret, c->Err);
    }
    else
    {
        OutRpcClientEnumCa(ret, &a);
        CiFreeClientEnumCa(&a);
    }
}

Nothing too special here; all the RPC commands tend to follow this pattern. Let us quickly look at the RPC_CLIENT_ENUM_CA struct before continuing on into CtEnumCa:

// Certificate enumeration item
struct RPC_CLIENT_ENUM_CA_ITEM
{
    UINT Key;                               // Certificate key  // [2]
    wchar_t SubjectName[MAX_SIZE];          // Issued to
    wchar_t IssuerName[MAX_SIZE];           // Issuer
    UINT64 Expires;                         // Expiration date
};

// Certificate enumeration
struct RPC_CLIENT_ENUM_CA
{
    UINT NumItem;                           // Number of items 
    RPC_CLIENT_ENUM_CA_ITEM **Items;        // Item             // [3]   
};


// Enumerate the trusted CA
bool CtEnumCa(CLIENT *c, RPC_CLIENT_ENUM_CA *e)
{
    // Validate arguments
    if (c == NULL || e == NULL)
    {
        return false;
    }

    Zero(e, sizeof(RPC_CLIENT_ENUM_CA));  

    LockList(c->Cedar->CaList);
    {
        UINT i;
        e->NumItem = LIST_NUM(c->Cedar->CaList);
        e->Items = ZeroMalloc(sizeof(RPC_CLIENT_ENUM_CA_ITEM *) * e->NumItem); // [4]

        for (i = 0;i < e->NumItem;i++)
        {
            X *x = LIST_DATA(c->Cedar->CaList, i);
            e->Items[i] = ZeroMalloc(sizeof(RPC_CLIENT_ENUM_CA_ITEM));
            GetAllNameFromNameEx(e->Items[i]->SubjectName, sizeof(e->Items[i]->SubjectName), x->subject_name);
            GetAllNameFromNameEx(e->Items[i]->IssuerName, sizeof(e->Items[i]->IssuerName), x->issuer_name);
            e->Items[i]->Expires = x->notAfter;
            e->Items[i]->Key = POINTER_TO_KEY(x); // [5]
        }
    }
    UnlockList(c->Cedar->CaList);

    return true;
}

The codeflow is rather simple, copying the c->Cedar->CaList into the fore-mentioned RPC_CLIENT_ENUM_CA *e struct inside of a loop. An allocation is made to hold all of the RPC_CLIENT_ENUM_CA_ITEM * pointers [4], and then each of these pointers is graced with a corresponding object inside of the subsequent loop. Nothing looks out of the ordinary, but when setting the Key member of each struct[2] the POINTER_TO_KEY macro[5] is rather eye-catching. Let’s see what it does:

// Convert the pointer to UINT
#define POINTER_TO_KEY(p)       ((sizeof(void *) == sizeof(UINT)) ? (UINT)(p) : HashPtrToUINT(p))  // [6]


// Hash a pointer to a 32-bit
UINT HashPtrToUINT(void *p)
{
    UCHAR hash_data[MD5_SIZE];
    UINT ret;
    // Validate arguments
    if (p == NULL)
    {
        return 0;
    }

    Hash(hash_data, &p, sizeof(p), false);  // [7]

    Copy(&ret, hash_data, sizeof(ret));     // [8]

    return ret;
}

// Hash function
void Hash(void *dst, void *src, UINT size, bool sha)
{
    // Validate arguments
    if (dst == NULL || (src == NULL && size != 0))
    {
        return;
    }

    if (sha == false)
    {
        // MD5 hash
        MD5(src, size, dst);               // [9]
    }
    // [...]
}

At [6], we quickly see that if we’re dealing with a 32-bit machine, then the e->Items[i]->Key is just the pointer passed into the function, and in our case would just be the address of the x509 object inside of our global list (i.e. ((X *)c->Cedar->CaList[x]->p)->x509). In the case of 64-bit installations, it’s slightly more complicated, as the pointer gets hashed [7] (which for this code flow is just an MD5 sum [9]), and then the top four bytes of the hash are copied over into our return value [8].

But what does this mean in summary? Well, if we send an EnumCa RPC request to a 32-bit RPC server, then we get the heap addresses of all the CA certificates that are currently loaded, which is a pretty clear-cut information disclosure. For 64-bit RPC servers, the vulnerability is a little more obtuse, as we would need to generate a set of MD5 rainbow tables in order to know the address of all of the loaded CA certificates. This is not unreasonable, however, given the limited set of possible heap addresses.

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.