Talos Vulnerability Report


Shadowsocks-libev ss-manager add_server Code Execution Vulnerability

December 3, 2019
CVE Number



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


CVSSv3 Score

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 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"}'

The above script will cause ss-manager to spawn an instance of ss-server to run with a configuration that looks like such:


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:

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,
             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);
    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");

    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);

    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.


  • Use a unix socket with ss-manager via --manager-socket.


2019-11-08 - Vendor Disclosure
2019-12-03 - Public Release


Discovered by Lilith [>_>] of Cisco Talos.