news 2026/10/1 10:50:04

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SMTP发件人伪造原理、检测与企业邮件网关防护实战

简介:这份资源围绕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:<ceo@company.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"] = "ceo@company.com" # 伪造的显示发件人 msg["To"] = "test@example.com" try: # 连接 SMTP 服务器,不登录 server = smtplib.SMTP(target_host, target_port, timeout=10) server.ehlo("test.local") # 关键:MAIL FROM 使用伪造地址 server.sendmail("ceo@company.com", ["test@example.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 "v=spf1" # 查询 DMARC 记录 dig TXT _dmarc.example.com +short # 查询 DKIM 选择器(常见选择器如 default、google、selector1) dig TXT default._domainkey.example.com +short

如果 SPF 返回v=spf1 -all,说明该域名拒绝所有未授权 IP,伪造难度高。如果返回v=spf1 ~all或没有记录,伪造成功率就大幅上升。DMARC 返回p=none表示只监控不拦截,返回p=reject才是强拦截。参数上,+short让输出更简洁,grep用于过滤关键行。这一步的目的是在发起测试前,先判断目标域名的防护水位,避免盲目操作。

2.3 开放中继与内部信任链的利用

开放中继是指那些允许任何来源 IP 通过它向任意目标域投递邮件的服务器。虽然现在公开的开放中继已经很少,但在企业内网里,内部邮件服务器之间的信任关系往往很宽松。比如,一台对外接收邮件的网关可能对来自内部网段的连接不做发件人验证,直接转发。如果你能进入内网,或者找到一台被攻陷的内网主机,就可以利用这条信任链伪造内部高管的发件人。

另一种常见情况是,某些邮件服务器对“本地域”的发件人特别信任。比如MAIL FROM写成admin@localdomain,服务器可能直接放行。测试时可以用swaks这个工具快速验证:

# 使用 swaks 测试发件人伪造 swaks --to test@example.com \ --from ceo@company.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(), hostname='127.0.0.1', port=8025) controller.start() print("SMTP 测试服务器已启动,监听 127.0.0.1:8025") input("按回车键停止...\n") controller.stop()

这段代码的核心是handle_DATA方法,它在邮件数据接收完成后被调用。envelope.mail_from是信封发件人,envelope.content是完整的邮件原文。参数上,hostname='127.0.0.1'表示只监听本地,port=8025避免与系统邮件服务冲突。启动后,你可以用前面的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"] = "ceo@company.com" # 伪造的显示发件人 msg["To"] = "finance@example.com" msg["Date"] = formatdate(localtime=True) msg["Message-ID"] = make_msgid(domain="company.com") # 连接本地测试服务器 server = smtplib.SMTP("127.0.0.1", 8025, timeout=10) server.ehlo("test.local") # 信封发件人也用伪造地址 server.sendmail("ceo@company.com", ["finance@example.com"], msg.as_string()) server.quit() print("邮件已发送到本地测试服务器")

这里的关键参数是msg["From"]和sendmail的第一个参数。前者控制收件人客户端显示的地址,后者控制 SMTP 会话中的MAIL FROM。两者都设为伪造地址,可以测试服务器是否对两者都放行。Message-ID里的domain参数也设成目标域名,让邮件看起来更真实。运行后,回到aiosmtpd的控制台,你应该能看到信封发件人和头部From都是ceo@company.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:<ceo@company.com> S: 250 OK C: RCPT TO:<finance@example.com> S: 250 OK C: DATA S: 354 End data with <CR><LF>.<CR><LF> 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 记录,格式如下:

v=spf1 ip4:203.0.113.0/24 include:_spf.google.com -all

ip4声明允许发信的 IP 段,include引入第三方发信服务,-all表示拒绝所有其他来源。注意不要用~all,那只是软失败,很多收件方依然会投递。DKIM 需要在邮件服务器上生成密钥对,公钥发布到 DNS,私钥用于签名。DMARC 记录放在_dmarc子域下:

v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com; pct=100

p=reject表示直接拒绝未通过验证的邮件,rua是接收汇总报告的地址,pct=100表示对 100% 的邮件生效。部署时建议先用p=none跑两周,收集报告确认没有误杀,再逐步切到p=quarantine和p=reject。

4.2 邮件网关的发件人验证规则配置

除了 DNS 层面的验证,邮件网关本身也需要配置发件人验证规则。以常见的 Postfix 为例,可以在main.cf里开启发件人域名校验:

smtpd_sender_restrictions = reject_unknown_sender_domain, reject_non_fqdn_sender, reject_unlisted_sender, permit_mynetworks, permit_sasl_authenticated

reject_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 "v=spf1" 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 "v=DMARC1" in r.to_text(): result["dmarc"] = r.to_text() except Exception: pass # 尝试连接 SMTP try: server = smtplib.SMTP(f"mail.{domain}", 25, timeout=5) 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 端口是否开放。参数上,timeout=5避免单个域名卡住太久,dns.resolver需要安装dnspython库。输出结果里,如果spf为None或包含~all,dmarc为None或p=none,同时smtp_open为True,这个域名的伪造风险就很高。我一般会把这个脚本跑一遍自己负责的域名列表,按风险等级排序,优先处理高风险项。

一个具体的技巧是:在EHLO之后,服务器返回的扩展列表里如果包含AUTH但不需要认证就能发信,说明配置有问题。你可以把server.ehlo()的返回值打印出来,观察AUTH后面的机制列表。如果返回250-AUTH PLAIN LOGIN但实际不强制认证,那就是一个明确的配置缺陷。这个指纹比单纯看端口是否开放更准确,因为它直接反映了服务器的认证策略。

我自己踩过的最大坑是:早期做批量筛查时,没有限制并发数,一次性对几百个域名发起连接,结果被目标防火墙判定为扫描行为,IP 被封了三天。后来改成每次最多 10 个并发,中间加 1 秒延迟,就再没出过问题。做安全评估,节奏比速度重要。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:49:19

CTF隐写术实战复盘:LSB、伪加密与音频频谱的五层套娃解法

1. 隐写术到底在玩什么&#xff1a;从一个Misc题选手的视角说起先说个现象。很多人觉得CTF里Misc&#xff08;杂项&#xff09;就是"送分题"&#xff0c;结果一上手就被各种文件格式、编码、隐写手法按在地上摩擦。我自己打CTF这几年&#xff0c;Misc题反而是最容易卡…

作者头像 李华
网站建设 2026/10/1 10:48:37

Win10禁用自动更新与Defender:服务、组策略、注册表实战指南

这活儿我在公司机房和帮朋友修机时干过太多次了。Win10的自动更新和Windows Defender安全中心&#xff0c;算是系统里脾气最倔的两个组件——你明明只是想让电脑干活&#xff0c;它偏要在你演示到一半的时候强制重启装补丁&#xff1b;你好不容易装了个偏门破解工具或老软件&am…

作者头像 李华
网站建设 2026/10/1 10:47:39

手写Redis分布式锁:原理、常见坑与工程实践选型指南

“手写Redis分布式锁”这几个字&#xff0c;放在招聘JD里是常规操作&#xff0c;放在面试题里是必考题&#xff0c;放在我实际写的代码里&#xff0c;却是一段反复推翻重来的血泪史。我最早接触分布式锁还停留在 SETNX 一把梭的时代&#xff0c;后来被线上事故教育了几次&…

作者头像 李华
网站建设 2026/10/1 10:47:22

设备追溯数据为何失效?时间同步与温湿度传感器校准是关键

设备追溯数据看着全&#xff0c;关键时候却调不出来&#xff0c;这种问题我在好几个元器件厂都撞上过。有一回去一家做电源模块的厂子做系统回访&#xff0c;可靠性工程师翻出三个月前某批次产品的追溯档案&#xff0c;发现AOI记录显示某块板子过完测试的时间&#xff0c;居然比…

作者头像 李华
网站建设 2026/10/1 10:46:46

Jenkins插件安装教程:依赖管理、离线部署与Java Web自动部署

1. 插件机制的底层逻辑&#xff1a;为什么 Jenkins 离开插件寸步难行刚接触 Jenkins 的人容易有一个错觉&#xff1a;装完 war 包、打开 8080 端口&#xff0c;这工具就能自动部署了。实际用下来你会发现&#xff0c;裸装的 Jenkins 除了能跑一个最简单的自由风格任务、执行几条…

作者头像 李华
网站建设 2026/10/1 10:46:29

SpringBoot2+Vue3+MySQL8.0疫情防控管理系统开发实战

接手过不少学生项目和内部管理系统&#xff0c;看到“Java Web 疫情防控管理系统”这个标题&#xff0c;第一反应是亲切——SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0&#xff0c;标准得不能再标准的技术栈组合。这类项目在毕设、课设、技能培训中出现的频率非常高&#xff0…

作者头像 李华