CVE-2017-14446
An exploitable stack-based buffer overflow vulnerability exists in Insteon Hub running firmware version 1012. The HTTP server implementation unsafely extracts parameters from the query string, leading to a buffer overflow on the stack. An attacker can send an HTTP GET request to trigger this vulnerability.
Insteon Hub 2245-222 - Firmware version 1012
http://www.insteon.com/insteon-hub
8.5 - CVSS:3.0/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
CWE-120: Buffer Copy without Checking Size of Input (‘Classic Buffer Overflow’)
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. One of these is the HTTPExecuteGet
function, which is called when a GET request is received.
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 the HTTPExecuteGet
function can thus access curHTTP
to implement its logic.
Note that HTTPExecuteGet
is only reached if a valid basic-auth string is provided.
seg000:9D02C758 insteon_HTTPExecuteGet:
seg000:9D02C758
seg000:9D02C758 filename= -0x98
seg000:9D02C758 req_pointers= -0x84
seg000:9D02C758 getparam_1= -0x80
seg000:9D02C758 getparam_2= -0x7C
seg000:9D02C758 getparam_3= -0x78
seg000:9D02C758 getparam_4= -0x74
seg000:9D02C758 var_20 = -0x20
seg000:9D02C758 var_1C = -0x1C
seg000:9D02C758 var_18 = -0x18
seg000:9D02C758 var_14 = -0x14
seg000:9D02C758 var_10 = -0x10
seg000:9D02C758 var_C = -0xC
seg000:9D02C758 var_8 = -8
seg000:9D02C758 var_4 = -4
seg000:9D02C758
seg000:9D02C758 000 58 FF BD 27 addiu $sp, -0xA8
seg000:9D02C75C 0A8 A4 00 BF AF sw $ra, 0xA8+var_4($sp)
seg000:9D02C760 0A8 A0 00 B6 AF sw $s6, 0xA8+var_8($sp)
seg000:9D02C764 0A8 9C 00 B5 AF sw $s5, 0xA8+var_C($sp)
seg000:9D02C768 0A8 98 00 B4 AF sw $s4, 0xA8+var_10($sp)
seg000:9D02C76C 0A8 94 00 B3 AF sw $s3, 0xA8+var_14($sp)
seg000:9D02C770 0A8 90 00 B2 AF sw $s2, 0xA8+var_18($sp)
seg000:9D02C774 0A8 8C 00 B1 AF sw $s1, 0xA8+var_1C($sp)
seg000:9D02C778 0A8 88 00 B0 AF sw $s0, 0xA8+var_20($sp)
seg000:9D02C77C 0A8 83 0F 41 0F jal sub_9d043e0c # [1]
seg000:9D02C780 0A8 24 00 A4 27 addiu $a0, $sp, 0xA8+req_pointers # [2]
seg000:9D02C784 0A8 21 90 40 00 move $s2, $v0
seg000:9D02C788 0A8 10 00 A5 27 addiu $a1, $sp, 0xA8+filename
seg000:9D02C78C 0A8 24 00 A5 AF sw $a1, 0xA8+req_pointers($sp) # [3]
seg000:9D02C790 0A8 00 A0 10 3C+ li $s0, curHTTP
seg000:9D02C798 0A8 18 00 04 92 lbu $a0, (curHTTP_file - 0xA0000CD8)($s0)
seg000:9D02C79C 0A8 DE 7F 41 0F jal MPFSGetFilename # [4]
seg000:9D02C7A0 0A8 14 00 06 24 li $a2, 0x14
The function calls sub_9d043e0c
at [1] passing a local array of pointers ([2]): this function is used for extracting GET parameters from the query string, inserting a pointer for each of them in the local array [2], which has room for 5 pointers in total.
After the call, the first slot of the array is used for the HTTP path requested [3], which in Insteon’s case is aways just a filename.
Note that the path is saved into the filename
variable by calling MPFSGetFilename
[4].
This means that the maximum number of GET parameters assumed is 4.
seg000:9D043E0C sub_9d043e0c:
seg000:9D043E0C
seg000:9D043E0C var_10 = -0x10
seg000:9D043E0C var_C = -0xC
seg000:9D043E0C var_8 = -8
seg000:9D043E0C var_4 = -4
seg000:9D043E0C
seg000:9D043E0C 000 E0 FF BD 27 addiu $sp, -0x20
seg000:9D043E10 020 1C 00 BF AF sw $ra, 0x20+var_4($sp)
seg000:9D043E14 020 18 00 B2 AF sw $s2, 0x20+var_8($sp)
seg000:9D043E18 020 14 00 B1 AF sw $s1, 0x20+var_C($sp)
seg000:9D043E1C 020 10 00 B0 AF sw $s0, 0x20+var_10($sp)
seg000:9D043E20 020 21 90 80 00 move $s2, $a0 # [6]
seg000:9D043E24 020 00 A0 02 3C+ lbu $v0, curHTTP_data # [5]
seg000:9D043E2C 020 0E 00 40 10 beqz $v0, loc_9D043E68
seg000:9D043E30 020 01 00 11 24 li $s1, 1
seg000:9D043E34 020 00 A0 10 3C+ la $s0, curHTTP_data
seg000:9D043E3C
seg000:9D043E3C loc_9D043E3C:
seg000:9D043E3C 020 80 10 11 00 sll $v0, $s1, 2
seg000:9D043E40 020 21 10 42 02 addu $v0, $s2, $v0
seg000:9D043E44 020 00 00 50 AC sw $s0, 0($v0) # [8]
seg000:9D043E48 020 32 E2 41 0F jal strlen
seg000:9D043E4C 020 21 20 00 02 move $a0, $s0
seg000:9D043E50 020 01 00 42 24 addiu $v0, 1
seg000:9D043E54 020 21 80 02 02 addu $s0, $v0
seg000:9D043E58 020 01 00 31 26 addiu $s1, 1
seg000:9D043E5C 020 00 00 02 92 lbu $v0, 0($s0) # [9]
seg000:9D043E60 020 F6 FF 40 14 bnez $v0, loc_9D043E3C # [7]
seg000:9D043E64 020 FF 00 31 32 andi $s1, 0xFF
seg000:9D043E68
seg000:9D043E68 loc_9D043E68:
seg000:9D043E68 020 FF FF 22 26 addiu $v0, $s1, -1
seg000:9D043E6C 020 FF 00 42 30 andi $v0, 0xFF
seg000:9D043E70 020 1C 00 BF 8F lw $ra, 0x20+var_4($sp)
seg000:9D043E74 020 18 00 B2 8F lw $s2, 0x20+var_8($sp)
seg000:9D043E78 020 14 00 B1 8F lw $s1, 0x20+var_C($sp)
seg000:9D043E7C 020 10 00 B0 8F lw $s0, 0x20+var_10($sp)
seg000:9D043E80 020 08 00 E0 03 jr $ra
seg000:9D043E84 020 20 00 BD 27 addiu $sp, 0x20
The function’s logic is very simple:
curHTTP.data
[5] already holds the sequence of all GET parameters, separated by a NULL byte.curHTTP.data
buffer.As we can see, the only existing stop condition [9] is checking that the next string in curHTTP.data
is NULL.
Since there is no check on the size of the destination buffer, this bug leads to a buffer overflow on the stack when more than 4 parameters are supplied, allowing an attacker to arbitrarily overwrite the saved $ra
and saved $s0
-$s6
registers in the context of the insteon_HTTPExecuteGet
function.
The following proof of concept shows how to overflow the buffer and crash the device.
$ curl -u username:password "http://$INSTEON_IP:25105/x?$(perl -e 'print "B&"x0x30')x"
2017-12-05 - Vendor Disclosure
2018-01-18 - Vendor advised issues under evaluation
2018-02-12 - 60 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
Discovered by Claudio Bozzato of Cisco Talos.