[翻譯] Magento2 – 在未授權服務器執行惡意PHP代碼 (CVE-2016-4010)

譯注:本文所關注的漏洞適用于Magento 2.0,并未發現與Magento 1.x相關的漏洞報告。漏洞報告于2016年5月17日。原文鏈接:Magento – Unauthenticated Remote Code Execution。

引子

該漏洞(CVE-2016-4010)允許攻擊者在未經驗證的情況下在有漏洞的服務器上執行PHP代碼。實際上,此漏洞是由許多小的漏洞組成,我們將在下文中一一描述。

Magento是一個非常流行的電商平臺,占有30%的市場份額。Magento被主流的企業所采用,如Rosetta Stone,Nike,BevMo和Dyson等,它同樣也適用于小規模線上商店??傊?,有超過250,000家在線商店使用Magento,每年的成交額約達500億美元。

可以想象,如此的成交額以及龐大的用戶信息庫,使Magento成為眾黑帽眼中的香餑餑。這也是我再一次審核它的原因。(本文作者Netanel Rubin于2015年4月20日發表Analyzing the Magento Vulnerability (Updated),分析Magento 1.x的安全隱患,其中涉及官方SUPEE-5344等重要補丁。譯注。)

該漏洞攻擊建立在“Magento系統中某一個RPC已啟用,如 REST 或 SOAP”的假設之上。而這兩種方法均被默認啟用,且其中一個實際上是系統所必需的,因此這種假設是合情合理的。

本文,我將采用SOAP的方法,因為XML更易于理解。

此漏洞會影響社區版與企業版。我建議所有的Magento管理員更新安裝2.0.6補丁。

受影響版本

Magento EE 2.0.6 與 Magento CE 2.0.6之前的版本。

技術說明

自從我上次審核Magento,其代碼發生了許多變化:重寫了大部分代碼,
改變了目錄結構,并改善了安全機制。所以,我由衷地樂意再重新審核這個龐大的代碼庫。

首先引起我注意的是,系統的面向對象的結構改進顯著。幾乎每一個類至少實現一個接口、繼承一個父類、使一些魔術函數,如”__call()”或”__get()”,來公開私有屬性。

當然,無論對我、研究者以及Magento的開發人員,如此極端的面向對象編程實現的例子,雖是巨大的進步,亦造就了一個非常復雜的系統?,F在,當開發人員編寫一個新的類時,類必須要繼承類或實現接口,而這些類或接口是由另一位開發者實現的。類似情況在軟件開發中經常發生,越來越多的安全因素被卷入到這種工作方法中,尤其是這個擁有動態代碼流、動態程序接口的特定系統 – Magento。

Magento的核心是一個由不同的“模塊”組成的系統。這些所謂的“模塊”一般都在不同的目錄下,包含著為系統提供的不同功能的代碼。例如,支付模塊、購物車模塊以及顧客模塊。

在每個模塊中有一個名為“API”的特殊目錄。該模塊所有公開給其它模塊的功能都被包含在該目錄中。例如,支付模塊需要與購物車模塊、顧客模塊與授權模塊、郵寄模塊與銷售模塊,這些模塊之間兩兩都需要通信。

“API”目錄由不同的PHP文件組成,每個文件包含一個PHP類,負責公平部分模塊的功能至系統其余部分。大多數的模塊API功能僅限于其余系統控制的模塊,因為大部分都比較敏感。但是,有一些API調用也可以被其它的API所利用,其中大部分被用戶所控制,也是就是惡名昭著的Web API。

Magento的Web API支持兩種不同的遠程過程調用(RPC) – REST 與 SOAP。它們提供相同的功能,唯一不同的是,前者使用JSON和HTTP但詢字符串作為輸入,而后者使用XML信封作為輸入。因為這兩種方法均默認開啟,本文中,我將使用SOAP,便 于理解。

毫無疑問,保守地說,Magento是我所見過的最全面的PHP內容管理系統之一,或者說在任何一種語言的內容管理系統中,也是最全面的之一。

為了是公開每個模塊的API的一部分,Magento為模塊開發人員提供了”webapi.xml”文件來方便地定義他們想要在Web API中公開的方法。

“webapi.xml”簡潔地、有序地以XML的形式描述了需要向Web API公開的類與方法。每一個公開的方法均指明特定的權限,從“匿名” – 允許任何用戶(包括Guest)調用方法,至“self” – 只允許注冊客戶或僅限管理員,如“Magento_Backend::admin”是僅僅允許能夠修改服務器配置的管理員訪問的權限。

當使用”webapi.xml”給模塊開發人員提供了一條從系統前端至后端捷徑的同時,也開通了一條直達模塊核心的后門。

當我們的身份僅是游客時,我們應當只可以調用僅需“匿名”權限的方法。這將大大縮小攻擊的可能性,但會移除某一些預處理的必要條件。

另人意外的是,即使是“匿名”特權,我們仍然可以使用非常動態輸入類型。這里不僅指常規XMLRPC的輸入類型,如數組或Base64編碼的字符串,而且指在系統中可用的不同對象類型。例如,“CustomerRepositoryInterface::save()”API函數允許我們在“$customer”變量是“CustomerInterface”對象的,正如以下原型中定義的:

如何通過RPC接口創建對象呢?有趣的是,這個問題的答案取決于Magento如何配置其SOAP服務器。

Magento使用PHP預裝的SOAP服務器 – “SoapServer”。為了正確配置SOAP服務器,我們需要一個WSDL文件用于描述所有的方法、參數以及在某些RPC請求中使用的自定義類型。對于每一個支持XMLRPC功能的模塊,Magento會生成一個不同的WSDL文件,其內容直接取自“webapi.xml”。

當服務器解析RPC請求時,服務器使用WSDL文件來判定該請求是否合法,檢查請求的方法、參數、以及它們的數據類型。若請求合法,請求將交全Magento作進一步解析。必須指出,“SoapServer”并不與Magento直接交互,其對于模塊的方法與參數的信息僅僅來自于WSDL文件。

至此,請求仍由嵌套數組組成 – 在SoapServer的解析階段并沒有創建對象實例。Magento將繼續獨立地處理輸入來創建所需的實例。

如前文代碼示例,Magento獲取方法的原型以提取方法的參數和數據類型。通常來說,字符串,數組,布爾變量等,系統只是將輸入轉換為合適的類型。而對于對象,解決方案就有些復雜。

若參數的數據類型是一個類的實例,Magento的會嘗試使用輸入創建實例。請注意,到現在輸入僅是一個字典 – 鍵是屬性名稱、值是屬性值。

首先,Magento將創建一個所需類的新實例。然后,它會嘗試使用以下算法來填充它:

  • 獲取 屬性名稱(輸入字典的“鍵”);
  • 尋找名為“Set[NAME]()”的公共方法,其中[NAME]是 屬性名稱;
  • 若該方法存在,將屬性值作為參數執行該方法;
  • 若該方法不存在,則忽略并繼續下一個。

Magento將對每個用戶嘗試設置的屬性該算法進行設置。當所有屬性均被檢查后,Magento將認為實例已準備就緒,并繼續處理下一個參數。當所有參數都處理完后,Magento將執行該API方法。

讓我再捋一下,Magento允許你創建一個對象,設置其公共屬性,并通過其RPC接口執行任何一個以“Set”為前綴的方法。出人意料的是,這種行為將會導致Magento的崩潰。

某些API調用允許我們設置購物車中的具體信息,如送貨地址、產品,甚至付款方式。當Magento安全地在購物車實例中設置我們的信息時,它使用實例中的“save”方法,將新數據存儲至數據庫中。

讓我們看一下“save”功能是如何實現:

Magento確保我們的對象是合法的,并序列化了所有應當被序列化的字段,將其存入數據庫,最后再反序列化先前序列化過的字段。

是不是聽起來很簡單?

不。

讓我們看一下Magento是如何決定哪些字段應該被序列化的:

可以發現,只有出現在事先已寫死的應被序列化字段的字典“_serializableFields”中的字段才可以被序列化。接著,此方法在繼續序列化之前,先會確保該字段的值是一個數組或對象。

現在,讓我們再來看一下Magento是如何決定哪些字段應該被反序列化的:

好吧,這幾乎是相同的。唯一不同的是,這一次Magento在操作之前確保該字段不是一個數組或對象。由于這兩次檢查,我們可以利用對象注入攻擊 – 簡單地給可序列化的字段設置一個普通字符串。

當我們給可序列化的字段設置一個普通字符串時,系統不會在存入數據庫之前將其序列化,因為它不是對象或數組。但當系統在數據庫查詢執行之后嘗試反序列化它時,該字符串將會被反序列化,因為它不是對象或數組。

有漏洞與安全的系統就在這一線之間,因為這個小到幾乎不易察覺的條件判斷。剩下的問題就是哪些字段是“可序列化”的,并且如何設置它們。

第一個問題很容易,我們只需搜索哪一個類定義了“_serializableFields”這個屬性。很快,我發現若干個定義了這個字段的類,但沒有一個可以被我們所鐘愛的XMLRPC所創建。

誠然,其中一個名為“Payment”的類,其負責收集付款細節,出現在某一個API方法中,但并沒有作為一個參數,所以我不能創建或控制它實例中的屬性。緊接著,其可序列化的字段 – “additional_information”,只能使用常規的“Set[PROPERTY_NAME]”方法設置為一個數組。這作為一個安全措施,使得我們不僅無法通過XMLRPC創建它,更不能將其設為一個字符串。

但是,我們可以通過一個巧妙的方式來設置。

當Magento設置參數實例的屬性時,其并沒有真正設置參數實例的屬性。相反,Magento將屬性存放在一個字典結構中,名為“_data”。這個字典結構將會被應用于絕大多數情況。對于我們來說,我們的可序列化字段“additional_information”實際上是被存儲在字典結構中,而不是通常意義的屬性內。

因此,如果我們能夠完全控制“_data”字典結構,我們就可以通過手動的設置來繞過對“additional_information”字段的數組限制,而不是使用“Set[PROPERTY_NAME]”方法。

但我們如何才能控制這個敏感的字典結構呢?

好吧,Magento將我們API的輸入作為必須存入“Payment”實例的支付細節信息,在保存前會先修改實例的屬性。這點可以參考以下方法:

可以看到,“Payment”數據通過調用“$method->getData()”方法獲取,而實際上返回的是“$method”變量的“_data”屬性。值得注意的是,“$method”是一個API調用時的參數,所以在我們的掌控之中。

當Magento調用“$method”參數的getData()方法時,返回的是該參數的“_data”屬性,包含所有我們插入的支付信息。之后,Magento使用我們的“_data”屬性作為調用“importdata()”的輸入,正好用我們的“_data”屬性取代 了“Payment”實例中原有的“_data”屬性。

這是一個重大的進展。我們現在可以用我們掌控的“_data”屬性取代 “Payment”實例中原有敏感的“_data”屬性,這意味著我們可以設置“additional_information”字段了。

The problem is that for our unserialize() to work we need that field set to a string, but the “Set[PROPERT_NAME]” method only allows it to be an array.

現在的問題是,為了讓我們的unserialize()得逞,我們需要將那個字段設置為一個字符串,但“Set[PROPERT_NAME]”方法限制其必須為一個數組。

但是,令人驚訝的是,因為“importData()“調用前的兩行代碼,我們的問題迎刃而解。

Magento一直以來致立于建立其最靈活的內容管理系統,允許開發者開發自己的支付方式,處理自身的數據和信息。因此,Magento使用用戶自定義的“additional_data”來實現這一點。

“additional_data”字段是一個字典結構,包含了額外的完全由用戶控制的支付方法相關的數據。為了使這些用戶數據成為原始數據的一部分,Magento將“additional_data“字典結構與原始的“data”的字典結構合并在一起,高效地使用“additional_data”字典結構中的數據覆蓋“data”字典結構中的相同字段,基本上意味著完全覆蓋。

這意味著,兩個字典結構合并后,用戶控制的“additional_data”字典結構成為了參數中“_data”字典結構,并且由于“importData()”,更成為了“Payment”實例中敏感的“_data”屬性。這意味著我們現在可以完全控制可序列化字段“additional_information”,并利用其進行對象注入攻擊。

反序列化

現在,我們可以根據需求反序列化任意字符串,是時候進行對象注入了。

首先,我們需要一個擁有“__wakeup()”或“__destruct()”方法的對象,這兩個方法在對象被反序列化或銷毀時會被自動調用。之所以需要這兩個方法,是因為即使我們可以控制對象屬性,我們仍然無法調用其方法。這就是為什么我們必須依靠PHP的魔術方法,其具體事件發生時,其會被自動調用。

第一個我們會使用的類是“Credis_Client”,包含了以下方法:

可以看出,該類確實有一個簡單的“__destruct()”方法(該方法在對象被銷毀時會被PHP自動調用),在“__destruct()”方法簡單調用了“close()”方法。另一方面,更有趣的是,若存在有效的Redis連接,“close()”方法將嘗試通過調用redis屬性的“close()”方法關閉該連接。

因為“unserialize()”允許我們控制控制所有的對象屬性,所以我們可以控制“redis”屬性,同樣,我們可以將任意的對象賦值給該屬性 (不僅僅Redis類的實例),也可以隨意地調用系統中任何類的“close()”方法。

事情一下豁然開朗。在Magento中存在一些其它的 “close()”方法。通常來說,“close()”方法用于終止流、關閉文件句柄、存儲對象數據,因此我們一定能發現一些有趣的調用。讓我們來看看“Transaction”類的“close()”方法:

這兩個方法都非常簡單?!癱lose()”方法調用“save()”方法,后者調用了“_resource”屬性的“save()”方法。

與之前邏輯相同,因為我們可以控制“_resource”的屬性,繼而控制具體的類,所以我們可以調用任何類中的任何“save()”方法。

這是一個重大的進展。我們可以大膽猜測,“save()”方法通常負責將一些數據存儲在文件系統或數據庫中?,F在我們需要做的是尋找某一個文件系統工具類的“save()”方法。

這很容易:

這個方法做的就是將“components”寫入一件文件中,文件名從“stat_file_name”中獲取。因為我們可以直接修改這兩個屬性,于是這就是一個可以在服務器上任意寫入文件的漏洞。

于是,剩下我們只需要找到一個服務器可寫、又可以通過網絡訪問的路徑,就可以攻擊成功了。其中之一就是“/pub”目錄,在所有的Magento2.0的系統中必不可少。該目錄通常用來存儲圖片、由管理員上傳的公共文件,所以該目錄必然對服務器可寫。更重要的是,這個目錄中的圖片均需要被瀏覽器訪問,所以其必然可以通過網絡訪問到。

最后,只需一段PHP代碼,加上“.php”擴展名,我們就可以在一臺未授權的服務器上執行任意的PHP代碼了。

17139大乐透奖金 奇趣腾讯分分彩官网app 中石油股票论坛 代理股票配资公司 怎样在线配资 福建快三是不是全省销售 北京pk是最稳全天计划 LB三分彩 新加坡七乐彩论坛 深圳风采什么时候开奖 秒速赛车大小单双计划