Talos Vulnerability Report

TALOS-2019-0956

Shadowsocks-libev ss-server UdpRelay Denial-of-Service Vulnerability

December 3, 2019
CVE Number

CVE-2019-5163

Summary

An exploitable denial-of-service vulnerability exists in the UDPRelay functionality of Shadowsocks-libev 3.3.2. When utilizing a Stream Cipher and a local_address, arbitrary UDP packets can cause a FATAL error code path and exit. An attacker can send arbitrary UDP packets to trigger this vulnerability.

Tested Versions

Shadowsocks-libev 3.3.2

Product URLs

https://shadowsocks.org/en/index.html

CVSSv3 Score

5.9 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/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.

For a basic usecase and overview of ShadowSocks-libev, a setup like the following is required:

 ______________________               nnnnnnnnnnnnnnnnnn             __________________
|              \ ss-   |             c                  3           |                  |
|  laptop or   \ local |             c  Untrusted       3           | Remote Server    |
|  home network\       | ------------c  Internet        3-----------| Running          |
|              \       |             c                  3     [-_-]^| ss-server        |
|______________\_______|             c__________________3           |__________________|

A given laptop or home network will have an ss-local instance which listens on a given port and then forwards all traffic out via a specific encryption method specified in a configuration file or command-line argument. Both the ss-local instance and ss-server must have the same parameters in order for the setup to work, and an example configuration file might look like:

{
    "server":"192.168.149.144",
    "server_port":9999,
    "local_address": "127.0.0.1",
    "local_port":1080,
    "password":"sample_password",
    "user":"sample_user",
    "timeout":600,
    "method":"aes-256-cfb"
}

To get more specific into what attack surface is being examined (since there's 2 ports for both ss-local and ss-remote), the [-_-]^ above designates the attack surface, the ss-server port that is accessible from the internet. Ideally, when a user has configured their browser of choice to use the Shadowsocks proxy, ss-local will read in the http or https request, encrypt it, and then send it off to the ss-server instance. The ss-server instance will decrypt the packet and then send it off to wherever it needs to go, which is specified in the message as either an ipv4, ipv6, or hostname.

It is very important to note that this particular vulnerability is only exploitable if three conditions are met.

First, ss-server must be using a stream cipher. Depending on the cipher mode chosen, encryption and decryption can be done many ways, but the most important decision is whether to use a stream cipher or an AEAD cipher. Normal stream ciphers only provide confidentiality and no sort of authentication or integrity checks, unlike the AEAD ciphers which provide all three. As mentioned in the documentation, it is recommended that users use AEAD ciphers whenever possible: https://shadowsocks.org/en/spec/AEAD-Ciphers.html, and this advisory will hopefully demonstrate another reason why.

The second precondition needed is that the user is using the UDPRelay functionality.

The third precondition is either that the local_address field is set in the shadowsocks configuration, or that ss-server is run with the -b <ip_address> flag. This option is used to prevent shadowsocks from sending decrypted traffic out interfaces that it shouldn't be.

Assuming that these three conditions (udprelay, local_address, stream cipher), an attacker can spam arbitrary UDP data to the ss-server and it will exit on its own:

boop@doop:~/shadowsocks/bin# cat config.server
{
    "server":"192.168.149.144",
    "server_port":9999,
    "local_address": "127.0.0.1",
    "local_port":1080,
    "password":"sample_password",
    "user": "sample_user",
    "timeout":600,
    "method":"aes-256-cfb"
}

Starting program: ~/shadowsocks/bin/ss-server -u -c config.server
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
 2019-10-16 11:38:47 INFO: binding to outbound IPv4 addr: 127.0.0.1
 2019-10-16 11:38:47 INFO: UDP relay enabled
 2019-10-16 11:38:47 INFO: initializing ciphers... aes-256-cfb
 2019-10-16 11:38:47 INFO: tcp server listening at 127.0.0.1:9999
 2019-10-16 11:38:47 INFO: udp server listening at 127.0.0.1:9999
 2019-10-16 11:38:50 ERROR: [udp] unable to resolve
 2019-10-16 11:38:50 ERROR: [udp] invalid header with addr type 171
 2019-10-16 11:38:50 ERROR: [udp] unable to resolve
 2019-10-16 11:38:50 ERROR: [udp] sendto_remote: Invalid argument
 2019-10-16 11:38:50 ERROR: [udp] unable to resolve
 2019-10-16 11:38:50 ERROR: [udp] unable to resolve
 2019-10-16 11:38:50 ERROR: [udp] unable to resolve
 2019-10-16 11:38:50 ERROR: [udp] unable to resolve
 2019-10-16 11:38:50 ERROR: [udp] unable to resolve
 2019-10-16 11:38:50 ERROR: [udp] unable to resolve
 2019-10-16 11:38:51 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] invalid header with addr type 171
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] sendto_remote: Invalid argument
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:38:53 ERROR: [udp] unable to resolve
 2019-10-16 11:39:05 ERROR: [udp] unable to resolve
 2019-10-16 11:39:06 ERROR: [udp] invalid header with addr type 171
 2019-10-16 11:39:06 ERROR: [udp] unable to resolve
 2019-10-16 11:39:06 ERROR: [udp] sendto_remote: Invalid argument
 2019-10-16 11:39:07 ERROR: [udp] unable to resolve
 2019-10-16 11:39:07 ERROR: [udp] unable to resolve
 2019-10-16 11:39:07 ERROR: [udp] unable to resolve
 2019-10-16 11:39:07 ERROR: [udp] unable to resolve
 2019-10-16 11:39:07 ERROR: [udp] unable to resolve
 2019-10-16 11:39:07 ERROR: [udp] unable to resolve
 2019-10-16 11:39:07 ERROR: [udp] invalid header with addr type 107
 2019-10-16 11:39:07 ERROR: [udp] unable to resolve
 2019-10-16 11:39:08 ERROR: [udp] invalid header with addr type 7
 2019-10-16 11:39:08 ERROR: [udp] unable to resolve
 2019-10-16 11:39:08 ERROR: bind_to_addr: Resource temporarily unavailable
 2019-10-16 11:39:08 ERROR: [udp] cannot bind remote
[Inferior 1 (process 2531) exited with code 0377]

The code involved in this exit can be found around udprelay.c:380:

    379          if (is_bind_local_addr) {
                     // remote_sock=0x7
->  380              if (bind_to_addr(&local_addr_v6, remote_sock) == -1) {
    381                  ERROR("bind_to_addr");
    382                  FATAL("[udp] cannot bind remote");
    383                  return -1;
    384              }
    385          } else {

If the address given by the udp back matches that of the configuration option (in this case "127.0.0.1"), then all is fine:

<(^_^)>  print *storage
$2 = {
  ss_family = 0x2,
  __ss_padding = "\000\000\177\000\000\001", '\000' <repeats 111 times>,
  __ss_align = 0x0
}

But if the socket parameters passed are all 0, the error occurs:

<(^_^)>  print *storage
$2 = {
  ss_family = 0x0,
  __ss_padding = '\000' <repeats 117 times>,
  __ss_align = 0x0
}

Mitigation

  • Use an AEAD Cipher.

Timeline

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

Credit

Discovered by a member of Cisco Talos.