Keenan Wurth
Keenan Wurth

Reputation: 61

Launch Elevated CMD.exe from Powershell

I am trying to launch an elevated CMD window from PowerShell but I am running into some issues. Below is the Code I have now. There is an admin account on the machine that has the username of "test" and a Password of "test"

$username = "test"
$password = ConvertTo-SecureString "test" -AsPlainText -Force
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $password
Start-Process "cmd.exe" -Credential $cred

This is all working fine for running an application from the user profile with no administrator rights that this script sits in, but when calling the cmd.exe it launches as expected with elevated rights but then immediately closes.

I have also tried calling it with the following:

Start-Process "cmd.exe" -Credential $cred -ArgumentList '/k'

This also is not working.
I tested the elevated permissions by passing in an argument as follows and this works fine.

Start-Process "cmd.exe" -Credential $cred -ArgumentList 'dir > dir.txt'

This will write out a dir.txt file to the C:\Windows\System32\WindowsPowerShell\v1.0 directory which is blocked on the user account but not for the administrator account test.

Any help on getting a persistent cmd window to show would be greatly appreciated.

Thanks

Upvotes: 5

Views: 26224

Answers (2)

mklement0
mklement0

Reputation: 440297

Note: SomeShinyObject came up with the fundamentals of the approach in his answer, but his parameter-passing technique is not robust (update: since corrected) - do not use script blocks in lieu of strings - see bottom.

  • -Verb RunAs is what makes Start-Process launch a process elevated.

  • However, -Verb RunAs cannot be combined with the -Credential parameter, so you cannot directly control under what user account the elevation happens - but that is generally not necessary:

    • If the current user is an administrator, elevation invariably happens in the context of that user, with the GUI prompt just asking for confirmation.
    • Otherwise, a GUI dialog is shown, asking for an administrator's username and password (with the username field blank).

Security caveats:

  • Storing a password as plain text is a security risk in general.
  • Additionally, if you let non-administrative users execute the code below with stored admin credentials, you're effectively giving them administrative rights.

If you still want to implement your script as specified, a workaround requires nesting 2 Start-Process calls:

  • The 1st one runs an (invariably) non-elevated command invisibly in the context of the specified user - assumed to be an administrative user - using -Credential.

    • Caveat: It is advisable to also specify a -WorkingDir argument that the specified user is known to have permissions to access[1] - otherwise, the call fails with a The directory name is invalid. error; -WorkingDirectory C:\ is usually a safe bet.
  • The 2nd one, embedded in the 1st, then uses -Verb RunAs to run the target command elevated, which then happens in the context of the specified user.

    • Note: Even with a credentials object that includes the password, you will still get the yes/no UAC prompt to confirm the intent to elevate - unless UAC has been turned off (which is not advisable).

    • The working directory will invariably be $env:SYSTEMROOT\System32; -Verb RunAs ignores even a -WorkingDirectory value; if you want to change to a specific directory, embed a cd command in the command passed to cmd.exe; the bottom section of this related answer shows this technique with a powershell.exe / Set-Location call.

This command does exactly what you asked for - please note the security caveat:

# Construct the credentials object
$username = "jdoe"
# CAVEAT: Storing a password as plain text is a security risk in general.
#         Additionally, if you let non-administrative users execute this 
#         code with a stored password, you're effectively giving them
#         administrative rights.
$password = ConvertTo-SecureString "test" -AsPlainText -Force    
$cred = New-Object PSCredential -Args $username, $password

# Start an elevated Command Prompt (cmd) as user $username.
# -WorkingDirectory C:\ is used to ensure that the target user has
# a valid working dir.
Start-Process powershell.exe -Credential $cred -WorkingDirectory C:\ -WindowStyle Hidden `
 '-noprofile -command "Start-Process cmd.exe -Verb RunAs"'

Note that the embedded, 2nd command is passed as a single string to the (implied)
-ArgumentList (a.k.a. -Args) parameter.

In this simple case, with only 1 level of embedded quoting - the " instances inside the '...' string - and no need for expansions (string interpolation), passing a single string is a viable option, but with more sophisticated commands quoting gets tricky.

-ArgumentList is defined as type [string[]], i.e., an array of string arguments. If you pass multiple, ,-separated arguments, it is PowerShell that synthesizes the command line for you:

  • Caveat: A long-standing bug unfortunately requires that argument with embedded spaces be enclosed in embedded double-quoting - see this answer for details.

The following command demonstrates this technique: It is a variant that passes a command for cmd.exe to execute through, and uses a variable reference in that command:

$msg = 'This is an elevated Command Prompt.'

Start-Process powershell.exe -Credential $cred -WindowStyle Hidden -Args `
 '-noprofile', '-command', "Start-Process cmd.exe -Verb RunAs -Args /k, echo, '$msg'"

The cmd.exe command that is ultimately executed (with elevation) is:
cmd /k echo This is an elevated Command Prompt.


Optional Reading: Why using script blocks in lieu of strings is ill-advised

tl;dr

  • Do not get into the habit of using script blocks where strings are expected. While convenient, it is not robust, and remembering when and why it will fail is nontrivial.

At first glance, script blocks ({ ... }) seem like a convenient option:

Start-Process cmd -ArgumentList { /k echo hi! }

The above executes cmd /k echo hi! in a new console window, as expected. The syntax is convenient, because the { ... } seemingly provide a context in which quoting is easy: you're free to use embedded " and ' instances to construct your command line.

However, what happens behind the scenes is that a script block is converted to a string, because that's the type of argument(s) -ArgumentList expects, and when a script block is converted to a string, its literal contents - everything between { and } - is used.
This means that no string interpolation takes place, so you cannot use variables or subexpressions.

Take this attempt to pass a command based on a variable:

 Start-Process cmd -ArgumentList { /k echo Honey, I`'m $HOME! }

What this will execute is: cmd /k echo Honey, I'm $HOME! - $HOME was not expanded.

By contrast, passing either an interpolated string or the arguments individually works as intended:

# As a single string (argument list):
Start-Process cmd -ArgumentList "/k echo Honey, I'm $HOME!"

# As an array of arguments:
Start-Process cmd -ArgumentList /k, echo, "Honey, I'm $HOME!"

$HOME is expanded (interpolated) in both cases, and something like
cmd /k echo Honey, I'm C:\Users\jdoe is executed.


[1] Note that while administrators usually have access to the same directories that a given user has, this only applies if the process running with an administrator user identity is already elevated, which is by definition not the case here; of necessity, a non-elevated process with as the target admin user must be created first, before elevation can be requested as that user.

Upvotes: 7

SomeShinyObject
SomeShinyObject

Reputation: 7821

The best way is to double up your Start-Process first with a -Credential parameter with your admin credentials and then with a -Verb runas on your second Start-Process. After that it gets a little complicated with quoting for CMD.exe.

Overall, it should look something like this.

Start-Process PowerShell -ArgumentList {-noexit -noprofile -Command "Start-Process powershell -argumentlist {-command cmd.exe -args \"/K #yourcommands# \"}" -verb runas} -Credential $Cred

So scratch this. Read mklement0's answer as to why

There's always opportunity to learn more and I didn't know wrapping an ArgumentList in a ScriptBlock prevented variable expansion. So...don't do that.

The method stays the same, though. You'll still need two Start-Process calls, just now you have to get the quoting right.

#Both of these work
Start-Process powershell -Credential $cred -ArgumentList "-noprofile", "-command", "Start-Process cmd.exe -Verb RunAs -ArgumentList /k, echo, 'something'"
# $Something will expand into it's value rather than literally
Start-Process powershell -Credential $cred -ArgumentList "-noprofile", "-command", "Start-Process cmd.exe -Verb RunAs -ArgumentList /k, echo, '$something'"

Upvotes: 2

Related Questions