CVE-2019-5164
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.
Shadowsocks-libev 3.3.2
https://shadowsocks.org/en/index.html
7.8 - CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE-306: Missing Authentication for Critical Function
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.
--manager-socket
.2019-11-08 - Vendor Disclosure
2019-12-03 - Public Release
Discovered by Lilith [>_>] of Cisco Talos.