CVE-2023-32275
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.
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
SoftEther VPN - https://www.softether.org/
5.5 - CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CWE-201 - Information Exposure Through Sent Data
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.
The vendor issued an advisory: https://www.softether.org/9-about/News/904-SEVPN202301
2023-06-12 - Vendor Disclosure
2023-06-30 - Vendor Patch Release
2023-10-12 - Public Release
Discovered by Lilith >_> of Cisco Talos.