CVE-2026-41113: qmail tls_quit RCE via popen() in notlshosts_auto
sagredo qmail before 2026.04.07 exposes a remote code execution path through unsanitized popen() calls in notlshosts_auto triggered during TLS negotiation teardown.
Email servers are the backbone of business communication, handling millions of messages daily. One widely-used email server software called sagredo qmail has a serious security hole that could let hackers take over the entire system.
Here's what's happening: When two email servers connect to each other to securely exchange messages, they go through a special handshake process — think of it like showing ID at a club. During this process, the software handles commands in an unsafe way, leaving a digital door wide open. An attacker can send a specially-crafted message that tricks the server into running malicious code.
The really scary part is that no hacker needs permission to try this. There's no password to crack or account to compromise. Anyone on the internet can attempt this attack on a vulnerable mail server. If successful, they'd gain the same access and power as the email server itself, potentially reading private messages, intercepting emails, or using the server to attack other systems.
If your company runs its own email infrastructure using this software, you're the most at risk. This includes small businesses, universities, government agencies, and large enterprises that manage their own mail servers. Hosting providers that run this software for multiple customers are equally vulnerable.
The good news is there's a fixed version available. Here's what you should do: Check if your organization uses this software by asking your IT team. If you do, update immediately to the latest version released after April 7, 2026. If updates aren't installed yet, consider temporarily limiting who can connect to your mail server from outside your organization. Finally, monitor your server logs for any suspicious connection attempts during the TLS handshake process.
Want the full technical analysis? Click "Technical" above.
CVE-2026-41113 is a remote code execution vulnerability in sagredo qmail affecting the qmail-remote component. The bug lives in notlshosts_auto(), which is invoked during TLS negotiation teardown — specifically when the remote SMTP server signals it cannot or will not negotiate TLS, triggering the internal tls_quit path. The function uses popen() to construct a shell command from a hostname that is, at the point of invocation, partially attacker-influenced. The result is unauthenticated RCE from the perspective of the mail relay's operating system, achievable by a remote SMTP peer during a TLS handshake.
CVSS 8.1 (HIGH) reflects the network-reachable attack surface and the absence of authentication requirements, tempered slightly by the complexity of controlling the hostname suffix reliably without additional primitives.
Root cause:notlshosts_auto() in qmail-remote.c passes an insufficiently sanitized remote hostname directly to popen(), allowing a malicious SMTP peer to inject shell metacharacters that execute arbitrary commands on the sending host when TLS negotiation fails.
Affected Component
The vulnerability is isolated to qmail-remote.c, the outbound SMTP delivery agent in sagredo's qmail fork. This process runs as the qmailr user by default, but many deployments — particularly those following older netqmail or qmail-src packaging — run it with elevated queue-read privileges or under root-owned wrappers. The affected call site is the notlshosts_auto() function, executed on the tls_quit branch when the remote MTA rejects STARTTLS.
Relevant source file: qmail-remote.c. Versions prior to the 2026.04.07 patch commit are affected.
Root Cause Analysis
When qmail-remote connects to a remote MX and STARTTLS negotiation is abandoned (either by remote refusal or a protocol error), it calls tls_quit(). Inside that path, notlshosts_auto() is responsible for dynamically determining whether the remote host should be added to the notlshosts exemption list. The function builds a shell command string to invoke an external helper — a pattern inherited from older patch sets — using the remote hostname retrieved from DNS resolution of the MX record.
/*
* qmail-remote.c — notlshosts_auto() (pre-patch, reconstructed)
*
* Called from tls_quit() when STARTTLS is not available or fails.
* 'partner_fqdn' is populated from the DNS MX lookup result and may
* contain attacker-controlled content if the adversary controls the
* authoritative DNS for the target domain.
*/
static void notlshosts_auto(const char *partner_fqdn)
{
char cmd[512];
FILE *fp;
/* BUG: partner_fqdn is shell-interpolated without sanitization.
* An attacker controlling the PTR/MX record for the peer can inject
* shell metacharacters (`;`, `$()`, `|`, etc.) into partner_fqdn.
* snprintf provides no protection against metacharacter injection —
* only against buffer overflow. The resulting string is passed
* verbatim to /bin/sh via popen(). */
snprintf(cmd, sizeof(cmd),
"grep -qxF '%s' %s/notlshosts/default || echo '%s' >> %s/notlshosts/default",
partner_fqdn, // BUG: attacker-controlled, shell-unescaped
auto_notlshosts_dir,
partner_fqdn, // BUG: appears twice — both injection points
auto_notlshosts_dir);
fp = popen(cmd, "r"); // BUG: popen() invokes /bin/sh -c cmd
if (fp)
pclose(fp);
}
static void tls_quit(void)
{
/* ... TLS teardown ... */
if (auto_notlshosts)
notlshosts_auto(partner_fqdn); // partner_fqdn from MX/PTR resolution
/* ... */
_exit(0);
}
The core issue is the use of single-quote wrapping as an attempted sanitization strategy. Single quotes in POSIX shell do prevent most expansions, but a hostname containing a literal single quote (') terminates the quoting context, allowing subsequent characters to be interpreted as shell syntax. Since RFC 1123 permits hostnames with only alphanumeric characters and hyphens, a standards-compliant peer cannot inject this way — but a malicious peer is not obligated to present an RFC-compliant hostname in its SMTP banner or PTR record.
Exploitation requires the attacker to control the hostname presented to qmail-remote during an outbound delivery attempt to a domain they control. Two reliable surfaces exist:
EXPLOIT CHAIN:
1. Attacker registers a domain (attacker-mx.tld) and configures its MX
record to point to a server under their control.
2. Attacker sends an email to any address @attacker-mx.tld from a system
running vulnerable sagredo qmail (or social-engineers such a send via
a web form, bounce, or auto-reply on the victim MTA).
3. qmail-remote resolves attacker-mx.tld MX → attacker SMTP server IP.
4. Attacker's SMTP server completes the TCP handshake, sends EHLO
advertising STARTTLS, then aborts TLS negotiation mid-handshake
(e.g., sends a malformed ServerHello or closes the connection after
the client's ClientHello).
5. qmail-remote enters the tls_quit() path. partner_fqdn is populated
from the SMTP banner (HELO/EHLO string) or the reverse DNS of the
peer IP — both attacker-controlled.
6. notlshosts_auto(partner_fqdn) is called. snprintf() builds cmd[]
with the injected hostname containing a single-quote breakout:
evil'; ; echo '
7. popen(cmd, "r") spawns /bin/sh -c cmd. The payload executes with
the privileges of the qmail-remote process (typically qmailr or root
depending on deployment).
8. qmail-remote exits normally via _exit(0) after pclose(). No crash,
no log anomaly by default. Delivery is silently retried or bounced.
The attack is fire-and-forget from the adversary's perspective — the victim's MTA initiates the outbound connection, so no inbound firewall rules protect against it. The only prerequisites are that the victim MTA attempts delivery to an attacker-controlled domain and that the auto_notlshosts feature flag is enabled (it is enabled by default in sagredo's configuration).
Memory Layout
This is not a memory-corruption vulnerability — it is a command injection primitive. The relevant stack state shows the injection surface:
The cmd[] buffer at 512 bytes is sufficient to contain typical payloads. If the payload exceeds 512 bytes, snprintf() truncates — but a closing single-quote and semicolon at the truncation boundary can still produce a valid injection depending on payload placement. An attacker should size the hostname to keep the injected command under ~400 bytes to guarantee reliable truncation-free execution.
Patch Analysis
The 2026.04.07 patch eliminates the popen() call entirely in favor of direct file I/O with explicit hostname validation, removing the shell interpreter from the code path.
// BEFORE (vulnerable — popen with unescaped hostname):
static void notlshosts_auto(const char *partner_fqdn)
{
char cmd[512];
FILE *fp;
snprintf(cmd, sizeof(cmd),
"grep -qxF '%s' %s/notlshosts/default || echo '%s' >> %s/notlshosts/default",
partner_fqdn, auto_notlshosts_dir,
partner_fqdn, auto_notlshosts_dir);
fp = popen(cmd, "r");
if (fp)
pclose(fp);
}
// AFTER (patched — direct file I/O, hostname validation, no shell):
static int hostname_is_safe(const char *h)
{
/* Permit only RFC 1123-compliant characters: [A-Za-z0-9.-] */
for (const char *p = h; *p; p++) {
if (!isalnum((unsigned char)*p) && *p != '.' && *p != '-')
return 0;
}
return (*h != '\0');
}
static void notlshosts_auto(const char *partner_fqdn)
{
char path[1024];
char line[256];
FILE *fp;
int found = 0;
/* FIXED: validate hostname before any use */
if (!hostname_is_safe(partner_fqdn))
return;
snprintf(path, sizeof(path), "%s/notlshosts/default", auto_notlshosts_dir);
/* FIXED: open file directly, never invoke a shell */
fp = fopen(path, "r");
if (fp) {
while (fgets(line, sizeof(line), fp)) {
line[strcspn(line, "\n")] = '\0';
if (strcmp(line, partner_fqdn) == 0) { found = 1; break; }
}
fclose(fp);
}
if (!found) {
fp = fopen(path, "a");
if (fp) {
fprintf(fp, "%s\n", partner_fqdn); /* safe: validated above */
fclose(fp);
}
}
}
The patch applies defense-in-depth: the allowlist check in hostname_is_safe() rejects any hostname containing shell-significant bytes before they reach any file operation, and the complete removal of popen() eliminates the shell interpreter as an attack surface regardless of validation correctness.
Detection and Indicators
Because the exploit leverages a clean exit path (_exit(0) after pclose()), qmail itself logs nothing anomalous. Detection must come from the OS layer:
DETECTION INDICATORS:
1. Process tree anomaly:
qmail-remote → sh → curl/wget/bash/nc
Look for qmail-remote as parent of any shell or network utility.
2. Auditd rule (Linux):
-a always,exit -F arch=b64 -S execve \
-F ppid=$(pgrep qmail-remote) -k qmail_rce
3. SMTP peer hostname in logs containing:
Single quotes ('), semicolons (;), backticks (`), $(), pipe (|)
These are RFC-invalid and should never appear in legitimate MX banners.
4. Outbound connections from qmail-remote process to non-port-25 destinations
(reverse shells, C2 beacons) visible in netflow/eBPF socket tracing.
5. Unexpected writes to filesystem paths outside /var/qmail/queue/ by
the qmailr UID.
RELEVANT LOG PATTERN (qmail-send, delivery log):
delivery NN: failure: _TLS_not_available/
— normal-looking failure entry; no injection evidence in qmail logs
Remediation
Immediate: Upgrade sagredo qmail to the 2026.04.07 release or any commit at or after the patch fixing notlshosts_auto(). Verify the fix is present by confirming popen does not appear in the compiled qmail-remote binary:
$ strings $(which qmail-remote) | grep popen
(no output expected on patched binary)
Workaround (if patching is not immediately possible): Disable the auto_notlshosts feature by removing or zeroing the control file that enables it (deployment-specific; check sagredo documentation). This prevents notlshosts_auto() from being called but does not affect core mail delivery.
Defense in depth: Restrict qmail-remote process capabilities using a seccomp profile that denies execve() syscalls. Run the process in a network namespace that prohibits outbound connections on ports other than 25/587/465 to limit post-exploitation reachability. Audit any other qmail patch sets that use popen() or system() with externally-derived input — this antipattern has historically appeared in multiple independent qmail patch series.