Powershell - rerun the script with different credentials from itself - powershell

I have currently had to take a huge leap from my unix scripting to the MS side of things and found myself overwhelmed with PowerShell.
My situation is as follows:
I have a script script.ps1 which can be only run under specific windows account. In order to facilitate the use, it was decided that if user runs the script from a different account, it will pop up a query for credentials and restart itself from within (similarly to recursion), but importantly - maintaining the input parameters.
I have found out, that the Invoke-Command is probably what I am looking for, but I cannot seem to be able to build the PS query for this.
my code snippet looks like
if(!([System.Environment]::UserName -eq $user)){
$Credential = Get-Credential -credential INTRANET\$user
Invoke-Command -FilePath $script -Credential $Credential -ArgumentList $arguments
}
where $user contains the desired user, $script contains filepath to the script.ps1 and $arguments contain command line arguments that were passed to the script as a String, i. e. -order 66 -location UAT
but currently I get an error
Parameter set cannot be resolved using the specified named parameters.
...
FullyQualifiedErrorId : AmbiguousParameterSet
I tried shuffling the parameters around, I tried using Start-Process instead of Invoke-Command, but everything resulted in same or similar errors.
Also, because I am really new to the powershell, please do not hesitate to offer different solution, if it is viable. I do not know the capabilities of the language well.
Lastly, please note that the starting point is always powershell prompt running with non-elevated user account. Unfortunately, the option to start up powershell under a different account in the first place is not available to us.

The problem probably is that you specify the parameters stored in the variable $arguments as string in the regular format like you said: -order 66 -location UAT
The parameter -ArgumentList works differently, its an array used for array splatting. So you can't pass the values by the parameter name. You have to pass the values by parameter order, e.g.:
$Arguments = #(66,'uat')
Invoke-Command -FilePath $script -Credential $Credential -ArgumentList $Arguments
See Parameter Argumentlist.
See Array Splatting.
The value 66 is passed to the first parameter, the value uat to the 2nd... So you must know the order of the parameters and insert the related values into the array at the right position.
To control the position of the parameters, the param specification in the other script should at least have:
param (
[parameter(Position=1)]
[int]$order,
[parameter(Position=2)]
[string]$location
)

Related

.MapNetworkDrive with password from Get-Credential

I am working on installing software across multiple servers with various versions of Powershell.
Originally I was using New-PSDrive cmdlets to mount a drive and copy/install files from. Some servers now complain that New-PSDrive does not accept -Credential argument despite having PS4.0.
I am now trying to use .MapNetworkDrive for legacy purposes but cannot seem to be able to pass Password to it via a variable:
$creds = Get-Credential
$net = New-Object -ComObject WScript.Network
$net.MapNetworkDrive("u:", "\\myshare", $false, "$creds.UserName","$creds.Password")
I get an error: Exception calling "MapNetworkDrive" with "5" argument(s): "The specified network password is not correct.
Any suggestions?
Many thanks,
you have two problems going on here. [grin]
WScript COM objects do not understand SecureString objects
your $Creds.Password is stored as a System.Security.SecureString and that is not something that the WScript stuff knows about.
you can see that with $Creds.Password.
you would need to use something like >>> $Creds.GetNetworkCredential().Password.
compound objects - like "$creds.UserName" - don't expand as expected when inside double quotes
you will get System.Management.Automation.PSCredential.UserName from that.
you need to force the variable-dot-property to evaluate before the string is expanded. thus >>> "$($Creds.UserName)".
so your line of code ...
$net.MapNetworkDrive("u:", "\\myshare", $false, "$creds.UserName","$creds.Password")
... would become something like this ...
$net.MapNetworkDrive(
"u:",
"\\myshare",
$false,
"$($creds.UserName)",
"$($Creds.GetNetworkCredential().Password)"
)
however, i suspect you could leave off the quotes for the last two lines ... and then would not need the $() wrapper. [grin]

Unable to pass hashtable of parameters to another script using Invoke-Command

I've built a script for dynamically changing an HTML template and sending it as an email, it uses a hashtable for the items to be changed due to a fluctuating number of items it may need to change (seemed a better option than an array)
The emailing and replacing of variables etc is all working correctly, where I'm having issues is when I try to call it from another script and pass in a hashtable with the replacements and to/fromsubject, I'm using Splatting as it seemed the best option, I have tried just passing in the hashtable as is, and tried start-job and Invoke-Expression
powershell version: 5.1
This is the code I'm using to call the emailing script
$TemplateReplacements = #{}
$TemplateReplacements.Add("EmailTo","firstname.lastname#company.com.au")
$TemplateReplacements.Add("EmailFrom","ISnotifications#company.com.au")
$TemplateReplacements.Add("EmailSubject","Test Test Test")
$TemplateReplacements.Add("EmailTemplate","\ZeroLicenses.html")
$TemplateReplacements.Add("Heading","Heading replaced from main script")
$TemplateReplacements.Add("1","Variable1 repalced from main script")
$TemplateReplacements.Add("2","Variable2 replaced from main script")
$TemplateReplacements.Add("3","Variable3 replaced from main script")
$TemplateReplacements.Add("4","Variable4 replaced from main script")
$ScriptToRun = "C:\Users\User1\Desktop\Projects\Powershell\EmailVarReplace-Test\Test3.ps1"
$params = #{
Replacements = $TemplateReplacements
}
Invoke-Command $ScriptToRun #Params
#start-job $ScriptToRun #params
In the email script thats been called I have this at the top hich I believe is all I need for it to accept the parameter?
[CmdletBinding()]
param ($Replacements)
I've been stumped on this for ages and just keep getting the error
Invoke-Command : A parameter cannot be found that matches parameter name 'Replacements'.
At C:\Users\User1\Desktop\Projects\Powershell\EmailTest.ps1:19 char:29
+ Invoke-Command $ScriptToRun #Params
+ ~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-Command], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.PowerShell.Commands.InvokeCommandCommand
What I'm expecting to happen is it passes the hashtable into the script so it can get the variables for where to send the email to, what address from, subject plus the replacements of variables in the email
I'm building the email script as a standalone module that later just needs templates created adn scripts calling it with the replacements made
Any help would be very appreciated
Also ignore the hard-coded path etc as it's still a testing script :p
Don't use Invoke-Command to invoke your script - Invoke-Command is virtually only ever useful in the context of remoting, i.e. if you use with the -ComputerName parameter to run a script of script block on one or more remote computers.
Instead, use &, the call operator, to invoke your script, then splatting should work as intended:
& $ScriptToRun #Params
As for what you tried:
If you try splatting via Invoke-Command, the splatting is applied to its parameters, not to the parameters of the script you're invoking.
Therefore, a -Replacements parameter is looked for among Invoke-Command's parameters rather than your script's - and there is no such parameter, which explains the error you got.

Start-Process, mklink and encrypted/stored credentials, oh my

I am working on a way to create a Symlink as a standard user, to address the situation outlined here.
I have created a password file and an AES key as shown here.
And I have this code, which without the credential stuff, but run from an elevated ISE, works as intended, creating a symlink in the root of C that points to the created folder in root of C.
But, when run unelevated it doesn't create the symlink, nor does it throw an error of any kind. It acts the same as if there was no credentials in use.
$passwordFile = "\\Mac\Support\Px Tools\x_PS Dev\SymLink_password.txt"
$keyFile = "\\Mac\Support\Px Tools\x_PS Dev\SymLink_AES.key"
$user = 'Px_Install'
$key = Get-Content $keyFile
$credential = New-Object -typeName:System.Management.Automation.PSCredential -argumentList:#($user, (Get-Content $passwordFile | ConvertTo-SecureString -key:$key))
if (-not (Test-Path 'C:\_Real')) {
New-Item 'C:\_Real' -itemType:directory > $null
}
if (-not (Test-Path 'C:\_Real\real.txt')) {
New-Item 'C:\_Real\real.txt' -itemType:file > $null
}
try {
Start-Process -filePath:cmd.exe -windowStyle:hidden -argumentList:('/c', 'mklink', '/d', 'C:\C4RLink', "`"C:\_Real`"") -credential:$credential -errorAction:stop
} catch {
Write-Host "Error"
}
So, three questions I guess.
1: Is there any way to test the validity of the created credential? I used $credential.GetType and it returns
OverloadDefinitions
-------------------
type GetType()
Which may or may not be correct, not sure.
2: Is there something wrong with my use of Start-Process?
3: Is there a way to actually trap meaningful errors or is cmd.exe so primitive I am stuck checking to see if the link exists post Start-Process and throwing my own error?
I tried
$results = Start-Process -filePath:cmd.exe -windowStyle:hidden -argumentList:('/c', 'mklink', '/d', 'C:\C4RLink', "`"C:\_Real`"") -credential:$credential -errorAction:stop -passThru
Write-Host "$results"
and it produces System.Diagnostics.Process (cmd) which isn't so helpful.
Speaking of Windows 7, I just tested it in Windows 7/PS2.0, and it DOES throw an error, but in Windows 10 it doesn't. Gawd Micros0ft, can't you get your shit together, EVER? but, maybe a thread to follow. Also going to try getting credentials another way, to eliminate that variable.
FWIW, I tried NOT wrapping the argument list in an array, in fact I started with that. But it didn't work so I tried the array on a lark.
EDIT: So, trying it in Windows 7 does produce an error, which is Parameter set cannot be resolved using the specified named parameters. I also realized I needed -verb:Runas in there. Added that, and switched my credentials to use Get-Credential for now. But still getting parameter set issues. Sigh.
Edit2: Seems to not like -verb or -windowsStyle in Windows 7/PS2.0. The latter is no big deal I guess, but -verb is pretty much required to get this to work methinks.
Edit3: nope, seems not to like -verb in Windows 10 either. But I have it reporting exceptions now, so thats a form of progress.
EDIT4: getting closer. I now have this
Start-Process powershell -credential (Get-Credential 'Px_Install') -argumentList "-noprofile -command &{Start-Process -filePath cmd.exe -argumentList '/c', 'mklink', '/d', 'C:\C4RLink', 'C:\_Real' -verb runas}"
And it works, but it raises a UAC dialog, which pretty much makes it useless.

How to start a new PowerShell session from another PS session, and run a CmdLet with splatted Parameters

The Goal:
Is to be able to test to see if PowerShell v6 is installed (which is OK), and if it is, then to invoke that shell for certain CmdLets. This will be invoked within a script running in PowerShell v5.1. I cannot shift fully to v6 as there are other dependencies that do not yet work in this environment, however, v6 offers significant optimisations on certain CmdLets that lead to an improvement in operation of over 200 times (specifically, an Invoke-WebRequest where the call will lead to a download of a large file - in v5.1 a 4GB file will take over 1 hour to download, in v6 this will take approximately 30 seconds using the same machines on the same subnet.
Additional Points:
However, I also build up a set of dynamic parameters that are used to splat into the CmdLets parameter list. For example, a built parameter list would look something like:
$SplatParms = #{
Method = "Get"
Uri = $resource
Credential = $Creds
Body = (ConvertTo-Json $data)
ContentType = "application/json"
}
And running the CmdLet normally would work as expected:
Invoke-RestMethod #SplatParms
What has been tried:
Over the past few days I have looked over various posts on this forum and elsewhere. We can create a simple script block the can be call and also works as expected:
$ConsoleCommand = { Invoke-RestMethod #SplatParms }
& $ConsoleCommand
However, attempting to pass the same thing in the Start-Process CmdLet fails, as I guess the parameter hash table is not being evaluated:
Start-Process pwsh -ArgumentList "-NoExit","-Command &{$ConsoleCommand}" -wait
Results in:
Invoke-RestMethod : Cannot validate argument on parameter 'Uri'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
At line:1 char:22
+ &{ Invoke-RestMethod #SplatParms }
Where next?
I guess I somehow have to pass in parameters as arguments so they can then be evaluated and splatted, however, the syntax eludes me. I'm not even sure if Start-Process is the best CmdLet to use, but rather should I look to something else, like Invoke-Command, or something completely different?
It would be awesome to get the result of this CmdLet back into the originating shell, but at the moment, it will simply take something that functions.
Note: In principle, the techniques in this answer can be applied not only to calling from Windows PowerShell to PowerShell Core, but also in the opposite direction, as well as to between instances of the same PowerShell edition, both on Windows and Unix.
You don't need Start-Process; you can invoke pwsh directly, with a script block:
pwsh -c { $SplatParms = $Args[0]; Invoke-RestMethod #SplatParms } -args $SplatParms
Note the need to pass the hashtable as an argument rather than as part of the script block.
Unfortunately, as of Windows PowerShell 5.1 / PowerShell Core 6.0.0, there is a problem with passing [PSCredential] instances this way - see bottom for a workaround.
This will execute synchronously and even print the output from the script block in the console.
The caveat is that capturing such output - either by assigning to a variable or redirecting to a file - can fail if instances of types that aren't available in the calling session are returned.
As a suboptimal workaround, you can use -o Text (-OutputFormat Text) thanks, PetSerAl to capture the output as text, exactly as it would print to the console
(run pwsh -h to see all options).
Output is by default returned in serialized CLIXML format, and the calling PowerShell sessions deserializes that back into objects. If the type of a serialized object is not recognized, an error occurs.
A simple example (execute from Windows PowerShell):
# This FAILS, but you can add `-o text` to capture the output as text.
WinPS> $output = pwsh -c { $PSVersionTable.PSVersion } # !! FAILS
pwsh : Cannot process the XML from the 'Output' stream of 'C:\Program Files\PowerShell\6.0.0\pwsh.exe':
SemanticVersion XML tag is not recognized. Line 1, position 82.
...
This fails, because $PSVersionTable.PSVersion is of type [System.Management.Automation.SemanticVersion] in PowerShell Core, which is a type not available in Windows PowerShell as of v5.1 (in Windows PowerShell, the same property's type is [System.Version]).
Workaround for the inability to pass a [PSCredential] instance:
pwsh -c {
$SplatParms = $Args[0];
$SplatParams.Credential = [pscredential] $SplatParams.Credential;
Invoke-RestMethod #SplatParms
} -args $SplatParms
Calling another PowerShell instance from within PowerShell using a script block involves serialization and deserialization of objects in CLIXML format, as also used in PowerShell remoting.
Generally, there are many .NET types that deserialization cannot faithfully recreate and in such cases creates [PSCustomObject] instances that emulate instances of the original type, with the (normally hidden) .pstypenames property reflecting the original type name prefixed with Deserialized.
As of Windows PowerShell 5.1 / PowerShell Core 6.0.0, this also happens with instances of [pscredential] ([System.Management.Automation.PSCredential]), which prevents their direct use in the target session - see this GitHub issue.
Fortunately, however, simply casting the deserialized object back to [pscredential] seems to work.
Try creating a New-PSSession against 6.0 within your 5.1 session.
After installing powershell core 6.0 and running Enable-PSRemoting, a new PSSessionConfiguration was created for 6.0:
PS > Get-PSSessionConfiguration
Name : microsoft.powershell
PSVersion : 5.1
StartupScript :
RunAsUser :
Permission : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
Name : microsoft.powershell.workflow
PSVersion : 5.1
StartupScript :
RunAsUser :
Permission : BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
Name : microsoft.powershell32
PSVersion : 5.1
StartupScript :
RunAsUser :
Permission : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
Name : PowerShell.v6.0.0
PSVersion : 6.0
StartupScript :
RunAsUser :
Permission : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
In your parent script, create new session using the 6.0 configuration name PowerShell.v6.0.0 and pass it to any subsequent Invoke-Command you require. Results are returned as objects. Scriptblocks may require local variables passed through -ArgumentList, per mklement0's answer.
$ps6sess = New-PSSession -ComputerName localhost -ConfigurationName 'PowerShell.v6.0.0'
$results = Invoke-Command -Session $ps60sess -ScriptBlock {Param($splatthis) Invoke-WebRequest #splatthis} -ArgumentList $SplatParms
It may also be useful to know that the session persists between Invoke-Command calls. For example, any new variables you create will be accessible in subsequent calls within that session:
PS > Invoke-Command -Session $ps60sess -ScriptBlock {$something = 'zers'}
PS > Invoke-Command -Session $ps60sess -ScriptBlock {write-host $something }
zers
Trouble with PSCredential passing doesn't seem to be a problem with this approach:
$ps6sess = New-PSSession -ComputerName localhost -ConfigurationName 'PowerShell.v6.0.0'
$credential = Get-Credential -UserName 'TestUser'
$IRestArgs = #{
Method='GET'
URI = 'https://httpbin.org'
Credential = $credential
}
$IRestBlock = {Param($splatval) Invoke-RestMethod #splatval}
Invoke-Command -Session $ps6sess -ScriptBlock $IRestBlock -ArgumentList $IRestArgs
# no error
pwsh -c {
Param ($SplatParms)
#$SplatParams.Credential = [pscredential] $SplatParams.Credential;
Invoke-RestMethod #SplatParms
} -args $IRestArgs
# error - pwsh : cannot process argument transformation on
# parameter 'Credential. username
Perhaps at the ps6 session knows it is receiving a block from ps5.1 and knows how to accommodate.
An immediate flash using start-process isn't proper, would have to research for that, regardless, my reaction is to construct an array of params to use not splat them for a try.

Correctly set the current user as the new owner in PowerShell [duplicate]

How do I get the current username in Windows PowerShell?
I found it:
$env:UserName
There is also:
$env:UserDomain
$env:ComputerName
On Windows, you can:
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
I thought it would be valuable to summarize and compare the given answers.
If you want to access the environment variable:
(easier/shorter/memorable option)
[Environment]::UserName -- #ThomasBratt
$env:username -- #Eoin
whoami -- #galaktor
If you want to access the Windows access token:
(more dependable option)
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name -- #MarkSeemann
If you want the name of the logged in user
(rather than the name of the user running the PowerShell instance)
$(Get-WMIObject -class Win32_ComputerSystem | select username).username -- #TwonOfAn on this other forum
Comparison
#Kevin Panko's comment on #Mark Seemann's answer deals with choosing one of the categories over the other:
[The Windows access token approach] is the most secure answer, because $env:USERNAME can be altered by the user, but this will not be fooled by doing that.
In short, the environment variable option is more succinct, and the Windows access token option is more dependable.
I've had to use #Mark Seemann's Windows access token approach in a PowerShell script that I was running from a C# application with impersonation.
The C# application is run with my user account, and it runs the PowerShell script as a service account. Because of a limitation of the way I'm running the PowerShell script from C#, the PowerShell instance uses my user account's environment variables, even though it is run as the service account user.
In this setup, the environment variable options return my account name, and the Windows access token option returns the service account name (which is what I wanted), and the logged in user option returns my account name.
Testing
Also, if you want to compare the options yourself, here is a script you can use to run a script as another user. You need to use the Get-Credential cmdlet to get a credential object, and then run this script with the script to run as another user as argument 1, and the credential object as argument 2.
Usage:
$cred = Get-Credential UserTo.RunAs
Run-AsUser.ps1 "whoami; pause" $cred
Run-AsUser.ps1 "[System.Security.Principal.WindowsIdentity]::GetCurrent().Name; pause" $cred
Contents of Run-AsUser.ps1 script:
param(
[Parameter(Mandatory=$true)]
[string]$script,
[Parameter(Mandatory=$true)]
[System.Management.Automation.PsCredential]$cred
)
Start-Process -Credential $cred -FilePath 'powershell.exe' -ArgumentList 'noprofile','-Command',"$script"
(you may need a hyphen before noprofile, like so)
Start-Process -Credential $cred -FilePath 'powershell.exe' -ArgumentList '-noprofile','-Command',"$script"
$env:username is the easiest way
I'd like to throw in the whoami command, which basically is a nice alias for doing %USERDOMAIN%\%USERNAME% as proposed in other answers.
Write-Host "current user:"
Write-Host $(whoami)
[Environment]::UserName returns just the user name. E.g. bob
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name returns the user name, prefixed by its domain where appropriate. E.g. SOMEWHERENICE\bob
Now that PowerShell Core (aka v6) has been released, and people may want to write cross-platform scripts, many of the answers here will not work on anything other than Windows.
[Environment]::UserName appears to be the best way of getting the current username on all platforms supported by PowerShell Core if you don't want to add platform detection and special casing to your code.
I have used $env:username in the past, but a colleague pointed out it's an environment variable and can be changed by the user and therefore, if you really want to get the current user's username, you shouldn't trust it.
I'd upvote Mark Seemann's answer:
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
But I'm not allowed to. With Mark's answer, if you need just the username, you may have to parse it out since on my system, it returns hostname\username and on domain joined machines with domain accounts it will return domain\username.
I would not use whoami.exe since it's not present on all versions of Windows, and it's a call out to another binary and may give some security teams fits.
Just building on the work of others here:
[String] ${stUserDomain},[String] ${stUserAccount} = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name.split("\")
$username=( ( Get-WMIObject -class Win32_ComputerSystem | Select-Object -ExpandProperty username ) -split '\\' )[1]
$username
The second username is for display only purposes only if you copy and paste it.
I didn't see any Add-Type based examples. Here is one using the GetUserName directly from advapi32.dll.
$sig = #'
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool GetUserName(System.Text.StringBuilder sb, ref Int32 length);
'#
Add-Type -MemberDefinition $sig -Namespace Advapi32 -Name Util
$size = 64
$str = New-Object System.Text.StringBuilder -ArgumentList $size
[Advapi32.util]::GetUserName($str, [ref]$size) |Out-Null
$str.ToString()
Sometimes the Username attribute has no data in Win32_ComputerSystem even though there's a user signed in. What works for me is to use quser and parse the output. It's not perfect, but it works. E.g.:
$quserdata = #()
$quserdata = quser
$userid = ($quserdata[1] -split ' ')[1]
$userid
Note: if this is run as the user who is logged in, quser adds '>' symbol to the output. Then you need to get rid of that symbol, but mostly this is needed for code run as system or another account than the one that is logged in.
If you're used to batch, you can call
$user=$(cmd.exe /c echo %username%)
This basically steals the output from what you would get if you had a batch file with just "echo %username%".
I find easiest to use: cd $home\Desktop\
will take you to current user desktop
In my case, I needed to retrieve the username to enable the script to change the path, ie. c:\users\%username%. I needed to start the script by changing the path to the users desktop. I was able to do this, with help from above and elsewhere, by using the get-location applet.
You may have another, or even better way to do it, but this worked for me:
$Path = Get-Location
Set-Location $Path\Desktop
In my case, I needed to retrieve the username to enable the script to change the path, ie. c:\users\%username%\. I needed to start the script by changing the path to the users desktop. I was able to do this, with help from above and elsewhere, by using the get-location applet.
You may have another, or even better way to do it, but this worked for me:
$Path = Get-Location
Set-Location $Path\Desktop