Magento 2.0.5 Remote Code Execution (RCE)

Op 17 mei 2016 heeft Magento een security update voor Magento 2 uitgebracht: Magento 2.0.6.  In dit blog ga ik technisch de diepte in van het belangrijkste probleem dat in deze nieuwe versie wordt aangepakt: Remote Code Execution. Ik loop nauwkeurig alle files en functies langs die bij een mogelijke hack via dit lek worden misbruikt. Hiermee hoop ik jou als Magento developer diepgaand inzicht te geven in waar precies de kwetsbaarheden zitten.

Met Remote Code Execution is het voor hackers mogelijk om de shop over te nemen. Hoe dit proces in zijn werk gaat heeft de onderzoeker Netanel Rubin in een blogartikel op zijn site beschreven. Het artikel is echter behoorlijk vaag gehouden (waarschijnlijk bewust om niet teveel weg te geven aan kwaadwillenden). Het is ook voor mij lastig om er doorheen te komen. Maar ik ben er even goed voor gaan zitten en heb het tot in detail uitgeplozen.

Remote Code Execution via REST API? (Wat is de rol van de REST API)

De oorzaak van de kwetsbaarheid ligt in het feit dat een aantal functies die worden gebruikt in het orderproces data op een minder handige wijze valideren en verwerken. Magento 2’s standaard theme maakt veel gebruik van javascript technieken en daarbij is de REST API belangrijk. Deze REST API maakt het bijvoorbeeld ook mogelijk om het gehele bezoekersproces als gast via REST requests te kunnen uitvoeren. Denk aan een quote starten, product in het mandje gooien, betaal- en adresgegevens invoeren om vervolgens door te gaan naar een order. Bij deze variant van Remote Code Execution worden vooral de stappen voor het instellen van een betaalmethode misbruikt. Deze ga ik daarom uitgebreid toelichten.

Voor leerdoeleinden heb ik alle interessante files en functies nog even opgesomd die bij dit verhaal worden behandeld:

  1. lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php line 380, save() + de public serialize() en unserialize() methodes.
  2. lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php line 128 _serializeField() (protected)
  3. lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php line 155 _unserializeField() (protected)
  4. app/code/Magento/Quote/Model/PaymentMethodManagement.php line 50, set($cartID PaymentInterface $method)
  5. lib/internal/Credis/Client.php line 298 __destruct() en line 488 close()
  6. app/code/Magento/Sales/Model/Order/Payment/Transaction.php -> namespace: Magento\Sales\Model\Order\Payment\Transaction::__destruct(), close()
  7. lib/internal/Magento/Framework/Simplexml/Config/Cache/File.php line 72 save()

Stap 1. Opslaan van data via de API

In Magento’s checkout proces wordt de web API gebruikt voor o.a. het instellen van je betaal- en verzendgegevens, zie file 1 t/m/ 3. Als zo’n API request wordt uitgevoerd, gebeurt in grote lijnen het volgende:

  1. isSaveAllowed() controleert of het valide data is alvorens op te slaan;
  2. _serializeFields() – zet arrays en objecten om naar serialized data;
  3. save() slaat de data op, of past het aan in de database indien het al bestaat;
  4. _unserialize vormt alle strings weer terug om naar arrays of objecten indien mogelijk.

Het instellen van bijvoorbeeld een betaalmethode staat een veld “additional_data” toe. Deze sturen we mee in het ‘set-payment-information’ verzoek en is dus een veld wat wordt toegestaan. Dit veld wordt als “serialized” string opgeslagen in de database. Functie 2 zet eventuele arrays of objecten om naar een serialized string voordat het in de database wordt opgeslagen. In functie 4 wordt deze weer unserialized (naar een array of object) zodat PHP er iets mee kan. Als je dus kwaadaardige code als een “string” in serialized format mee geeft bij “additional_data”, wordt het in stap 2 niet omgezet naar een serialized string omdat het geen array of object is. Maar in stap 4 wordt het wel omgezet van serialized string naar array of object. Het is een kwetsbaarheid op zichzelf, maar vereist extra handelingen om er iets mee te kunnen. Dit brengt ons bij de volgende reeks stappen die Rubin met een knap staaltje reversed engineering te weten is gekomen.

Stap 2. array_merge() bij de payment set()

Als we kijken bij file 4 naar de set() functie zien we in grote lijnen het volgende:

  1. $cartId wordt opgehaald, daarvan weer de betaalmethode en bijbehorende data;
  2. als er “additional_data” in dit data object zit, wordt dit middels array_merge() samengevoegd met de data die we hebben aangeleverd via de API (ons object wordt dus samengevoegd of overschrijft het de huidige waarden);
  3. Vervolgens importeert Magento dit naar zijn “_data” dictionary.

We kunnen nu dus “_data” aanpassen of overschrijven met hetgeen wat we meegeven via de API in “additional_data” van onze betaalmethode. Normaliter accepteert de data setter alleen arrays. Omdat deze array_merge() plaats vindt, gaan we hier omheen. Dit is tevens de actie die wordt uitgevoerd als je in de checkout je betaalgegevens invult bij een betaalmethode. De http://www.store.nl/rest/V1/guest-carts/:cartId/set-payment-information actie wordt hiervoor gebruikt. Deze data is ook weer terug te vinden in de quote_payment tabel. Mocht je benieuwd zijn of er pogingen zijn gedaan deze hack toe te passen op je shop: controleer de “quote_payment” tabel, in de kolom “additional_data” voor een serialized string met de term Credis. Als je daar een waarde ziet wat redelijk lang, serialized en deze term bevat, ben je vermoedelijk gehacked en is de shop vatbaar hiervoor.

Stap 3. Object Injecton via de Credis Client

Nu we naar eigen inzicht objecten binnen Magento kunnen krijgen, hebben we nog een manier nodig om deze ergens te injecteren / uit te voeren. We hebben een object met een __wakeup() of __destruct() methode nodig. Deze methoden zijn namelijk vaak voor streams zoals filestreams, verbindingen. Deze worden dankzij PHP’s Magic methods automatisch aangeroepen als zo’n stream, dus moet starten of stoppen. Een goed voorbeeld, zoals gebruikt door Rubin is de Credis_Client, file nummer 5. De destruct() functie in de Redis client wordt automatisch door PHP aangeroepen wanneer een object vernietigd wordt, destruct() roept de close() functie aan. Als close() een Redis verbinding detecteert zal hij deze proberen te sluiten en gebruikt daarvoor de redis property. Dankzij unserialize() hebben we macht over alle object properties, ook die van Redis. We kunnen in die Redis property alles stoppen wat we willen (niet alleen Redis) en daarmee eventueel verdere close() functies aanspreken binnen Magento.

Stap 4. Afronding

Vanuit hackers perspectief zou het natuurlijk mooi zijn als we ook iets kunnen wegschrijven naar een publiekelijk toegankelijke file. En dat kan, dankzij files 6 en 7. File 6 heeft namelijk een close() functie wat resulteert in een save() op een ‘_resource’ object. We kunnen dus de trucs van eerder toepassen om data op te slaan middels bijvoorbeeld file 7 wat de afhandeling van cache verzorgt en ook weer een save() functie heeft.

Binnen deze save() functie wordt de functie @file_put_contents($this->getStatFileName(), serialize($this->getComponents())); uitgevoerd. $this->getStatFileName() relateert naar het pad van de file en $this->getComponents() geeft ons de mogelijkheid hier onze data in te gooien. Hiermee zouden we dus de code die we eerder als serialized string hebben meegestuurd kunnen laten terugkomen in een file. De volgende snippet is een stukje uit een exploit die onlangs is verschenen voor deze kwetsbaarheid.

Je ziet hier de eerder benoemde concepten terugkomen. Verder zou het nog nodig zijn om bijvoorbeeld een .php file extensie aan deze file te plakken, gezien we code kunnen uitvoeren is hier wel iets op te bedenken. Zelf heb ik ook gemerkt dat de code nogmaals wordt uitgevoerd als je de quote inhoud ophaalt middels de “rest/default/V1/guest-carts/cuoteId:/selected-payment-method” API functie. Op deze manier zou je de door jou aangebrachte code al meerdere malen kunnen triggeren zonder dat je het laat terugkomen in een file.

Conclusie

Het is een knap staaltje speurwerk van Netanel Rubin. Als we kijken naar de grotere hacks uit het verleden zien we dat deze toch wel een van de meest complexere is met daarbij ook veel mogelijkheden voor een hacker. Het is raadzaam direct patch 2.0.6 te installeren en even de ‘quote_payment’ tabel te controleren. Het dicht zetten van de API is niet handig omdat Magento’s default theme hier al veel gebruik van maakt, maar dit is mogelijk.

Ik heb zelf al een aantal keer de update uitgevoerd op mijn Magento 2 shop en demo shops waarbij ik nog geen problemen heb ondervonden. Natuurlijk is het raadzaam het even te testen op een ontwikkelomgeving, maar ik verwacht weinig problemen.

 op

Fabio is een echte Magento developer in hart en nieren. Hij heeft niet alleen 3 Magento certificaten (Solution Specialist, Front End Developer en Developer), maar ook het logo op zijn enkel getatoeëerd en de muren van zijn studio beplakt met 16 m2 Magento mindmap.