In my last post, I described Custom Protocol Handler and PowerShell in general. This time I will focus on using PowerShell with it. It really has a big potential, which I’ll try to show in few examples.
Just to remind, last time we created a simple protocol showing console message with passed URL and working directory:
"powershell.exe" "Write-Output 'Hello %1 %w'; Read-Host"
Parse arguments
The protocol handler passes entire URL to the called program. The first thing to do is to transform it and get values of arguments. We are not forced to use a pre-defined URL format. In fact, we can create our own, making it more a custom URI than a URL. But let’s try to keep the format somehow similar to URL with query string – for simplicity (check in Wikipedia). It’s well-known and allows others to quickly understand the syntax and the purpose of our custom protocol. We want to pass two arguments, name and surname, and write them back in the console. The syntax will be:
ProtocolName:Application/Method?Arg1=Value&Arg2=Value
And calling link:
Test:MyApp/Hello?Name=John&Surname=Smith
To parse it, firstly get the entire URL and method:
$a = '%1'
$method = $a[($a.IndexOf('/')+1)..($a.IndexOf('?')-1)] -join ''
Then get all characters after the question mark and split them into an array of arguments:
$p = $a.Substring($a.IndexOf('?')+1).Replace('Test:','').Split('&')
Then get the value of each argument. Notice that we are avoiding double-quotes because the protocol definition is a Windows Shell Command. Within it, we use double quotes to enclose the script content passed to the PowerShell process. However, when using the ‘-like’ operator we need to escape the double quotes in a CMD way – not a PowerShell way. Triple double quotes are doing the job (look at this StackOverflow answer). We cannot use a single quote in this place because of the wildcard.
$fName=($p | ? {$_ -like """Name=*"""}).Split('=')[1]
$sName=($p | ? {$_ -like """Surname=*"""}).Split('=')[1]
And finally, define the action for the method:
If($method -eq 'Hello'){'Welcome ' + $fName + ' ' + $sName}
In the end, we need a CMD one-liner:
"powershell.exe" "$a = '%1'; $method = $a[($a.IndexOf('/')+1)..($a.IndexOf('?')-1)] -join ''; $p = $a.Substring($a.IndexOf('?')+1).Replace('Test:','').Split('&'); $fName=($p | ? {$_ -like """Name=*"""}).Split('=')[1]; $sName=($p | ? {$_ -like """Surname=*"""}).Split('=')[1]; If($method -eq 'Hello'){'Welcome ' + $fName + ' ' + $sName}; Read-Host"
The effect is:
We haven’t used the app name for anything. I just keep it to make the URL syntax easier to understand. Technically we can remove it from our link and there will be no difference:
Call another script from Custom Protocol Handler and PowerShell
Our protocol definition is quite difficult to read and maintain. Especially if we would like to extend its functionality, troubleshoot, add new methods, etc. The solution is to call a launcher script which you can maintain as a .ps1 file in VS Code or any other code editor. Keep in mind that:
- Users must be able to access the script
- It needs to be secured from overwriting
We protect our custom protocol handler by registering it in the Local Machine registry’s key. At least from others than administrator. But whenever we call another script or executable – we need to protect it separately. I suggest putting the file on a share with appropriate rights. For simplicity, we will use a share on localhost but that can be any file share available for users.
The script is:
param (
$a
)
$method = $a[($a.IndexOf('/')+1)..($a.IndexOf('?')-1)] -join ''
$p = $a.Substring($a.IndexOf('?')+1).Replace('Test:','').Split('&')
$fName=($p | ? {$_ -like "Name=*"}).Split('=')[1]
$sName=($p | ? {$_ -like "Surname=*"}).Split('=')[1]
If($method -eq 'Hello'){'Welcome ' + $fName + ' ' + $sName}
Read-Host
And the definition of the protocol is much simpler now:
"powershell.exe" "& \\localhost\share\launcher.ps1 '%1'"
Depending on the execution policy we might get “[…] cannot be loaded because running scripts is disabled on this system.”
error:
The best approach is to properly sign the script, so it doesn’t conflict with security settings. However, in our case we can just bypass it by adding the execution policy to our protocol handler:
"powershell.exe" "Set-ExecutionPolicy RemoteSigned -Scope Process -Force;& \\localhost\share\launcher.ps1 '%1'"
For optimal security let’s keep the scope as narrow as possible, we need it just for this process. Force parameter avoids confirmation prompt.
Call another script as another user
Another thing we can achieve with Custom Protocol Handler and PowerShell is executing the launcher script as another user. To do it, switch to a cmdlet that allows change of security context. We can do it by collecting credentials and starting a process manually. However much more convenient and secure (because we use native Microsoft flow) is to use ‘Start-Process’
with
parameter. It’ll cause a UAC prompt in which we can provide both administrator and non-administrator credentials.‘
RunAs’
The protocol handler definition is:
"powershell.exe" "start-process powershell.exe -Verb RunAs """ & \\localhost\share\launcher.ps1 '%1' """ "
For this test, I entered credentials of an account with local administrator rights. You can see the security context switch:
Code Injection and Custom Protocol Handler
Almost everyone heard about SQL injection. But be cautious about code injection with PowerShell as well. It’s important whenever you rely on user’s input. In this case, the PowerShell process is running in the user security context so user cannot do anything that he cannot do normally in PowerShell console. It minimizes the risk. However, a malicious person can create a potentially dangerous link. So, user does something else than he thinks he does – for example, downloads a malicious file. I’ll demonstrate it.
Let’s add 'Read-Host'
to the protocol handler definition to catch the result:
"powershell.exe" "& \\localhost\share\launcher.ps1 '%1';read-host"
Then let’s inject unintended code ('Get-Date'
for example). We need to inject it between the '%1'
(full URL) and the closing single quote. Firstly, escape from the string by an additional single quote, then terminate the line by a colon, add your code, terminate the line again, and open again the string by a single quote (if you miss the last part, PowerShell will return an error because of incorrect syntax):
Start-Process "Test:MyApp/Hello?Name=John&Surname=Smith';get-date;'"
This example can be mitigated by adding 'Stop-Process $pid'
at the end of the launcher.ps1
. It will kill the PowerShell process regardless of what waits for execution in the queue. Just ensure that this 'Stop-Process'
is always executed, for example, put the entire script into the Try-Catch
block and stop the process in the Finally
block.
Base64 encoding
Sometimes it might be necessary to pass complex text with non-standard characters. It’s firstly processed by Windows Shell Command – it includes handling all escape characters like triple double quotes ("""
) and variables like %1
or %w
. Then, it’s passed to PowerShell and processed by it – now it includes PowerShell escape and special characters, etc. The probability of failure or unexpected text conversion might be high. Of course, depending on the situation. Sometimes the passed text might be dynamically created based on user input so we cannot be sure which characters will be there. If we add to it that URL could be generated by JavaScript on a website, then passed to a custom protocol handler, then to PowerShell… You should get now what I mean.
The solution for this issue is Base64
encoding – a well-known approach in inter-app communication on the Web. There is no cmdlet ready for us but it’s still quite easy to make it thanks to .NET classes. Look here for a detailed explanation of this subject. In short, we need to create a Base64
string from the link we pass (that can be done in JavaScript from a web app or any other way). With PowerShell we can do it like that:
[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('Test:MyApp/Hello?Name=John&Surname=Smith'))
We get the following Base64
string:
VGVzdDpNeUFwcC9IZWxsbz9OYW1lPUpvaG4mU3VybmFtZT1TbWl0aA==
Now, let’s add this decoding line to the launcher.ps1
(right after param()
block):
$a = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($a.Replace("test:","")))
And we can use the link with encoded string:
Start-Process "Test:VGVzdDpNeUFwcC9IZWxsbz9OYW1lPUpvaG4mU3VybmFtZT1TbWl0aA=="
Run another application with PowerShell
We can use PowerShell to wrap execution on another application as well. For example, to inform a user about something, get his input, etc. Let’s try to launch a RDP connection, giving the user chance to cancel within 5 seconds:
"powershell.exe" "Write-Output 'Loading Remote Desktop Connection in 5 seconds. Press CTRL+C to cancel...'; Start-Sleep 5;$a='%1'.Replace('test:MyApp/RDP?Hostname=',''); mstsc /v:$a"
The link:
Start-Process "Test:MyApp/RDP?Hostname=testpc001"
End
In this post, I provided few examples of Custom Protocol Handler and PowerShell. There are of course many more ways to utilize it, your imagination and power of PowerShell are the limits 🙂
Share your thoughts about this topic in the comments – maybe you have a better way to achieve what I described.
- Use PowerShell to integrate OpenAI GPT with context menu - October 25, 2023
- Let OpenAI improve and correct your PowerShell code - October 11, 2023
- Create Web Link with PowerShell in Intune - September 27, 2023
Very interesting (again). One note about parsing URLs. Instead of using all those string manipulation methods (Subscring, IndexOf, Replace, Split) I would use the type system and dotnet to do the work for me. It’s already been written and heavily tested, so why not use it.
I would parse the URL like that:
$url = “Test:MyApp/Hello?Name=John&Surname=Smith”
$uri = [System.Uri]$url
Now you can use $uri.Segments[-1] to get your method name (Hello).
And then parse the query string:
$parsed = [System.Web.HttpUtility]::ParseQueryString($uri.Query)
$i = 0
foreach ($parameter in $parsed) {
Write-Output “$parameter $($parsed[$i])”
$i++
}
Of course, your way of parsing the query string works fine, but I really like to reuse as much of already written code as I can. It’s extremely easy to use dotnet types in PowerShell so I prefer that approach.
Thanks for another good comment 🙂 I like your approach and agree with the argumentation. I haven’t thought about using those dotnet classes and I think they improve the overall solution – and are definately good for production usage. Once that’s said, I can only add that for self-developement I actually like those “manual” actions. They help to understand what’s happening behind the scene, increasing overall understanding of the code.
Hello Wiktor and thanks for your highly informative and interesting articles (parts 1 and 2) on this subject.
I’m reaching the end of my development and the only functional part that is missing may be resolved using the mechanisms you described. A brief description of what I’m developing as well as my needs follow.
The Web application I’m developing consists of two files: Index.html and Index.js (I’m also using wo libraries, Bootstrap and Font Awesome). It is configured is such way that it can ran using a Web Server as well as fully stand alone (i.e. you open the HTML file and it works as needed).
At some point, the application needs to invoke an external process (due to access limitations of the browsers) to:
1. Get information (e.g. list of sub-folders within a KNOWN folder or list of files with one or more possible extensions); the expected data needs to be returned as a comma-separated string and it may have rather height length (say, up to 50KB).
2. Received a relative large JSON (could exceed 300KB) and save it into a file whose name and location are known.
From your description, I understood that running a Powershell script PASSES through CMD, meaning, the limitations of CMD would also be applicable to the Powershell script. If so, the application would not be able to pass large chunks of data (JSON) to the script not the script would be able to return anything different from a single integer.
I would very much appreciate if you could refer to the two points presented above and let me know their implementation is feasible at all or not.
Thanks,
Fernando.