CVE-2023-27516
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.
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/
7.3 - CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L
CWE-453 - Insecure Default Variable Initialization
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.
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.