Last active
February 27, 2024 13:50
-
-
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 |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How are the initialization vectors generated? This is using RC2_CBC correct?