SMTP发件人伪造实验:从协议原理到SPF验证的隔离环境搭建

发布时间:2026/10/9 3:06:23
SMTP发件人伪造实验:从协议原理到SPF验证的隔离环境搭建
简介这是一份围绕SMTP协议邮件伪造原理的网络安全学习资源面向对邮件协议、钓鱼攻击与邮件安全防护感兴趣的安全初学者与运维人员。压缩包共36个文件约11.97MB以h头文件、cpp源码、obj与pdb调试文件、res资源文件及vcproj、sln工程文件为主另含exe可执行程序与ReadMe说明构成一套可直接编译运行的VC工程。资源从SMTP连接建立、身份验证、MAIL FROM与RCPT TO命令提交到邮件传输的完整流程切入演示篡改发件人地址实现伪造发件的过程并延伸讲解SPF、DKIM、DMARC三种邮件验证机制的防护思路。已有1314人学习下载读者可借此理解SMTP协议的安全缺陷、掌握邮件伪造的检测与防范方法适合作为邮件安全实验与钓鱼攻击防御的参考案例。1. SMTP 发件人伪造一封“来自老板”的邮件是怎么绕过基础校验的你收到过那种邮件吗发件人显示是公司财务总监内容让你紧急处理一笔转账点开详情却发现回复地址指向一个完全陌生的域名。这不是魔法而是 SMTP 协议设计之初就留下的口子——它默认信任客户端声明的发件人身份不强制核验。标题里的“SMTP 伪造发件”说的就是利用这套信任机制在实验环境里构造一封看起来来自任意地址的邮件。它解决的核心问题是让你理解邮件网关的信任边界到底画在哪里为什么仅靠“看发件人”做安全决策会翻车。适合做邮件安全测试、企业内网钓鱼演练、或者单纯想搞明白邮件协议底层逻辑的运维和开发人员。下面所有操作都在你自建的隔离实验环境里完成别拿真实业务域名和线上 SMTP 去试。2. 先搞懂 SMTP 会话里谁说了算信封发件人与信头发件人的分离2.1 一封邮件其实有两套“发件人”很多人以为邮件里的“发件人”只有一个实际上 SMTP 会话中存在两个独立字段。一个是信封发件人对应MAIL FROM:命令它决定了邮件投递失败时退信往哪走也是邮件服务器之间路由的依据。另一个是信头发件人对应邮件正文里的From:头部它才是收件人客户端最终显示的那个名字和地址。问题就出在这里SMTP 协议并不要求这两个字段一致。你完全可以在MAIL FROM:里写一个真实存在的地址保证投递同时在From:头部写一个完全不同的地址来伪装显示。绝大多数基础邮件客户端只展示From:头部这就给了伪造发件人可乘之机。2.2 用 Python 的 smtplib 手动控制两个字段下面这段代码演示了如何在一个本地 SMTP 调试服务器上分别指定信封发件人和信头发件人。注意sendmail方法的第一个参数是信封发件人而From:头部需要你自己拼进消息体。import smtplib from email.mime.text import MIMEText from email.header import Header # 连接本地调试用的 SMTP 服务器端口 1025 是常见调试端口 # 如果你用 Python 自带的 aiosmtpd 或 smtpd默认监听 127.0.0.1:8025 server smtplib.SMTP(127.0.0.1, 1025) # 构造邮件正文 msg MIMEText(这是一封用于测试发件人显示效果的邮件。, plain, utf-8) # 关键点信头发件人写的是伪造的显示地址 msg[From] Header(财务总监 cfoexample-bank.com, utf-8) # 收件人正常写 msg[To] Header(员工 staffyour-company.com, utf-8) msg[Subject] Header(紧急请处理一笔转账, utf-8) # 信封发件人用真实可投递的地址保证 SMTP 会话能通过 envelope_sender noreplyyour-company.com # 信封收件人列表 envelope_recipients [staffyour-company.com] # 发送第一个参数是信封发件人第二个是信封收件人列表 server.sendmail(envelope_sender, envelope_recipients, msg.as_string()) server.quit()这段代码的逻辑很直白sendmail的第一个参数envelope_sender决定了 SMTP 会话中MAIL FROM:的值服务器用它来做退信路由和部分反垃圾判断。而msg[From]是写在邮件数据区的头部收件人客户端读取的是这个值来显示发件人。两者不一致时大部分客户端只显示From:头部。参数上唯一需要注意的是envelope_sender必须是一个你的实验 SMTP 服务器允许中继的地址否则会话会在MAIL FROM:阶段就被拒绝。如果你用本地调试服务器通常不校验这个随便填都行。2.3 为什么收件服务器不直接拦掉不一致的邮件因为历史上很多合法场景也依赖这种不一致。比如邮件列表转发时信封发件人通常是列表地址而信头发件人保留原始作者再比如一些自动通知系统信封发件人用bounce地址处理退信信头显示notifications。如果收件服务器一刀切拒绝所有不一致的邮件会误杀大量正常邮件。所以主流做法是引入 SPF、DKIM、DMARC 这套组合拳来做更细粒度的验证而不是简单比对两个字段。理解这一点你就明白为什么单靠伪造From:头部在某些配置不严的服务器上依然能得逞。3. 在 CentOS 上搭一套隔离实验环境从 Postfix 到调试收件箱3.1 最小化安装 Postfix 并限制为仅本地监听在 CentOS 上做这类实验我一般用 Postfix 作为实验 SMTP 服务器因为它配置透明、日志详细出问题容易排查。先装好# CentOS 7/8 通用先装 Postfix 和 mailx 方便测试 sudo yum install -y postfix mailx # 备份原始配置 sudo cp /etc/postfix/main.cf /etc/postfix/main.cf.bak # 编辑 main.cf只监听本地回环避免实验流量外泄 sudo vi /etc/postfix/main.cf在main.cf里改这几个关键参数# 只监听本地绝对不要监听 0.0.0.0 inet_interfaces loopback-only inet_protocols ipv4 # 设置实验用的域名随便写一个不存在的 myhostname lab.local mydomain lab.local myorigin $mydomain # 只接收本地投递不对外中继 mydestination $myhostname, localhost.$mydomain, localhost, $mydomain mynetworks 127.0.0.0/8 # 关闭不必要的限制方便观察原始行为 smtpd_recipient_restrictions smtpd_relay_restrictions permit_mynetworks, reject改完后启动并检查监听状态sudo systemctl restart postfix sudo systemctl enable postfix # 确认只监听 127.0.0.1:25 ss -tlnp | grep :25这里inet_interfaces loopback-only是最关键的安全阀它保证你的实验 SMTP 不会成为开放中继。mynetworks限定为本地回环smtpd_relay_restrictions里permit_mynetworks允许本地投递reject拒绝其他所有中继请求。这套配置下来你只能从本机发信到本机实验产生的邮件不会跑到外网去。3.2 用 Python 的 aiosmtpd 搭一个“照单全收”的调试收件箱Postfix 负责发你还需要一个收件端来观察邮件到底长什么样。Python 的aiosmtpd比自带的smtpd模块更现代而且可以自定义行为把收到的每一封邮件原样打印出来。# 安装 aiosmtpd pip3 install aiosmtpd写一个最简单的调试服务器脚本# debug_smtp_server.py import asyncio from aiosmtpd.controller import Controller class DebugHandler: async def handle_DATA(self, server, session, envelope): # 打印信封发件人和收件人 print( * 60) print(f信封发件人 (MAIL FROM): {envelope.mail_from}) print(f信封收件人 (RCPT TO): {envelope.rcpt_tos}) print(- * 60) # 打印原始邮件内容包括所有头部 print(原始邮件数据:) print(envelope.content.decode(utf-8, errorsreplace)) print( * 60) return 250 Message accepted for delivery # 监听 1025 端口绑定本地回环 controller Controller(DebugHandler(), hostname127.0.0.1, port1025) controller.start() print(调试 SMTP 服务器已启动监听 127.0.0.1:1025) try: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: controller.stop()运行这个脚本后任何发到127.0.0.1:1025的邮件都会被完整打印出来。handle_DATA方法里的envelope.mail_from就是信封发件人envelope.content是完整的邮件数据你能清楚看到From:头部和MAIL FROM:的差异。这个脚本的参数很简单hostname和port按需改handle_DATA的返回值必须是合法的 SMTP 响应码250表示接受投递。3.3 把发信脚本和收信调试服务器串起来跑通现在把第 2 章的发送脚本改一下目标端口指向1025# 只改连接部分 server smtplib.SMTP(127.0.0.1, 1025) # 其余代码不变先运行debug_smtp_server.py再运行发送脚本。你会在调试服务器的终端里看到类似这样的输出信封发件人 (MAIL FROM): noreplyyour-company.com 信封收件人 (RCPT TO): [staffyour-company.com] 原始邮件数据: From: ?utf-8?b?6LSh5Yqh5oC755uR...? To: ?utf-8?b?5ZGY5bel...? Subject: ?utf-8?b?57Sn5oCl77ya6K35aSE55CG...? ...注意From:头部经过 Base64 编码后显示的是“财务总监 cfoexample-bank.com ”而信封发件人是noreplyyour-company.com。两者不一致但邮件被正常接收。这就是伪造发件人显示效果的最小闭环。整个过程完全在本地回环内完成不涉及任何外部网络。4. 避坑与排查为什么你的伪造邮件被拒收或进了垃圾箱4.1 现象MAIL FROM 阶段直接被 550 拒绝原因通常是你用的信封发件人域名不在实验服务器的允许中继列表里。Postfix 的smtpd_relay_restrictions如果配了reject而mynetworks又没包含你的客户端 IP就会在MAIL FROM:阶段直接拒绝。解决方法是确认你的发送脚本连接的是127.0.0.1并且 Postfix 的mynetworks包含127.0.0.0/8。如果你在另一台机器上跑发送脚本把它的 IP 加进mynetworks但仅限实验内网。4.2 现象邮件发出去了但收件端显示的 From 是信封发件人这通常是因为你用的邮件客户端或 Web 邮箱做了“代发”显示处理。当From:头部和信封发件人不一致时一些客户端会显示“由 xxx 代发”或者直接显示信封发件人。这不是协议问题是客户端行为。在实验里用aiosmtpd打印原始数据就能看到真实头部不受客户端显示逻辑干扰。如果你要测试真实客户端的显示效果换不同的客户端多试几次记录差异。4.3 现象Postfix 日志里出现 “warning: hostname does not resolve”这是myhostname设置了一个无法解析的域名导致的。Postfix 启动时会尝试解析myhostname如果 DNS 查不到就会告警。解决方法是把myhostname设成lab.local这种本地域名同时在/etc/hosts里加一行127.0.0.1 lab.local。这样既避免告警也不影响实验逻辑。4.4 现象发送脚本报 “SMTPDataError: 554 Message rejected”554 通常是内容过滤或反垃圾规则触发的。如果你在 Postfix 上启用了content_filter或者smtpd_data_restrictions实验邮件可能因为关键词或头部异常被拒。排查时先看/var/log/maillog找到具体的 reject 原因。实验环境里我一般把smtpd_data_restrictions清空只保留最基本的限制避免干扰观察。4.5 现象用真实域名做实验导致邮件被外部服务器拒收这是最需要避免的坑。如果你把From:头部写成cfo某真实银行.com然后通过实验服务器往外部发对方服务器的 SPF 检查会直接失败因为你的实验服务器 IP 不在该域名的 SPF 记录里。更严重的是你可能触发对方的安全告警。所以再次强调所有实验都在本地回环或隔离内网完成信封收件人也只写本地地址。不要用任何真实存在的第三方域名做伪造目标。5. 进阶验证用 SPF 记录反向理解伪造的边界在哪里前面几章你学会了怎么在实验环境里构造一封发件人显示被伪造的邮件。但真正有价值的不是“能伪造”而是搞清楚“什么情况下伪造会失败”。SPF 就是第一道门槛。它的逻辑很简单域名所有者在 DNS 里发布一条 TXT 记录声明哪些 IP 有权以该域名发信。收件服务器收到邮件后拿信封发件人的域名去查 SPF再比对发信 IP 是否在允许列表里。你可以自己验证这个机制。在本地搭一个 DNS 服务器或者直接修改/etc/hosts模拟域名解析然后给实验域名加一条 SPF 记录。更简单的做法是用dig命令查真实域名的 SPF# 查一个真实域名的 SPF 记录观察格式 dig TXT example.com short你会看到类似vspf1 include:_spf.example.com ~all的结果。~all表示软失败-all表示硬失败。如果你的实验服务器 IP 不在include的范围内收件服务器就会根据这个策略决定是拒收还是标记垃圾。理解了这个你就明白为什么伪造From:头部在大型邮件服务商那里越来越难——它们不只看From:还看信封发件人和发信 IP 的 SPF 对齐情况。再进一步是 DKIM 和 DMARC。DKIM 用私钥对邮件头部签名收件方用 DNS 里的公钥验证签名是否匹配。DMARC 则把 SPF 和 DKIM 的结果与From:头部做对齐检查并告诉收件方对齐失败时怎么处理。这三者组合起来基本封死了随意伪造发件人的空间。但它们的部署率参差不齐很多中小型服务器仍然只做基础检查这就是为什么在特定实验场景下伪造依然能观察到效果。我自己的习惯是每搭好一个实验环境先用一封完全合规的邮件跑通投递确认 SPF 通过、DKIM 签名有效然后再逐步修改From:头部观察在哪一步开始触发拒收或标记。这样你就能精确知道目标环境的信任边界画在哪里。做这类实验最大的教训是——永远在隔离环境里验证永远不要用真实业务域名和真实收件人做测试。一次疏忽可能导致你的实验服务器 IP 被列入黑名单恢复起来非常麻烦。希望帮到你。本文还有配套的精品资源点击获取