Powershell: sudo command does not work in remote session to Linux machine

Created on 27 Jul 2016  ·  13Comments  ·  Source: PowerShell/PowerShell

Steps to reproduce

  1. Build and setup SSH based remoting (see docs) on a Linux machine
  2. Start an interactive remote session to the same machine
    Enter-PSSession -HostName -UserName
    [] C:> sudo ls /proc/powershell

    Expected behavior

sudo command to run in remote interactive session.

Actual behavior

sudo : sudo: no tty present and no askpass program specified
    + CategoryInfo          : NotSpecified: (sudo: no tty pr...ogram specified:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

Environment data

Name Value


PSVersion 5.1.10032.0
PSEdition PowerShellCore
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 3.0.0.0
GitCommitId v0.6.0-392-ga5b5dc576b8006c0d0a83a5c538f382d9fa65dc6
CLRVersion
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1

Size-MultipleWeeks WG-Remoting

Most helpful comment

Just to add some more info to the conversation here is a quick and dirty script that @SeeminglyScience wrote (slightly tweaked)

#!/usr/bin/env pwsh

# Parent is sudo, grandparent is the pwsh process
$parentPid = (Get-Process -Id $pid).Parent.Parent.Id

$ci = [Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($parentPid)
$rs = $ps = $null
try {
    $rs = [runspacefactory]::CreateRunspace($ci)
    $rs.Open()
    $ps = [powershell]::Create()
    $ps.Runspace = $rs
    $null = $ps.AddScript{
        $rs = Get-Runspace 1
        $remoteHost = $rs.GetType().GetProperty('Host', 60).GetValue($rs)
        $remoteHost.UI.ReadLine()
    }

    return $ps.Invoke()
} finally {
    if ($null -ne $ps) {
        $ps.Dispose()
    }

    if ($null -ne $rs) {
        $rs.Dispose()
    }
}

Making that executable will allow sudo to prompt the user for the password and will work over a PSRemoting session. While this "works" it is not a workable solution because:

  • ReadLine() echos the input characters to the console exposing your password
  • Because the user input is read as a string then the data transferred across the wire is the plaintext value

    • The transport may be encrypted but if using a recording device you have now exposed the password

  • There's no indication that the user is being prompted, should be solved by just writing a message to the host beforehand, might be something that the askpass interface provides

    • I think the process args may contain the prompt message which would be helpful

  • It access' the PSHost through an internal property, would be nice to have a public way of getting this

The last 2 issues aren't the end of the world and should be easily fixed but the first 2 are problematic. I was hoping to utilise secure strings as PSRP has a really nice mechanism to exchange the values themselves. Unfortunately it seems like the secure string session key exchange in PSRP does not work on Linux so I can't call ReadLineAsSecureString.

Using secure strings across PSRemoting should be achievable cross platform as it doesn't rely on DPAPI but rather an ephemeral 16 byte key that's encrypted using an RSA public key. All these things should be achievable in OpenSSL so hopefully it could be implemented one day.

Just for further info on secure strings through PSRemoting this is the error I get when trying to copy one to a remote session

$cred = Get-Credential
Invoke-Command hostname -ScriptBlock { $using:cred }

OpenError: 


$Error[0] | Select *

PSMessageDetails      : 
Exception             : System.Management.Automation.Internal.PSCryptoException
                           at System.Management.Automation.Internal.PSCryptoNativeUtils.CryptAcquireContext(PSSafeCryptProvHandle& phProv, 
                        String szContainer, String szProvider, UInt32 dwProvType, UInt32 dwFlags)
                           at System.Management.Automation.Internal.PSRSACryptoServiceProvider.GenerateKeyPair()
                           at System.Management.Automation.Internal.PSRemotingCryptoHelperClient.ExportLocalPublicKey(String& publicKeyAsString)
                           at System.Management.Automation.Remoting.ClientRemoteSessionImpl.StartKeyExchange()
TargetObject          : server2019.domain.test
CategoryInfo          : OpenError: (server2019.domain.test:String) [], PSCryptoException
FullyQualifiedErrorId : PSSessionStateBroken
ErrorDetails          : 
InvocationInfo        : 
ScriptStackTrace      : 
PipelineIterationInfo : {}


$error[0].Exception | Select *

TargetSite     : Boolean CryptAcquireContext(System.Management.Automation.Internal.PSSafeCryptProvHandle ByRef, System.String, System.String, 
                 UInt32, UInt32)
StackTrace     :    at System.Management.Automation.Internal.PSCryptoNativeUtils.CryptAcquireContext(PSSafeCryptProvHandle& phProv, String 
                 szContainer, String szProvider, UInt32 dwProvType, UInt32 dwFlags)
                    at System.Management.Automation.Internal.PSRSACryptoServiceProvider.GenerateKeyPair()
                    at System.Management.Automation.Internal.PSRemotingCryptoHelperClient.ExportLocalPublicKey(String& publicKeyAsString)
                    at System.Management.Automation.Remoting.ClientRemoteSessionImpl.StartKeyExchange()
Message        : 
Data           : {}
InnerException : 
HelpLink       : 
Source         : System.Management.Automation
HResult        : -2146233088

All 13 comments

Hit this trying to implement a way to run the Azure Arc for Server agent (which requires sudo) over a PSRP session... It'd be really great to have this.

@SteveL-MSFT @TylerLeonhardt I see this issue has been open for many years now, probably because there is no easy fix. This is pretty much the only major limitation of pwsh on Linux to make it a viable shell alternative. Is this ticket the best place to discuss actively working towards making it happen?

I suppose one approach could be to make a special "pwsh-askpass" that could really just be "pwsh" launched in "--askpass" mode, where it would implement a regular askpass interactive prompt. The twist would be that we'd detect the parent pwsh shell, and use one of the PowerShell mechanisms for IPC, allowing us to somehow prompt interactively in the PowerShell client side.

Bonus points for properly delegating credentials over PSRP such that they never appear in logs anywhere (actually, that should be a requirement, but an intermediate solution would be acceptable). What do you think?

Conceptually that makes sense to me. Assuming a pwsh-askpass can connect to the parent pwsh where the actual remote host is, we'd maybe do something similar to what Get-Credential or Read-Host -AsSecureString does:

image

@TylerLeonhardt do you think this would require an improvement to the PowerShell remoting protocol? I know MS-PSRP treats credentials differently when transferring them to the remote system, maybe all we need is to use a secure string to benefit from that. What you suggested looks perfectly fine in terms of usability at least for a V1 (right now it is just plain unusable).

For a V2, I wonder if we couldn't go one step further and make it possible to override the client-side "askpass" implementation, to allow PowerShell clients to either show a GUI prompt, or even make a special module that would grab the username+password from one of the new secret management modules.

Anyway, back to just getting a V1 done for now. Here is my understanding:

  • an 'askpass' program just prompts for the password, and outputs it to stdout, which is captured by the parent process
  • "pwsh-askpass" could detect its parent pwsh process id, and then somehow attach to a runspace inside that parent
  • once attached to the parent pwsh, find a way to trigger the dynamic password prompt inside the current shell (is it possible?)
  • when the password prompt completes, pwsh-askpass outputs the password to stdout, and everybody is happy.

I'm thinking that one could literally try making a PoC using a single PowerShell script called "pwsh-askpass" that you make executable. One quicky and dirty way to get started would be to make it output a hardcoded password just to see if the plumbing is done right at the "askpass" level. Once that is confirmed, we can work towards prompting the password for real.

As for attaching to the parent pwsh runspace, I don't know if it is feasible the way I described, but this presentation on PowerShell Remoting Internals does show a lot of interesting things that could be used as a starting point: https://www.youtube.com/watch?v=MyQGk29w-BM&t=780s

Yep this all sounds great to me. In PowerShell, you can get the parent process by doing:

(Get-Process $PID).Parent

We'd probably continue grabbing .Parent until we find another PowerShell... Then use Enter-PSHostProcess to enter said parent.

That's when it gets interesting. I don't know how to trigger that secure string prompt but it's possible that all you need to do is the Read-Host without having to go into another runspace...

.... This actually could work.

@awakecoding, @TylerLeonhardt thanks for reviving this. What is discussed here sounds possible. I too was thinking about using askpass to provide the password, but I am not very knowledgeable about it. I suspect the remoting protocol (PSRP) will have to be updated as mentioned above.

So I fiddled with this a tad...

❯❯❯ Enter-PSSession $windows
[tyleonha-desk1.redmond.corp.microsoft.com]: PS C:\Users\tyleonha\Documents> $rs = [runspacefactory]::CreateRunspace($Host)
[tyleonha-desk1.redmond.corp.microsoft.com]: PS C:\Users\tyleonha\Documents> $rs.Open()
[tyleonha-desk1.redmond.corp.microsoft.com]: PS C:\Users\tyleonha\Documents> $ps = [PowerShell]::Create($rs)
[tyleonha-desk1.redmond.corp.microsoft.com]: PS C:\Users\tyleonha\Documents> $ps.AddCommand("Read-Host").AddParameter("AsSecureString").Invoke()
WARNING: A script or application on the remote computer TYLEONHA-DESK1.REDMOND.CORP.MICROSOFT.COM is asking to read a line securely. Enter sensitive information, such as your credentials, only if you trust the remote computer and the application or script that is requesting it.
****
System.Security.SecureString
[tyleonha-desk1.redmond.corp.microsoft.com]: PS C:\Users\tyleonha\Documents>

Looks like as long as the _other_ runspace has the RemoteHost, then it might work...

$host.UI.ReadLineAsSecureString()

is also an option...

The outstanding questions I have are:

  • Can another runspace get a reference to the PSHost object we need?
  • Is it thread safe?

I did some further experimentation and discussed it with some of the people in #powershell-help. For the first step, which is to get sudo to use our own askpass program, it turns out that you need to do two things:

  • Add '-A' or "--askpass" as an argument to sudo, which is often done using an alias in bash.
  • Set the SUDO_ASKPASS variable (only works with -A), but this can also be set in sudo.conf

I do not yet know of a way to make it accept our own askpass program without adding the -A argument, but I would consider this a minor issue for now that we can solve later. The end goal is to make it without having to modify scripts calling sudo directly. As a matter of fact, I came up with a quick and dirty solution to work around it for now, using a fake "sudo" program that I added at the beginning of PATH such that it would get picked up instead of the real one.

I created a directory called "/yolo" and placed two simple scripts in there:

  • simple 'askpass' script that echoes my password to stdout
  • simple 'sudo' script that sets SUDO_ASKPASS and calls the real sudo, adding -A

Here is what it looks like:

export PATH=/yolo:$PATH
wayk@pwsh-demo:~$ sudo whoami
root
wayk@pwsh-demo:~$ cat /yolo/sudo
#!/bin/sh
export SUDO_ASKPASS=/yolo/askpass
/usr/bin/sudo -A -n $@
wayk@pwsh-demo:~$ cat /yolo/askpass 
#!/bin/sh
echo "password"

I consider the first part of the PoC (finding a way to inject our own askpass program that gets picked up by sudo) completed with the above, moving on to getting the askpass program to prompt inside the parent PowerShell runspace. We can come back to finding better, less hackish ways to achieve the same result afterwards.

Just to add some more info to the conversation here is a quick and dirty script that @SeeminglyScience wrote (slightly tweaked)

#!/usr/bin/env pwsh

# Parent is sudo, grandparent is the pwsh process
$parentPid = (Get-Process -Id $pid).Parent.Parent.Id

$ci = [Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($parentPid)
$rs = $ps = $null
try {
    $rs = [runspacefactory]::CreateRunspace($ci)
    $rs.Open()
    $ps = [powershell]::Create()
    $ps.Runspace = $rs
    $null = $ps.AddScript{
        $rs = Get-Runspace 1
        $remoteHost = $rs.GetType().GetProperty('Host', 60).GetValue($rs)
        $remoteHost.UI.ReadLine()
    }

    return $ps.Invoke()
} finally {
    if ($null -ne $ps) {
        $ps.Dispose()
    }

    if ($null -ne $rs) {
        $rs.Dispose()
    }
}

Making that executable will allow sudo to prompt the user for the password and will work over a PSRemoting session. While this "works" it is not a workable solution because:

  • ReadLine() echos the input characters to the console exposing your password
  • Because the user input is read as a string then the data transferred across the wire is the plaintext value

    • The transport may be encrypted but if using a recording device you have now exposed the password

  • There's no indication that the user is being prompted, should be solved by just writing a message to the host beforehand, might be something that the askpass interface provides

    • I think the process args may contain the prompt message which would be helpful

  • It access' the PSHost through an internal property, would be nice to have a public way of getting this

The last 2 issues aren't the end of the world and should be easily fixed but the first 2 are problematic. I was hoping to utilise secure strings as PSRP has a really nice mechanism to exchange the values themselves. Unfortunately it seems like the secure string session key exchange in PSRP does not work on Linux so I can't call ReadLineAsSecureString.

Using secure strings across PSRemoting should be achievable cross platform as it doesn't rely on DPAPI but rather an ephemeral 16 byte key that's encrypted using an RSA public key. All these things should be achievable in OpenSSL so hopefully it could be implemented one day.

Just for further info on secure strings through PSRemoting this is the error I get when trying to copy one to a remote session

$cred = Get-Credential
Invoke-Command hostname -ScriptBlock { $using:cred }

OpenError: 


$Error[0] | Select *

PSMessageDetails      : 
Exception             : System.Management.Automation.Internal.PSCryptoException
                           at System.Management.Automation.Internal.PSCryptoNativeUtils.CryptAcquireContext(PSSafeCryptProvHandle& phProv, 
                        String szContainer, String szProvider, UInt32 dwProvType, UInt32 dwFlags)
                           at System.Management.Automation.Internal.PSRSACryptoServiceProvider.GenerateKeyPair()
                           at System.Management.Automation.Internal.PSRemotingCryptoHelperClient.ExportLocalPublicKey(String& publicKeyAsString)
                           at System.Management.Automation.Remoting.ClientRemoteSessionImpl.StartKeyExchange()
TargetObject          : server2019.domain.test
CategoryInfo          : OpenError: (server2019.domain.test:String) [], PSCryptoException
FullyQualifiedErrorId : PSSessionStateBroken
ErrorDetails          : 
InvocationInfo        : 
ScriptStackTrace      : 
PipelineIterationInfo : {}


$error[0].Exception | Select *

TargetSite     : Boolean CryptAcquireContext(System.Management.Automation.Internal.PSSafeCryptProvHandle ByRef, System.String, System.String, 
                 UInt32, UInt32)
StackTrace     :    at System.Management.Automation.Internal.PSCryptoNativeUtils.CryptAcquireContext(PSSafeCryptProvHandle& phProv, String 
                 szContainer, String szProvider, UInt32 dwProvType, UInt32 dwFlags)
                    at System.Management.Automation.Internal.PSRSACryptoServiceProvider.GenerateKeyPair()
                    at System.Management.Automation.Internal.PSRemotingCryptoHelperClient.ExportLocalPublicKey(String& publicKeyAsString)
                    at System.Management.Automation.Remoting.ClientRemoteSessionImpl.StartKeyExchange()
Message        : 
Data           : {}
InnerException : 
HelpLink       : 
Source         : System.Management.Automation
HResult        : -2146233088

Looks like https://github.com/PowerShell/PowerShell/pull/11185 has fixed the SecureString issue making it cross platform. I still need to test the latest preview to make sure but it definitely seems to be possible.

Thanks for looking into this. Yes, PowerShell remoting does now support passing SecureString objects over all platforms, but unfortunately was only added to 7.1+ versions.

I am not worried about prompting for sudo password over a remote session, that can be handled with remote host calls, and probably an update to the protocol (PSRP). My main concern was how to interface with sudo, and it looks like @awakecoding 'AskPass' is the mechanism to use. It would be nice if there was some sort of API hook, but the above PoC may be workable.

make it so

Anyone interested in prototyping this?

Was this page helpful?
0 / 5 - 0 ratings