Skip to content

Instantly share code, notes, and snippets.

@ykoster
Last active February 27, 2024 13:50
Show Gist options
  • Save ykoster/0a475e4f09e8e5c714ae741933ab21a2 to your computer and use it in GitHub Desktop.
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
@Safety1st
Copy link

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>

Result is the same (for password 123):
image

@ykoster
Copy link
Author

ykoster commented Sep 14, 2020

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?

@Safety1st
Copy link

Now everything is fine. Thank you!

@sidrile3310
Copy link

How are the initialization vectors generated? This is using RC2_CBC correct?

@ykoster
Copy link
Author

ykoster commented May 5, 2022

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.

@sidrile3310
Copy link

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

@ykoster
Copy link
Author

ykoster commented May 6, 2022

@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