Orikon Enterprise Suite — SSTI vers le coffre-fort d'audit
Contexte
Compte utilisateur standard ([email protected], tier: standard) sur le portail SaaS Orikon utilisé par ACME Corporation France. Objectif : atteindre le coffre-fort d’audit /internal/admin/vault, réservé à l’administration globale et aux passerelles de maillage interne.
Reconnaissance
Trois modules exposés : personnalisation de factures, intégrations/webhooks, profil d’entreprise.
Piste explorée (sans issue) — SSRF via le testeur de webhook (module Intégrations)
Un champ “URL du Webhook cible” effectuait une vraie requête serveur, avec un filtre bloquant uniquement la chaîne littérale localhost/127.0.0.1. Contournable facilement (127.0.0.1.nip.io), mais cette voie n’a pas permis de localiser le vault sur le réseau interne.
Piste retenue — SSTI via le module Facturation
Le champ “Mention de Pied de Page Personnalisée” annonçait explicitement un support de syntaxe {{ expression }} pour des “variables dynamiques d’environnement ou d’organisation” signe d’un moteur de templates interprétant l’input utilisateur côté serveur, sans sandboxing suffisant.
Exploitation
1. Confirmation de la SSTI
{{ 7*7 }}→ Rendu : 49. L’expression est évaluée, pas juste affichée.
2. Identification du moteur
{{ ''.__class__ }}→ <class 'str'> : confirme un objet Python natif, donc moteur Jinja2.
3. Pivot vers les objets internes de l’application
{{ config }}→ <module 'config' from '/app/config.py'> : le moteur exposait directement le module de configuration de l’application elle-même, sans aucun filtrage de l’espace de noms accessible depuis le contexte du template.
4. Extraction du contenu
{{ config.__dict__ }}→ Dump complet des attributs du module config, révélant la clé de certification de l’infrastructure globale.
Cause racine
- Le moteur de rendu de templates interprétait des entrées utilisateur non fiables comme du code Jinja2, au lieu de les traiter comme de simples chaînes à substituer dans des variables prédéfinies.
- Aucun sandboxing de l’environnement Jinja2 (pas de
SandboxedEnvironment, pas de restriction sur__class__/__dict__/accès aux objets internes). - Le module
config.pyapplicatif (contenant potentiellement des secrets) était directement référencé dans le contexte global du moteur de templates, le rendant accessible à n’importe quel compte standard capable d’y injecter une expression.
Remédiation
- Ne jamais évaluer des entrées utilisateur comme templates : utiliser un vrai mécanisme de substitution de variables (placeholders simples) ou échapper strictement la syntaxe
{{ }}avant rendu. - Si un moteur de templates doit traiter de l’input utilisateur, utiliser un environnement sandboxé (
jinja2.sandbox.SandboxedEnvironment) avec une liste blanche stricte de variables/fonctions exposées. - Ne jamais injecter d’objets applicatifs sensibles (modules de config, objets contenant des secrets) dans le contexte global accessible aux templates utilisateur.
- Appliquer le principe de moindre privilège : un compte “standard” ne devrait jamais pouvoir atteindre, même indirectement, des informations réservées à l’administration globale.
Ce que j’ai appris
La piste la plus bruyante (la SSRF, avec son filtre contournable) n’était pas la bonne. Le signal décisif était une ligne d’UI : un champ qui documente lui-même une syntaxe {{ }}. Savoir reconnaître qu’une fonctionnalité “variables dynamiques” = moteur de template côté serveur fait gagner tout le temps qu’on aurait passé à forcer la SSRF.