← Tous les write-ups

Orikon Enterprise Suite — SSTI vers le coffre-fort d'audit

StarHack CTF Moyen Web

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.py applicatif (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.