Headline
CVE-2019-5164: TALOS-2019-0958 || Cisco Talos Intelligence Group
An exploitable code execution vulnerability exists in the ss-manager binary of Shadowsocks-libev 3.3.2. Specially crafted network packets sent to ss-manager can cause an arbitrary binary to run, resulting in code execution and privilege escalation. An attacker can send network packets to trigger this vulnerability.
Summary
An exploitable code execution vulnerability exists in the ss-manager binary of Shadowsocks-libev 3.3.2. Specially crafted network packets sent to ss-manager can cause an arbitrary binary to run, resulting in code execution and privilege escalation. An attacker can send network packets to trigger this vulnerability.
Tested Versions
Shadowsocks-libev 3.3.2
Product URLs
https://shadowsocks.org/en/index.html
CVSSv3 Score
7.8 - CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE
CWE-306: Missing Authentication for Critical Function
Details
Shadowsocks is a multi-platform and easy to use socks proxy with a focus on censorship evasion, thus highly popular in countries with restrictive internet policies. For the purposes of this advisory, we will be focusing on Shadowsocks-libev, a pure C implementation for lower end and embedded devices.
Inside shadowsocks-libev live a few different binaries, in focus for this writeup being ss-manager and ss-server. By default, when running ss-manager, the binary will bind to UDP 127.0.0.1 8839 and listen for json requests of commands ([ "add", "list", "remove", "ping", “stat” ]). The simplest way for communicating with ss-manager would be a script like so:
#!/usr/bin/env python2
import socket
def main():
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
buf = 'add: {"server_port":9998,"password":"<(^~^)>","method":"aes-256-cfb"}'
sock.sendto(buf,("127.0.0.1",8839))
The above script will cause ss-manager to spawn an instance of ss-server to run with a configuration that looks like such:
{
"server_port":9998,
"password":"<(^~^)>",
"method":"aes-256-cfb",
}
Which is also the bare minimum of parameters that one can provide. However, the complete list is [server_port, password, method, fast_open, no_delay, plugin, plugin_opts, mode]. Of most interest is the plugin parameter, which will cause ss-server to utilize another binary for further obfuscation. A high-level overview of shadowsocks plugins can be found here: https://shadowsocks.org/en/spec/Plugin.html. In any case, here is the source for how ss-server initializes a plugin:
int
start_plugin(const char *plugin,
const char *plugin_opts,
const char *remote_host,
const char *remote_port,
const char *local_host,
const char *local_port,
#ifdef __MINGW32__
uint16_t control_port,
#endif
enum plugin_mode mode)
{
[...]
if (!strncmp(plugin, "obfsproxy", strlen("obfsproxy")))
ret = start_obfsproxy(plugin, plugin_opts, remote_host, remote_port,
local_host, local_port, mode);
else
ret = start_ss_plugin(plugin, plugin_opts, remote_host, remote_port,
local_host, local_port, mode);
For our purposes, either of these paths, obfsproxy or not, would work, the only difference being that if the plugin binary’s name starts with obfsproxy, the plugin_opts are passed in as command line args instead of what we will see occur in start_ss_plugin:
static int
start_ss_plugin(const char *plugin,
const char *plugin_opts,
const char *remote_host,
const char *remote_port,
const char *local_host,
const char *local_port,
enum plugin_mode mode)
{
cork_env_add(env, "SS_REMOTE_HOST", remote_host);
cork_env_add(env, "SS_REMOTE_PORT", remote_port);
cork_env_add(env, "SS_LOCAL_HOST", local_host);
cork_env_add(env, "SS_LOCAL_PORT", local_port);
if (plugin_opts != NULL)
cork_env_add(env, "SS_PLUGIN_OPTIONS", plugin_opts); // [1]
exec = cork_exec_new(plugin);
cork_exec_add_param(exec, plugin); // argv[0]
extern int fast_open;
if (fast_open)
cork_exec_add_param(exec, "--fast-open");
#ifdef __ANDROID__
extern int vpn;
if (vpn)
cork_exec_add_param(exec, "-V");
#endif
cork_exec_set_env(exec, env);
sub = cork_subprocess_new_exec(exec, NULL, NULL, &exit_code);
#ifdef __MINGW32__
cork_subprocess_set_control(sub, sub_control_port);
#endif
return cork_subprocess_start(sub); // [2]
}
In quick summary, our plugin_opts are exported into the libcork subprocess [1] (which doesn’t really matter), and then the binary is run via execvp eventually inside of [2]. Thus, since we already need to have local access in order to talk with the ss-manager localhost socket, if a local attacker creates a binary that provides a reverse or bind shellcode, it can be caused to be run with the privileges of the ss-server process, resulting in privilege escalation.
It should be noted that a very similar bug was filed in 2017: https://www.x41-dsec.de/lab/advisories/x41-2017-010-shadowsocks-libev/, however this issue was fixed. The same vector is used in this advisory, just going further into the code path. Suggestions for a workaround were given by the author of shadowsocks in the github issue: https://github.com/shadowsocks/shadowsocks-libev/issues/1734#issuecomment-336589576:
madeye commented on Oct 13, 2017
For anyone using ss-manager, please use unix domain socket path if possible.
Exposing ss-manager to public is always dangerous.
However, since unix domain sockets are not the default setting for ss-manager, it is necessary to reiterate the above.
Mitigation
- Use a unix socket with ss-manager via --manager-socket.
Timeline
2019-11-08 - Vendor Disclosure
2019-12-03 - Public Release
Discovered by Lilith [>_>] of Cisco Talos.