Back to the articles
IoT Bug Hunting - Part 2 - Walkthrough of discovering command injections in firmware binaries
Table of contents
Case Study of CVE-2023-4249
In Part 1, we have shown the basics of the bug-hunting process to help you start with BugProve. Now, we continue our journey with a case study on how our proprietary PRIS™ engine can help you find serious bugs lurking in the hidden depths of IoT devices. We have recently unveiled a comprehensive security advisory highlighting a series of vulnerabilities that Attila Szász found within several firmware versions running on multiple Zavio IP cameras. With this article, we would like to give you some insight into the bug-hunting process, using an OS command injection vulnerability of Zavio IP cameras as an example.
Choosing your target for bug hunting
The selection of the appropriate research area, especially the target device, is decisive during the bug-hunting process. In general, you will succeed more with less researched targets. Current IP cameras are infamous for their poor security, while their advanced features and functionality make them excellent targets, Zavio devices are no exception. Though the manufacturer's official website was still available at http[:]//zavio.com during our original investigation, we now need to use the Wayback Machine to see older versions of the website and explore the various models the vendor offered.
You should still be able to download the last and latest available firmware version
01_C043S2 for the CF7300 3MP Box Camera from here, as Figure 1. shows. Of course, you can also choose another model and try to reproduce already known vulnerabilities by following the steps described in this article, or you can even find some new bugs. Be aware that the same OS command injection vulnerability affects many models and multiple firmware versions. However, there may be differences between them regarding exploitation.
Firmware-focused program analysis to the rescue - beyond binwalk and IDA
Once you have the firmware in your hands, the next step is to extract the contents of the compressed
CF7300_01_C043S2.rar file for further analysis. Although you could use some basic tools like
binwalk, mentioned in our article discussing the fundamentals of binary analysis, it is faster, more convenient and more efficient to upload the binary for automatic firmware analysis using your free BugProve account.
Besides extracting valuable artifacts, such as the root file system and the kernel, for further analysis, you can quickly find a lot of additional information on the Overview page regarding the overall security posture of the firmware, including, but not limited to, the architecture, the kernel version, the number of known vulnerabilities and their risk distribution, as well as the number of unsafe function calls, which are good predictors of possible zero-day vulnerabilities. You can find plenty of information in the documentation on how to create firmware and binary scans, so I will focus on details relevant to our current analysis.
As the analysis progresses, the partial results become available continuously, and although the dependency analysis and PRIS™ scans require some time, you don't have to wait long for the first results. As a bug hunter, one of the most important pages is the Weak Binaries page shown in Figure 3., which summarizes the identified binaries, showing the number of unsafe function calls and the status of common binary protection mechanisms.
By default, zero-day scans will be started automatically for the top five binaries most likely to have vulnerabilities. However, you can also take the decision into your own hands and run PRIS™ analysis on the binaries of your choosing. For example, if you are interested in command injection vulnerabilities, one possible approach would be to sort the binaries in descending order based on the number of
system() calls which can potentially lead to command injection vectors, and start scanning the
The next most important page is the Zero-Day Scans page shown in Figure 4., which summarizes the results of the PRIS™ scans. Although the scan of the
ir_control and the
streamd binaries did not yield any results, the analysis flagged a high number of potential vulnerabilities in the
param binary, which is definitely worth investigating.
Although the binary analysis report contains a lot of buffer overflows, you can read about those in our security advisory. Here and now, we want to draw your attention to another vulnerability that hides among the many buffer overflows. As you scroll through the decompiled source code of the
param binary shown in Figure 5., you might notice some potential command injection vectors due to some potentially unsafe
popen() calls that execute command strings like
/usr/sbin/check_param resolution %d %s, constructed with user-provided input. In addition to the reported memory corruption vulnerabilities, you should pay close attention to other potential issues and investigate them further.
While you have several options for downloading the extracted artifacts, including the opportunity to download the relevant binaries from the sub-scan result pages, for further manual analysis, it may be easier to simply grab the archive containing the entire unpacked file system from the File Explorer, as shown in Figure 6.
IoT device emulation for vulnerability analysis
If you don't have an actual device, you will need an emulator to validate your findings. QEMU is an open-source emulator and virtualization tool that emulates different CPU architectures. With user-mode emulation, you can run binaries compiled for different CPUs. In contrast, full-system emulation allows you to simulate the entire target system, including the corresponding processor and various peripherals such as the disk, the network controller, etc.
If you need full-system emulation for a new target, try FirmAE, a prototype of so-called arbitrated emulation based on Firmadyne. While this new prototype can do most of the weight lifting to address some high-level failure problems and boost the success rate of automated full-system emulation with QEMU, you may still face some errors you must address manually.
- The IP address
192.168.0.1is the most commonly used private address in the Class C address range from
192.168.255.255. If you use the private IP address range
192.168.0.0/24for your local network, you will likely face IP address conflicts. Using Docker mode, you can isolate the emulated device's network environment from the host network environment while still being able to access the camera's web interface by exposing the container's HTTP(S) port with the
-p 8080:80option appended to the
docker runcommand within the
- The emulator container must be able to access the PostgreSQL database running either in another container or on the host machine. If the emulation fails, because the container cannot connect to the PostgreSQL database, you may need to modify the default IP address
172.17.0.1of the database set in the
firmae.configfile and in the
First, move the downloaded firmware image into the
firmwares directory and run the
./docker-helper.py script as Figure 7. illustrates to check whether the tool can automatically emulate the device. The very first run will take a while, so be patient. During the process the tool creates several files in the
scratch directory, including log files
qemu.final.serial.log, that you can monitor for potential errors.
$ ./docker-helper.py -ec zavio firmwares/CF7300_01_C043S2.rar
Next, if the firmware emulation is successful, you can launch the emulation in Debug mode as shown in Figure 8., so FirmAE will configure the device to provide convenient remote access via
netcat on port
$ ./docker-helper.py -ed firmwares/CF7300_01_C043S2.rar
With some patience and a little luck, the emulation succeeds. Due to the emulation running in debug mode, you should be able to connect to the emulated Zavio CF7300 camera as the
root user via
telnet on port
$ docker exec -it docker0_CF7300_01_C043S2.rar telnet 192.168.0.1 31338
One of the first issues you may face is the fact that the expected services have not been started. Though FirmAE aims to run the emulated firmware's web server and correctly serve the web interface, it still falls short sometimes. In this case, you can see in the
qemu.final.serial.log file that the
init process exits early with the "
init: must be run as PID 1" message; hence, you will need to invoke the initialization sequence either by manually running
/etc/init.d/rcS or by modifying the
You can mount and unmount the firmware's filesystem to and from
./scratch/[IID]/image with the helper scripts
mount.sh [IID] and
umount.sh [IID], where
IID is the internal identification number of the firmware image. After mounting the filesystem, you can copy files over to the firmware image and also make additional adjustments to the
./scratch/[IID]/image/firmadyne/preInit.sh script. Since the tool incorrectly detected the
/usr/sbin/httpd binary instead of
/usr/sbin/boa as the webserver, feel free to remove the
/firmadyne/run_service.sh & line from the script. Otherwise, the
httpd binary may bind to TCP port
80, thus the
boa server will start on a randomly chosen port.
If the initialization process was successful, you should see most of the services up and running as shown in Figure 11., including the
Onvif daemon bound to UDP port
3702 and the
boa web server waiting for incoming requests on TCP ports
To access the camera's web interface on your host machine as shown in Figure 12., you must forward port
80 of the Docker container to the camera's port
80. Among the many possible applications, you can use the socat utility as a TCP port forwarder. By creating a bidirectional tunnel between the host machine and the target device through the Docker container, you can access the camera's web interface on TCP port
8080 of your host machine with your chosen browser. You can use the classic admin/admin pair when the browser prompts for username and password.
$ docker exec -it docker0_CF7300_01_C043S2.rar socat TCP-LISTEN:80,fork,reuseaddr TCP4:192.168.0.1:80 &
Reproducing the command injection vulnerability
Now that the web interface is up and running, you can explore the camera's inner workings in more detail. Carefully reading the decompiled source code shown in the sub-scan results of the
param binary suggests looking for the string
resolution in the web interface. Searching for this keyword in the HTML files, you can see that it occurs most often in the
basic_video_profile_pop.htm file, as shown in Figure 13.
After thoroughly crawling the web interface, you can deduce that the vulnerability might be hiding somewhere in the video settings configuration page under the menu items Basic Setup > Video > Profile > Stream Profile > Profile1 [Edit] available at the path
/basic_video_profile_pop.htm?mod_0. Submitting the configuration form and monitoring the HTTP requests will reveal the relevant
StreamProfile.I0.Video.Resolution query string parameter, as Figure 14. shows.
Upon examining this specific parameter further, error messages like the one shown below in Figure 15. indicate that the backend validates the user-provided values against a list of possible resolutions. It seems that the vulnerability cannot be triggered directly through the web interface by manipulating only the value of the
StreamProfile.I0.Video.Resolution query string parameter.
However, we can change the name of the query string parameter and specify a command that affects the application's behavior in a way that is recognizable to us when the system executes it. For example, modifying the name of the affected parameter to
StreamProfile.I0.Video.Resolution.Bugprove and injecting the
sleep command will cause an approximately
5-second time delay. As Figure 16. indicates the application took 5.44 seconds to respond to the request, compared with 136 milliseconds for the original request, indicating that the injected command caused a time delay as expected. The vulnerability can be further confirmed by increasing the argument of the injected
sleep command and observing the response time. However, since you have complete control over the emulated device, you can explore other ways to exploit the vulnerability.
We can identify the exact command injection sink by using the
ps command that gives a snapshot of all running processes and redirecting its output to a file. Figure 17. seems to confirm that the user-controlled value is incorporated in the previously shown command string
/usr/sbin/check_param resolution %d %s. The readily available curl utility with the below command can send the HTTP request required to demonstrate the discussed proof-of-concept exploit.
curl -G 'http://172.28.57.76:8080/cgi-bin/operator/param?action=update' \ -d 'StreamProfile.I0.Video.Resolution.Bugprove=$(ps>/tmp/foobar)' \ -H 'Authorization: Basic YWRtaW46YWRtaW4='
Vulnerability and root-cause analysis
Further investigation reveals that configuration values are validated according to a set of rules stored in the XML file
/etc/device_rule.conf. Note that there are three different validators specified for the value of the
StreamProfile.I0.Video.Resolution setting parameter. We have already encountered
check.path.Properties.image.ResolutionFormat.List when the system rejected the injected
sleep command. It seems that
check.path.X.Y.Z validators specify path expressions to select nodes in the XML file
device.conf, as Figure 18. shows.
However, the validation process takes a different route by appending additional parts to the
StreamProfile.I0.Video.Resolution parameter, effectively making it point to unexisting nodes that are supposed to be on a deeper level in the XML structure. Besides the
check.stream.resol validator defined in the
device_rule.conf file, the XML file
actions.conf defines additional so-called actions that will execute specific command strings. Besides normal actions, there are so-called pre-actions that are executed before the requested operation, while post-actions are executed after the requested operation. Figure 19 shows that the
check.stream.resol validators use the same format string to construct a command string for checking the user-provided resolution value.
You could use the
strace utility already present in the
/firmadyne directory to capture system calls to
open() made by the
param binary, allowing you to keep track of files accessed by the program. The trace log shown in Figure 20. confirms that the program opens the
action.conf file looking for any actions related to the
update operation performed on the
StreamProfile.I0.Video.Resolution setting, when the parameter name does not directly match with any XML nodes in the
# /firmadyne/strace -e trace=open \ /usr/share/www/cgi-bin/admin/param \ "action=update&StreamProfile.I0.Video.Resolution=1x1" 2>&1 | grep -v lib # /firmadyne/strace -e trace=open \ /usr/share/www/cgi-bin/admin/param \ "action=update&StreamProfile.I0.Video.Resolution.BugProve=1x1" 2>&1 | grep -v lib
Note that this is a variant of CVE-2023-4249 demonstrated on firmware version
01_C043S2 for Zavio CF7300 cameras and you may experience different results in case of other models or firmware versions.