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
@lloydmoran
Copy link

Thank you so much for writing this!

@ykoster
Copy link
Author

ykoster commented Aug 26, 2020

Thank you so much for writing this!

Glad to hear you find it useful 👍

@Safety1st
Copy link

Safety1st commented Sep 8, 2020

There are extraneous symbols int the recovered password: <Password>1&#x0;2&#x0;3&#x0;</Password> (password is 123).

@ykoster
Copy link
Author

ykoster commented Sep 9, 2020

@Safety1st can you provide the source XML?

@Safety1st
Copy link

Safety1st commented Sep 10, 2020

<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>CORE</DisplayName>
				<UID>{53BEB999-3851-4B2B-9A9F-7F70B1326EB5}</UID>
				<ServerName>192.168.2.1</ServerName>
				<PuttyConType>4</PuttyConType>
				<Port>0</Port>
				<UserName>onadm</UserName>
				<Password>HY0sf47p+pM=</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>

@ykoster
Copy link
Author

ykoster commented Sep 11, 2020

Thanks @Safety1st. Unfortunately, I'm not able to decrypt the password. CryptDecrypt returns 0x80004005 (Bad Data). The encryption key is derived from the user name and server name, so if you've change those it wouldn't work.

There are scenarios where it is not possible to decrypt because the config file was created with an older version of MTPuTTY. This doesn't seem to be the case here as your config suggest that 1.7(+) is used. The script was tested against 1.6.x so it could be that the encryption logic was changed. This will require some more digging into though.

@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