Talos Vulnerability Report

TALOS-2018-0565

Yi Technology Home Camera 27US Firmware Update Code Execution Vulnerability

October 31, 2018
CVE Number

CVE-2018-3890

Summary

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.

Tested Versions

Yi Technology Home Camera 27US 1.8.7.0D

Product URLs

https://www.yitechnology.com

CVSSv3 Score

7.6 - CVSS:3.0/AV:P/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-78 - Improper Neutralization of Special Elements used in an OS Command

Details

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.

Credit

Discovered by Lilith <(^_^)> of Cisco Talos. http://talosintelligence.com/vulnerability-reports/

Timeline

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

Credit

Discovered by Lilith <(^_^)> of Cisco Talos.