In 2019, I published an article on 4 CVEs found on a very popular router SDK used by Comcast.
Today we will be presenting several vulnerabilities (CVE-2019–6961…6964) on the RDK (Reference Design Kit) platform, a code base used by over 40 million routers, set top boxes and IoT devices around the world. It is developed by a consortium of leading service providers and vendors and comprises of various open source libraries. The RDK relies on several other components like the Linux kernel, GNU libraries and SoC-dependent code. As a leader in IoT security, we identify the grave dangers in attacks that compromise routers and strive to minimize the attack surface on the smart home.
Before going into specific details on each one, here is the vulnerabilities summary:
1. Authentication bypass (CVE-2019–6961) in several admin panel AJAX pages allows users to control DDNS, QoS and RIP (routing) configurations normally restricted to the network operator.
2. Shell injection (CVE-2019–6962) in the Wi-Fi password change form allows arbitrary code execution as root, if the device supports mesh Wi-Fi.
3. Heap overflow (CVE-2019–6963) in the IP reservation form which lets users preconfigure a fixed IP for an upcoming device on the router. The “comments” text field overflows the heap allowing data and metadata corruption which may result in remote code execution.
4. Null-byte overflow (CVE-2019–6964) in the DDNS configuration form accessible by the discussed vulnerability corrupts a string stored on the heap, potentially resulting in remote code execution.
Now let’s dig deeper, explore the bugs and how they may be abused.
Authentication bypass:
The RDK exposes a web administration interface with standard features like setting the Wi-Fi network name, password, IP addresses and so on. Also included are several advanced settings pages exclusively used by a technician on-site, which log on using the “mso” (Multiple system operator) account.
Access control is enforced by a “header.php” header file imported by every GET page:
Access control in header.php
As we can see, the not_admin_pages array is populated with names of PHP files that are reserved for the mso user. Then, the script checks if the requested page ( $_SERVER[‘SCRIPT_FILENAME’] ) exists in the array and returns an “Access denied” message if an admin account tried to get this page.
If we ignore the fact strstr is not a great way to check if the two file names are the same, this check is fine. However, studying the POST pages requested by the filtered pages, it seems they are not protected by a similar validation:
dynamic_dns.php code — for MSO use
ajax_ddns.php imports a different header
Only CSRF protection, no access control in actionHandlerUtility.php
So it seems like in order to bypass the access controls one can simply send the corresponding AJAX request via Postman or any other tool, avoiding the prohibited GET page. The CSRF protection is easily bypassed by first sending a GET request to any of the normal admin pages and getting a CSRF token which is used globally for all POSTs.
Shell Injection:
In the RDK users can freely change the Wi-Fi password, which occurs in the ajaxSet_wireless_network_configuration_edit.php AJAX call. The newly set password must adhere to the following regex, which is basically a set of 8–63 printable characters or 64 hex digits:
Here [ -~] covers the entire printable character range of ASCII (32–126).
Let us sidestep for a moment to understand how components in the RDK interact with each other. Since the mechanism will appear in the rest of the vulnerabilities, this intermezzo is necessary.
TR181 — The Device Data Model
Technical reference #69 (TR69) is a well-known specification covering the CWMP protocol for accessing device configurations and parameters. It defines the InternetGatewayDevice data model, a hierarchal structure that defines attributes exported by the device for either read access or read/write. For example, examine the InternetGatewayDevice.LANDevice.{i}.Hosts.Host.{i}. node:
Under …Host.{i} there will be six subfields which describe the i-th host (managed LAN device).
The TR181 specification essentially defines a new data model, whose root is “Device”. While the main use of these data models is for remote CWMP configuration via ACS (auto configuration services), The RDK project adopted TR181 for inter-component communication. As such, if the WebUI component, the process which handles the web server, wishes to update the CcspWifiSsp component, the Wifi controller, it would do so by changing a parameter in the data model. CcspWifiSsp will have subscribed for this parameter change and now shall perform the actual operation (as an example, for a Wi-Fi password change it would change the hostapd configuration and reboot the daemon). Behind the scenes, this is done by the CcspCommonLibrary shared object, linked to component executables, which itself implements IPC via D-Bus.
Now let’s examine the function calls that handle setting the Wi-Fi password. After parameter validation, the PHP page calls setStr, passing as parameters the data model path to set, a new password and whether to commit the change right now.
setStr is a function implemented in a PHP extension called cosa.so. Basically, this means it is implemented in C code yet callable from PHP scripts. The setStr code parses PHP parameters into C structs and arrays, and calls CcspBaseIf_setParameterValues, implemented in the mentioned CcspCommonLibrary. Via D-Bus, it sends the parameter change update to CcspWifiSsp, who using the common library registered to receive incoming updates. This important registration is done via the obscurely named DslhCpecoRegisterDataModel, which receives a data model XML and the component to register. Let’s find the function which handles a password change:
TR181.XML
The snippet defines a Security object, declaring the functions which are called when getting and settings parameters under it. The setter function receives a context pointer (pointer to a struct which holds configuration values), parameter name and parameter value. Scrolling down the parameters we find our X_COMCAST-COM_KeyPassphrase node:
As we can see, it is a writable string parameter with a maximum length of 64 bytes. Now we are interested in what Security_SetParamStringValue does when given the new passphrase:
We can see that if the parameter to change is X_COMCAST-COM_KeyPassphrase, the input string is copied to the KeyPassphrase struct member after three pointer dereferences from the context pointer.
So now we have a good idea of what variable holds the password and can see where it is used. Inside a function called CosaDmlWiFiRadioApplyCfg, we see the following code inside a loop:
If mesh Wi-Fi is enabled, RDK uses the sysevent utility to let peripheral Wi-Fi nodes know the AP configuration changed. The update loop calls sysevent for each RDK radio channel and is triggered by Radio_Commit, which similarly to Security_SetParamStringValue, is registered from the data model inside the CcspWifiSsp process. The Commit function is called when the third parameter of setStr, isCommit, is true. In the password setStr() call isCommit is set to true, however the Security_Commit function will be called, not Radio_Commit. Anyways, the MiniApplySSID function called at the end of our PHP function will call Radio_Commit:
The main point here is that we have demonstrated a clear path between the password editing form and the system(cmd) call with a formatted string. Since the form accepts any printable password, consider the effect of entering this password:
“;dropbear -p 0.0.0.0:5000;#
System is obviously just a fork-exec, so the execve() argv parameter will look like
{ “/bin/bash”,
“-c”, “/usr/bin/sysevent set wifi_ApSecurity\“RDK|1|\”;dropbear -p 0.0.0.0:5000;#secMode|encryptMode”,
NULL
}
And as such, we can run arbitrary commands via shell injection on the CcspWifiSsp process running as root. A particularly dangerous vulnerability, as differently from the upcoming ones, exploiting it does not require substantial memory acrobatics and is as easy to perform as changing a password.
Heap overflow:
The attentive reader will have noticed that the Wi-Fi password was copied to KeyPassphrase via AnscCopyString without a max size parameter, and indeed it is just a macro to the infamous strcpy. If length checking is done externally like for the Wi-Fi password, strcpy is not an issue. However, when the coding practice doesn’t enforce safe copying, very often a validation will be forgotten. Such is the case for the IP reservation form.
The POST page parses a DeviceInfo parameter and validates the fields. We can see that the Comments field is only checked for printable characters, not length.
Assuming all checks are passed, a new table (object in data model language) is created under StaticAddress and its values are set.
AddTblObj and getInstanceIds are also functions implemented in the cosa.so PHP extension. As previously, let’s see how X_CISCO_COM_Comment is set.
Notice the difference between the comment type, string, and the Wi-Fi password type, string(64). The hypothesis was at this point that without parenthesis there is no limitation on the size of a given string. Later when delving into the XML parsing functions this was proven to be the case, and for the non-believers amongst our readers, the verification is demonstrated below.
DslhWmpdoMprRegParameter is the function which registers a given parameter to the handler object. It calls ParseParamDataType to receive the parsed XML attributes.
ParseParamDataType is of course a function pointer, set by the object initializer to DslhWmpdoParseParamDataType. For our purposes, what it does in case of string type is place the max size (for example 64) in ulFormatValue1. If no size is specified the variable is set to 0. Later in DslhWmpdoMprRegParameter the ulFormatValue1 is placed in the pVarEntity object in FormatValue1.
So now we have all the knowledge we need to understand the validation function, DslhVarroTstValue, called before actually changing a parameter via a call to its setter. If FormatValue1 is not 0 or 0xFFFFFFFF, the incoming string varString has to be smaller or equal to the XML size. If the value is 0 like in our case, no check is performed on parameter size.
So we now understand the size is not checked by the POST page and by CcspCommonLibrary, so let’s actually see what happens in StaticAddress_SetParamStringValue, the setter function for our “comment” parameter:
Our controlled parameter is strcpy-ed to hInsContext->hContext->Comment, a buffer in PCOSA_DML_DHCPS_SADDR struct (which represents a static address entry):
So we have an arbitrary size write to a 256 byte buffer that is not validated at any stage in the call stack!
To find where our buffer overflow overruns, we print the destination of the strcpy call:
Via address mappings we immediately learn the overflow is in the heap segment. We can see our printed ‘A’s and some other elements in the struct. A little higher up the address space…
These values looked like addresses and a quick look at the process address map shows they are in fact in the libtr181.so code segment, the library where the data model functions are implemented. Rebasing the addresses and opening the library in a disassembler shows the function pointers lead to CosaDNSCreate/Remove/Initialize. The only place these functions are placed in memory is in CosaDNSCreate:
This function is called early in the CcspPandMSsp (Provisioning and management) process, in CosaBackEndManagerInitialize.
This init function calls the initializers of all the structs used by the “Provision and Management” process. Since our overflow occurs in StaticAddress and we know it is a little before CosaDNSCreate in the heap, it seems like allocation occurs in CosaDhcpv4Create. Indeed inside there is a call to the following function:
It looks like we found the PCOSA_DML_DHCPS_SADDR struct StaticAddress_SetParamStringValue overflows! Here is the allocation:
So now we know exactly where the overflow occurs and therefore what we can overwrite. We will not go through the entire exploitation process but there are diverse ways to approach achieving code execution, for example:
1. Corrupting heap metadata and abusing unlink() mechanism
2. Corrupting a function pointer in memory to controlled code — more difficult in NX bit environments and probably requires a stack pivot to a controlled ROP chain
3. Crafting a structure in memory that will be used by other code components for further exploitation
The main constraint in our overflow scenario is that all characters up to the null terminator are printable (0x20–0x7e) which severely limits standard exploitation procedures. Additionally NX bit and ASLR are enabled.
ASLR can be somewhat overcome by running multiple iterations of the exploitation and hoping to land on the guessed base address. The PandM process is automatically restarted by systemd if it crashes so no harm done. Because of page alignment and limited randomization, there are only 8192 options for heap base, not many at all for brute force. An exploit which requires multiple base addresses, like different libraries for ROPs, is more challenging to enumerate.
Sometimes it is possible to pivot a difficult heap overflow exploitation to an easier stack overflow. Consider the DslhObjcoGetParamValueByName function, called every time a value is retrieved from the data model via getStr().
The function operates as a router, calling the get function registered on the given object for the specific parameter type. For strings, it calls the function with a 1024-byte buffer. What would happen if the “get” function uses strcpy to the received pTempBuf and overflows it? Like in Server1_GetParamStringValue:
The pUlsize passed is wrongly ignored and if the DNSServer value is large enough, pValue, a copy of the pTempBuf pointer, will be overflown. This is an interesting case of stack overflow, overflowing the calling function and not the called one. DslhObjcoGetParamValueByName would then return to a controlled address.
In order to reach this flow, the source, DNSServer, must be a long controlled string. We will not delve deeper into exactly how to control such parameters, but it is possible to do so via our heap overflow using an intermediary struct corruption.
Null byte overflow:
Recall that in DslhVarroTstValue, the setStr parameter validation function, a “set” operation is allowed if the incoming string’s length is up to and including the size of the XML defined parameter size, such as string(64). The string’s length is calculated via strlen(), which returns the number of bytes in the string not including the null terminator. It follows that the receiving buffer length must have at least 64 + 1 = 65 bytes to accommodate for the null byte. In some places, this fails to be the case. For example, in the DDNS configuration form already abused in the first vulnerability discussed:
Function for setting DDNS strings
Copying input string to Password member
Parameters are defined as 64 bytes
Where is the null byte padding?
The COSA_DML_DDNS_SERVICE does not have a null byte for the username, password and domain fields, and therefore the username string can be corrupted to contain up to 64*4=256 bytes. This may be exploited as previously by initiating a stack overflow on users of this DDNS structure who assume the username or password are of max size 64.
Conclusion:
As can be observed, the RDK has severe security flaws largely stemming from its immense complexity and interaction between various different components. In an age where attackers can tap relatively easily into the admin panel using keylogging malware, default passwords, social engineering and so on, it is important to protect non-technical users by limiting admin functionality to the necessary minimum. RDK has failed to do that, granting attackers potentially root access to the network core. From there, attackers are at a commanding point to both passively sniff network traffic and perform man in the middle attacks.
Disclosure timeline: 24/01 — First disclosure 10/04 — Patches have been pushed upstream 17/06 — Public release
Comments