Created
May 30, 2026 13:57
-
-
Save kipavy/910e7ddc8efea7aeb9202c0241486a1d to your computer and use it in GitHub Desktop.
MobaXterm v25+/v26 password decryptor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| MobaXterm v25+/v26 password decryptor (handles CVE-2025-0714 random-IV format). | |
| Older tools (HyperSine, XMCyber, clemthi) assume a fixed IV = AES-ECB(key, 0). | |
| MobaXterm >= 25.0 switched to a random per-password IV stored inline. Format: | |
| stored = "_@" + IV(16 chars) + base64(AES-256-CFB8(key, IV, password)) | |
| key = SHA512(master_password)[:32] | |
| IV = the 16 ASCII characters right after the "_@" marker (used as the AES IV bytes) | |
| body = base64-decode the FULL stored string (alphabet @->+, _->/), then drop the | |
| first 15 bytes -> the CFB8 ciphertext of the password. | |
| This script auto-derives the key from the DPAPI-protected master-password hash in the | |
| registry (so you don't even need to type the master password), then decrypts the | |
| P (session passwords) and C (credentials) subkeys. | |
| Requires: pycryptodome (pip install pycryptodome) | |
| Windows only (reads HKCU and uses DPAPI). | |
| """ | |
| import os, sys, platform, base64, hashlib, itertools, winreg, ctypes | |
| from ctypes import wintypes | |
| from Crypto.Cipher import AES | |
| REG_BASE = r'Software\Mobatek\MobaXterm' | |
| DPAPI_HEADER = bytes.fromhex('01000000d08c9ddf0115d1118c7a00c04fc297eb') | |
| # ---- DPAPI (CryptUnprotectData) via Win32, with optional entropy ---- | |
| class DATA_BLOB(ctypes.Structure): | |
| _fields_ = [('cbData', wintypes.DWORD), ('pbData', ctypes.POINTER(ctypes.c_char))] | |
| def _blob(data: bytes) -> DATA_BLOB: | |
| buf = ctypes.create_string_buffer(data, len(data)) | |
| return DATA_BLOB(len(data), ctypes.cast(buf, ctypes.POINTER(ctypes.c_char))) | |
| def dpapi_unprotect(data: bytes, entropy: bytes) -> bytes: | |
| pin, pent, pout = _blob(data), _blob(entropy), DATA_BLOB() | |
| if not ctypes.windll.crypt32.CryptUnprotectData( | |
| ctypes.byref(pin), None, ctypes.byref(pent), None, None, 0, ctypes.byref(pout)): | |
| raise ctypes.WinError() | |
| out = ctypes.string_at(pout.pbData, pout.cbData) | |
| ctypes.windll.kernel32.LocalFree(pout.pbData) | |
| return out | |
| def derive_key_from_registry() -> bytes: | |
| """Derive the AES key from the DPAPI-protected master-password hash (no typing needed).""" | |
| base = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_BASE) | |
| session_p, _ = winreg.QueryValueEx(base, 'SessionP') | |
| m = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_BASE + r'\M') | |
| val, _ = winreg.QueryValueEx(m, os.getlogin() + '@' + platform.node()) | |
| blob = DPAPI_HEADER + base64.b64decode(val) | |
| master_hash = dpapi_unprotect(blob, str(session_p).encode('utf-8')) | |
| return base64.b64decode(master_hash)[:32] | |
| def derive_key_from_password(master_password: str) -> bytes: | |
| return hashlib.sha512(master_password.encode('utf-8')).digest()[:32] | |
| def decrypt_value(key: bytes, stored: str) -> bytes: | |
| """Decrypt one MobaXterm v25+/v26 stored password string.""" | |
| if not stored.startswith('_@'): | |
| # Old (<=v24) format: full string is base64 (@->+,_->/), fixed IV = ECB(key,0) | |
| ct = base64.b64decode(stored, altchars=b'@_') | |
| iv = AES.new(key=key, mode=AES.MODE_ECB).encrypt(b'\x00' * 16) | |
| return AES.new(key=key, iv=iv, mode=AES.MODE_CFB, segment_size=8).decrypt(ct) | |
| iv = stored[2:18].encode('latin1') # 16 chars after _@ = AES IV | |
| body = base64.b64decode(stored, altchars=b'@_')[15:] | |
| return AES.new(key=key, iv=iv, mode=AES.MODE_CFB, segment_size=8).decrypt(body) | |
| def _decode(b: bytes) -> str: | |
| try: | |
| return b.decode('utf-8') | |
| except UnicodeDecodeError: | |
| return b.decode('latin1') | |
| def enum_values(subkey: str): | |
| try: | |
| k = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_BASE + '\\' + subkey) | |
| except FileNotFoundError: | |
| return | |
| for i in itertools.count(0): | |
| try: | |
| yield winreg.EnumValue(k, i)[:2] | |
| except OSError: | |
| break | |
| def main(): | |
| if platform.system().lower() != 'windows': | |
| sys.exit('Windows only.') | |
| try: | |
| key = derive_key_from_registry() | |
| print('[+] AES key derived from registry master-password hash.\n') | |
| except Exception as e: | |
| pw = input('Could not auto-derive key (%s).\nEnter MobaXterm master password: ' % e) | |
| key = derive_key_from_password(pw) | |
| print('=== Session passwords (P) ===') | |
| for name, value in enum_values('P'): | |
| print(f' {name:<32} {_decode(decrypt_value(key, value))!r}') | |
| creds = list(enum_values('C')) | |
| if creds: | |
| print('\n=== Credentials (C) ===') | |
| for name, value in creds: | |
| user, _, enc = value.partition(':') | |
| print(f' {name:<20} user={user!r:<14} {_decode(decrypt_value(key, enc))!r}') | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment