-
-
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 |
<MTPutty version="1.7">
<Globals>
<PuttyLocation>C:\Program Files\PuTTY\putty.exe</PuttyLocation>
<GUI>
<StartPage HideOnTab="1"/>
</GUI>
<General>
<TabCaption>1</TabCaption>
<NormalTermination>0</NormalTermination>
<AbnormalTermination>1</AbnormalTermination>
<AbnormalAutoReconnect>0</AbnormalAutoReconnect>
<QuitNoConfrim>1</QuitNoConfrim>
<CloseSessionNoConfirm>1</CloseSessionNoConfirm>
<CloseButtonOnTabs>1</CloseButtonOnTabs>
<TabPosition>0</TabPosition>
</General>
<Advanced>
<SaveLayout>0</SaveLayout>
<SaveSessions>0</SaveSessions>
</Advanced>
</Globals>
<Servers>
<Putty>
<Node Type="1">
<SavedSession>Default Settings</SavedSession>
<DisplayName>CORE</DisplayName>
<UID>{53BEB999-3851-4B2B-9A9F-7F70B1326EB5}</UID>
<ServerName>192.168.2.1</ServerName>
<PuttyConType>4</PuttyConType>
<Port>0</Port>
<UserName>onadm</UserName>
<Password>HY0sf47p+pM=</Password>
<PasswordDelay>0</PasswordDelay>
<CLParams>192.168.2.1 -ssh -l onadm</CLParams>
<ScriptDelay>0</ScriptDelay>
</Node>
</Putty>
</Servers>
<Internals>
<Putty/>
</Internals>
<Hotkeys>
<NextTab>0</NextTab>
<PrevTab>0</PrevTab>
<AppSwitch>16576</AppSwitch>
<acHideConnections>0</acHideConnections>
<acPuttyLocation>0</acPuttyLocation>
<acSettings>0</acSettings>
<acHideServers>16450</acHideServers>
<acHideToolbar>16468</acHideToolbar>
<acAddServer>0</acAddServer>
<acAddGroup>0</acAddGroup>
<acRemove>0</acRemove>
<acDetach>0</acDetach>
<acConnect>0</acConnect>
<acConnectTo>16459</acConnectTo>
<acTreeProps>0</acTreeProps>
<acSendScript>0</acSendScript>
<acAttach>0</acAttach>
<acRenameTab>0</acRenameTab>
<acImportTree>0</acImportTree>
<acExportTree>0</acExportTree>
<acHotkeysMap>0</acHotkeysMap>
<acDuplicate>0</acDuplicate>
<acMultiUpdate>0</acMultiUpdate>
</Hotkeys>
</MTPutty>
Thanks @Safety1st. Unfortunately, I'm not able to decrypt the password. CryptDecrypt returns 0x80004005 (Bad Data). The encryption key is derived from the user name and server name, so if you've change those it wouldn't work.
There are scenarios where it is not possible to decrypt because the config file was created with an older version of MTPuTTY. This doesn't seem to be the case here as your config suggest that 1.7(+) is used. The script was tested against 1.6.x so it could be that the encryption logic was changed. This will require some more digging into though.
Ok, these are matched values :)
<?xml version="1.0" encoding="UTF-8"?>
<MTPutty version="1.7">
<Globals>
<PuttyLocation>C:\Program Files\PuTTY\putty.exe</PuttyLocation>
<GUI>
<StartPage HideOnTab="1"/>
</GUI>
<General>
<TabCaption>1</TabCaption>
<NormalTermination>0</NormalTermination>
<AbnormalTermination>1</AbnormalTermination>
<AbnormalAutoReconnect>0</AbnormalAutoReconnect>
<QuitNoConfrim>1</QuitNoConfrim>
<CloseSessionNoConfirm>1</CloseSessionNoConfirm>
<CloseButtonOnTabs>1</CloseButtonOnTabs>
<TabPosition>0</TabPosition>
</General>
<Advanced>
<SaveLayout>0</SaveLayout>
<SaveSessions>0</SaveSessions>
</Advanced>
</Globals>
<Servers>
<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>
</Servers>
<Internals>
<Putty/>
</Internals>
<Hotkeys>
<NextTab>0</NextTab>
<PrevTab>0</PrevTab>
<AppSwitch>16576</AppSwitch>
<acHideConnections>0</acHideConnections>
<acPuttyLocation>0</acPuttyLocation>
<acSettings>0</acSettings>
<acHideServers>16450</acHideServers>
<acHideToolbar>16468</acHideToolbar>
<acAddServer>0</acAddServer>
<acAddGroup>0</acAddGroup>
<acRemove>0</acRemove>
<acDetach>0</acDetach>
<acConnect>0</acConnect>
<acConnectTo>16459</acConnectTo>
<acTreeProps>0</acTreeProps>
<acSendScript>0</acSendScript>
<acAttach>0</acAttach>
<acRenameTab>0</acRenameTab>
<acImportTree>0</acImportTree>
<acExportTree>0</acExportTree>
<acHotkeysMap>0</acHotkeysMap>
<acDuplicate>0</acDuplicate>
<acMultiUpdate>0</acMultiUpdate>
</Hotkeys>
</MTPutty>
I got the same results now @Safety1st. It looks like they've switched to Unicode strings, the code assumes UTF-8 resulting in the extra zeroes. I've changed the code a bit to (try to) detect Unicode strings. Could you give it a try?
Now everything is fine. Thank you!
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

@Safety1st can you provide the source XML?