From cb01e92fe2484bd53722dddbe93dd940ba990cf1 Mon Sep 17 00:00:00 2001 From: Schmitti91 Date: Sun, 15 Oct 2023 21:13:36 +0200 Subject: [PATCH 1/2] Enhancing the PowerShell Connection. The PowerShell script is designed to simplify multiple login attempts to a remote host using user-provided credentials, or not. In addition, granular error handling has been added to prevent errors, such as the conversion of empty or null strings to SecureString. Furthermore, hostname, username, and password are now passed as parameters in the PowerShell script block (#2195). --- .../Connection.Protocol.PowerShell.cs | 152 +++++++++++++++++- mRemoteNG/Language/Language.Designer.cs | 36 +++++ mRemoteNG/Language/Language.de.resx | 16 ++ mRemoteNG/Language/Language.nl.resx | 26 ++- mRemoteNG/Language/Language.resx | 16 ++ 5 files changed, 239 insertions(+), 7 deletions(-) diff --git a/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs b/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs index 6864a7ee..3a2d7922 100644 --- a/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs +++ b/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs @@ -41,8 +41,156 @@ namespace mRemoteNG.Connection.Protocol.PowerShell Padding = new Padding(0, 20, 0, 0) }; - _consoleControl.StartProcess(@"C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell.exe", - $@"-NoExit -Command ""$password = ConvertTo-SecureString ""'{_connectionInfo.Password}'"" -AsPlainText -Force; $cred = New-Object System.Management.Automation.PSCredential -ArgumentList @('{_connectionInfo.Domain}\{_connectionInfo.Username}', $password); Enter-PSSession -ComputerName {_connectionInfo.Hostname} -Credential $cred"""); + /* + * Prepair powershell script parameter and create script + */ + // Path to the Windows PowerShell executable; can be configured through options. + string psExe = @"C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell.exe"; + + // Maximum number of login attempts; can be configured through options. + int psLoginAttempts = 3; + + string psUsername; + if (string.IsNullOrEmpty(_connectionInfo.Domain)) + // Set the username without domain + psUsername = _connectionInfo.Username; + else + // Set the username to Domain\Username if Domain is not empty + psUsername = $"{_connectionInfo.Domain}\\{_connectionInfo.Username}"; + + /* + * The PowerShell script is designed to facilitate multiple login attempts to a remote host using user-provided credentials, + * with an option to specify the maximum number of attempts. + * It handles username and password entry, attempts to establish a PSSession, and reports on login outcomes, ensuring a graceful exit in case of repeated failures. + */ + string psScriptBlock = $@" + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [String] $Hostname, # The hostname you want to connect to (mandatory parameter) + [String] $Username, # The username, if provided + [String] $Password, # The password for authentication + [int] $LoginAttempts = 3 # The number of login attempts, default set to 3 + ) + + # Dynamically parameters + DynamicParam {{ + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary; + + # SecurePassword + $ParameterName = 'SecurePassword'; + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]; + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute; + $ParameterAttribute.Mandatory = $False; + $AttributeCollection.Add($ParameterAttribute); + try {{ + # Try converting the stored password to a secure string + $PSBoundParameters.$($ParameterName) = ConvertTo-SecureString $Password -AsPlainText -Force -ErrorAction Stop; + }} + catch{{ + # Create an empty SecureString if the password cannot be converted (if the password is empty) + $PSBoundParameters.$($ParameterName) = [SecureString]::new(); + }} + $PSBoundParameters.Password = $null; + $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [SecureString], $AttributeCollection); + $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter); + + return $RuntimeParameterDictionary; + }} + + process {{ + # Initialize the $cred variable + $cred = $null; + + # Check if a username is provided. + # Please note that some logins may not require a password. Therefore, the first attempt can fail if a username is set and a password is not. + if (-not [string]::IsNullOrEmpty($PSBoundParameters.Username)) {{ + # Create a PSCredential object with the provided username and password + $cred = New-Object System.Management.Automation.PSCredential ($PSBoundParameters.Username, $PSBoundParameters.SecurePassword); + + # It will be needed to determine whether the login credentials were provided or not. + $providedCred = $true; + }} + + # At least one login attempt is required to ensure functionality + if ($LoginAttempts -lt 0) {{$LoginAttempts = 1;}} + + # Loop for connection attempts for $LoginAttempts + for ($i = 0; $i -lt $LoginAttempts; $i++) {{ + <# + The cases for when 'Get-Credential' is needed: + 1. `$i -gt 0`: Indicates the first login attempt has failed. + 2. `-not $cred`: Implies that no credentials have been sent to the function. + 3. `$cred -and $cred.UserName -match ""^([^\\]+\\)$""`: Implies that only the regular Windows domain name is parameterized. + + NOTE: + If the regular expression is used in an if statement such as if (.... -match ""^[^\\]+\\$"")..., + there will be conversion problems with the string. This can then lead to errors when executing PowerShell. + + To work around this problem, create the $regex variable and enclose the expression in single quotes. + Due to the use of variables, double quotes are no longer required in the if statement, and it can be written as follows: if (.... -match $Regex).... + This approach avoids possible string conversion problems caused by double quotes. + #> + [string] $regex = '^[^\\]+\\$' + if ($i -gt 0 -or (-not $cred) -or ($cred -and $cred.Username -match $regex)){{ + # Prompt for credentials with a message and pre-fill username if available + try {{ + if (-not [string]::IsNullOrEmpty($cred.UserName)) {{ + $cred = Get-Credential -Message $Hostname -UserName $cred.UserName -ErrorAction Stop; + }} + else {{ + $cred = Get-Credential -Message $Hostname -ErrorAction Stop; + }} + + $providedCred = $false; # provided creds are overwritten + }} + catch {{ + # If something is wrong for $cred + $cred = $null + }} + }} + + # Try PSSession + try {{ + # If credentials are not provided, abort the loop (mean Get-Credential is canceled) + if ( $cred ) {{ + Enter-PSSession -ComputerName $Hostname -Credential $cred -ErrorAction Stop; + break; # Successfully entered PSSession, exit the loop + }} + else {{ + write-Host '{Language.PsCanceled}'; + exit; + }} + }} + # Handle the case when PSSession entry fails + catch [System.Management.Automation.Remoting.PSRemotingTransportException]{{ + If (-not $providedCred) {{ + Write-Host '{Language.PsConnectionFailed}'; + Write-Host; + }} + else {{ + $LoginAttempts++; + }} + }} + catch {{ + # Handle other exceptions + Write-Host $_.Exception.Message; + Write-Host; + Write-Host '{Language.PsFailed}'; + exit; + }} + }} + + # Maximum login attempts reached + if ($i -ge $LoginAttempts) {{ + Write-Host '{Language.PsLoginAttempts}'; + exit; + }} + }} + "; + + // Setup process for script with arguments + _consoleControl.StartProcess(psExe, $@"-NoExit -NoProfile -Command ""& {{ {psScriptBlock} }}"" -Hostname ""'{_connectionInfo.Hostname}'"" -Username ""'{psUsername}'"" -Password ""'{_connectionInfo.Password}'"" -LoginAttempts {psLoginAttempts}"); while (!_consoleControl.IsHandleCreated) break; _handle = _consoleControl.Handle; diff --git a/mRemoteNG/Language/Language.Designer.cs b/mRemoteNG/Language/Language.Designer.cs index bbca400d..39bcb24a 100644 --- a/mRemoteNG/Language/Language.Designer.cs +++ b/mRemoteNG/Language/Language.Designer.cs @@ -4513,6 +4513,42 @@ namespace mRemoteNG.Resources.Language { } } + /// + /// Looks up a localized string similar to Login canceled! Restart if necessary.. + /// + internal static string PsCanceled { + get { + return ResourceManager.GetString("PsCanceled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please verify username and password and try again.If PSRemoting is not enabled on the server, enable it first.. + /// + internal static string PsConnectionFailed { + get { + return ResourceManager.GetString("PsConnectionFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login failed!. + /// + internal static string PsFailed { + get { + return ResourceManager.GetString("PsFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum login attempts exceeded. Please connect again.. + /// + internal static string PsLoginAttempts { + get { + return ResourceManager.GetString("PsLoginAttempts", resourceCulture); + } + } + /// /// Looks up a localized string similar to Dispose of Putty process failed!. /// diff --git a/mRemoteNG/Language/Language.de.resx b/mRemoteNG/Language/Language.de.resx index 9ffeed4a..8810ce90 100644 --- a/mRemoteNG/Language/Language.de.resx +++ b/mRemoteNG/Language/Language.de.resx @@ -2031,4 +2031,20 @@ Nightly umfasst Alphas, Betas und Release Candidates. Vererbung auf Kinder anwenden + + Anmeldung abgebrochen! Bei bedarf neu einleiten. + C# to Powershell transfer issue with encoding possible + + + Bitte Benutzernamen und Kennwort abgleichen und erneut versuchen. Wenn PSRemoting auf dem Server nicht aktiviert ist, bitte zuerst aktivieren. + C# to Powershell transfer issue with encoding possible + + + Anmeldung fehlgeschlagen! + C# to Powershell transfer issue with encoding possible + + + Maximale Anmeldeversuche erreicht. Verbindung erneut initiieren. + C# to Powershell transfer issue with encoding possible + \ No newline at end of file diff --git a/mRemoteNG/Language/Language.nl.resx b/mRemoteNG/Language/Language.nl.resx index b35f4082..195e808f 100644 --- a/mRemoteNG/Language/Language.nl.resx +++ b/mRemoteNG/Language/Language.nl.resx @@ -59,7 +59,7 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - + @@ -105,17 +105,17 @@ - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - text/microsoft-resx 2.0 + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Over @@ -1572,4 +1572,20 @@ mRemoteNG zal nu worden gesloten en beginnen met de installatie. Ja + + Inloggen geannuleerd! Herstart indien nodig. + C# to Powershell transfer issue with encoding possible + + + Controleer de gebruikersnaam en wachtwoord en probeer het opnieuw. Als PSRemoting niet is ingeschakeld op de server, schakel dit dan eerst in. + C# to Powershell transfer issue with encoding possible + + + Aanmelding mislukt! + C# to Powershell transfer issue with encoding possible + + + Maximaal aantal inlogpogingen overschreden. Maak opnieuw verbinding. + C# to Powershell transfer issue with encoding possible + \ No newline at end of file diff --git a/mRemoteNG/Language/Language.resx b/mRemoteNG/Language/Language.resx index 9c6c99bd..d2cbc20f 100644 --- a/mRemoteNG/Language/Language.resx +++ b/mRemoteNG/Language/Language.resx @@ -2343,4 +2343,20 @@ Nightly Channel includes Alphas, Betas & Release Candidates. Use RD Gateway access token + + Login canceled! Restart if necessary. + C# to Powershell transfer issue with encoding possible + + + Please verify username and password and try again.If PSRemoting is not enabled on the server, enable it first. + C# to Powershell transfer issue with encoding possible + + + Login failed! + C# to Powershell transfer issue with encoding possible + + + Maximum login attempts exceeded. Please connect again. + C# to Powershell transfer issue with encoding possible + \ No newline at end of file From 7132c6e7c25574e8c87b8355d55316dd8efc1f13 Mon Sep 17 00:00:00 2001 From: Schmitti91 Date: Sun, 15 Oct 2023 21:48:01 +0200 Subject: [PATCH 2/2] Removing the "-NoProfile" option from PowerShell.exe parameters. --- .../Protocol/PowerShell/Connection.Protocol.PowerShell.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs b/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs index 3a2d7922..cb8d0da9 100644 --- a/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs +++ b/mRemoteNG/Connection/Protocol/PowerShell/Connection.Protocol.PowerShell.cs @@ -190,7 +190,8 @@ namespace mRemoteNG.Connection.Protocol.PowerShell "; // Setup process for script with arguments - _consoleControl.StartProcess(psExe, $@"-NoExit -NoProfile -Command ""& {{ {psScriptBlock} }}"" -Hostname ""'{_connectionInfo.Hostname}'"" -Username ""'{psUsername}'"" -Password ""'{_connectionInfo.Password}'"" -LoginAttempts {psLoginAttempts}"); + //* The -NoProfile parameter would be a valuable addition but should be able to be deactivated. + _consoleControl.StartProcess(psExe, $@"-NoExit -Command ""& {{ {psScriptBlock} }}"" -Hostname ""'{_connectionInfo.Hostname}'"" -Username ""'{psUsername}'"" -Password ""'{_connectionInfo.Password}'"" -LoginAttempts {psLoginAttempts}"); while (!_consoleControl.IsHandleCreated) break; _handle = _consoleControl.Handle;