From andrew.fasano@nist.gov Sat Nov 22 02:40:13 2025 Subject: Multiple vulnerabilities in Exim 4.99 SQLite integration: SQL injection + heap buffer overflow From: "Fasano, Andrew S. (Fed)" To: "security@exim.org" Date: Sat, 22 Nov 2025 01:39:46 +0000 Return-Path: Authentication-Results: mx10.schlittermann.de; iprev=pass (cumin.exim.org) smtp.remote-ip=152.53.204.32; spf=permerror smtp.mailfrom=nist.gov; dkim=pass header.d=nist.gov header.s=selector2 header.a=rsa-sha256; dmarc=pass header.from=nist.gov Authentication-Results: exim.org; iprev=pass (mail-northcentralusazon11011052.outbound.protection.outlook.com) smtp.remote-ip=40.107.199.52; spf=pass smtp.mailfrom=nist.gov; dkim=pass header.d=nist.gov header.s=selector2 header.a=rsa-sha256; dmarc=pass header.from=nist.gov; arc=pass (i=1) header.s=arcselector10001 arc.oldest-pass=1 smtp.remote-ip=40.107.199.52 authentication-results: dkim=none (message not signed) header.d=none;dmarc=none action=none header.from=nist.gov; X-Spam-Score: -2.3 (--) MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Content-Type: text/plain; charset=utf-8 Hello, I'm writing to disclose two security vulnerabilities in Exim 4.99's SQLite hints database implementation. One is related to the recently patched CVE-2025-26794 (SQL injection), but the fix doesn't fully address the issue. I've also discovered a new heap buffer overflow vulnerability in the same code path. In vulnerable configurations, a remote, unauthenticated attacker can achieve heap corruption. I was unable to develop an end-to-end exploit chain for remote code execution, but it may be possible with further work. I'm reporting this to you immediately upon discovery so you can assess and remediate. ================================================================================ OVERVIEW ================================================================================ Two distinct vulnerabilities exist in the SQLite hints database code: 1. Incomplete SQL injection fix - CVE-2025-26794's patch doesn't escape single quotes 2. Heap buffer overflow - Unvalidated database field used as array bound (NEW) IMPORTANT: Only specific ratelimit configurations expose these vulnerabilities. ================================================================================ VULNERABILITY #1: SQL INJECTION VIA RATELIMIT KEY (SAME ROOT CAUSE AS CVE-2025-26794) ================================================================================ Related to: CVE-2025-26794 (same vulnerable code, different attack vector) CWE: CWE-89 -------------------------------------------------------------------------------- Background -------------------------------------------------------------------------------- CVE-2025-26794 was assigned for SQL injection via ETRN commands when SQLite hints are used. That vulnerability was fixed by hashing ETRN input before using it as a database key. However, the underlying SQL injection primitive in xtextencode() was not fixed, and the commit message noted: "The hints db remains injectable, in case of USE_SQLITE." This report documents a separate attack vector exploiting the same vulnerable code via ratelimit ACLs, which warrants a new CVE as it affects different configurations and users. -------------------------------------------------------------------------------- Technical Details -------------------------------------------------------------------------------- The xtextencode() function sanitizes database keys before SQL query construction, but doesn't handle single quotes (ASCII 39): // xtextencode.c:35-37 for(uschar ch; len > 0; len--, clear++) g = (ch = *clear) < 33 || ch > 126 || ch '+' || ch '=' ? string_fmt_append(g, "+%.02X", ch) // Only encodes: <33, >126, +, = : string_catn(g, clear, 1); // Single quote (39) passes through! This encoded key is directly interpolated into SQL: // hints_sqlite.h:129 # define FMT "SELECT dat FROM tbl WHERE ky = '%s';" // hints_sqlite.h:137, 153 encoded_key = xtextencode(key->data, key->len); qry = string_sprintf(FMT, encoded_key); // No escaping! -------------------------------------------------------------------------------- Exploitation -------------------------------------------------------------------------------- An attacker can inject SQL via quoted SMTP addresses: MAIL FROM:<"x'/**/UNION/**/SELECT/**/X''--"@attacker.com> The single quote breaks out of the SQL string context, allowing arbitrary queries including returning attacker-controlled binary blobs. -------------------------------------------------------------------------------- Configuration Requirement -------------------------------------------------------------------------------- This is only exploitable when attacker-controlled data flows into the database key. For ratelimit ACLs, this happens when: - Using an explicit key parameter: ratelimit = 100 / 1h / per_rcpt / $sender_address - Using per_addr with an explicit key: ratelimit = 100 / 1h / per_addr / $sender_address - Using unique= with attacker data: ratelimit = 100 / 1h / per_rcpt / unique=$sender_address NOT VULNERABLE: Default per_addr (without explicit key) uses client IP address, which isn't attacker-controlled via SMTP. -------------------------------------------------------------------------------- Remediation -------------------------------------------------------------------------------- Add single quote escaping to xtextencode(): case '\'': g = string_catn(g, US"+27", 3); break; Alternatively (preferred), migrate to parameterized queries throughout the SQLite integration. ================================================================================ VULNERABILITY #2: HEAP BUFFER OVERFLOW VIA UNVALIDATED DATABASE FIELD ================================================================================ CWE: CWE-122, CWE-787, CWE-843 -------------------------------------------------------------------------------- Technical Details -------------------------------------------------------------------------------- Database records are cast directly to internal structures without validation: // acl.c:2666 dbdb = dbfn_read_with_length(dbm, key, &dbdb_size); // acl.c:2671-2675 if (dbdb) { /* Locate the basic ratelimit block inside the DB data. */ HDEBUG(D_acl) debug_printf_indent("ratelimit found key in database\n"); dbd = &dbdb->dbd; // Direct cast, trusting database content The structure definition: // hintsdb_structs.h:142-147 typedef struct { dbdata_ratelimit dbd; time_t bloom_epoch; // 8 bytes unsigned bloom_size; // 4 bytes - ATTACKER CONTROLLED uschar bloom[40]; // Fixed 40 bytes, but bloom_size can be larger! } dbdata_ratelimit_unique; No validation occurs to ensure bloom_size matches the actual array size. This value is then used directly as an array bound in the bloom filter code: // acl.c:2798-2808 seen = TRUE; for (n = 0; n < 8; n++, hash += hinc) { int bit = 1 << (hash % 8); int byte = (hash / 8) % dbdb->bloom_size; // bloom_size from DB, not validated! if ((dbdb->bloom[byte] & bit) == 0) { dbdb->bloom[byte] |= bit; // HEAP BUFFER OVERFLOW! seen = FALSE; } } The bloom array is declared as 40 bytes, but if bloom_size is set to a large value, writes can occur far beyond the allocated buffer. -------------------------------------------------------------------------------- Configuration Requirement -------------------------------------------------------------------------------- The bloom filter code only executes when a unique value is set. The unique parameter is set either: 1. Explicitly: unique=$sender_address 2. Implicitly via per_addr mode in RCPT context (sets unique to recipient) NOT VULNERABLE: Ratelimit ACLs without unique= or per_addr never enter the bloom filter code. -------------------------------------------------------------------------------- Exploitation -------------------------------------------------------------------------------- Using the SQL injection from vulnerability #1, an attacker injects an 80-byte blob with bloom_size set to values far exceeding the actual 40-byte allocation (e.g., 1505996). When the bloom filter code executes, this oversized value is used as the modulo bound for calculating array indexes: int byte = (hash / 8) % dbdb->bloom_size; // If bloom_size = 1505996... dbdb->bloom[byte] |= bit; // ...writes up to 1.5MB past buffer The write offset is determined by the MD5 hash of the unique value: hash = first_4_bytes(MD5(unique_value)); byte_offset = (hash / 8) % bloom_size; bit_value = 1 << (hash % 8); bloom[byte_offset] |= bit_value; This provides: - Controlled offset: Choose recipient addresses to target specific heap locations - Bit-level control: Each recipient performs a bitwise OR operation - Arbitrary byte values: Multiple recipients can OR together to construct 0x00-0xFF -------------------------------------------------------------------------------- Proof of Concept Results -------------------------------------------------------------------------------- I successfully demonstrated: - Using bloom_size = 1505996 (37,649x larger than the 40-byte array) - Targeting specific heap offsets by pre-computing recipient addresses with desired MD5 hashes - Writing arbitrary byte values through multiple recipients that OR together - Reliable crash oracle distinguishing valid vs. invalid heap addresses -------------------------------------------------------------------------------- Why RCE Wasn't Achieved -------------------------------------------------------------------------------- The theoretical exploitation path involves corrupting SQLite internal structures to gain arbitrary write capability, then overwriting GOT entries to hijack control flow. However, this requires knowledge of PIE base, libc base, and heap addresses to defeat ASLR/PIE. I couldn't find a reliable information leak to get around this. But it may be possible. -------------------------------------------------------------------------------- Remediation -------------------------------------------------------------------------------- Validate database records before using their contents: if (dbdb_size < sizeof(dbdata_ratelimit_unique)) { log_write(0, LOG_MAIN, "ratelimit: invalid record size %d", dbdb_size); dbdb = NULL; } else if (dbdb->bloom_size == 0 || dbdb->bloom_size > sizeof(dbdb->bloom)) { HDEBUG(D_acl) debug_printf_indent("ratelimit: invalid bloom_size %u (max %zu)\n", dbdb->bloom_size, sizeof(dbdb->bloom)); log_write(0, LOG_MAIN, "ratelimit: bloom_size %u exceeds maximum %zu", dbdb->bloom_size, sizeof(dbdb->bloom)); dbdb->bloom_size = sizeof(dbdb->bloom); // Clamp to safe value dbdb->bloom_epoch = 0; // Invalidate bloom filter seen = FALSE; } ================================================================================ AFFECTED CONFIGURATIONS ================================================================================ -------------------------------------------------------------------------------- Compilation Requirement -------------------------------------------------------------------------------- Exim must be compiled with SQLite support (USE_SQLITE=yes and hints_database = sqlite in configuration). -------------------------------------------------------------------------------- Exploitable Configurations -------------------------------------------------------------------------------- The full vulnerability chain requires BOTH conditions: Condition 1: Attacker data used in ratelimit database key (for SQL injection) Condition 2: Bloom filter triggered (for heap overflow) Examples: EXPLOITABLE - per_addr with explicit key: warn ratelimit = 100 / 1h / per_addr / $sender_address [✓] Sender address goes into key (SQL injection) [✓] Recipient address triggers bloom filter (heap overflow) EXPLOITABLE - explicit unique with attacker data: warn ratelimit = 100 / 1h / per_rcpt / unique=$sender_address [✓] Sender address goes into key (SQL injection) [✓] Sender address triggers bloom filter (heap overflow) NOT EXPLOITABLE - default per_addr: warn ratelimit = 100 / 1h / per_addr [✗] Client IP in key (not attacker-controlled via SMTP) [✓] Recipient address triggers bloom filter Result: Heap overflow reachable but can't inject malicious blob NOT EXPLOITABLE - per_rcpt without unique: deny ratelimit = 50 / 1h / per_rcpt [✗] No unique parameter (bloom filter never runs) Result: Even with SQL injection, no heap overflow NOT EXPLOITABLE - per_mail: accept ratelimit = 10 / 1h / per_mail [✗] No unique parameter (bloom filter never runs) Result: Even with SQL injection, no heap overflow -------------------------------------------------------------------------------- Key Construction -------------------------------------------------------------------------------- The database key is built from configuration parameters: key = string_sprintf("%s/%s/%s%s", sender_rate_period, ratelimit_option_string[mode], unique == NULL ? "" : "unique/", key); With ratelimit = 100 / 1h / per_addr / $sender_address, the key becomes: 1h/per_rcpt/unique/"x'UNION SELECT..."@attacker.com ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Attacker-controlled With ratelimit = 100 / 1h / per_addr (no explicit key), it becomes: 1h/per_rcpt/unique/192.168.1.100 ^^^^^^^^^^^^^ Client IP (not attacker-controlled) -------------------------------------------------------------------------------- Real-World Impact -------------------------------------------------------------------------------- I successfully tested the exploit against the per_addr / $sender_address pattern. The vulnerabilities require specific configuration, but these patterns are not unreasonable for production deployments. Administrators using ratelimit with sender-based keys for granular rate limiting would be affected. ================================================================================ CVE ASSIGNMENT RECOMMENDATIONS ================================================================================ I recommend two new CVEs: 1. SQL Injection via Ratelimit (NEW) - Same root cause as CVE-2025-26794 but different attack vector. Requires ratelimit ACLs with attacker-controlled keys (e.g., $sender_address). The fix for CVE-2025-26794 addressed ETRN specifically and does not protect ratelimit configurations. 2. Heap Buffer Overflow (NEW) - Unvalidated database field used as array bound, requires validation of bloom_size before use. ================================================================================ DISCLOSURE TIMELINE ================================================================================ I'm disclosing this immediately upon discovery. I will not publish details publicly until the following conditions are met, or 60 days have passed. - You've had adequate time to develop and test patches - We agree on a coordinated disclosure date - CVEs are assigned (if appropriate) I'm happy to provide additional technical details, testing assistance, or clarification on any points. Thank you for maintaining Exim. I look forward to working with you on remediation. Thanks, Andrew – Andrew Fasano, PhD Cyber Lead Center for AI Standards and Innovation (CAISI) National Institute of Standards and Technology U.S. Department of Commerce