Like most sysadmins that manage a lot of computers, I have a need for startup scripts.
Also, I deploy those startup scripts to the computer via GPO.
However, I prefer to download those scripts to the machine and run them locally.
This allows them to run even if the computer isn’t currently connected to the network, like a laptop that’s off-site.
Additionally, I like logs of things I’m doing, so a simple log for something like this is to use PowerShell’s Start-Transcript
function.
It should be the first command that you run.
We’ll also need to send stuff to the log; Write-Host
or Write-Output
will do the job well.
Here’s the download command that we use, including a legit $path
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<span class="nf">Start-Transcript</span><span class="w"> </span><span class="nt">-LiteralPath</span><span class="w"> </span><span class="p">(</span><span class="s1">'{0}LogsDownload-HiddenPowershell.ps1.log'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemRoot</span><span class="p">)</span><span class="w"> </span><span class="nt">-IncludeInvocationHeader</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="s1">'{0}UNT'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemRoot</span><span class="p">)</span><span class="w"> </span><span class="nv">$uri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://raw.githubusercontent.com/UNT-CAS/HiddenPowershell/v1.0/HiddenPowershell.vbs'</span><span class="w"> </span><span class="nf">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="s1">'# Path: {0}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$path</span><span class="p">)</span><span class="w"> </span><span class="nf">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="s1">'# URI: {0}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$uri</span><span class="p">)</span><span class="w"> </span><span class="nf">Write-Host</span><span class="w"> </span><span class="s1">'# Ensure Path Exists ...'</span><span class="w"> </span><span class="nf">New-Item</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="s1">'Directory'</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$path</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nf">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="s1">'# Net.ServicePointManager: {0}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="p">[</span><span class="no">Net.</span><span class="kt">ServicePointManager</span><span class="p">]::</span><span class="nf">SecurityProtocol</span><span class="p">)</span><span class="w"> </span><span class="nf">Write-Host</span><span class="w"> </span><span class="s1">'# Setting TLS 1.2 ...'</span><span class="w"> </span><span class="p">[</span><span class="no">Net.</span><span class="kt">ServicePointManager</span><span class="p">]::</span><span class="nf">SecurityProtocol</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="no">Net.</span><span class="kt">SecurityProtocolType</span><span class="p">]::</span><span class="nf">Tls12</span><span class="w"> </span><span class="nx">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="s1">'# Net.ServicePointManager: {0}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="p">[</span><span class="no">Net.</span><span class="kt">ServicePointManager</span><span class="p">]::</span><span class="nf">SecurityProtocol</span><span class="p">)</span><span class="w"> </span><span class="nf">Write-Host</span><span class="w"> </span><span class="s1">'# Downloading from URI ...'</span><span class="w"> </span><span class="nf">Invoke-WebRequest</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nv">$uri</span><span class="w"> </span><span class="nt">-OutFile</span><span class="w"> </span><span class="p">(</span><span class="s1">'{0}HiddenPowershell.vbs'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$path</span><span class="p">)</span><span class="w"> </span><span class="nt">-UseBasicParsing</span><span class="w"> </span><span class="nt">-Verbose</span><span class="w"> </span><span class="mi">4</span><span class="err">></span><span class="o">&</span><span class="mi">1</span><span class="w"> </span><span class="nf">Stop-Transcript</span><span class="w"> </span> |
Now, we just have to make that a one liner and pass it to the powershell.exe
via -Command
:
1 2 |
<span class="nf">powershell.exe</span><span class="w"> </span><span class="nt">-WindowStyle</span><span class="w"> </span><span class="nx">Hidden</span><span class="w"> </span><span class="nt">-ExecutionPolicy</span><span class="w"> </span><span class="nx">Bypass</span><span class="w"> </span><span class="nt">-NoProfile</span><span class="w"> </span><span class="nt">-NonInteractive</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"Start-Transcript -LiteralPath ('{0}LogsDownload-HiddenPowershell.ps1.log' -f </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemRoot</span><span class="s2">) -IncludeInvocationHeader -Force; </span><span class="nv">$path</span><span class="s2"> = ('{0}UNT' -f </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemRoot</span><span class="s2">); </span><span class="nv">$uri</span><span class="s2"> = 'https://raw.githubusercontent.com/UNT-CAS/HiddenPowershell/v1.0/HiddenPowershell.vbs'; Write-Host ('# Path: {0}' -f </span><span class="nv">$path</span><span class="s2">); Write-Host ('# URI: {0}' -f </span><span class="nv">$uri</span><span class="s2">); Write-Host '# Ensure Path Exists ...'; New-Item -Type 'Directory' -Path </span><span class="nv">$path</span><span class="s2"> -Force; Write-Host ('# Net.ServicePointManager: {0}' -f [Net.ServicePointManager]::SecurityProtocol); Write-Host '# Setting TLS 1.2 ...'; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Write-Host ('# Net.ServicePointManager: {0}' -f [Net.ServicePointManager]::SecurityProtocol); Write-Host '# Downloading from URI ...'; Invoke-WebRequest -Uri </span><span class="nv">$uri</span><span class="s2"> -OutFile ('{0}HiddenPowershell.vbs' -f </span><span class="nv">$path</span><span class="s2">) -UseBasicParsing -Verbose 4>&1; Stop-Transcript"</span><span class="w"> </span> |
Too easy, right?
Issue
The problem with deploying this via a GPO Startup Script with PowerShell’s -Command
parameter is that GPO’s Script Parameter has a limit: 520 characters.
This command/script is currently 972 characters, just counting the parameters.
Resolution
There are two ways, that I know of, to reduce the size of this command:
- Use/Create PowerShell Aliases.
- Use just enough of a function’s parameter name to be unique.
- i.e.: The
Invoke-WebRequest
function’sUseBasicParsing
parameter can be shortened to-UseB
(case-insensitive of course); not-U
because there’s also aUserAgent
parameter and the first three letters of both parameters are the same.
- i.e.: The
Let’s start trimming things down …
PowerShell Command Line parameters
PowerShell’s command line parameters follow the same rule shortening as as functions.
So here’s what we want to shorten; 82 characters not including the ellipsis:
1 2 |
<span class="nt">-WindowStyle</span><span class="w"> </span><span class="kr">Hidden</span><span class="w"> </span><span class="nt">-ExecutionPolicy</span><span class="w"> </span><span class="nf">Bypass</span><span class="w"> </span><span class="nt">-NoProfile</span><span class="w"> </span><span class="nt">-NonInteractive</span><span class="w"> </span><span class="nt">-Command</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span> |
Here’s the shortest we can make it; 24 characters not including the ellipsis:
1 2 |
<span class="nt">-W</span><span class="w"> </span><span class="nf">H</span><span class="w"> </span><span class="nt">-Ex</span><span class="w"> </span><span class="nx">B</span><span class="w"> </span><span class="nt">-NoP</span><span class="w"> </span><span class="nt">-NonI</span><span class="w"> </span><span class="s2">"..."</span><span class="w"> </span> |
Note: The -Command
parameter is assumed.
Parameter values that have a set list of possible values are auto-completed, just like the parameter name.
Adjust Code Using Aliases and Shortened Parameters
If we’re going to create an alias:
- Be sure it’s not already in use.
- Be sure we’re using the function enough times to make a difference.
Take advantage of positional parameters when code shortening.
Here’s the adjusted code block from the top of this article:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<span class="nv">$a</span><span class="o">=</span><span class="s1">'HiddenPowershell'</span><span class="w"> </span><span class="nv">$b</span><span class="o">=</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemRoot</span><span class="w"> </span><span class="nf">Start-Transcript</span><span class="w"> </span><span class="nv">$b</span><span class="s1">'LogsDownload-'</span><span class="nv">$a</span><span class="s1">'.ps1.log'</span><span class="w"> </span><span class="nt">-I</span><span class="w"> </span><span class="nt">-F</span><span class="w"> </span><span class="nv">$c</span><span class="o">=</span><span class="nv">$b</span><span class="o">+</span><span class="s1">'UNT'</span><span class="w"> </span><span class="nv">$d</span><span class="o">=</span><span class="s1">'https://raw.githubusercontent.com/UNT-CAS/{0}/v1.0/{0}.vbs'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$a</span><span class="w"> </span><span class="s1">'# Path: '</span><span class="o">+</span><span class="nv">$c</span><span class="w"> </span><span class="s1">'# URI: '</span><span class="o">+</span><span class="nv">$d</span><span class="w"> </span><span class="s1">'# Ensure Path Exists …'</span><span class="w"> </span><span class="nf">ni</span><span class="w"> </span><span class="nt">-I</span><span class="w"> </span><span class="nx">D</span><span class="w"> </span><span class="nv">$c</span><span class="w"> </span><span class="nt">-F</span><span class="w"> </span><span class="nv">$e</span><span class="o">=</span><span class="p">[</span><span class="no">Net.</span><span class="kt">ServicePointManager</span><span class="p">]::</span><span class="nf">SecurityProtocol</span><span class="w"> </span><span class="nv">$f</span><span class="o">=</span><span class="s1">'Net.ServicePointManager'</span><span class="w"> </span><span class="s1">'# '</span><span class="o">+</span><span class="nv">$f</span><span class="o">+</span><span class="s1">': '</span><span class="o">+</span><span class="nv">$e</span><span class="w"> </span><span class="s1">'# Setting TLS 1.2 …'</span><span class="w"> </span><span class="nv">$e</span><span class="o">=</span><span class="p">[</span><span class="no">Net.</span><span class="kt">SecurityProtocolType</span><span class="p">]::</span><span class="nf">Tls12</span><span class="w"> </span><span class="s1">'# '</span><span class="o">+</span><span class="nv">$f</span><span class="o">+</span><span class="s1">': '</span><span class="o">+</span><span class="nv">$e</span><span class="w"> </span><span class="s1">'# Downloading from URI …'</span><span class="w"> </span><span class="nf">iwr</span><span class="w"> </span><span class="nv">$d</span><span class="w"> </span><span class="nt">-O</span><span class="w"> </span><span class="nv">$c</span><span class="s1">''</span><span class="nv">$a</span><span class="s1">'.vbs'</span><span class="w"> </span><span class="nt">-UseB</span><span class="w"> </span><span class="nt">-V</span><span class="w"> </span><span class="nx">4</span><span class="err">></span><span class="o">&</span><span class="nx">1</span><span class="w"> </span><span class="nf">Stop-Transcript</span><span class="w"> </span> |
Turns out I didn’t need to make any aliases.
If I needed to, I would have done so something like this: nal w echo
.
Explained:
nal
is an alias forNew-Alias
w
is my alias nameecho
is an alias forWrite-Output
- Yes, it passes through.
I could have shortened the strings even more, but I can only use single quotes inside of the command because the whole command needs to be passed inside of double quotes. There’s a few options for a putting a variable in a string; the first option can’t be done and the third is the shortest, in this case, so we used it:
"$bLogsDownload-$a.ps1.log"
('{0}LogsDownload-{1}.ps1.log' -f $b,$a)
$b'LogsDownload-'$a'.ps1.log'
- Can only use this one when passing to a parameter.
$b+'LogsDownload-'+$a+'.ps1.log'
I switch to using the ellipsis ascii character (…
>; …
) instead of three dots (...
). Super handy to know that exists.
Note: Consider shortening the URL with your favorite URL shortening service; such as Google URL Shortener.
If you make the URL within an account, you can usually see click/download counts.
Note: I kept the empty lines in there for keeping a readabile comparison, but I will remove them before proceeding to the next section.
Final Command
So, let’s use our command, from the previous section, but reduced down even more; 492 characters:
1 2 |
<span class="nv">$a</span><span class="o">=</span><span class="s1">'HiddenPowershell'</span><span class="p">;</span><span class="nv">$b</span><span class="o">=</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemRoot</span><span class="p">;</span><span class="nf">Start-Transcript</span><span class="w"> </span><span class="nv">$b</span><span class="s1">'LogsDownload-'</span><span class="nv">$a</span><span class="s1">'.ps1.log'</span><span class="w"> </span><span class="nt">-I</span><span class="w"> </span><span class="nt">-F</span><span class="p">;</span><span class="nv">$c</span><span class="o">=</span><span class="nv">$b</span><span class="o">+</span><span class="s1">'UNT'</span><span class="p">;</span><span class="nv">$d</span><span class="o">=</span><span class="s1">'https://raw.githubusercontent.com/UNT-CAS/{0}/v1.0/{0}.vbs'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$a</span><span class="p">;</span><span class="s1">'# Path: '</span><span class="o">+</span><span class="nv">$c</span><span class="p">;</span><span class="s1">'# URI: '</span><span class="o">+</span><span class="nv">$d</span><span class="p">;</span><span class="s1">'# Ensure Path Exists …'</span><span class="p">;</span><span class="nf">ni</span><span class="w"> </span><span class="nt">-I</span><span class="w"> </span><span class="nx">D</span><span class="w"> </span><span class="nv">$c</span><span class="w"> </span><span class="nt">-F</span><span class="p">;</span><span class="nv">$e</span><span class="o">=</span><span class="p">[</span><span class="no">Net.</span><span class="kt">ServicePointManager</span><span class="p">]::</span><span class="nf">SecurityProtocol</span><span class="p">;</span><span class="nv">$f</span><span class="o">=</span><span class="s1">'Net.ServicePointManager'</span><span class="p">;</span><span class="s1">'# '</span><span class="o">+</span><span class="nv">$f</span><span class="o">+</span><span class="s1">': '</span><span class="o">+</span><span class="nv">$e</span><span class="p">;</span><span class="s1">'# Setting TLS 1.2 …'</span><span class="p">;</span><span class="nv">$e</span><span class="o">=</span><span class="p">[</span><span class="no">Net.</span><span class="kt">SecurityProtocolType</span><span class="p">]::</span><span class="nf">Tls12</span><span class="p">;</span><span class="s1">'# '</span><span class="o">+</span><span class="nv">$f</span><span class="o">+</span><span class="s1">': '</span><span class="o">+</span><span class="nv">$e</span><span class="p">;</span><span class="s1">'# Downloading from URI …'</span><span class="p">;</span><span class="nf">iwr</span><span class="w"> </span><span class="nv">$d</span><span class="w"> </span><span class="nt">-O</span><span class="w"> </span><span class="nv">$c</span><span class="s1">''</span><span class="nv">$a</span><span class="s1">'.vbs'</span><span class="w"> </span><span class="nt">-UseB</span><span class="w"> </span><span class="nt">-V</span><span class="w"> </span><span class="nx">4</span><span class="err">></span><span class="o">&</span><span class="nx">1</span><span class="p">;</span><span class="nf">Stop-Transcript</span><span class="w"> </span> |
You probably want to test it to make sure it’s working.
Here’s how I do it:
1 2 3 4 5 |
<span class="nv">$command</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@' $a='HiddenPowershell';$b=$env:SystemRoot;Start-Transcript $b'LogsDownload-'$a'.ps1.log' -I -F;$c=$b+'UNT';$d='https://raw.githubusercontent.com/UNT-CAS/{0}/v1.0/{0}.vbs' -f $a;'# Path: '+$c;'# URI: '+$d;'# Ensure Path Exists …';ni -I D $c -F;$e=[Net.ServicePointManager]::SecurityProtocol;$f='Net.ServicePointManager';'# '+$f+': '+$e;'# Setting TLS 1.2 …';$e=[Net.SecurityProtocolType]::Tls12;'# '+$f+': '+$e;'# Downloading from URI …';iwr $d -O $c''$a'.vbs' -UseB -V 4>&1;Stop-Transcript '@</span><span class="w"> </span><span class="nf">Invoke-Expression</span><span class="w"> </span><span class="nv">$command</span><span class="w"> </span> |
If that runs as expected, we can run $command
via a powershell.exe command line parameter:
1 2 |
<span class="nf">powershell.exe</span><span class="w"> </span><span class="nt">-W</span><span class="w"> </span><span class="nx">H</span><span class="w"> </span><span class="nt">-Ex</span><span class="w"> </span><span class="nx">B</span><span class="w"> </span><span class="nt">-NoP</span><span class="w"> </span><span class="nt">-NonI</span><span class="w"> </span><span class="s2">"</span><span class="nv">$a</span><span class="s2">='HiddenPowershell';</span><span class="nv">$b</span><span class="s2">=</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemRoot</span><span class="s2">;Start-Transcript </span><span class="nv">$b</span><span class="s2">'LogsDownload-'</span><span class="nv">$a</span><span class="s2">'.ps1.log' -I -F;</span><span class="nv">$c</span><span class="s2">=</span><span class="nv">$b</span><span class="s2">+'UNT';</span><span class="nv">$d</span><span class="s2">='https://raw.githubusercontent.com/UNT-CAS/{0}/v1.0/{0}.vbs' -f </span><span class="nv">$a</span><span class="s2">;'# Path: '+</span><span class="nv">$c</span><span class="s2">;'# URI: '+</span><span class="nv">$d</span><span class="s2">;'# Ensure Path Exists …';ni -I D </span><span class="nv">$c</span><span class="s2"> -F;</span><span class="nv">$e</span><span class="s2">=[Net.ServicePointManager]::SecurityProtocol;</span><span class="nv">$f</span><span class="s2">='Net.ServicePointManager';'# '+</span><span class="nv">$f</span><span class="s2">+': '+</span><span class="nv">$e</span><span class="s2">;'# Setting TLS 1.2 …';</span><span class="nv">$e</span><span class="s2">=[Net.SecurityProtocolType]::Tls12;'# '+</span><span class="nv">$f</span><span class="s2">+': '+</span><span class="nv">$e</span><span class="s2">;'# Downloading from URI …';iwr </span><span class="nv">$d</span><span class="s2"> -O </span><span class="nv">$c</span><span class="s2">''</span><span class="nv">$a</span><span class="s2">'.vbs' -UseB -V 4>&1;Stop-Transcript"</span><span class="w"> </span> |
Yah!!
That’s 516 characters of arguments!!
We can even put the URL in a shortener, as previously suggested, to get it down to 479 characters:
1 2 |
<span class="nf">powershell.exe</span><span class="w"> </span><span class="nt">-W</span><span class="w"> </span><span class="nx">H</span><span class="w"> </span><span class="nt">-Ex</span><span class="w"> </span><span class="nx">B</span><span class="w"> </span><span class="nt">-NoP</span><span class="w"> </span><span class="nt">-NonI</span><span class="w"> </span><span class="s2">"</span><span class="nv">$a</span><span class="s2">='HiddenPowershell';</span><span class="nv">$b</span><span class="s2">=</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemRoot</span><span class="s2">;Start-Transcript </span><span class="nv">$b</span><span class="s2">'LogsDownload-'</span><span class="nv">$a</span><span class="s2">'.ps1.log' -I -F;</span><span class="nv">$c</span><span class="s2">=</span><span class="nv">$b</span><span class="s2">+'UNT';</span><span class="nv">$d</span><span class="s2">='https://goo.gl/PZcPxi';'# Path: '+</span><span class="nv">$c</span><span class="s2">;'# URI: '+</span><span class="nv">$d</span><span class="s2">;'# Ensure Path Exists …';ni -I D </span><span class="nv">$c</span><span class="s2"> -F;</span><span class="nv">$e</span><span class="s2">=[Net.ServicePointManager]::SecurityProtocol;</span><span class="nv">$f</span><span class="s2">='Net.ServicePointManager';'# '+</span><span class="nv">$f</span><span class="s2">+': '+</span><span class="nv">$e</span><span class="s2">;'# Setting TLS 1.2 …';</span><span class="nv">$e</span><span class="s2">=[Net.SecurityProtocolType]::Tls12;'# '+</span><span class="nv">$f</span><span class="s2">+': '+</span><span class="nv">$e</span><span class="s2">;'# Downloading from URI …';iwr </span><span class="nv">$d</span><span class="s2"> -O </span><span class="nv">$c</span><span class="s2">''</span><span class="nv">$a</span><span class="s2">'.vbs' -UseB -V 4>&1;Stop-Transcript"</span><span class="w"> </span> |
Now, just put it in GPO’s Script Parameters field; as shown:
Notes
Of course, the command is totally unreadable to most people, but it sure is short! :smirk:
Be sure to write some documentation and/or create a description on the GPO with some details.
Maybe I should write an updated blog post about this.
I have this implemented in production to make HiddenPowershell and HiddenRun available on all of the systems that I manage.
This allows me to execute PowerShell and other process completely hidden.
Check out those if your interested.
If you don’t have room for it, remove the Stop-Transcript
from the end.
- It just gives a nice log footer with an end time>; thus you can calculate total run time, if you so desire.
- Additionally, you don’t have to do logging at all.
If you have a longer PowerShell script that you want run on mobile devices, implement something like this:
- Drop the script in a repo.
- Public: github.com, gist.github.com, gitlab.com, gitlab.com/snippets, or pastebin.com work great.
- Private: gitlab.com or gitlab.com/snippets works great. Just make a Personal Access Token and tack it on the end of the URL for the raw download, like this:
https://gitlab.com/UNT-CAS/StartupScripts/raw/master/deploy.ps1?private_token=9koXpg98eAheJpvBs5tK
- Download the script with the first Startup Script.
- Be sure to download the raw version of the script.
- Execute it with a second Startup Script.
- GPO Startup Scripts are run in order; hence the ability to order them.