Last active
May 19, 2026 23:58
-
-
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
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
| <# | |
| .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 |
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
Uh oh!
There was an error while loading. Please reload this page.