Skip to content

Instantly share code, notes, and snippets.

@ykoster
Last active May 19, 2026 23:58
Show Gist options
  • Select an option

  • Save ykoster/0a475e4f09e8e5c714ae741933ab21a2 to your computer and use it in GitHub Desktop.

Select an option

Save ykoster/0a475e4f09e8e5c714ae741933ab21a2 to your computer and use it in GitHub Desktop.
Invoke-MTPuTTYConfigDump - read an MTPuTTY configuration file, decrypt the passwords and dump the result
<#
.Synopsis
Decrypt an MTPuTTY configuration file
.Description
Read an MTPuTTY configuration file, decrypt the passwords and dump the result
.Parameter ConfigFile
Path to the MTPuTTY configuration file
.Example
Invoke-MTPuTTYConfigDump mtputty.xml
#>
function Invoke-MTPuTTYConfigDump {
[CmdletBinding(DefaultParameterSetName="ConfigFile")]
Param(
[Parameter(ParameterSetName = "ConfigFile", Position = 0, Mandatory = $true)]
[String]
$ConfigPath
)
$PROV_RSA_FULL = 1
$CRYPT_VERIFYCONTEXT = 0xF0000000
$CALG_SHA = 0x00008004
$CALG_RC2 = 0x00006602
Function Get-CryptoAPI {
$MethodDefinition = @"
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptAcquireContext(ref IntPtr hProv, string pszContainer, string pszProvider, uint dwProvType, long dwFlags);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptReleaseContext(IntPtr hProv, uint dwFlags);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptCreateHash(IntPtr hProv, uint algId, IntPtr hKey, uint dwFlags, ref IntPtr phHash);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptDestroyHash(IntPtr hHash);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptHashData(IntPtr hHash, byte[] pbData, uint dataLen, uint flags);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptDeriveKey(IntPtr hProv,int Algid, IntPtr hBaseData, int flags, ref IntPtr phKey);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptDestroyKey(IntPtr hKey);
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
[return : MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptDecrypt(IntPtr hKey, IntPtr hHash, int Final, uint dwFlags, byte[] pbData, ref uint pdwDataLen);
"@
try {
$CryptoAPI = Add-Type -MemberDefinition $MethodDefinition -name advapi32 -Namespace CryptoAPI -PassThru
} catch {}
return [CryptoAPI.advapi32]
}
<# https://devblogs.microsoft.com/powershell/format-xml/ #>
Function Format-XML ([System.Xml.XmlElement]$xml, $indent=2) {
$StringWriter = New-Object System.IO.StringWriter
$XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter
$xmlWriter.Formatting = "indented"
$xmlWriter.Indentation = $Indent
$xml.WriteContentTo($XmlWriter)
$XmlWriter.Flush()
$StringWriter.Flush()
Write-Output $StringWriter.ToString()
}
try {
[xml]$config = Get-Content -Path $ConfigPath -ErrorAction Stop
} catch {
Write-Host $_ -ErrorAction Stop
return
}
$CryptoAPI = Get-CryptoAPI
[System.IntPtr]$hProv = 0
$servers = $config.SelectNodes("//Servers")
if($servers.Count -gt 0 -and $CryptoAPI::CryptAcquireContext([ref]$hProv, $null, $null, $PROV_RSA_FULL, $CRYPT_VERIFYCONTEXT) -ne $false) {
foreach($node in $config.SelectNodes("//Node")) {
if($node.Type -eq 0) {
Write-Host "$($node.DisplayName):"
} elseif ($node.Type -eq 1) {
[System.IntPtr]$hHash = 0
[System.IntPtr]$hKey = 0
$password = [system.Text.Encoding]::UTF8.GetBytes("1$($node.UserName.Trim())$($node.ServerName.Trim())")
$ciphertext = [System.Convert]::FromBase64String($node.Password.Trim())
$ciphertextLength = $ciphertext.Length
if($CryptoAPI::CryptCreateHash($hProv, $CALG_SHA, 0, 0, [ref]$hHash) -ne $false) {
if($CryptoAPI::CryptHashData($hHash, $password, $password.Length, 0) -ne $false) {
if($CryptoAPI::CryptDeriveKey($hProv, $CALG_RC2, $hHash, 0, [ref]$hKey) -ne $false) {
if($CryptoAPI::CryptDecrypt($hKey, 0, $true, 0, $ciphertext, [ref]$ciphertextLength) -ne $false) {
$ciphertext = $ciphertext[0..($ciphertextLength-1)]
if($ciphertextLength -ge 2 -and $ciphertext[1] -eq 0) {
$node.Password = [system.Text.Encoding]::Unicode.GetString($ciphertext)
} else {
$node.Password = [system.Text.Encoding]::UTF8.GetString($ciphertext)
}
}
$null = $CryptoAPI::CryptDestroyKey($hKey);
}
}
$null = $CryptoAPI::CryptDestroyHash($hHash);
}
Format-XML $node
Write-Host
}
Write-Host
}
$null = $CryptoAPI::CryptReleaseContext($hProv, 0)
}
}
Export-ModuleMember -Function Invoke-MTPuTTYConfigDump
@sidrile3310

Copy link
Copy Markdown

How are the initialization vectors generated? This is using RC2_CBC correct?

@ykoster

ykoster commented May 5, 2022

Copy link
Copy Markdown
Author

How are the initialization vectors generated? This is using RC2_CBC correct?

Hi @sidrile3310, since there is no call to CryptSetKeyParam I assume the IV is all zeroes, which is the default for the Microsoft Base Cryptographic Provider.

@sidrile3310

Copy link
Copy Markdown

@ykoster Thanks! I am trying to write a version of this using Python but so far not so good. Thought the IV may be the issue but it looks like the problem lies elsewhere. This was a great primer though.

@ykoster

ykoster commented May 6, 2022

Copy link
Copy Markdown
Author

@sidrile3310 could be related to the way the key is derived. There is a Python implementation of CryptDeriveKey here https://www.fireeye.com/content/dam/fireeye-www/global/en/blog/threat-research/flareon2016/challenge2-solution.pdf. Haven't tested it myself.

@VeNoMouS

VeNoMouS commented May 19, 2026

Copy link
Copy Markdown
#!/usr/bin/env python3
# Encrypt and decrypt MTPuTTY XML password values.
# Created by VeNoMouS <venom@gen-x.co.nz> on 2026-05-20

import argparse
import base64
import binascii
import hashlib

try:
    from Crypto.Cipher import ARC2
    from Crypto.Util.Padding import pad, unpad
except ImportError as exc:
    raise SystemExit(
        "Missing dependency: pycryptodome\n"
        "Install it with: python3 -m pip install pycryptodome"
    ) from exc

# ------------------------------------------------------------------------------- #

BLOCK_SIZE = 8
IV = b"\x00" * BLOCK_SIZE

# ------------------------------------------------------------------------------- #

def keyFor(username, servername):
    """Return the RC2 key derived from MTPuTTY's XML node fields."""
    seed = f"1{username.strip()}{servername.strip()}".encode("utf-8")
    return hashlib.sha1(seed).digest()[:16]

# ------------------------------------------------------------------------------- #

def decryptPassword(username, servername, b64_password):
    try:
        plain = unpad(
            ARC2.new(
                keyFor(username, servername),
                ARC2.MODE_CBC,
                iv=IV,
                effective_keylen=128,
            ).decrypt(base64.b64decode(b64_password.strip(), validate=True)),
            BLOCK_SIZE,
        )
    except binascii.Error as exc:
        raise SystemExit("Could not decrypt: password is not valid Base64") from exc
    except ValueError as exc:
        raise SystemExit(
            "Could not decrypt: check the UserName and ServerName from the same XML node"
        ) from exc

    if len(plain) >= 2 and plain[1] == 0:
        return plain.decode("utf-16le")
    return plain.decode("utf-8")

# ------------------------------------------------------------------------------- #

def encryptPassword(username, servername, password):
    return base64.b64encode(
        ARC2.new(
            keyFor(username, servername),
            ARC2.MODE_CBC,
            iv=IV,
            effective_keylen=128,
        ).encrypt(pad(password.encode("utf-16le"), BLOCK_SIZE))
    ).decode("ascii")

# ------------------------------------------------------------------------------- #

def build_parser():
    parser = argparse.ArgumentParser(
        description="Encrypt or decrypt MTPuTTY XML Password values.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "examples:\n"
            "  mtputty_password.py decrypt root server01 U2FtcGxlUGFzc3dvcmQ=\n"
            "  mtputty_password.py decrypt '' 192.168.0.15 k6LrFZbnRFk=\n"
            "  mtputty_password.py encrypt root server01 password123\n\n"
            "note:\n"
            "  Empty decrypted passwords are printed as [EMPTY]."
        ),
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    decrypt_parser = subparsers.add_parser("decrypt", help="decrypt a Base64 XML password")
    decrypt_parser.add_argument("username", help="Node UserName value")
    decrypt_parser.add_argument("servername", help="Node ServerName value")
    decrypt_parser.add_argument("password", help="Node Password Base64 value")

    encrypt_parser = subparsers.add_parser("encrypt", help="encrypt a plaintext password")
    encrypt_parser.add_argument("username", help="Node UserName value")
    encrypt_parser.add_argument("servername", help="Node ServerName value")
    encrypt_parser.add_argument("password", help="plaintext password")

    return parser

# ------------------------------------------------------------------------------- #

args = build_parser().parse_args()

if args.command == "decrypt":
    print(decryptPassword(args.username, args.servername, args.password) or "[EMPTY]")
elif args.command == "encrypt":
    print(encryptPassword(args.username, args.servername, args.password))
<Putty>
	<Node Type="1">
		<SavedSession>desert</SavedSession>
		<DisplayName>192.168.0.15</DisplayName>
		<ServerName>192.168.0.15</ServerName>
		<PuttyConType>4</PuttyConType>
		<Port>22</Port>
		<UserName></UserName>
		<Password>k6LrFZbnRFk=</Password>
		<PasswordDelay>0</PasswordDelay>
		<CLParams>-load desert 192.168.0.15 -ssh -P 22</CLParams>
		<ScriptDelay>0</ScriptDelay>
	</Node>
</Putty>
./mtputty_password.py decrypt "" 192.168.0.15 k6LrFZbnRFk=
[EMPTY]

./mtputty_password.py encrypt "" 192.168.0.15 ""
k6LrFZbnRFk=
<Putty>
	<Node Type="1">
		<SavedSession>Default Settings</SavedSession>
		<DisplayName>TEST</DisplayName>
		<UID>{55DF5F51-222D-409A-A8F4-0A0B7D00A5A2}</UID>
		<ServerName>192.168.2.1</ServerName>
		<PuttyConType>4</PuttyConType>
		<Port>0</Port>
		<UserName>onadm</UserName>
		<Password>R7eUjM9rncA=</Password>
		<PasswordDelay>0</PasswordDelay>
		<CLParams>192.168.2.1 -ssh -l onadm</CLParams>
		<ScriptDelay>0</ScriptDelay>
	</Node>
</Putty>
./mtputty_password.py decrypt onadm 192.168.2.1 R7eUjM9rncA=
123

@VeNoMouS

VeNoMouS commented May 19, 2026

Copy link
Copy Markdown
  Key String Build
  This is the save path around the password encryption call:

  004f3bb1  push   0x4f3dcc              ; literal string "1"
  004f3bb6  lea    edx,[ebp-0x40]
  004f3bb9  mov    eax,esi
  004f3bbb  call   0x4f3664              ; get one node field
  004f3bc0  mov    eax,[ebp-0x40]
  004f3bc3  lea    edx,[ebp-0x3c]
  004f3bc6  call   0x4097cc              ; trim/copy field
  004f3bcb  push   [ebp-0x3c]

  004f3bce  lea    edx,[ebp-0x44]
  004f3bd1  mov    eax,[esi+0x18]        ; another stored field
  004f3bd4  call   0x4097cc              ; trim/copy field
  004f3bd9  push   [ebp-0x44]

  004f3bdc  lea    eax,[ebp-0x38]
  004f3bdf  mov    edx,0x3
  004f3be4  call   0x4050b4              ; concat 3 strings: "1" + field + field

  004f3be9  mov    eax,[ebp-0x38]
  004f3bec  push   eax                   ; key string
  004f3bed  lea    edx,[ebp-0x48]
  004f3bf0  mov    eax,esi
  004f3bf2  call   0x4f3694              ; get Password field
  004f3bf7  mov    eax,[ebp-0x48]
  004f3bfa  lea    ecx,[ebp-0x34]
  004f3bfd  pop    edx                   ; key string into edx
  004f3bfe  call   0x4f0940              ; encrypt password with key string

  The local string table around 0x4f3db8 is:

  0x4f3db8 UserName............1...........Password........
  0x4f3dcc 1...........Password............PasswordDelay...
  0x4f3dd8 Password............PasswordDelay...........CLPa

  Crypto Constructor
  This is TStrCryptor setup. It hashes the provided key string and derives an RC2 key:

  004f068e  push   0xf0000000            ; CRYPT_VERIFY_CONTEXT
  004f0693  push   0x1                   ; PROV_RSA_FULL
  004f0695  push   0x0
  004f0697  push   0x0
  004f0699  lea    eax,[esi+0x4]
  004f069c  push   eax
  004f069d  call   0x4f01a4              ; CryptAcquireContextA

  004f06aa  push   0x8004                ; CALG_SHA1
  004f06af  mov    eax,[esi+0x4]
  004f06b2  push   eax
  004f06b3  call   0x4f01cc              ; CryptCreateHash

  004f06b8  push   0x0
  004f06ba  mov    eax,[ebp-0x4]         ; key string
  004f06bd  call   0x404ff4              ; string length
  004f06c2  push   eax
  004f06c3  lea    eax,[ebp-0x4]
  004f06c6  call   0x40524c              ; string bytes
  004f06cb  push   eax
  004f06cc  mov    eax,[ebp-0x8]
  004f06cf  push   eax
  004f06d0  call   0x4f01d4              ; CryptHashData

  004f06df  push   0x6602                ; CALG_RC2
  004f06e4  mov    eax,[esi+0x4]
  004f06e7  push   eax
  004f06e8  call   0x4f01b4              ; CryptDeriveKey

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment