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
@ykoster
Copy link
Copy Markdown
Author

ykoster commented Sep 9, 2020

@Safety1st can you provide the source XML?

@Safety1st
Copy link
Copy Markdown

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
Copy Markdown
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
Copy Markdown

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
Copy Markdown
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
Copy Markdown

Now everything is fine. Thank you!

@sidrile3310
Copy link
Copy Markdown

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

@ykoster
Copy link
Copy Markdown
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
Copy Markdown

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

@VeNoMouS
Copy link
Copy Markdown

VeNoMouS commented May 19, 2026

#!/usr/bin/env python3
# Encrypt and decrypt MTPuTTY XML password values.
# Created by VeNoMouS <venom@gen-x.co.nz> on 2026-05-20

import argparse
import base64
import binascii
import hashlib

try:
    from Crypto.Cipher import ARC2
    from Crypto.Util.Padding import pad, unpad
except ImportError as exc:
    raise SystemExit(
        "Missing dependency: pycryptodome\n"
        "Install it with: python3 -m pip install pycryptodome"
    ) from exc

# ------------------------------------------------------------------------------- #

BLOCK_SIZE = 8
IV = b"\x00" * BLOCK_SIZE

# ------------------------------------------------------------------------------- #

def keyFor(username, servername):
    """Return the RC2 key derived from MTPuTTY's XML node fields."""
    seed = f"1{username.strip()}{servername.strip()}".encode("utf-8")
    return hashlib.sha1(seed).digest()[:16]

# ------------------------------------------------------------------------------- #

def decryptPassword(username, servername, b64_password):
    try:
        plain = unpad(
            ARC2.new(
                keyFor(username, servername),
                ARC2.MODE_CBC,
                iv=IV,
                effective_keylen=128,
            ).decrypt(base64.b64decode(b64_password.strip(), validate=True)),
            BLOCK_SIZE,
        )
    except binascii.Error as exc:
        raise SystemExit("Could not decrypt: password is not valid Base64") from exc
    except ValueError as exc:
        raise SystemExit(
            "Could not decrypt: check the UserName and ServerName from the same XML node"
        ) from exc

    if len(plain) >= 2 and plain[1] == 0:
        return plain.decode("utf-16le")
    return plain.decode("utf-8")

# ------------------------------------------------------------------------------- #

def encryptPassword(username, servername, password):
    return base64.b64encode(
        ARC2.new(
            keyFor(username, servername),
            ARC2.MODE_CBC,
            iv=IV,
            effective_keylen=128,
        ).encrypt(pad(password.encode("utf-16le"), BLOCK_SIZE))
    ).decode("ascii")

# ------------------------------------------------------------------------------- #

def build_parser():
    parser = argparse.ArgumentParser(
        description="Encrypt or decrypt MTPuTTY XML Password values.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "examples:\n"
            "  mtputty_password.py decrypt root server01 U2FtcGxlUGFzc3dvcmQ=\n"
            "  mtputty_password.py decrypt '' 192.168.0.15 k6LrFZbnRFk=\n"
            "  mtputty_password.py encrypt root server01 password123\n\n"
            "note:\n"
            "  Empty decrypted passwords are printed as [EMPTY]."
        ),
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    decrypt_parser = subparsers.add_parser("decrypt", help="decrypt a Base64 XML password")
    decrypt_parser.add_argument("username", help="Node UserName value")
    decrypt_parser.add_argument("servername", help="Node ServerName value")
    decrypt_parser.add_argument("password", help="Node Password Base64 value")

    encrypt_parser = subparsers.add_parser("encrypt", help="encrypt a plaintext password")
    encrypt_parser.add_argument("username", help="Node UserName value")
    encrypt_parser.add_argument("servername", help="Node ServerName value")
    encrypt_parser.add_argument("password", help="plaintext password")

    return parser

# ------------------------------------------------------------------------------- #

args = build_parser().parse_args()

if args.command == "decrypt":
    print(decryptPassword(args.username, args.servername, args.password) or "[EMPTY]")
elif args.command == "encrypt":
    print(encryptPassword(args.username, args.servername, args.password))
<Putty>
	<Node Type="1">
		<SavedSession>desert</SavedSession>
		<DisplayName>192.168.0.15</DisplayName>
		<ServerName>192.168.0.15</ServerName>
		<PuttyConType>4</PuttyConType>
		<Port>22</Port>
		<UserName></UserName>
		<Password>k6LrFZbnRFk=</Password>
		<PasswordDelay>0</PasswordDelay>
		<CLParams>-load desert 192.168.0.15 -ssh -P 22</CLParams>
		<ScriptDelay>0</ScriptDelay>
	</Node>
</Putty>
./mtputty_password.py decrypt "" 192.168.0.15 k6LrFZbnRFk=
[EMPTY]

./mtputty_password.py encrypt "" 192.168.0.15 ""
k6LrFZbnRFk=
<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>
./mtputty_password.py decrypt onadm 192.168.2.1 R7eUjM9rncA=
123

@VeNoMouS
Copy link
Copy Markdown

VeNoMouS commented May 19, 2026

  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