-
-
Save ykoster/0a475e4f09e8e5c714ae741933ab21a2 to your computer and use it in GitHub Desktop.
| <# | |
| .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 |
How are the initialization vectors generated? This is using RC2_CBC correct?
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.
@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.
@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.
#!/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
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
Now everything is fine. Thank you!