关联漏洞
标题:
Magento 安全漏洞
(CVE-2016-4010)
描述:Magento是美国Magento公司的一套开源的PHP电子商务系统,它提供权限管理、搜索引擎和支付网关等功能。Magento Community Edition(CE)是一个社区版。Magento Enterprise Edition(EE)是一个企业版。 Magento CE和EE 2.0.5及之前的版本中存在安全漏洞。远程攻击者可利用该漏洞执行任意的PHP代码。
描述
Magento Unauthorized Remote Code Execution (CVE-2016-4010)
介绍
# Magento未授权远程代码执行漏洞(CVE-2016-4010)的分析与利用
------
**0x00 前言**
5月17日,国外的安全研究人员Netanel Rubin公开了Magento的一个未授权远程代码执行漏洞(CVE-2016-4010)。该漏洞实际上包含了多个小的漏洞并且允许攻击者在有漏洞的Magento服务器上未授权执行PHP代码。Magento是一个非常流行的电商平台,它在2011年时被eBay收购。一些知名企业,如:三星,尼康,联想,以及众多的小型电商都在使用它。据悉,Magento被250,000个在线商城使用,每年将涉及金额达600亿美金。
**0x01 分析**
该漏洞的利用条件:
- Magento开启了RPCs(REST或者SOAP),且大部分都是默认开启的
- Magento的CE&EE版本<2.0.6
Magento的web API允许2种不同方式的RPCs,分别是REST RPC和SOAP API。这2种方式都提供了相同的功能,唯一的区别在于前者使用JSON和HTTP请求去传递输入,后者则使用XML。
为了仅仅公开部分模块的API,Magento提供给开发者们一个方便的方法就是在“webapi.xml”文件里仅仅声明他们想要能够访问的模块的API。webapi.xml文件包含了所有需要被公开的Web API的类和方法,每一个方法也指定了它需要的具体的权限。这些权限包括:
- anonymous - 允许任何人访问的方法
- self - 仅仅允许注册的用户和具体的管理员的权限,如: “Magento_Backend::admin”权限就是仅仅允许可以编辑服务器配置的管理员去访问
当然,这种允许开发者使用webapi.xml文件在系统的前端以及后端(Web API)之前通信的方式,实际上也打开了一扇直接进入模块核心的后门。
另外,即使我们已经有了“anonymous”权限我们仍然需要一个可以动态传值的方式。这里指的可在系统里使用的不同的对象,例如:“CustomerRepositoryInterface::save()” API功能允许我们在“$customer”变量里使用“CustomerInterface”的对象,代码原型如下:
interface CustomerRepositoryInterface
{
/**
* Create customer.
*/
public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
}
那么如何使用RPC接口来创建对象呢?事实上,这个问题的答案在于Magento如何配置SOAP服务器。
Magento使用默认捆绑了PHP“SoapServer”的SOAP服务器。为了能够正确的配置,“SoapServer”需要一个WSDL文件,在这个文件里去定义所有的方法,参数,以及在实际RPC请求种使用的定制内型。Magento为每个支持XMLRPC功能的模块生成不同的WSDL文件,并且直接设置来自于模块的webapi.xml文件里的值。
当一个RPC请求被服务器解析的时候,服务器使用在WSDL文件里找到的数据去判断请求是否有效,检查请求的方法,参数和类型。如果请求是有效的,就传递已解析的请求对象至Magento做进一步的解析。一个非常重要的点是,“SoapServer”不会以任何方式与Magento进行交互,所有关于模块的的方法和参数的信息都是来自于WSDL文件。此时,发送的请求仍然是由嵌套的数组组成,在SoapServer的解析阶段没有对象会被创建。为了创建需要的对象,Magento会继续自己处理输入。
为了抽取参数名和数据类型,Magento会从请求的方法里获取原型(可以参见前面的代码)。对于一些基本的数据类型, 如字符串,数组,布尔型等,系统将把输入对应到相应的类型。但是对于对象类型,解决的方法比较麻烦。
如果参数的数据类型是一个类的实例,Magento将会尝试使用提供的输入去简历实例。记住,此时的输入仅仅是一个字典,它的key是属性名称,value饰属性值。
首先,Magento将会创建一个需要的类的新实例。接着,它将会尝试使用以下的方法去填充:
1. 获取属性名称(来自于输入的字典的key)
2. 寻找公共的方法叫“Set[Name]”,其中[Name]是属性名称
3. 如果有这样的方法,使用属性值作为参数去执行
4. 如果没有这样的方法,忽略该属性并且继续查看下一个属性
Magento将会按照这个方法去处理每一个的用户正在尝试设置的属性。当所有的属性都被检查了,Magento将会认为该实例已经设置完成并且处理下一个参数。当所有的参数都被这样处理了,Magento将会最终执行这个API方法。
总而言之,Magento让你去创建一个对象,并设置它的公共属性,最后通过它的RPC去执行任何一个以“Set”开头的方法。而正是这种行为导致了Magento的漏洞的产生。
研究发现,一些API的调用是允许在购物车里设置一些具体的信息,这些信息可以是我们的邮寄地址,商品,甚至是我们的支付方式。
当Magento在购物车实例种设置我们的信息的时候,它会使用实例的“save”方法往数据库中存储新添加的数据。
下面我们来看看“save”方法是如何工作的吧!
/**
* Save object data
*/
public function save(\Magento\Framework\Model\AbstractModel $object)
{
...
// If the object is valid and can be saved
if ($object->isSaveAllowed()) {
// Serialize whatever fields need serializing
$this->_serializeFields($object);
...
// If the object already exists in the DB, update it
if ($this->isObjectNotNew($object)) {
$this->updateObject($object);
// Otherwise, create a new record
} else {
$this->saveNewObject($object);
}
// Unserialize the fields we serialized
$this->unserializeFields($object);
}
...
return $this;
}
// AbstractDb::save()
Magento确保我们的对象都是有效的,然后序列化所有应该被序列化的部分并存储在数据库里,最后再反序列化之前序列化的部分。
看起来很简单,对吧?其实不然,让我们继续看看Magento是如何判断哪些部分应该被序列化。
/**
* Serialize serializable fields of the object
*/
protected function _serializeFields(\Magento\Framework\Model\AbstractModel $object)
{
// Loops through the '_serializableFields' property
// (containing hardcoded fields that should be serialized)
foreach ($this->_serializableFields as $field => $parameters) {
// Get the field's value
$value = $object->getData($field);
// If it's an array or an object, serialize it
if (is_array($value) || is_object($value)) {
$object->setData($field, serialize($value));
}
}
}
// AbstractDb::_serializeFields()
正如我们看到的,仅仅是出现在硬编码字典“_serializableFields”中的那部分能够被序列化。最重要的是,这个方法在确保了field的值是一个数组或者对象的之后才会继续去序列化。
现在,我们看看Magento是如何判断哪些部分应该被反序列化。
/**
* Unserialize serializeable object fields
*/
public function unserializeFields(\Magento\Framework\Model\AbstractModel $object)
{
// Loops through the '_serializableFields' property
// (containing hardcoded fields that should be serialized)
foreach ($this->_serializableFields as $field => $parameters) {
// Get the field's value
$value = $object->getData($field);
// If it's not an array or an object, unserialize it
if (!is_array($value) && !is_object($value)) {
$object->setData($field, unserialize($value));
}
}
}
// AbstractDb::unserializeFields ()
好吧,看起来非常类似。唯一的不同点是,这次Magento需要确保field的值不是一个数组或者对象。因为这2次的检查,我们应该能够实施一个对象注入攻击,即简单地在一个可序列化的field中设置一个一定规则的字符串。当我们如此设置后,系统在存储对象至数据库之前将不会序列化这个field,因为它不是对象或者数组。但是,当系统将会尝试反序列化它时,在数据库查询被执行之后,它将会被反序列化,因为它不是一个对象或者数组。
但是正是这种小到几乎看不见的条件却造成了漏洞。剩下的问题就是考虑哪些field被认为是“可序列化的”,并且我们如何设置它。
当然,第一个问题很简单,就是我仅仅需要搜索哪个类包含了“_serializableFields”属性。很快,在“Payment”类中发现了一个API方法,但是不是作为一个参数,所以不能创建或者控制它的实例属性。最重要的是,它的可序列化的field“additional_information”仅能被设置成一个数组,且使用“Set[PROPERTY_NAME]”技术作为一个额外的安全措施,所以不仅不能创建,即使能我们也不能设置成一个字符串。
但很有趣的是,它可以以另外一种“骚气”的方式去设置。当Magento设置参数实例的属性时,事实上不是真的设置属性,而是保存他们在一个命名为“_data”的字典中。当一个实例的属性被使用时,这个字典将会被使用。这对于我们来说,意味着我们的可序列化field - “additional_information”事实上被保存在一个内置的字典中而不是一个正常的属性。
所以,如果我们能够完全控制“_data”字典,那么我们就能轻松地绕过“additional_information”field的数组限制,因为我们可以手动设置它而不是去调用“Set[PROPERTY_NAME]”。
但是,我们又如何控制这个敏感的字典呢?
在保存我们“Payment”实例之前,Magento要做的一件事就是去编辑它的属性。Magento将我们的API输入当作需要被存储在“Payment”实例中的支付信息,如下:
/**
* Adds a specified payment method to a specified shopping cart.
*/
public function set($cartId, \Magento\Quote\Api\Data\PaymentInterface $method)
{
$quote = $this->quoteRepository->get($cartId); // Get the cart instance
$payment = $quote->getPayment(); // Get the payment instance
// Get the data from the user input
$data = $method->getData();
// Check for additional data
if (isset($data['additional_data'])) {
$data = array_merge($data, (array)$data['additional_data']);
unset($data['additional_data']);
}
// Import the user input to the Payment instance
$payment->importData($data);
...
}
// PaymentMethodManagement::set()
正如我们看到的,“Payment”数据通过调用“$method->getData()”从“$method”参数中返回“_data”属性来获取。记住,因为“$method”是API方法的一个参数,所以我们能够控制它。
当Magenta在我们的“$method”参数里调用“getData()”时,参数的“_data”属性将会返回,并包含了我们插入的所有的支付信息。之后,它以“_data”属性作为输入去调用“importData()”,用我们的“_data”属性去替换掉“Payment”实例的“_data”属性。至此,我们现在能够使用我们可以控制的“_data”属性去替“Payment”实例中敏感的“_data”属性,也就意味着,我们现在可以设置“addition_information”field。
为了让unserialize()起作用,我们需要field能否被设置成字符串,但是“Set[PROPERTY_NAME]”方法仅仅允许数组。解决方法是在调用“importData()”之前放2行代码。Magento允许开发者去增加他们自己的支付方法,提供他们自己的数据和信息。为了实现这个,Magento使用了“addition_data”field。而这个field则是一个包含更多数据的支付方法且完全用户可控的字典。为了能让定制化的内容成为原始数据的一部分,Magento将“additional_data”字典与原始的“data”字典合并在一起,实际上就是允许“additional_data”字典去覆盖“data”字典里的所有的值,基本上也就是可以完全覆写。这也就意味着,在2个字典合并之后,用户可控的“additional_data”字典现在变成了参数“_data”字典,并且因为“importData()”,它也变成了“Payment”实例中敏感的“_data”属性。换句话说,我们现在已经完全控制了可序列化的field“additional_information”,并可以实施对象注入攻击了。
既然我们可以反序列化任何我们想要的字符串,那么是时候进行对象注入攻击了。
首先,我们需要一个带有“__wakeup()”或者“__destruct()”方法的对象,以便当对象被反序列化或者销毁时能够被自动调用。这是因为即使我们能够控制对象的属性,但是我们不能调用它的方法。这也是为什么我们必须依赖PHP的magical方法,当某个事件发生时它能够被自动调用。
我们将使用的第一个对象是“Credis_Client”类的一个实例,它包含如下的方法:
/*
* Called automaticlly when the object is destrotyed.
*/
public function __destruct()
{
if ($this->closeOnDestruct) {
$this->close();
}
}
/*
* Closes the redis stream.
*/
public function close()
{
if ($this->connected && ! $this->persistent) {
...
$result = $this->redis->close();
}
...
}
// Credis_Client::__destruct(), close()
我们可以看到,这个类有一个简单的“__destruct”方法(当对象被销毁时它将会被PHP自动调用)去调用“close()”方法。有意思的是,“close()”方法如果发现有一个主动连接至Redis服务器,它就会去调用“redis”属性中的“close()”去关闭它。
由于“ unserialize()”允许我们去控制所有的对象属性,所以我们也可以控制“redis”属性。我们可以在属性里(不仅仅是Redis)设置任意一个我们想要的对象,并在系统的任意一个类中调用任意一个“close()”方法。这也大大地扩大了我们的攻击面。在Magento中有一些”close()”方法并且由于这些方法通常是用来终止流,关闭文件句柄以及存储对象数据,故而我们应该可以找到一些有趣的调用。
正如我们预期的,我们找到了下面这个在“Transaction”类中的“close()”方法:
/**
* Close this transaction
*/
public function close($shouldSave = true)
{
...
if ($shouldSave) {
$this->save();
}
...
}
/**
* Save object data
*/
public function save()
{
$this->_getResource()->save($this);
return $this;
}
// Magento\Sales\Model\Order\Payment\Transaction::__destruct(), close()
看起来很简单,“close()”方法调用“save()”方法接下来调用“_resource”属性中的“save()”方法。相同的思路,因为我们控制了“_resource”属性所以我们也能控制它的类,故我们能调用任何我们想要的类的“save()”方法。
又向前迈了一大步了。正如我们猜想的那样,“save()”方法通常是用来在各种存储介质里(如:文件系统,数据库等)保存各种数据。现在我们需要做的事情就是找到一个使用文件系统当做存储介质的“save()”方法。
很快,我找到了一个:
/**
* Try to save configuration cache to file
*/
public function save()
{
...
// save stats
file_put_contents($this->getStatFileName(), $this->getComponents());
...
}
// Magento\Framework\Simplexml\Config\Cache\File::save()
这个方法其实是将“components”field中的数据保存在一个文件中。因为文件的路径是从“stat_file_name”field中获取的,另外由于我们控制了这2个参数,我们实际上控制了文件的路径和内容,这就产生了一个任意文件写入的漏洞。
现在我们只需要考虑找到一个有效的可写的并且可被web服务器访问的路径去写入文件。在所有的Magento安装目录中有一个“/pub”的目录,它是用来存储图片或者管理员上传的文件,这是一个可有效利用的路径。
最后我们只需要简单的写一个PHP的webshell文件到服务器上,就可以在Magento服务器上未授权执行任意PHP代码。
**0x02 利用**
*测试环境搭建*
1. 下载有漏洞的安装包(这里使用的是2.0.0版本)
下载地址:https://github.com/magento/magento2/archive/2.0.0.zip
2. 安装Magento
安装步骤:https://github.com/magento/magento2/tree/2.0.0
注意:此处可能会遇到一些问题可参见:
- http://magento2king.com/magento2-insta-be-downloaded/
- https://github.com/magento/magento2/issues/2419
*漏洞利用*
exploit-db上公开的漏洞exp的下载地址:https://www.exploit-db.com/exploits/39838/
利用方法如下:
1. 找到有漏洞的Magento网站
Magento版本在线检查:http://magentoversion.com/
2. 添加一个商品进入购物车
![此处输入图片的描述][1]
3. 进入购物车点击“结算”
![此处输入图片的描述][2]
4. 填写邮寄地址并查看POST请求/rest/default/V1/guest-carts/[guestCartId]/shipping-information并获取[guestCartID]
![此处输入图片的描述][3]
![此处输入图片的描述][4]
5. 保存上面的exp为magento_exp.php并执行:php magento_exp.php [Magento_URL] [guestCartID] ([webshell写入路径])
![此处输入图片的描述][5]
*批量检测*
经过对上面exp的研究发现该利用需要满足下面几个条件:
1. 目标站点的Magento版本需要小于2.0.6且开启了REST API
2. 目标站点首页需要存在下面这段JS
![此处输入图片的描述][6]
因此,写了一个简单的批量验证脚本来配合上面的exp进行利用:
#!/usr/bin/env python
import urllib
import sys
import socket
timeout = 5
socket.setdefaulttimeout(timeout)
input = sys.argv[1] #包含Magento站点的URL的文件
output = sys.argv[2] #结果的保存文件,可以为:output.txt
def logFile(str):
f = open(output,'a')
f.write(str+"\n")
f.close()
def checkVul(url):
try:
html = urllib.urlopen(url).read()
if "guest-carts" in html:
print url,"is vulnerable!"
logFile(url)
else:
print url,"is not vulnerable!"
except Exception:
pass
if __name__ == '__main__':
inp = open(input,'r')
for i in inp:
url=i.strip()
#print url
checkVul(url)
print "All Done!"
执行效果:
![此处输入图片的描述][7]
**0x03 防御**
升级Magento到最新版(2.0.6),下载地址: https://www.magentocommerce.com/download
**参考**
- http://netanelrub.in/2016/05/17/magento-unauthenticated-remote-code-execution/
- https://www.exploit-db.com/exploits/39838/
[1]: http://avfisher.win/wp-content/uploads/2016/05/20160525062249_51756.png
[2]: http://avfisher.win/wp-content/uploads/2016/05/20160525062313_15594.png
[3]: http://avfisher.win/wp-content/uploads/2016/05/20160525062328_48240.png
[4]: http://avfisher.win/wp-content/uploads/2016/05/20160525062349_74331.png
[5]: http://avfisher.win/wp-content/uploads/2016/05/20160525062824_99645.png
[6]: http://avfisher.win/wp-content/uploads/2016/05/20160525062954_93034.png
[7]: http://avfisher.win/wp-content/uploads/2016/05/20160525063403_54110.png
文件快照
[4.0K] /data/pocs/b72528a9e322e17a2a01d5ba82112f2d7df543e7
└── [ 19K] README.md
0 directories, 1 file
备注
1. 建议优先通过来源进行访问。
2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。