-
-
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 |
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.
Ok, these are matched values :)
Result is the same (for password 123):
