
This repo has a blog post about my analysis for CVE-2018-19987 an authenticated OS command injection affecting multiple D-Link routers
Hello there!
In this short blog post I'll break down a quick analysis that I performed to better understand the details behind CVE-2018-19987. At the beginning I went after this CVE because I thought that there wasn't any public information or exploit. Shortly after doing a first analysis, I found two public PoCs on GitHub that you can find here and here.
As I had already done most of the work and hadn't found a complete analysis, I decided to write the blog post anyways.
I hope you enjoy it!
Update: Well.. after googling while I was stuck with this blog post I found the following article that describes a very similar vulnerability (CVE-2018-19986) in the D-Link DIR-818. Router vulnerability analysis series (5): CVE-2018-19986 DIR-818LW&828 command injection vulnerability analysis and reproduction (Google translated) by Ogur1.
I started my analysis gathering all the information I could about the device:
Note: I performed all the analysis with firmware v3.01B02.
Public information for this vulnerability, available on Mitre's page, provided enough information to start:
D-Link DIR-822 Rev.B 202KRb06, DIR-822 Rev.C 3.10B06, DIR-860L Rev.B 2.03.B03, DIR-868L Rev.B 2.05B02, DIR-880L Rev.A 1.20B01_01_i3se_BETA, and DIR-890L Rev.A 1.21B02_BETA devices mishandle IsAccessPoint in /HNAP1/SetAccessPointMode. In the SetAccessPointMode.php source code, the IsAccessPoint parameter is saved in the ShellPath script file without any regex checking. After the script file is executed, the command injection occurs. A vulnerable /HNAP1/SetAccessPointMode XML message could have shell metacharacters in the IsAccessPoint element such as the `telnetd` string.
To begin the analysis I needed something to take a look at, so I proceeded to extract the router's FS.
binwalk -eM DIR822C1_FW301WWb02.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/1"
10380 0x288C LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 4213444 bytes
1376372 0x150074 PackImg section delimiter tag, little endian size: 10505216 bytes; big endian size: 5021696 bytes
1376404 0x150094 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 5019773 bytes, 2282 inodes, blocksize: 131072 bytes, created: 2016-03-18 09:35:31
As we can see Binwalk does detect a Linux file system. If we take a deeper look at the information we have, we can see that the advisory mentions the /HNAP1 path. So, we can start looking at how the router handles these URLs.
As explained in the excellent /dev/tty0 blog post, these URLs at the end are handled by a binary called cgibin, located at htdocs/cgibin. But I wanted to better understand how this was configured and in the end it led me to have to understand how the HTTP server was configured to process requests. I proceeded to look for the httpd.conf file name, assuming that was used to configure the HTTP server, and found a file called HTTP.php under the /etc/services/ folder, containing among other things the following interesting lines:
$httpd_conf = "/var/run/httpd.conf";
fwrite("a",$START, "xmldbc -P /etc/services/HTTP/httpcfg.php > ".$httpd_conf."\n");
fwrite("a",$START, "event PREFWUPDATE add /etc/scripts/prefwupdate.sh\n");
fwrite("a",$START, "httpd -f ".$httpd_conf."\n");
fwrite("a",$START, "event HTTP.UP\n");
fwrite("a",$START, "exit 0\n");
I assumed that at some point this file is executed and writes the /var/run/httpd.conf file. To learn how the web server was configured, I proceeded to analyze the /etc/services/HTTP/httpcfg.php file (I only included the relevant parts):
if ($hnap > 0)
{
echo
" Control". "\n".
" {". "\n".
" Alias /HNAP1". "\n".
" Location /htdocs/HNAP1". "\n".
" External". "\n".
" {". "\n".
" /usr/sbin/hnap { hnap }". "\n".
" }". "\n".
" IndexNames { index.hnap }". "\n".
" }". "\n";
}
Now we have all the pieces! We can assume that the web server is configured to handle HTTP requests to /HNAP1 using the /usr/sbin/hnap, and based on /dev/tty0 blogpost we know that it will be a link to the htdocs/cgibin binary.
The next step was to take a look at the cgibin binary to understand how it handled HNAP requests. In the following image we can see part of the decompiled main function where the path URL path passed to it is compared against different paths (such as session.cgi, authentication.cgi, captcha.cgi, to name a few) until hnap is found and our function hnap_main is called.
note: While analyzing the main function, I had some issues identifying the hnap string as Ghidra did not detect it as string during the initial analysis.
The next step was to understand what this function did. I'll provide only snippets of code for the relevant parts that will help to better understand what the function does, also I will complete other relevant parts with some pseudocode —I renamed some variables to clarify its purpose.
...
HTTP_SOAPACTION = getenv("HTTP_SOAPACTION");
REQUEST_METHOD = getenv("REQUEST_METHOD");
HNAP_AUTH = getenv("HTTP_HNAP_AUTH");
__haystack = getenv("HTTP_COOKIE");
pcVar1 = getenv("HTTP_REFERER");
...
if (HTTP_SOAPACTION != "") {
if (HTTP_SOAPACTION == GetDeviceSettings) {
...
} else {
// These actions will occur during auth. Process
if ("GetCAPTCHAsetting" in HTTP_SOAPACTION) {
sess_generate_captcha();
} else {
if ("Login" in HTTP_SOAPACTION) {
perform_login();
}
// We'll land here once auth.
if (HNAP_AUTH != "") {
if ("uid=" in HTTP_COOKIE){
is_valid_auth = perform_auth_process()
if (is_valid_auth) {
if("logout" in HTTP_SOAPACTION){
perform_logout();
// If we are auth. and NOT trying to logout
// the code will try to perform the action
// we requested
} else {
// If we arrive here we win
// interesting code below
goto LAB_004141d4;
}
}
}
}
// If we are not authenticated we can't do anything
Return "You need proper authorization to use this resource"
}
}
} else {
...
}