Powershell task: Hide not the output but the actual command containing sensitive info in devops logs - powershell

I have a powershell script in my release pipeline stage that runs a command and passes values of a secret variable to it. The issue is that the Logs show each and every command as they are run including each arguments passed, one of which is from a secret variable.
How do I make the powershell output not show the command it is running? output of the command is okay to show if it can't be hidden.

Secrets shouldn't be converted to plain text but kept as such and passed as a SecureString to your application. In other words, the solution lays in making sure that your concerned application accepts a hashed password, a SecureString or a PSCredential object also knowing that sending a plain text password to an application isn't secure by itself.
#iRon Say this to Microsoft. I am trying to call their schtasks
I just did: #16502: Set-ScheduledTask shouldn't accept a plain text Password
As a workaround, you might keep your password covered in a SecureString as long as possible:
$Credentials = Get-Credential
Set-ScheduledTask -User $Credential.UserName -Password $Credential.GetNetworkCredential().Password
This will prevent that the passwords are revealed by logging but as there is still a potentially security risk that the password could be read from memory, I recommend to do a garbage collection ([system.gc]::Collect()) right after this command.
⚠️ Important
A SecureString object should never be constructed from a String, because the sensitive data is already subject to the memory persistence consequences of the immutable String class. The best way to construct a SecureString object is from a character-at-a-time unmanaged source, such as the Console.ReadKey method.
To be completely safe, you might also consider to run Set-ScheduledTask (without -User and -Password) under the credentials of the targeted user Start-Process -Credential $Credential ...
Update 2022-02-24:
Sadly😭, I got zero response on my feedback hub "Set-ScheduledTask shouldn't accept a plain text Password" (security) issue. Therefore, I have also just created a new Microsoft Feedback Portal issue for this: Windows-PowerShell: Set-ScheduledTask shouldn't accept a plain text Password
Anyhow, the organization I work for, deals with the same general issue where the use-case is defined as: "how can we hide sensitive information as passwords used by invoked 3rd party applications in PowerShell scripts"
As suggested before: the problem in not due to any (PowerShell) scripting limitations but how the information (as plain text) is provided (input) to the script and how it is expected to be passed (output) to any other application.
To make this clear and to supply at least some (easy) solution, I have created an [HiddenString] class that might be used in a script to hide information as much as possible end-to-end inside the script itself.
class HiddenString {
hidden [SecureString]$SecureString = [SecureString]::new()
HiddenString([Object]$String) {
if ($String -is [SecureString]) { $This.SecureString = $String }
else {
foreach ($Character in [Char[]]$String) { $This.SecureString.AppendChar($Character) }
}
}
[String]Reveal(){
$Ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($This.SecureString)
$String = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($Ptr)
[System.Runtime.InteropServices.Marshal]::ZeroFreeCoTaskMemUnicode($Ptr)
Return $String
}
}
Note that I am using the SecureString type in the class not for better security but just for better hiding the concerned string.
Usage example:
function MyScript([String]$TaskName, [String]$UserName, [HiddenString]$Password) {
Start-Transcript -Path .\Transcript.txt
Write-Host "Scheduling $TaskName for $UserName/$Password" # Write-Log ...
Set-ScheduledTask -TaskName $TaskName -User $UserName -Password $Password.Reveal()
Stop-Transcript
}
Recommended invocation of MyScript:
$SecuredString = Read-Host 'Enter Password' -AsSecuredString
MyScript NotePad.Exe JohnDoe $SecuredString
Just hiding the sensitive information inside the MyScript:
$String = 'Sensitive Information'
MyScript NotePad.Exe JohnDoe $String
Transcript started, output file is .\Transcript.txt
Scheduling NotePad.Exe for JohnDoe/HiddenString
Transcript stopped, output file is .\Transcript.txt
Again, (I can't stress this enough):
warning: as a whole, this workaround is nothing more than security through obscurity
As Microsoft states themselves at SecureString shouldn't be used:
The general approach of dealing with credentials is to avoid them and instead rely on other means to authenticate, such as certificates or Windows authentication.
(which they should also do in their own cmdlets along with Set-ScheduledTask)
I have created an enhancement request for this idea:
#16921 Add [HiddenString] Class

You can register secrets with the agent to ensure they're scrubbed from the logs.
Write this to the output:
write-output "##vso[task.setsecret]THEVALUEYOUWANTHIDDEN"
This should register the secret with the agent. If you know your script will also popentially log the base64 value or another representation of the secret, make sure you register all permutations.

Related

Storing encrypted password Powershell

I am having challenge with one of my Powershell scripts. The idea is to run a tool from a client, which will kill some processes using the PID. Proof of concept works - ps1 script is converted to exe (PS2GUI) and the tool is successfully killing the PID of the user on the server.
$credentials = New-Object System.Management.Automation.PsCredential("domain\user",$password)
$scriptBlockSQL = {
sqlplus.exe command
}
Invoke-Command -ComputerName "server" -Credential $credentials -scriptBlock $scriptBlockSQL
However I have a bit of a problem, because I currently store the password in the script as clear text which is very unsafe - since the .exe can be decompiled in less than a few seconds.
I have tried the ConvertTo-SecureString with the encrypted string, however this ties it with the user account & pc - which is not an option. The usage of the Key file is also not an option.
Do you have any suggestion how to make the script safer & usable? Or any other solution which will work in the same way?
Thanks!
Well, when you are encrypting password with a SecureString then you have to define the password somewhere in the code or in another file. So this is not a good idea. Then you have to look for a way to protect script source code in an efficient way.
PS2GUI will encrypt your source code in a reversible encryption, that means when the script is ran by powershell, the engine will decrypt the code. It is also weak because of using symmetric key algorithm.
Powershell scripts are also encrypted in Base64 format, and PS2GUI does the quite same like thing.
The best way I think now is to prompt the user for credentials. Like this:
$password = Read-Host "Enter password" -AsSecureString

Why would one do this when storing a secure string?

Would this in some way make it more secure than just a secure string and storing it in a credential? Why would someone do this?
$secVal = read-host -AsSecureString -prompt "Enter xstoreuser password"
$strVal = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secVal)
$dasPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($strVal)
and then later
psftp -l somelogin -pw $dasPassword -P 22 -b .\ftp.txt somehost
The technique you're showing is not about making the command more secure - on the contrary:
You need to do extra work to recover the plain-text password from the [securestring] instance, because in order to pass a password to an external program such as psftp.exe you need an insecure plain-text representation, because external programs know nothing about .NET secure strings.
How secure is SecureString? tells you about .NET secure strings and their limitations in general.
As an aside: The command for extracting the plain-text password can be simplified a little via an aux. [pscredential] instance:
# Returns the plain-text content of secure string $secVal:
(New-Object pscredential somelogin, $secVal).GetNetworkCredential().Password
The only secure approach is to avoid plain-text passwords altogether and use a PKI approach (based on public and private keys) instead, if supported by the target program.
To demonstrate that the plain-text approach is insecure (even without the use of an intermediate variable to store the unencrypted password):
# Create a [pscredential] instance with a secure-string password:
# Note: To make this demo self-contained, I'm converting *from* plain
# text in order to construct the credential object.
# Note that you should never do that in production code.
# The need to specify -Force to do it is a reminder that it
# normally shouldn't be done.
$cred = New-Object pscredential someuser,
(ConvertTo-SecureString -AsPlainText -Force 'actualPassword')
# Open a new hidden window that passes the password *as plain text* to
# `cmd /c echo` (and then waits for a keypress, to keep the process alive).
$ps = Start-Process -PassThru -WindowStyle Hidden cmd "/c echo $($cred.GetNetworkCredential().Password) & pause"
# Now inspect the process' command line using CIM (WMI):
(Get-CimInstance Win32_Process -Filter "ProcessID = $($ps.ID)").CommandLine
# Kill (stop) the sample process.
$ps | Stop-Process
The above yields "C:\WINDOWS\system32\cmd.exe" /c echo actualPassword & pause, demonstrating that the plain-text password can indeed be obtained by other processes.
You can also use GUI methods for inspecting a running process' command line: Task Manager (Taskmgr.exe) or, without potential truncation, SysInternals' Process Explorer (procexp.exe), which, however, must be installed on demand - see this ServerFault answer.

Prevent PowerShell script from being read

I have the below PowerShell script (myscript.ps1) in which I ask for username and password. Depending on the username and password it copies a file to a certain destination.
$credentials = Get-Credential
if ($credentials.Username -eq "user1" -And $credentials.GetNetworkCredential().password -eq "pass1")
{ Copy-Item "test1.pdf" "\test\test1.pdf"; }
else
{ Copy-Item "test2.pdf" "\test\test2.pdf"; }
Requirement: I want to make this file protected so no one can edit it and see the username and password.
PS2EXE
I found a solution found here which converts the PowerShell script to an .exe file. When I originally run the script using PowerShell a dialog box appears allowing me to enter the username and password:
After the .exe is generated and when I run it the credentials dialog box no longer appears. Instead, the console appears saying "Credential:"
I don't know why? I want the credentials form to still appear when running the exe. Any thoughts please?
Q: Why does the EXE prompt with "Credential"?
This isn't an answer to the real question, and is based on guessing/supposition about PS2EXE, but I hope it is useful to clear up some confusion.
Having looked briefly at the PS2EXE page linked above, it seems that this utility encodes the script in Base64 and bundles it with a lightweight (?) custom PowerShell host. When run, I suppose the EXE starts the host, decodes the script and runs it.
The problem is that the Get-Credential cmdlet is running within a PS host that probably can't interact with the desktop. That is, it can't put up the GUI prompt for credentials. It therefore needs to prompt for the Credential property on the command line, explaining why you see that behaviour.
Workaround with Read-Host?
Instead of trying to use Get-Credential to prompt for username and password, you could embrace what PS2EXE seems to be doing and just use Read-Host:
$UserName = Read-Host "Enter username"
$Password = Read-Host "Enter password" -AsSecureString
$Credentials = New-Object System.Management.Automation.PSCredential $UserName,$Password
if ($credentials.Username -eq "user1" -And $credentials.GetNetworkCredential().password -eq "pass1")
{ ... }
Using -AsSecureString will hide the password on the screen. The $Password variable will be of type System.Security.SecureString, which can be used to create a PSCredential object as shown.
You'd need to test this, but it seems that you're able to read from the shell but not from a GUI prompt.
And just to be clear: none of this is anywhere near best-practice security. If you need authentication/authorization for these activities, step back and look at the problem again.
Workaround with two scripts?
It seems that PS2EXE doesn't support -AsSecureString in the same way that normal PowerShell does, i.e. it doesn't hide the characters. A possible workaround for this would be to collect the username and password from the user in one script and then pass them to a PS2EXE-converted script for processing.
Launch-MyScript.ps1:
$Credentials = Get-Credential
& MyScript.exe $Credentials.Username $Credentials.Password
MyScript.exe (coverted with PS2EXE):
param($Username,$Password)
$Credentials = New-Object System.Management.Automation.PSCredential $Username,$Password
if ($Credentials.Username -eq "user1" -and
$Credentials.GetNetworkCredential().password -eq "pass1")
{
...
}
The user runs Launch-MyScript.ps1 and completes the password prompt. Then the EXE is run automatically with the username and password passed in as arguments. Note that, as shown above, the password is a Secure String. Test this; I'm not using PS2EXE so it's a theoretical solution at the moment.
If you can't pass $Password along the pipeline as a Secure String object, you can convert it to text with ConvertFrom-SecureString in the first script, then conver it back with ConvertTo-SecureString in the second one.
According to this article http://windowsitpro.com/powershell/protect-your-powershell-scripts you should first set ur execution policy to AllSigned by Set-ExecutionPolicy AllSigned, then create a certificate using makecert cmdlet.
Then u can sign single script using Set-AuthenticodeSignature cmdlet or use .pfx File to Sign a Script which appears even safer.
Hope it helps a bit.

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

Creating file in a user context in powershell

I am trying to create a file using powershell in a specific user context. E.g I have a user user01 on my local machine and I want to create a file in its context.
I am doing something like
New-Item c:\file.txt -Credential User01
It works but prompts me for password which I dont want it to. Is there any way I can accomplish this without having it prompt for password ?
The credential parameter on new-item is not actually supported for filesystems, so I'm not sure what you mean by "it works." It does NOT create the file as the passed user. In fact, the filesystem provider will say:
"The provider does not support the use of credentials. Perform the operation again without specifying credentials."
Taking an educated guess, I'd say you're trying to create a file with a different owner. PowerShell cannot do this on its own, so you'll need the following non-trivial script:
http://cosmoskey.blogspot.com/2010/07/setting-owner-on-acl-in-powershell.html
It works by enabling the SeBackup privilege for your security token (but you must already be an administrator.) This allows you to set any arbitrary owner on a file. Normally you can only change owner to administrators or your own account.
Oh, and this script is for powershell 2.0 only.
Rather than use a PowerShell cmdlet or .NET scripting on this one, you might take a look at the Windows utility takeown.exe. However, even it requires you supply the user's password that you're assigning ownership to.
Ok, I do start process in the user context and then create a file. Works like a charm.
Password, FilePath and UserName are passed in as arguments from command line.
$pw = convertto-securestring "$Password" -asplaintext –force
$credential = new-object -typename system.management.automation.pscredential -argumentlist "-default-",$pw
$localArgs = "/c echo>$FilePath"
[System.Diagnostics.Process]::Start("cmd", $localArgs, "$UserName", $credential.Password, "$Computer")
Or just make a call to SUBINACL.EXE? No need for password then.