
Magento تنفيذ أكواد عن بعد غير مصرح به (CVE-2016-4010)
0x00 مقدمة
في 17 مايو، كشف باحث الأمن الأجنبي Netanel Rubin عن ثغرة تنفيذ تعليمات برمجية عن بُعد غير مصرح بها في Magento (CVE-2016-4010). تحتوي هذه الثغرة في الواقع على عدة ثغرات صغيرة وتسمح للمهاجم بتنفيذ كود PHP غير مصرح به على خادم Magento المعرض للخطر. Magento هي منصة تجارة إلكترونية شائعة جدًا، استحوذت عليها eBay في عام 2011. تستخدمها بعض الشركات المعروفة، مثل: Samsung و Nikon و Lenovo، بالإضافة إلى العديد من المتاجر الإلكترونية الصغيرة. يُذكر أن Magento يُستخدم من قبل 250,000 متجر عبر الإنترنت، وتتعلق مبالغ سنوية تصل إلى 60 مليار دولار أمريكي.
0x01 التحليل
شروط استغلال الثغرة:
تسمح واجهة برمجة تطبيقات الويب في Magento بنوعين مختلفين من RPCs: REST RPC و SOAP API. يوفر كلا النوعين نفس الوظائف، والفرق الوحيد هو أن الأول يستخدم JSON وطلبات HTTP لنقل الإدخال، بينما يستخدم الثاني XML.
لإتاحة واجهة برمجة تطبيقات بعض الوحدات فقط، يوفر Magento للمطورين طريقة ملائمة: إعلان واجهات API للوحدات التي يريدون الوصول إليها فقط في ملف "webapi.xml". يحتوي ملف webapi.xml على جميع فئات وطرق Web API التي يجب إتاحتها، وتحدد كل طريقة الأذونات المحددة التي تحتاجها. تتضمن هذه الأذونات:
بالطبع، هذه الطريقة التي تسمح للمطورين باستخدام ملف webapi.xml للاتصال بين الواجهة الأمامية للنظام والواجهة الخلفية (Web API) تفتح في الواقع بابًا خلفيًا مباشرًا إلى نواة الوحدة.
علاوة على ذلك، حتى لو كان لدينا صلاحية "anonymous"، ما زلنا بحاجة إلى طريقة لتمرير القيم ديناميكيًا. يشير هذا إلى الكائنات المختلفة التي يمكن استخدامها في النظام، على سبيل المثال: تسمح وظيفة API "CustomerRepositoryInterface::save()" باستخدام كائن "CustomerInterface" في متغير "$customer"، النموذج الأولي للكود كما يلي:
interface CustomerRepositoryInterface
{
/**
* Create customer.
*/
public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
}
فكيف نستخدم واجهة RPC لإنشاء كائنات؟ في الواقع، تكمن إجابة هذا السؤال في كيفية تكوين Magento لخادم SOAP.
يستخدم Magento خادم SOAP المضمن مع PHP "SoapServer" افتراضيًا. لكي يتم تكوينه بشكل صحيح، يحتاج "SoapServer" إلى ملف WSDL، الذي يحدد جميع الطرق والمعلمات والأنواع المخصصة المستخدمة في طلبات RPC الفعلية. ينشئ Magento ملفات WSDL مختلفة لكل وحدة تدعم وظائف XMLRPC، ويضبط القيم مباشرة من ملف webapi.xml الخاص بالوحدة.
عندما يتم تحليل طلب RPC بواسطة الخادم، يستخدم الخادم البيانات الموجودة في ملف WSDL لتحديد ما إذا كان الطلب صالحًا، والتحقق من طريقة الطلب والمعلمات والأنواع. إذا كان الطلب صالحًا، يتم تمرير كائن الطلب الذي تم تحليله إلى Magento لمزيد من التحليل. نقطة مهمة جدًا هي أن "SoapServer" لا يتفاعل مع Magento بأي شكل من الأشكال، فجميع المعلومات حول طرق ومعلمات الوحدة تأتي من ملف WSDL. في هذه المرحلة، لا يزال الطلب المرسل يتكون من مصفوفات متداخلة، ولا يتم إنشاء أي كائنات خلال مرحلة تحليل SoapServer. لإنشاء الكائنات المطلوبة، يواصل Magento معالجة الإدخال بنفسه.
لاستخراج أسماء المعلمات وأنواع البيانات، يحصل Magento على النموذج الأولي من طريقة الطلب (راجع الكود السابق). بالنسبة لبعض أنواع البيانات الأساسية، مثل السلاسل والمصفوفات والقيم المنطقية وما إلى ذلك، سيقوم النظام بتعيين الإدخال إلى النوع المقابل. ولكن بالنسبة لأنواع الكائنات، تكون طريقة الحل أكثر تعقيدًا.
إذا كان نوع بيانات المعلمة هو مثيل لفئة، سيحاول Magento إنشاء مثيل باستخدام الإدخال المقدم. تذكر، الإدخال في هذه المرحلة هو مجرد قاموس، مفاتيحه هي أسماء الخصائص وقيمه هي قيم الخصائص.
أولاً، سيقوم Magento بإنشاء مثيل جديد للفئة المطلوبة. بعد ذلك، سيحاول ملؤه باستخدام الطريقة التالية:
سيتعامل Magento بهذه الطريقة مع كل خاصية يحاول المستخدم تعيينها. عندما يتم فحص جميع الخصائص، سيعتبر Magento أن المثيل قد تم تعيينه وسيعالج المعامل التالي. عندما تتم معالجة جميع المعاملات بهذه الطريقة، سيقوم Magento في النهاية بتنفيذ طريقة API هذه.
باختصار، يسمح لك Magento بإنشاء كائن، وتعيين خصائصه العامة، وأخيرًا تنفيذ أي طريقة تبدأ بـ "Set" عبر RPC الخاص به. وهذا السلوك هو الذي أدى إلى ظهور الثغرة في 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 أن كائناتنا صالحة، ثم يقوم بتسلسل (Serialize) جميع الأجزاء التي يجب تسلسلها وتخزينها في قاعدة البيانات، ثم يقوم بإلغاء تسلسل (Unserialize) الأجزاء التي تم تسلسلها سابقًا.
يبدو الأمر بسيطًا، أليس كذلك؟ في الواقع ليس كذلك، دعنا نستمر في رؤية كيف يحدد 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" هي التي يمكن تسلسلها. الأهم من ذلك، أن هذه الطريقة تستمر في التسلسل فقط بعد التأكد من أن قيمة الحقل هي مصفوفة أو كائن.
الآن، دعنا نرى كيف يحدد 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 يحتاج هذه المرة إلى التأكد من أن قيمة الحقل ليست مصفوفة أو كائنًا. بسبب هذين الفحصين، يجب أن نكون قادرين على تنفيذ هجوم حقن كائن (Object Injection)، ببساطة عن طريق تعيين سلسلة ذات نمط معين في حقل قابل للتسلسل. عندما نفعل ذلك، لن يقوم النظام بتسلسل هذا الحقل قبل تخزين الكائن في قاعدة البيانات، لأنه ليس كائنًا أو مصفوفة. ولكن عندما يحاول النظام إلغاء تسلسله، بعد تنفيذ استعلام قاعدة البيانات، سيتم إلغاء تسلسله، لأنه ليس كائنًا أو مصفوفة.