
Magento Esecuzione Remota di Codice Non Autorizzata (CVE-2016-4010)
0x00 Premessa
Il 17 maggio, il ricercatore di sicurezza Netanel Rubin ha reso pubblica una vulnerabilità di esecuzione remota di codice non autenticata in Magento (CVE-2016-4010). Questa vulnerabilità include in realtà diverse vulnerabilità minori e consente a un attaccante di eseguire codice PHP non autorizzato su un server Magento vulnerabile. Magento è una piattaforma e-commerce molto popolare, acquisita da eBay nel 2011. Alcuni marchi noti, come Samsung, Nikon, Lenovo e molte altre piccole attività di e-commerce la utilizzano. Si stima che Magento venga usato da 250.000 negozi online, con un volume di affari annuo di 60 miliardi di dollari.
0x01 Analisi
Condizioni per lo sfruttamento della vulnerabilità:
Le API web di Magento consentono due diversi tipi di RPC: REST RPC e SOAP API. Entrambi i metodi offrono le stesse funzionalità; l'unica differenza è che il primo usa JSON e richieste HTTP per trasmettere l'input, mentre il secondo usa XML.
Per esporre soltanto le API di alcuni moduli, Magento offre agli sviluppatori un metodo comodo: dichiarare in un file "webapi.xml" soltanto le API dei moduli che si desidera rendere accessibili. Il file webapi.xml contiene tutte le classi e i metodi delle Web API da esporre, e per ogni metodo specifica anche l'autorizzazione concreta richiesta. Queste autorizzazioni includono:
Naturalmente, questo modo di consentire agli sviluppatori di usare il file webapi.xml per comunicare tra il frontend e il backend (Web API) del sistema apre di fatto una backdoor diretta al cuore dei moduli.
Inoltre, anche se disponiamo dell'autorizzazione "anonymous", abbiamo comunque bisogno di un modo per passare valori in modo dinamico. Qui si fa riferimento ai diversi oggetti che possono essere usati nel sistema. Ad esempio, la funzione API "CustomerRepositoryInterface::save()" ci consente di utilizzare un oggetto "CustomerInterface" nella variabile "$customer". Il codice prototipo è il seguente:
interface CustomerRepositoryInterface
{
/**
* Create customer.
*/
public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
}
Allora, come possiamo creare oggetti usando l'interfaccia RPC? In realtà, la risposta sta nel modo in cui Magento configura il server SOAP.
Magento usa un server SOAP che include di default il "SoapServer" di PHP. Per essere configurato correttamente, "SoapServer" richiede un file WSDL, in cui vengono definiti tutti i metodi, i parametri e i tipi personalizzati usati nelle richieste RPC effettive. Magento genera file WSDL differenti per ogni modulo che supporta XMLRPC e imposta direttamente i valori provenienti dal file webapi.xml del modulo.
Quando una richiesta RPC viene analizzata dal server, il server usa i dati trovati nel file WSDL per determinare se la richiesta è valida, controllando metodi, parametri e tipi. Se la richiesta è valida, l'oggetto richiesta analizzato viene passato a Magento per un'ulteriore elaborazione. Un punto molto importante è che "SoapServer" non interagisce in alcun modo con Magento: tutte le informazioni su metodi e parametri dei moduli provengono dal file WSDL. In questa fase, la richiesta inviata è ancora composta da array nidificati; nessun oggetto viene creato durante l'analisi di SoapServer. Per creare gli oggetti necessari, Magento continua a elaborare l'input da solo.
Per estrarre i nomi dei parametri e i tipi di dati, Magento recupera il prototipo dal metodo richiesto (come nel codice precedente). Per alcuni tipi di dati di base, come stringhe, array, booleani, ecc., il sistema associa l'input al tipo corrispondente. Ma per i tipi di oggetti, la soluzione è più complicata.
Se il tipo di dato di un parametro è l'istanza di una classe, Magento tenterà di creare un'istanza usando l'input fornito. Ricordiamo che in questo momento l'input è soltanto un dizionario, le cui chiavi sono nomi di proprietà e i valori sono valori di proprietà.
In primo luogo, Magento creerà una nuova istanza della classe richiesta. Poi cercherà di popolarla usando il seguente metodo:
Magento applicherà questo metodo a ogni proprietà che l'utente sta cercando di impostare. Quando tutte le proprietà sono state controllate, Magento considera l'istanza completata e passa al parametro successivo. Quando tutti i parametri sono stati elaborati, Magento esegue infine il metodo API.
In sintesi, Magento ti consente di creare un oggetto, impostarne le proprietà pubbliche e infine eseguire, tramite le sue RPC, qualsiasi metodo che inizi con "Set". È proprio questo comportamento a causare la vulnerabilità di Magento.
Dalla ricerca è emerso che alcune chiamate API consentono di impostare determinate informazioni nel carrello degli acquisti; tali informazioni possono essere il nostro indirizzo postale, i prodotti e persino la nostra modalità di pagamento.
Quando Magento imposta le nostre informazioni nell'istanza del carrello, usa il metodo "save" dell'istanza per memorizzare i nuovi dati nel database.
Vediamo come funziona il metodo "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 verifica che l'oggetto sia valido, quindi serializza tutte le parti che devono essere serializzate e le memorizza nel database; infine deserializza le parti precedentemente serializzate.
Sembra semplice, vero? In realtà non lo è. Continuiamo a vedere come Magento determina quali parti devono essere serializzate.
/**
* 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()
Come possiamo vedere, solo le parti presenti nel dizionario hardcoded "_serializableFields" possono essere serializzate. Soprattutto, questo metodo serializza solo dopo essersi assicurato che il valore del campo sia un array o un oggetto.
Ora vediamo come Magento determina quali parti devono essere deserializzate.
/**
* 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 ()
Bene, sembra molto simile. L'unica differenza è che questa volta Magento deve assicurarsi che il valore del campo non sia un array o un oggetto. Grazie a questi due controlli, dovremmo essere in grado di realizzare un attacco di object injection, impostando semplicemente una stringa con una determinata regola in un campo serializzabile. Dopo averlo fatto, il sistema non serializzerà questo campo prima di memorizzare l'oggetto nel database, perché non si tratta di un oggetto o di un array. Tuttavia, quando il sistema tenterà di deserializzarlo, verrà deserializzato dopo l'esecuzione della query sul database, poiché non è un array o un oggetto.
Ma è proprio questa condizione, così piccola da essere quasi invisibile, a creare la vulnerabilità. La questione rimanente è capire quali campi sono considerati "serializzabili" e come possiamo impostarli.
Naturalmente, il primo problema è semplice: basta cercare quale classe contiene la proprietà "_serializableFields". Ben presto è stata trovata una classe "Payment", ma non come parametro, quindi non è possibile creare o controllare le proprietà della sua istanza. Soprattutto, il suo campo serializzabile "additional_information" può essere impostato solo come array, e la tecnica "Set[NOME_PROPRIETÀ]" viene usata come ulteriore misura di sicurezza: quindi non solo non possiamo crearlo, ma anche se potessimo non potremmo impostarlo come stringa.
Ma la cosa interessante è che può essere impostato in un modo alternativo, per così dire "elegante". Quando Magento imposta le proprietà dell'istanza di un parametro, in realtà non imposta davvero le proprietà, ma le salva in un dizionario chiamato "_data". Quando una proprietà dell'istanza viene utilizzata, viene consultato questo dizionario. Questo per noi significa che il nostro campo serializzabile – "additional_information" – viene in realtà salvato in un dizionario interno, non in una proprietà normale.
Quindi, se riusciamo a controllare completamente il dizionario "_data", possiamo facilmente aggirare il vincolo di array del campo "additional_information", perché possiamo impostarlo manualmente invece di chiamare "Set[NOME_PROPRIETÀ]".
Ma come possiamo controllare questo dizionario sensibile?
Prima di salvare la nostra istanza "Payment", Magento deve modificarne le proprietà. Magento tratta il nostro input API come informazioni di pagamento da memorizzare nell'istanza "Payment", come segue:
/**
* 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()
Come possiamo vedere, i dati "Payment" vengono ottenuti chiamando "$method->getData()" sul parametro "$method", che restituisce la proprietà "_data". Ricordiamo che, poiché "$method" è un parametro del metodo API, possiamo controllarlo.
Quando Magento chiama "getData()" sul nostro parametro "$method", viene restituita la proprietà "_data" del parametro, contenente tutte le informazioni di pagamento da noi inserite. Successivamente, chiama "importData()" usando la proprietà "_data" come input, sostituendo la proprietà "_data" dell'istanza "Payment" con la nostra proprietà "_data". A questo punto, possiamo usare la proprietà "_data" da noi controllata per sostituire la sensibile proprietà "_data" dell'istanza "Payment"; ciò significa che ora possiamo impostare il campo "additional_information".
Affinché unserialize() funzioni, abbiamo bisogno che il campo possa essere impostato come stringa, ma il metodo "Set[NOME_PROPRIETÀ]" consente solo array. La soluzione sta nelle due righe di codice precedenti alla chiamata "importData()". Magento consente agli sviluppatori di aggiungere i propri metodi di pagamento, fornendo i propri dati e informazioni. Per fare ciò, Magento usa il campo "additional_data". Questo campo è un dizionario che contiene ulteriori dati del metodo di pagamento ed è completamente controllabile dall'utente. Per far sì che i contenuti personalizzati diventino parte dei dati originali, Magento unisce il dizionario "additional_data" con il dizionario "data" originale, consentendo di fatto al dizionario "additional_data" di sovrascrivere qualsiasi valore nel dizionario "data", sostanzialmente sovrascrivendolo del tutto. Ciò significa che, dopo l'unione dei due dizionari, il dizionario "additional_data" controllabile dall'utente diventa ora il dizionario "_data" del parametro e, grazie a "importData()", diventa anche la sensibile proprietà "_data" dell'istanza "Payment". In altre parole, ora abbiamo pieno controllo sul campo serializzabile "additional_information" e possiamo effettuare un attacco di object injection.
Ora che possiamo deserializzare qualsiasi stringa vogliamo, è il momento di effettuare l'attacco di object injection.
Per prima cosa, abbiamo bisogno di un oggetto con un metodo "__wakeup()" o "__destruct()", così che venga chiamato automaticamente quando l'oggetto viene deserializzato o distrutto. Questo perché, anche se possiamo controllare le proprietà dell'oggetto, non possiamo chiamare i suoi metodi. Per questo dobbiamo affidarci ai metodi magici di PHP, che vengono chiamati automaticamente quando si verifica un determinato evento.
Il primo oggetto che useremo è un'istanza della classe "Credis_Client", che contiene i seguenti metodi:
/*
* 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()
Possiamo vedere che questa classe ha un semplice metodo "__destruct" (che viene chiamato automaticamente da PHP quando l'oggetto viene distrutto) che invoca "close()". La cosa interessante è che il metodo "close()", se rileva una connessione attiva al server Redis, chiama il metodo "close()" della proprietà "redis" per chiuderla.
Poiché "unserialize()" ci consente di controllare tutte le proprietà dell'oggetto, possiamo anche controllare la proprietà "redis". Possiamo inserire nella proprietà qualsiasi oggetto vogliamo (non solo Redis) e chiamare un qualsiasi metodo "close()" in una qualsiasi classe del sistema. Questo amplia notevolmente la nostra superficie di attacco. In Magento esistono diversi metodi "close()" e, poiché di solito servono a terminare stream, chiudere handle di file e salvare dati di oggetti, dovremmo trovare qualche chiamata interessante.
Come previsto, abbiamo trovato il seguente metodo "close()" nella classe "Transaction":
/**
* 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()
Sembra semplice: "close()" chiama "save()", che a sua volta chiama il metodo "save()" della proprietà "_resource". Con lo stesso ragionamento, poiché controlliamo la proprietà "_resource", possiamo controllarne anche la classe, quindi possiamo chiamare il metodo "save()" di qualunque classe vogliamo.
Abbiamo fatto un altro grande passo avanti. Come previsto, il metodo "save()" viene solitamente usato per salvare vari dati in vari supporti di memorizzazione (file system, database, ecc.). Ora dobbiamo solo trovare un metodo "save()" che usi il file system come supporto di memorizzazione.
Presto ne ho trovato uno:
/**
* 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()
Questo metodo salva sostanzialmente i dati del campo "components" in un file. Poiché il percorso del file viene recuperato dal campo "stat_file_name", e poiché controlliamo entrambi i parametri, in realtà controlliamo il percorso e il contenuto del file: questo genera una vulnerabilità di scrittura arbitraria di file.
Ora dobbiamo solo trovare un percorso valido, scrivibile e accessibile dal web server in cui scrivere il file. In tutte le installazioni di Magento esiste una directory "/pub", usata per memorizzare immagini o file caricati dagli amministratori; è un percorso sfruttabile.
Alla fine, ci basta semplicemente scrivere un file webshell PHP sul server per eseguire codice PHP arbitrario non autenticato sul server Magento.
0x02 Sfruttamento
Configurazione dell'ambiente di test
Nota: è possibile incontrare alcuni problemi; fare riferimento a:
Sfruttamento della vulnerabilità
L'exploit pubblico su exploit-db è scaricabile qui: https://www.exploit-db.com/exploits/39838/
Metodo di sfruttamento:
Trovare un sito Magento vulnerabile Controllo online della versione di Magento: http://magentoversion.com/
Aggiungere un prodotto al carrello

Entrare nel carrello e fare clic su "Checkout"

Compilare l'indirizzo di spedizione, controllare la richiesta POST /rest/default/V1/guest-carts/[guestCartId]/shipping-information e ottenere [guestCartID]

Salvare l'exploit di cui sopra come magento_exp.php ed eseguire: php magento_exp.php [Magento_URL] [guestCartID] ([percorso di scrittura webshell])

Rilevamento di massa
Dallo studio dell'exploit di cui sopra, si evince che lo sfruttamento richiede le seguenti condizioni:

Pertanto, ho scritto un semplice script di verifica di massa da usare insieme all'exploit di cui sopra:
#!/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!"
Risultato dell'esecuzione:

0x03 Difesa
Aggiornare Magento all'ultima versione (2.0.6). Indirizzo di download: https://www.magentocommerce.com/download
Riferimenti