Bypassing the PowerShell execution policy to run and schedule scripts
There may be times when running PowerShell scripts is disabled on a system; this can be a pain if we need to run or schedule a script but don’t possess administrative permissions. This post explores one way we can work around this. As always, I’ll be walking through the thought process of solving the various problems encountered but feel free to skip ahead to the relevant section for your needs.
To start addressing the problem, we must first understand what we can or cannot do and this relies upon the PowerShell execution policy.
What is the PowerShell Execution Policy?
The PowerShell execution policy dictates what scripts can be run via PowerShell. There two components to an execution policy which can typically be amended by a local administrator but may be controlled by a management plane such as group policy preventing alteration.
Policy setting – this defines what level of restriction/freedom is being applied e.g. “Unrestricted” means any script can be run whereas “AllSigned” means the script must be signed by a trusted publisher before it can be run
Scope – this defines what scope the policy setting applies to. Scopes are either for the machine (all users), the current user or the current process.
By default, Windows sets the local machine execution policy (affecting all users) to “Restricted” which is defined as:
Permits individual commands, but does not allow scripts.
Prevents running of all script files, including formatting and configuration files (.ps1xml), module script files (.psm1), and PowerShell profiles (.ps1).
So the fact that we can run “individual commands” can lead us somewhere fruitful. For the rest of this post, I’ll be working with a restricted execution policy set at the local machine scope.
N.B. You can check what your execution policies are set at by running “Get-ExecutionPolicy -List”
One-off script execution
Let’s run an example, I have a very simple script that loops through an array of words and prints each of them to the screen, importantly the PowerShell execution policy set to restricted. As you can see when I try to execute the script, I receive an error that it has been blocked. N.B. This is the same whether running in PowerShell or PowerShell ISE (Integrated Scripting Environment).
The first and easiest way to execute a script as a one off is to open the script file via PowerShell ISE and instead of executing using “Run script” (or F5), highlight all the code and instead select “Run Selection” (or press F8). Despite the content being contained within a script file this will instead run the code as intended.
As a note – if a cmdlet in your script requires administrator permissions it still won’t work as you’re running under the logged in user context (unless your user is an administrator in which case just change the local machine execution policy!)
Scheduling a task
Running a script as a one-off can get us out of a pinch but what if we want to schedule task, once again without local admin. If we save the file as a regular PowerShell script file (.ps1) and pass it as an argument within a scheduled task it will fail to run in the same way as before.
Instead let’s have a look at the PowerShell executable parameters and see if we can find something useful. Two jump out, “-Command” and “-EncodedCommand”.
Using “-Command” we could pass in our entire script as an argument, but this would be a bit unwieldly, and we’d have to flatten our code down to make it work (e.g. removing all line breaks and considering use of quotes to prevent them being parsed by the command prompt). Alternatively, “-EncodedCommand” may be more fruitful, this allows us to pass a Base64 encoded string which PowerShell will decode upon execution to run the script. I’m not going to explain Base64 encoding in this post so have a Google if you are interested in learning more about how this works. The string below is the same script from before but Base64 encoded (Unicode). As you can see it’s increased in length but has handled all the complexity that would prevent us from feeding the original code through as an argument to PowerShell.
But how do we get to this? Well you can use an online encoder, but we can also encode using PowerShell. The script below takes the code within the scriptContent variable and converts to a Base64 string. It then also constructs the full statement we need to run it using command prompt and copies it to the clipboard so we can easily try it out.
Let’s paste it the output into a command prompt and see what happens:
Ok great, no errors means that we are good to move onto turning it into a scheduled task. Even after you’ve got your code working in ISE, if you're encoding, I’d always recommend running a test like this before adding to a task as it’s much easier to see if anything has gone wrong.
Pulling again from the PowerShell executable parameters the “-WindowStyle” parameter also looks useful as we can select hidden which should hide the PowerShell window from the user. This time as we will be running the script in the background, I’ve changed the code to append text to a file rather than write to screen.
Setting up the scheduled task, the key bit is in the “Actions” tab. Ideally to set up the task we need to separate out the program being executed from the parameters. This would be setting the “Program/script” field to PowerShell.exe and the “Add arguments” field to everything else e.g. -WindowStyle Hidden -EncodedCommand “theEncodedCommand”.
To avoid errors if you paste the whole thing into the “Program/script” box it will suggest and automatically split the parameters out for you :) What you end up with should look something like this:
Finish creating the task (specify triggers etc.), and we’ll run it as a one off to prove it works ok.
What you may notice if you're following along is that even though we have specified the script to run with a hidden window style, we still end up with the blue PowerShell window popping up, this isn’t great if we want silent background execution, so we’ll address that problem next.
Silent task execution
As you saw, despite our best-efforts PowerShell really wants to pop a window when its running. The easiest way around this is to wrap your script in VBScript.
The script below takes the code within the scriptContent variable, and this time creates the VBScript file in the location specified by outputLocation.
If you inspect the generated VBScript file (right click, edit or see below snippet) you’ll see that we are using a WScript Shell object use its Run method. We then specify our PowerShell command but also pass a zero which hides the window. Note we don’t need to pass in a WindowStyle to PowerShell this time. Lastly, we clean up after ourselves by setting the Shell to Nothing (analogous to setting an object to $null in PowerShell).
We can then take this and amend our scheduled task; this time the program to call is wscript.exe and our argument is the path to the VBScript file. Note I have also encapsulated the file path in double quotes, while this isn’t needed for my path it is required if your path has spaces in, so for the sake of two seconds work save yourself a potential future headache! It should look like this:
Run the task again and this time you will not see a pop-up window and a new entry will have been added to our output text file. All done!
More than anything this illustrates that there are always creative ways to work around problems, I hope this helps someone else out. For anyone concerned with bypassing controls put in with good intention, in my experience, help is often requested to troubleshoot problems but there can be a reluctance to grant permissions such as local administrator that would make life easier, this is perfect for those cases. It is also worth mentioning from a security perspective when using Base64 strings, all code must be decoded into plain text prior to execution by the PowerShell engine. This means, on Windows 10/11, everything will be run through the anti-malware scan interface (AMSI). This allows your endpoint security solution such as Defender for Endpoint to scan it prior to run to ensure that any code is not malicious in nature.