Intro to PowerShell Profile
PowerShell has some really cool features that can take a while to discover. One of the more interesting and underutilized components of PowerShell is profiles. In this blog post, I'll share how I came to learn about this feature, what problems I ran into, what I'm currently doing with this capability, and will make some suggestions on further research for your own studies.
If you're like me, and you learn by doing, I suggest you open up a PowerShell terminal and follow along. The things we will be doing in this post are 100% safe. Specifically, we will not be doing any attacks. However, it is highly likely you will trip PowerShell attack indicators on any host based monitoring solution installed. Every host protection suite that I've tested alerts on several commands used in this posting. I will note the commands likely to trip such alerts with a callout box like so:
The command below will likely trigger a host alert
As such, you probably should NOT do these activities on a work system until you get written approval. Instead, work on a personal system, or use a VM that does not have host based monitoring tools installed.
Setting Up Your $PROFILE
One of the most interesting bits of PowerShell as an environment is how customizable it is. Though it pains my Unix sysadmin self to admit this, I find PowerShell configurations to be significantly more approachable and more powerful than other terminal configurations.
PowerShell tracks where your terminal configuration file is set in a variable called $PROFILE. Like any variable in PowerShell, you can enter it by itself at the prompt to see what value it has.
There are slight differences in $PROFILE location based on OS. Throughout this article, I will show any differences with Windows 11, Debian (bookworm/testing), and macOS Monterey. If there are no differences between the OSs only a single example will be given.
Here's the profile location on Windows 11 machines:
$PROFILE C:\Users\mickd\OneDrive\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Next, here's the profile location on my Debian (bookworm/testing) Linux machine:
$PROFILE /home/douglas/.config/powershell/Microsoft.PowerShell_profile.ps1
Lastly, here's the location on my macOS Monterey (12.4) machine:
$PROFILE /home/douglas/.config/powershell/Microsoft.PowerShell_profile.ps1
It's confusing to me, but just because this variable is set with a location doesn't mean you actually have a config file set! You can check to see if your configuration file actually exists with the Test-Path cmdlet. If there is a file, Test-Path returns TRUE. If no file is set, the response is FALSE.
On Windows 11
Test-Path -Path $PROFILE TRUE
On Debian
Test-Path -Path $PROFILE FALSE
On macOS
Test-Path -Path $PROFILE FALSE
The command below may trigger a false positive host alert: Some attackers have used $PROFILE files as a form of persistence. As such, creating them is sometimes caught by EDR tools. In this case, we are not doing anything evil, and any warnings should be ignored.
I'm going to create a $PROFILE for my Debain machine, but the commands will work for any version and any OS.
To create a $PROFILE, run this command (note: this works regardless of OS)
New-Item -ItemType File -Path $PROFILE -Force
Once we have created the $PROFILE file, we can start doing all sorts of customization!
Changing Your $PROFILE
I love making extensive changes to my profile, and I think you will too! Let's go on a journey as I recreate the process that I went on to get a PowerShell prompt that I simply loved.
New Line After Path
A few years ago, I started looking into changing the profile because I ran into too many instances where long file paths, coupled with how lengthy PowerShell commands can be, caused lots of line wraps.
Here's a sample of what I'm talking about
PS C:\Users\mickd\OneDrive - InfoSec Innovations\SANS\Classes\504\SEC504_H01_02_POWERPOINTS> get-childitem -path . -Recurse -Force
It made me sad... and motivated. I wanted to have the path show on the line above with my commands being written below.
On Unix terminals it's not too difficult to change the prompt. I knew there had to be a way to do it in PowerShell too. After reading this Microsoft Technet Article, I learned that the prompt function inside the $PROFILE is incredibly straightforward.
Open up your $PROFILE by using whatever text editor you prefer.
Windows:
notepad $PROFILE
Linux and MacOS:
nano $PROFILE
With the file open, copy and paste this code.
function prompt { "$pwd`nPS > " }
This code broken down:
- prompt is the name of the function that PowerShell uses to create your prompt. You must use this name if you want to adjust the prompt. Anything inside the function is reflected in your prompt once you start a new PowerShell session or reload your profile.
- $pwd is an alias for Get-Location. In other words, it shows where you currently are in the file system.
- `n is how you write a new line in a PowerShell string. This should allow the path to be on the line above, and the prompt to be on the line below.
- PS > is what I just want to show for my prompt.
Pro Tip: the prompt code must be a single line.
At this point, save your file, and exit the text editor.
Now let's test this newly changed prompt out. To do that we need to have our profile re-loaded. If you want to, you can close your PowerShell console and open a new one. Or you can just load the updated $PROFILE
. $PROFILE
Note: it's not .$PROFILE... there is a space between . and $PROFILE
"Side Quest": Execution Restriction Policies
Caution: You likely will get host intrusion prevention alerts for the commands in this section. Most of the commands in this section have been used by attackers as part of attack sequences. Though these commands may be an acceptable indicator of early post exploitation behavior, we will not be doing anything evil.
If you are running this on a Windows system, you may get a message like this:
PS C:\Users\mickd\OneDrive - InfoSec Innovations\workshop\powercat> . $PROFILE . : File C:\Users\mickd\OneDrive\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170. At line:1 char:3 + . $PROFILE + ~~~~~~~~ + CategoryInfo : SecurityError: (:) [], PSSecurityException + FullyQualifiedErrorId : UnauthorizedAccess
This is because our $PROFILE is a PowerShell script (note that it ends in PS1, so it is an actual PowerShell script) and Microsoft prevents the loading of them by default via execution restriction policies.
You can see what your execution restriction policy is by running this command:
Get-ExectionPolicy
On Windows, PowerShell defaults to restricted mode. This prevents the running of any PowerShell script files... including your $PROFILE
You can either temporarily allow the execution of PowerShell scripts (this is what I and many folks I know do) by placing the current session in bypass mode. This mode allows one to run any component they want.
For Windows PowerShell:
PowerShell -ExecutionPolicy Bypass
For PowerShell 7 on Windows:
pwsh.exe -ExecutionPolicy Bypass
If your Linux or macOS system is in restricted mode, you can put PowerShell in bypass mode by using this command: For PowerShell on Linux or macOS:
pwsh -ExecutionPolciy Bypass
Tip: most people take advantage of shortcuts and aliases, especially when working on the command line. Less typing is less work! Rather than typing (or tab auto completion) -ExecutionPolicy bypass, you can use the shortcut -ep instead.
PowerShell -ep bypass pwsh -ep bypass
Or... you can set this option set permanently.
Set-ExecutionPolicy -ExecutionPolicy Unrestricted
Note: I personally do not like this, when this setting is permanent, ANY script can now auto launch. However, this appears to be the default behavior on Linux and macOS PowerShell installs.
Testing the New Prompt
Now that we've solved our execution restriction issues, let's re-start our PowerShell instance...
. $PROMPT
It worked!! We now have a new line after the path.
C:\Users\mickd\OneDrive - InfoSec Innovations\SANS\Classes\504\SEC504_H01_02_POWERPOINTS PS > get-childitem -path . -Recurse -Force
Though there's not much to be done with a long path, I found this to be a massive improvement and have kept this as part of my profile even years after I first found this.
Now armed with this knowledge, I started to think and experiment... what are some nice things to do with $PROFILE?
While I knew I wanted to do more with the prompt, the realization that $PROFILE was a full fledged PowerShell script intrigued me... can I run arbitrary PowerShell commands? What about system commands?
Let's find out!
function prompt { $pwd`nPS > " } hostname whoami
hostname will show the computer's name whoami will show the current user's name
My test is to see if arbitrary PowerShell commands can be automatically run when invoking a PowerShell prompt.
Let's reload the profile and see what happens:
C:\Users\mickd\OneDrive - InfoSec Innovations\SANS\Classes\504\SEC504_H01_02_POWERPOINTS PS > . $PROFILE shida shida\mickd C:\Users\mickd\OneDrive - InfoSec Innovations\SANS\Classes\504\SEC504_H01_02_POWERPOINTS PS >
Note: I didn't need to deal with Execution Policy settings because I'm still running in the same PowerShell instance. The setting from above still applies.
YES!!! This means that I can start making not just changes to the prompt, but making a series of events happen when I open up any PowerShell prompt at all.
At this point I removed the hostname and whoami commands because I didn't really want them there. I just wanted to prove that I can run any command I want from within the $PROFILE.
With the proof of concept commands removed, I then ran a command to see what options I had.
Get-Command > commands.txt
Note: PowerShell purists likely will tell you to use Out-File and do a command like this.
Get-Command | Out-File -Path .\commands.txt
But I'm lazy and I use the > shell redirect anytime I'm working directly on the command line. When I'm scripting, I try to remember to use Out-File (mainly for readability reasons), but I make no promises!
Opening the commands.txt file, I saw there were a few candidate commands... but one immediately jumped out to me: Start-Transcript.
Transcripts
Transcriptions were added in PowerShell v 5.0. They record the input and output of anything that is show in the PowerShell terminal.
After a little experimentation (and use of Get-Help Start-Transcript -ShowWindow) I crafted this block of code for the $PROFILE.
### Transcript Section ### # used to setup automated transcript start when PowerShell starts ## Transcript Section Variables ## # On Windows, I like to put it in the C drive directly. if ($IsWindows) { $TranscriptDir = "C:\transcripts\" } # use linux or Mac path for those OSs if ($IsLinux -Or $IsMac) { $TranscriptDir = "/home/douglas/.transcripts/" } # transcript log sets up the file's name. It will tell you: # - the computer the transcript came from # - the user's PowerShell session that is recordedf # - the day the transcript was made $TranscriptLog = (hostname)+"_"+$env:USERNAME+"_"+(Get-Date -UFormat "%Y-%m-%d") # Transcript Path is the full path and file name of the transcript log. # (putting it into a single variable increases readability below) $TrascriptPath = $TranscriptDir + $TranscriptLog ## end of transcript section variables ## # Test to see if the transcript directory exists. If it doesn't create it. if (!($TranscriptDir)) { New-Item $TranscriptDir -Type Directory -Force } # start the transcription based on the path we've created above Start-Transcript -LiteralPath $TrascriptPath -Append ### end of transcript section ###
A few things to point out:
PowerShell has the following variables that allow you to quickly determine which OS you are working with:
$IsWindows $IsLinux $IsMac
These variables will return TRUE for whichever OS the PowerShell script is running on.
You should note that I made extensive comments in my $PROFILE. It's rare that I'll remember what a particular block of code does... especially on something as important, but rarely changed as my $PROFILE.
Now when you start any PowerShell session, it will automatically record what you're doing and the output.
Pro Tip: I think everyone who uses PowerShell should take advantage of transcripts!
- As a pen tester: it's fantastic to have this... report writing has never been easier.
- As an forensics or incident responder: having a log of all commands, and the output... it's amazing. It makes my workflow so much faster and more convenient.
- As a PowerShell hacker: it's just really nice to have a log of anything you did... sometimes you do things that are quite clever and it's great to be able to quickly find and save those bits of inspired CLI magic.
Now that I have automated recording of my sessions (again, how freaking cool is that?!?) I want to further extend my prompt.
Who Are You?
Note that, in PowerShell, it can be difficult to know if you're running with admin powers.
I would like my prompt to show what user and machine I am logged into. I thought this would be easy... boy was I wrong!
First Try
All I want is the username and system name above the path in my better prompt.
### Prompt section ###
# Prompt is a reserved name for the function that covers how your prompt is setup.
function prompt {
"n$env:USERNAME on $(hostname)n$pwd`nPS > "
}
### end of prompt section ###
With another . $PROFILE, I'm rewarded with:
mickd on SHIDA C:\Users\mickd PS >
OK! This seems to be helpful.
What Privilege Do You Have?
One thing that constantly trips me up is how Windows PowerShell will need to be launched as Administrator if you are going to do certain commands. This is especially true with the Set-* PowerShell cmdlets. I thought it would be nice to have my Windows PowerShell prompt show if I was admin or not.
On the off chance that it would work, I tested to see if there was an $IsAdmin, or $IsAdministrator variable. Unfortunately, when I ran them, they were not set. So after some googling around, I found a helpful Stack Overflow posting that explains how to do this... it is in my opinion not intuitive.
### Prompt section ### # Prompt is a reserved name for the function that covers how your prompt is setup. function prompt { # Shout out to the Stack Overflow article that helped with this test. # https://serverfault.com/questions/95431/in-a-powershell-script-how-can-i-check-if-im-running-with-administrator-privil if ($IsWindows) { $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { "n$env:USERNAME as Administrator on $(hostname)n$pwd`nPS > " } else { "n$env:USERNAME as standard user on $(hostname)n$pwd`nPS > " } } if ($isLinux -Or $isMac) { "n$env:USERNAME on $(hostname)n$pwd`nPS > " } } ### end of prompt section ###
Once again I reload the $PROFILE with . $PROFILE
mickd as standard user on SHIDA C:\Users\mickd PS >
Date and Time
Now that my prompt is looking pretty good, I thought it would be neat to add the date and time to the prompt.
First I needed to know how to even get the date and time formatted correctly. Of course, I started with Get-Help!
Get-Help Get-Date -Online
This loaded the help information for Get-Date in my default browser.
After reading the documentation, I tested the -Format options until I found what I was looking for:
Get-Date -Format "yyyy-MM-dd HH:mm:ss K" 2022-07-01 06:52:24 -04:00
Note: I'm including UTC offset. It is K in the format string. It will help normalize time across time zones. PowerShell by default uses local time, and this can make things confusing very quickly if you are working on systems in a different timezone. As I'm writing this post, I'm in the US Eastern timezone which is currently UTC -4 due to daylight savings time.
Now that I had this information, I could add it to my prompt function and put it as the content as the first line.
### Prompt section ### # Prompt is a reserved name for the function that covers how your prompt is setup. function prompt { # Shout out to the Stack Overflow article that helped with this test. # https://serverfault.com/questions/95431/in-a-powershell-script-how-can-i-check-if-im-running-with-administrator-privil if ($IsWindows) { $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { "n$(Get-Date -Format "yyyy-MM-dd HH:mm:ss K")n$env:USERNAME as Administrator on $(hostname)n$pwdnPS > " } else { "n$(Get-Date -Format "yyyy-MM-dd HH:mm:ss K")n$env:USERNAME as standard user on $(hostname)n$pwdnPS > " } } if ($isLinux -Or $isMac) { "n$(Get-Date -Format "yyyy-MM-dd HH:mm:ss K")n$env:USERNAME on $(hostname)n$pwdnPS > " } } ### end of prompt section ###
With a final . $PROFILE I'm rewarded with:
2022-07-01 08:02:42 -04:00 mickd as standard user on SHIDA C:\Users\mickd PS >
This works PERFECTLY! Some things that I really like, including now when I hit my enter key, I get the latest time.
2022-07-01 08:02:53 -04:00 mickd as standard user on SHIDA C:\Users\mickd PS >
This is so amazing because now I have the time -- down to the seconds -- as well UTC offset. But what's more, these are now all recorded in my transcript files! It makes analysis, correlation, and even report writing so much easier!!
Summary
PowerShell is just amazing. It is so powerful, flexible and a little fun. You have nearly full control over your prompt. You can and should leverage transcripts to make a record of what you did.
When doing a set of changes like this, you should take an iterative approach and incrementally add elements. It might have seemed odd to keep reloading the PowerShell profile $PROFILE, but it's safer -- and easier to troubleshoot -- make many small changes than one large set.
Happy hacking!
Mick
Return to Getting Started With PowerShell
Mick Douglas is a principal instructor for the SANS Institute, teaches SEC504: Hacker Tools, Techniques, and Incident Handling, and is the managing partner for InfoSec Innovations. Mick is an active open-source developer and a PowerShell believer.