CVE-2018-3890
An exploitable code execution vulnerability exists in the firmware update functionality of Yi Home Camera 27US 1.8.7.0D. A specially crafted file can cause a logic flaw and command injection, resulting in code execution. An attacker can insert an SD card to trigger this vulnerability.
Yi Technology Home Camera 27US 1.8.7.0D
7.6 - CVSS:3.0/AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE-78 - Improper Neutralization of Special Elements used in an OS Command
Yi Home Camera is an IoT home camera sold globally. The 27US version is one of the newer models sold in the U.S., and is the most basic model out of the Yi Technology camera lineup. It still, however, includes all the functionality that one would expect from an IoT device: the ability to view video from anywhere, offline and subscription-based cloud storage, and ease of setup.
When updating the firmware of the Yi Home Camera, there are two options: OTA or via a microSD card inserted physically into the device. Directions for this process can be found at http://www.yitechnology.com/Public/index/file/FirmwareUpdate.pdf, and the firmware files themselves are available at https://www.yitechnology.com/yi-720p-home-firmware. For the 27US version, the tar archive contains two files, home
and home_y18m
, but it only actually updates with the home_y18m
, which will be the main subject of this writeup. Please note this issue is referring to a local update only.
While the home
file is a simple Uboot image and subsequent JFFS2 filesystem, the home_y18m
firmware file is a lot more opaque:
Len | Type | Label | Example (if applicable)
(bytes) | | |
___________________________________________
| 22 bytes | String | Version | 1.8.7.0D_201708091510 |
------------------------------------------------------------------
| 1344 | Bytes | enc_key| N/A |
-------------------------------------------------------------------
| Leftover | Bytes | 7z Part | N/A |
-------------------------------------------------------------------
During the power-on stage, the following code in /home/base/init.sh
runs:
if [ -f /tmp/sd/home_y18m ]; then //[1]
dd if=/tmp/sd/home_y18m of=/tmp/newver bs=22 count=1
newver=$(cat /tmp/newver)
curver=$(cat /home/homever)
if [ $newver != $curver ]; then //[2]
insmod /home/base/mmz.ko mmz=anonymous,0,0x81600000,10M anony=1 || report_error //[3]
insmod /home/base/hi_cipher.ko //[4]
mkdir /tmp/update
cp -rf /home/base/tools/extpkg.sh /tmp/update/extpkg.sh
/tmp/update/extpkg.sh /tmp/sd/home_y18m //[5]
Which at [1] checks to see if the /tmp/sd/home_y18m
file exits on the SD card root directory. If it exists, then it looks at the Version
string inside the firmware image, and compares it to the current firmware version at [2]. The kernel modules loaded at [3] and [4] will become relevant soon, but to continue, the actual firmware unpacking occurs in [5], when the /tmp/update/extpkg.sh
script is run, which will be broken down now:
[...]
dd if=home_y18m of=ver bs=22 count=1
dd if=home_y18m of=home1 bs=22 skip=1 count=999999999999999
rm home_y18m
dd if=home1 of=enc_key bs=1344 count=1
dd if=home1 of=home2 bs=1344 skip=1 count=99999999999999
rm home1
At first, the script will extract the portions of the firmware as listed above, ver
,enc_key
, and the rest as home2
. The interesting things happen next, though:
/tmp/update/rsa_pub_dec enc_key // [1]
cat dec_enc_key home2 > home3 // [2]
rm home2
dd if=home3 of=home4 bs=66 skip=1 count=99999999
dd if=home3 of=md5 bs=33 count=1 // [3]
dd if=home3 of=key bs=33 skip=1 count=1 // [4]
dd if=home4 of=verkey bs=22 count=1 // [5]
rm home3
dd if=home4 of=home_v200.7z bs=22 skip=1 count=9999999999999 //[6]
At [1], the enc_key
is decrypted by a binary, presumably doing RSA decryption with a pub_key
file that was copied into the directory earlier. The result of this decryption is placed in dec_enc_key
, which gets prepended to home2
and thrown into home3
. The first 33 bytes [3] of home3 are the md5sum of the firmware, and then the next 33 bytes [4] are the password used to decrypt a .7z archive, which comes from home4. Finally, the first 22 bytes [5] of home4 contain a version string used for version comparison.
After this, some nonsensical version comparisons fail, due to reasons explained in TALOS-2018-0566, and then the following command is run: /home/base/tools/exthome $key
. In turn, the exthome
binary runs the following command:
LDR R1, =aHomeBaseTools7 ; "/home/base/tools/7za x -p%s home4"
BL sprintf
STR R0, [R11,#var_8]
SUB R3, R11, #-var_400
SUB R3, R3, #4
SUB R3, R3, #4
MOV R0, R3 ; command
BL system
The -p
parameter provided to 7za is just the value from the key
field earlier, and as shown, home4 is hardcoded as the .7z archive being decrypted. After the decryption has happened, the /tmp/update/home/app/script/update.sh
script is run to actually copy over the new firmware.
Unfortunately, there are a few oversights in this process. For starters, the return value of the /tmp/update/rsa_pub_dec
binary is never checked, such that if it fails for any reason, the process still continues. One case of this is if an “encrypted” firmware file is presented that is less than the expected size of 1344, which causes rsa_pub_dec to fail, and for the dec_enc_key
file to be empty.
An even more interesting case though, is that rsa_pub_dec will actually just not decrypt anything if it’s given a buffer with a length of less than 0x100, it will simply write the input into the output dec_enc_key
file:
loc_8ebc:
[..]
LDR R3, [R11,#bytes_read]
CMP R3, #0xFF
BHI path_to_RsaPublicDec
LDR R0, [R11,#var_1C] ; stream
BL ftell
MOV R3, R0
STR R3, [R11,#var_24]
loc_8EE4
LDR R3, [R11,#var_24]
LDR R0, [R11,#var_1C] ; stream
MOV R1, #0 ; off
MOV R2, R3 ; whence
BL fseek
LDR R0, =unk_20A38 ; ptr
MOV R1, #1 ; size
LDR R2, [R11,#bytes_read] ; n
LDR R3, [R11,#var_1C] ; s
BL fwrite
Thus, since the output file dec_enc_key
is user-controlled, referring back to the extpkg.sh:
cat dec_enc_key home2 > home3 // [1]
rm home2
dd if=home3 of=home4 bs=66 skip=1 count=99999999
dd if=home3 of=md5 bs=33 count=1
dd if=home3 of=key bs=33 skip=1 count=1 // [2]
dd if=home4 of=verkey bs=22 count=1 // [3]
Since dec_enc_key is prepended, and home2 is user-controlled, it follows that home3 is fully user-controlled. The first 33 bytes can be used to pass any md5sum checks, and the next 33 bytes get used in the system call to 7za, resulting in command injection. Since the /home/base/tools/exthome $key
line takes our value for $key as argv[1], we cannot have any spaces inside of the resulting characters passed to /home/base/tools/7za x -p%s home4"
.
Since /bin/sh points to busybox, typical bash command injection tricks such as ${/bin/nc,…,-e,/bin/sh} won’t work. However, if one just sets key
to “;sh” then the resulting string passed to system is /home/base/tools/7za x -p;sh home4
, allowing one to run anything they want to put inside of home4.
Discovered by Lilith <(^_^)> of Cisco Talos. http://talosintelligence.com/vulnerability-reports/
2018-05-01 - Vendor disclosure
2018-09-03 - Vendor submitted build to Talos for testing
2018-09-05 - Talos confirmed issue patched
2018-10-22 - Vendor released new firmware
2018-10-31 - Public release
Discovered by Lilith <(^_^)> of Cisco Talos.