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
@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