尧图精选

SMTP发件人伪造原理、检测与企业邮件网关防护实战

🕒 发布时间:2026/10/1 10:50:04 📁 来源:尧图网络
简介这份资源围绕SMTP协议与邮件伪造机制展开面向网络安全初学者、邮件系统运维人员及对钓鱼攻击防护感兴趣的开发者。内容从SMTP连接建立、身份验证、邮件提交到传输关闭的完整流程讲起重点剖析篡改MAIL FROM发件人地址实现伪造的原理并延伸至SPF、DKIM、DMARC三类主流验证技术的防护思路帮助读者理解伪造邮件的成因与检测方法。压缩包共36个文件约11.97MB以h头文件、cpp源码、obj与pdb调试文件、res资源文件及vcproj、sln工程文件为主另含exe可执行程序与ReadMe说明构成一套可编译运行的Visual Studio示例工程便于对照代码理解SMTP命令交互与base64编码处理。目前已有1314人学习下载适合作为邮件安全入门实践与协议分析的参考素材。1. SMTP 发件人伪造从协议缺口到企业邮件安全的真实威胁企业邮箱收到一封来自“CEO”的邮件要求财务紧急转账——发件人地址、签名、甚至历史往来邮件引用都看起来毫无破绽。这背后往往不是邮箱被盗而是 SMTP 协议本身的一个设计缺口被利用了。SMTP 在设计之初只关心“把邮件投递到目标服务器”并不强制校验发件人地址是否真的属于发信人。这意味着只要找到一台允许匿名中继或未做发件人验证的邮件服务器任何人都可以声称自己是任意地址的发件人。本文要拆解的就是这个技术方向SMTP 发件人伪造的原理、可复现的验证方法、企业侧的真实风险以及从运维角度如何检测和缓解。适合邮件系统运维、安全测试人员以及需要评估企业邮件网关防护有效性的工程师阅读。我不会教你用来做恶意投递而是把攻击面摊开让你知道自己的邮件系统到底有多容易被“冒名顶替”。2. SMTP 协议为什么允许伪造发件人三个关键机制拆解2.1 MAIL FROM 与 From 头部的分离设计SMTP 会话中发件人信息出现在两个完全独立的位置。第一个是信封发件人由MAIL FROM:命令指定这个地址用于投递失败时的退信路由。第二个是邮件头部里的From:字段这是收件人客户端最终看到的发件人显示地址。协议本身不要求这两个地址一致也不要求MAIL FROM地址必须经过身份验证。很多邮件服务器只检查MAIL FROM是否属于本地域对From:头部则完全放行。这就给了伪造空间你可以用一个合法的MAIL FROM通过中继检查同时在From:头部写入任意伪造地址。常见做法是直接连接目标邮件服务器的 25 端口用EHLO打招呼后直接声明MAIL FROM:ceocompany.com。如果对方没有开启发件人验证扩展会话就会继续。下面是一段用 Python 的smtplib做本地验证的最小代码仅用于测试自己的邮件网关是否拦截import smtplib from email.mime.text import MIMEText # 目标邮件服务器地址替换为你自己的测试服务器 target_host mail.example.com target_port 25 # 构造邮件内容 msg MIMEText(这是一封发件人伪造测试邮件仅用于安全评估。) msg[Subject] SMTP 伪造测试 msg[From] ceocompany.com # 伪造的显示发件人 msg[To] testexample.com try: # 连接 SMTP 服务器不登录 server smtplib.SMTP(target_host, target_port, timeout10) server.ehlo(test.local) # 关键MAIL FROM 使用伪造地址 server.sendmail(ceocompany.com, [testexample.com], msg.as_string()) print(投递成功目标服务器未拦截伪造发件人) server.quit() except Exception as e: print(f投递失败或被拦截: {e})这段代码的逻辑很直接连接目标服务器的 25 端口用EHLO声明自己的主机名然后直接以伪造地址作为信封发件人投递。参数说明上target_host必须是你有权限测试的服务器timeout建议设 10 秒以内避免长时间阻塞。如果返回“投递成功”说明目标服务器至少没有在 SMTP 会话层拦截伪造发件人。注意很多云服务商默认封禁 25 端口出站你需要在测试环境里放行或者用内部邮件网关做目标。2.2 发件人验证机制的缺失与绕过理论上SPF、DKIM、DMARC 三件套可以大幅提高伪造门槛。SPF 通过 DNS 记录声明哪些 IP 有权代表某个域发信DKIM 用数字签名保证邮件内容未被篡改DMARC 则告诉收件方当 SPF 或 DKIM 校验失败时该怎么处理。但现实是大量企业域名只配了 SPF 的~all软失败甚至完全没有 DMARC 记录。软失败意味着收件方可以标记为可疑但依然投递。更常见的情况是攻击者伪造的是那些没有配置任何验证记录的外部域名或者利用邮件网关对内部域名的信任关系。我一般会先用dig查目标域名的 SPF 和 DMARC 记录判断伪造难度# 查询 SPF 记录 dig TXT example.com short | grep vspf1 # 查询 DMARC 记录 dig TXT _dmarc.example.com short # 查询 DKIM 选择器常见选择器如 default、google、selector1 dig TXT default._domainkey.example.com short如果 SPF 返回vspf1 -all说明该域名拒绝所有未授权 IP伪造难度高。如果返回vspf1 ~all或没有记录伪造成功率就大幅上升。DMARC 返回pnone表示只监控不拦截返回preject才是强拦截。参数上short让输出更简洁grep用于过滤关键行。这一步的目的是在发起测试前先判断目标域名的防护水位避免盲目操作。2.3 开放中继与内部信任链的利用开放中继是指那些允许任何来源 IP 通过它向任意目标域投递邮件的服务器。虽然现在公开的开放中继已经很少但在企业内网里内部邮件服务器之间的信任关系往往很宽松。比如一台对外接收邮件的网关可能对来自内部网段的连接不做发件人验证直接转发。如果你能进入内网或者找到一台被攻陷的内网主机就可以利用这条信任链伪造内部高管的发件人。另一种常见情况是某些邮件服务器对“本地域”的发件人特别信任。比如MAIL FROM写成adminlocaldomain服务器可能直接放行。测试时可以用swaks这个工具快速验证# 使用 swaks 测试发件人伪造 swaks --to testexample.com \ --from ceocompany.com \ --server mail.example.com \ --header Subject: 伪造测试 \ --body 这是一封测试邮件swaks的参数很直观--to是收件人--from是伪造的发件人--server是目标邮件服务器。如果服务器返回250 OK说明投递被接受。如果返回550或553说明被拦截。这个工具的好处是能完整打印 SMTP 会话过程方便你看到在哪一步被拒绝。注意--from和--header From:可以分别设置用来测试服务器到底校验的是信封发件人还是头部发件人。3. 从零搭建一个可控的 SMTP 伪造验证环境3.1 用 Python 的 aiosmtpd 搭建本地测试服务器要理解伪造是怎么成功的最好的办法是自己搭一个接收服务器观察完整的 SMTP 会话。aiosmtpd是一个纯 Python 实现的 SMTP 服务器库适合做本地测试。安装很简单pip install aiosmtpd然后写一个最简的接收服务器把所有收到的邮件打印到控制台import asyncio from aiosmtpd.controller import Controller class DebugHandler: async def handle_DATA(self, server, session, envelope): # 打印信封发件人和收件人 print(f信封发件人: {envelope.mail_from}) print(f信封收件人: {envelope.rcpt_tos}) # 打印邮件头部中的 From 字段 print(f邮件内容前 500 字节:\n{envelope.content[:500]}) return 250 Message accepted for delivery # 监听本地 8025 端口避免与系统 25 端口冲突 controller Controller(DebugHandler(), hostname127.0.0.1, port8025) controller.start() print(SMTP 测试服务器已启动监听 127.0.0.1:8025) input(按回车键停止...\n) controller.stop()这段代码的核心是handle_DATA方法它在邮件数据接收完成后被调用。envelope.mail_from是信封发件人envelope.content是完整的邮件原文。参数上hostname127.0.0.1表示只监听本地port8025避免与系统邮件服务冲突。启动后你可以用前面的smtplib代码把目标指向127.0.0.1:8025观察服务器打印出的信封发件人和头部From是否一致。这个环境完全隔离不会对外发送任何邮件适合反复测试。3.2 构造一封带伪造发件人的完整邮件有了接收服务器下一步是构造一封结构完整的邮件。用email库可以精确控制每一个头部字段from email.mime.text import MIMEText from email.utils import formatdate, make_msgid import smtplib # 构造邮件正文 body 您好 这是一封用于验证 SMTP 发件人伪造的测试邮件。 如果您看到这封邮件说明目标服务器未拦截伪造发件人。 msg MIMEText(body, plain, utf-8) msg[Subject] 紧急请确认付款信息 msg[From] ceocompany.com # 伪造的显示发件人 msg[To] financeexample.com msg[Date] formatdate(localtimeTrue) msg[Message-ID] make_msgid(domaincompany.com) # 连接本地测试服务器 server smtplib.SMTP(127.0.0.1, 8025, timeout10) server.ehlo(test.local) # 信封发件人也用伪造地址 server.sendmail(ceocompany.com, [financeexample.com], msg.as_string()) server.quit() print(邮件已发送到本地测试服务器)这里的关键参数是msg[From]和sendmail的第一个参数。前者控制收件人客户端显示的地址后者控制 SMTP 会话中的MAIL FROM。两者都设为伪造地址可以测试服务器是否对两者都放行。Message-ID里的domain参数也设成目标域名让邮件看起来更真实。运行后回到aiosmtpd的控制台你应该能看到信封发件人和头部From都是ceocompany.com。如果目标服务器只校验其中一个你就能通过调整另一个来绕过。3.3 用 Wireshark 抓包看 SMTP 会话的明文传输SMTP 在未加密的情况下是纯明文协议用 Wireshark 抓包可以看到完整的会话过程。在本地测试环境中选择回环接口lo过滤条件设为tcp.port 8025。然后运行上面的发送脚本Wireshark 会捕获到类似下面的交互S: 220 test.local SMTP Ready C: EHLO test.local S: 250-test.local S: 250-8BITMIME S: 250-SMTPUTF8 S: 250 HELP C: MAIL FROM:ceocompany.com S: 250 OK C: RCPT TO:financeexample.com S: 250 OK C: DATA S: 354 End data with CRLF.CRLF C: (邮件内容) C: . S: 250 Message accepted for delivery从抓包结果可以清楚看到MAIL FROM和RCPT TO都是明文没有任何加密或签名。参数上Wireshark 的过滤表达式tcp.port 8025精确匹配测试端口避免抓到无关流量。如果你在真实网络里抓包注意 25 端口可能被加密的 SMTPS 或 STARTTLS 覆盖需要先确认是否启用了 TLS。这个抓包过程的价值在于它让你直观看到服务器在MAIL FROM阶段没有做任何身份验证直接返回250 OK。这也是为什么说 SMTP 伪造的门槛极低——协议层面就没有强制校验。4. 企业邮件网关的检测与拦截配置4.1 SPF、DKIM、DMARC 的部署顺序与参数企业侧要防伪造第一步是把 SPF、DKIM、DMARC 配齐。顺序很重要先配 SPF 和 DKIM确认通过率后再上 DMARC。SPF 记录是一个 TXT 记录格式如下vspf1 ip4:203.0.113.0/24 include:_spf.google.com -allip4声明允许发信的 IP 段include引入第三方发信服务-all表示拒绝所有其他来源。注意不要用~all那只是软失败很多收件方依然会投递。DKIM 需要在邮件服务器上生成密钥对公钥发布到 DNS私钥用于签名。DMARC 记录放在_dmarc子域下vDMARC1; preject; ruamailto:dmarc-reportsexample.com; pct100preject表示直接拒绝未通过验证的邮件rua是接收汇总报告的地址pct100表示对 100% 的邮件生效。部署时建议先用pnone跑两周收集报告确认没有误杀再逐步切到pquarantine和preject。4.2 邮件网关的发件人验证规则配置除了 DNS 层面的验证邮件网关本身也需要配置发件人验证规则。以常见的 Postfix 为例可以在main.cf里开启发件人域名校验smtpd_sender_restrictions reject_unknown_sender_domain, reject_non_fqdn_sender, reject_unlisted_sender, permit_mynetworks, permit_sasl_authenticatedreject_unknown_sender_domain拒绝发件人域名没有 DNS 记录的邮件reject_non_fqdn_sender拒绝非完整域名reject_unlisted_sender拒绝本地域中不存在的发件人。permit_mynetworks和permit_sasl_authenticated放行内网和已认证用户。参数上这些限制按顺序执行一旦匹配就停止。配置后需要用postfix reload重载然后用前面的测试脚本验证是否被拦截。如果返回550 5.7.1 Sender address rejected说明规则生效。4.3 用日志和报告定位伪造尝试邮件网关的日志是发现伪造尝试的第一手资料。Postfix 的日志通常在/var/log/mail.log关注NOQUEUE和reject关键字# 查找被拒绝的发件人 grep reject /var/log/mail.log | grep sender | tail -50 # 统计伪造尝试的来源 IP grep reject /var/log/mail.log | awk {print $NF} | sort | uniq -c | sort -rn | head -20第一条命令列出最近 50 条发件人被拒的记录第二条统计来源 IP 的频次。参数上awk {print $NF}取每行最后一个字段通常是 IP 地址。如果发现某个 IP 频繁尝试伪造不同发件人可以直接在防火墙层封禁。DMARC 的汇总报告也会每天发到rua指定的邮箱里面包含 SPF 和 DKIM 的通过率、失败来源 IP 等信息。我一般会把这些报告导入到表格里按周对比观察伪造尝试的趋势。5. 避坑与排查SMTP 伪造测试中的五个血泪教训5.1 测试邮件被真实投递到外部邮箱现象用伪造发件人向外部邮箱发测试邮件结果真的投递成功收件人看到了一封莫名其妙的邮件。原因测试时没有把目标限制在本地或授权环境直接用了真实的外部地址。解决所有测试必须在隔离环境或明确授权的目标上进行。我一般会先用aiosmtpd在本地跑通确认代码逻辑无误后再向自己的测试邮箱发送绝不碰第三方地址。5.2 云服务器 25 端口出站被封导致连接超时现象脚本运行后一直卡在连接阶段最后报Connection timed out。原因主流云服务商默认封禁 25 端口的出站流量防止垃圾邮件。解决在测试环境里改用 587 端口提交端口或 8025 等自定义端口或者在云控制台申请解封 25 端口。如果只是本地验证直接用127.0.0.1:8025最省事。5.3 SPF 记录配了但没生效现象明明配了-all的 SPF 记录伪造邮件还是能投递。原因SPF 记录有 10 次 DNS 查询限制include嵌套过多会导致查询超限SPF 校验直接失败。解决用dig检查 SPF 记录确保include不超过 10 个并且没有语法错误。可以用在线的 SPF 检查工具验证或者手动数一下include、a、mx的数量。5.4 DMARC 报告邮箱收不到汇总数据现象配了rua地址但一直没收到 DMARC 报告。原因rua地址的域名必须和 DMARC 记录的域名一致或者需要在目标域名的 DNS 里添加授权记录。解决先用同域名的邮箱作为rua确认能收到报告后再添加外部域名的授权。另外报告是每天发送一次不是实时需要等 24 小时。5.5 内部邮件服务器信任内网导致绕过现象从内网某台主机发伪造邮件邮件网关完全放行。原因邮件网关对mynetworks里的 IP 不做发件人验证。解决收紧mynetworks范围只包含必要的邮件服务器 IP而不是整个内网段。同时开启reject_unlisted_sender即使来自内网发件人不存在也拒绝。这个坑很隐蔽因为外部测试全部被拦截但内网一测就穿。6. 用 SMTP 会话指纹做批量风险筛查当你需要评估一批域名的伪造风险时逐个发测试邮件效率太低。更高效的做法是提取 SMTP 会话的指纹特征做批量筛查。具体来说可以写一个脚本对每个目标域名做三件事查 SPF 和 DMARC 记录、尝试连接 25 端口看是否开放、发送EHLO后观察服务器返回的扩展列表。下面是一个批量筛查的代码框架import smtplib import dns.resolver def check_domain(domain): result {domain: domain, spf: None, dmarc: None, smtp_open: False} # 查 SPF try: answers dns.resolver.resolve(domain, TXT) for r in answers: if vspf1 in r.to_text(): result[spf] r.to_text() except Exception: pass # 查 DMARC try: answers dns.resolver.resolve(f_dmarc.{domain}, TXT) for r in answers: if vDMARC1 in r.to_text(): result[dmarc] r.to_text() except Exception: pass # 尝试连接 SMTP try: server smtplib.SMTP(fmail.{domain}, 25, timeout5) server.ehlo(check.local) result[smtp_open] True server.quit() except Exception: pass return result # 批量检查 domains [example.com, test.com] for d in domains: print(check_domain(d))这段代码的核心是三个检查点SPF 记录是否存在且为硬失败、DMARC 记录是否存在且为拒绝策略、SMTP 端口是否开放。参数上timeout5避免单个域名卡住太久dns.resolver需要安装dnspython库。输出结果里如果spf为None或包含~alldmarc为None或pnone同时smtp_open为True这个域名的伪造风险就很高。我一般会把这个脚本跑一遍自己负责的域名列表按风险等级排序优先处理高风险项。一个具体的技巧是在EHLO之后服务器返回的扩展列表里如果包含AUTH但不需要认证就能发信说明配置有问题。你可以把server.ehlo()的返回值打印出来观察AUTH后面的机制列表。如果返回250-AUTH PLAIN LOGIN但实际不强制认证那就是一个明确的配置缺陷。这个指纹比单纯看端口是否开放更准确因为它直接反映了服务器的认证策略。我自己踩过的最大坑是早期做批量筛查时没有限制并发数一次性对几百个域名发起连接结果被目标防火墙判定为扫描行为IP 被封了三天。后来改成每次最多 10 个并发中间加 1 秒延迟就再没出过问题。做安全评估节奏比速度重要。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →