Reading transcripts as an object

Transcription now built-in the PowerShell 5.0 engine is a great feature for watching over-the-shoulder what happens.

Unfortunately, it dumps everything into text files and PowerShell is all about objects.

Instead of just using grep (or Select-String cmdlet) and to ease forensics investigations by providing context, I wrote the following function:

Yeah, less than 200 lines 😎 and tough job, here’s why:

  • The function has the ability to read a transcript file that is still active (the shell or host application didn’t exit yet).
  • It also reports when a shell (or host application) has been launched but nothing was typed-in.
  • Sometimes, there’s a new header block appended in the middle of the transcript file, usually when an error occurs as far as I can tell.
  • The function is able to parse both transcripts that have enabled “Invocation Header” or not.
  • Transcripts timestamp commands if you’ve enabled “Invocation Header” and it adds additional separators.

More info about the behavior of the above function:
-I didn’t transform the datetime data parsed from the text file into a real datetime object as I didn’t know if they are always in the yyyyMMddHHmmss format.
-If “Invocation Header” aren’t enabled the CommandStartTime property returned by the function is null.
-The CommandContext property is also null when a shell (or host application) has been launched but nothing was typed-in.
-The CommandContext property will contain one or many commands as well as their output

To see the function in action, you just do:

Get-ChildItem -Path C:\Transcripts -Include *.txt -Recurse | 
ForEach-Object {
    Get-TranscriptContent -FilePath $_.FullName -Verbose

You can even pipe the result into the Out-GridView cmdlet

Get-ChildItem -Path C:\Transcripts -Include *.txt -Recurse | 
ForEach-Object {
    Get-TranscriptContent -FilePath $_.FullName
} | Out-GridView


And you can search for whatever you want using the properties of each object sent through the pipeline:

Get-ChildItem -Path C:\Transcripts -Include *.txt -Recurse | 
ForEach-Object {
    Get-TranscriptContent -FilePath $_.FullName -Verbose
} | Where CommandContext -match "attack"


Compressing transcripts

I’ve been using PowerShell 5.0 transcripts and to save some space (2/3 actually), I’ve written the following code snippet to compress these files.

The compression ratio formula is taken from this Wikipedia page

$PSTranscripts is a string that points the location where my transcripts are stored (the same location set by GPO). It can be for example: C:\Transcripts

Now, we need the equivalent of bzgrep in the Windows world to be able to parse compressed files 😉

Create a GPO for PowerShell 5.0 settings

I’ve written the following piece of code to automate the creation of a Group Policy that would configure all the new PowerShell 5 settings – ScriptBlock Logging, Protected EventLog and Transcripts – that Lee Holmes mentioned in the PowerShell ♥ the Blue Team post.

It should be run from a Windows 10 computer joined to the domain where you’ve the Remote Server Administration Tools (RSAT) installed because of the New-SelfsignedCertificate cmdlet and the GroupPolicy module requirement. If you’ve a PKI, you don’t need the self-signed certificate and may prefer using a certificate issued by your PKI. In this case, you start by enrolling the certificate from your PKI and the demo code below will use that one instead.

Last warning, the C:\Transcripts folder should exist on the computer targeted by the GPO and the NTFS security should be adjusted before applying the GPO.
Unfortunately the GPO doesn’t handle that requirement.

What’s behind ‘Run with PowerShell’ context menu?

To find out what the “Run with PowerShell” action does, let’s dig into the registry:

.ps1 file exetension is associated with the Microsoft.PowerShellScript.1 type.

I can find the “Run with PowerShell” string into the cache.

Using the verb, it clearly uses the registry key named 0 and its command below

I can see what action will be performed on the file with I click ‘Run with PowerShell’:

(Get-ItemProperty -Path "HKLM:\SOFTWARE\Classes\Microsoft.PowerShellScript.1\Shell\0\Command").'(default)'

It will do the following: (%1 represents the location (fullpath) of the ps1 file being launched)

"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" "-Command" "if((Get-ExecutionPolicy ) -ne 'AllSigned') { Set-ExecutionPolicy -Scope Process Bypass }; & '%1'"

If the Execution policy is set to AllSigned, there’s no attempt to modify the execution policy in the process scope.

In my case, this prompt is expected considering what the ‘run with PowerShell’ action does

But sometimes, I don’t get a prompt. Strange 😦

There’s an about file about_Run_With_PowerShell that states:

The “Run with PowerShell” feature starts a Windows PowerShell
session that has an execution policy of Bypass, runs the
script, and closes the session.

It runs a command that has the following format:
PowerShell.exe -File -ExecutionPolicy Bypass

“Run with PowerShell” sets the Bypass execution policy only
for the session (the current instance of the PowerShell process)
in which the script runs. This feature does not change the execution
policy for the computer or the user.

The “Run with PowerShell” feature is affected only by the AllSigned
execution policy. If the AllSigned execution policy is effective for
the computer or the user, “Run with PowerShell” runs only signed
scripts. “Run with PowerShell” is not affected by any other execution
policy. For more information, see about_Execution_Policies.

Troubleshooting Note: Run with PowerShell command might prompt you
to confirm the execution policy change.

If I compare to what I see in the registry, Powershell.exe isn’t invoked with the -File parameter and the -Executionpolicy parameter set to bypass.
Also note that the help file states that the command might prompt you to confirm the execution policy change.
“Might” actually means: it’s true, I can get a prompt, but it’s not very likely.
This is actually the definition of an inconsistent behavior.

My Applocker policy seems to catch and block the execution.

But, I also immediatly get the event 4104 in the Microsoft-Windows-PowerShell/Operational as I turned on (non protected) scriptblock logging

Whether I get a prompt or not, whether I answer yes or no to the prompts, the code inside the ps1 file is executed.
The code being executed is a single WMI query that requires administrative privileges.
Its execution ends with a terminating error because the parent process of PowerShell.exe is Explorer and PowerShell.exe inherits from its integrity level set to “Medium”, running in the standard user context. (i.e. not running as administrator).

Anyway, I also turned on Transcripts and I clearly get the confirmation that the code inside .ps1 file is executed:



PowerShell scriptblock logging and transcripts are more reliable than Applocker.
“Run with PowerShell” actually bypassed the Appplocker policy by launching powershell.exe -command “& ”” and dot sourcing the script the same way malware do in their post-exploitation phase.

Better safe than sorry, I changed the behavior of the “Run with PowerShell” context menu.
I set it to the default behavior when you double-click on a .ps1 file: notepad.exe opens it, nothing is executed.

Set-ItemProperty -Path `
"HKLM:\SOFTWARE\Classes\Microsoft.PowerShellScript.1\Shell\0\Command" `
-Name '(default)' -Value '"C:\Windows\System32\notepad.exe" "%1"'

Anybody logging on this machine will now benefit from the new behavior.

Backup/Restore a local Windows Internal Database

I’ve got Direct Access servers where I want to backup the local Windows Internet Database.

The built-in VSS (Volume Shadow Copy Services) has 3 components:

Let’s see if there’s a writer for the Windows Internal Database component (WID)

vssadmin list writers | 
Select-String -Pattern "Writer\sname:\s" -Context 0,3 |
ForEach-Object {
        Id = (($_.Context.PostContext -split "\r\n")[0] -split ':')[1].Trim()
        Name = ($_.Line -split ':\s',2)[1] -replace "'",''
} | Select Name,Id | ft -AutoSize

Fortunately, there’s a WIDWriter in the above list 😀

It also means that the built-in tools are able perform a live backup. But, I first need to add the Windows Backup feature:

Add-WindowsFeature Windows-Server-Backup  -Restart:$false

Now, I’m ready to backup and restore the internal database files.
The backup operation will create a WindowsImageBackup directory on the target partition (P: in my case).
The recovery operation will extract files from the VHD previously created. It will not restore files in their original location under C:\Windows\…
but will rather move them to another folder (P:\BackupDB) and preserve the original folders tree.

Quick ‘n dirty solution but it allowed me to extract the database files in a consistent way using only the built-in tools and the capabilities of the Volume Shadow Copy Services (VSS).

Import the convenience update into WSUS

My WSUS server runs on a Windows Core edition where I don’t have Internet Explorer installed.

Whatif I want to import the Convenience rollup update for Windows 7 SP1 and Windows Server 2008 R2 SP1 from the catalog?

Here’s the way to go:

On a client computer with a GUI, launch internet explorer with high privileges to be able to install the ActiveX.

$HT = @{
 FilePath = 'C:\Program Files (x86)\Internet Explorer\iexplore.exe' ;
 ArgumentList = '' ;
 Verb = 'Runas'
Start-Process @HT


Install the ActiveX, add the update to the basket, view your basket, download, browse…
Move the file onto the WSUS server…and check if the file is digitally signed

Get-AuthenticodeSignature -FilePath ~\downloads\Catalog\*\*.msu

The WSUS API has an ImportUpdateFromCatalogSite, documented here on msdn.

I’m missing the UpdateID. Where do I find this Id?

It appears that if you click on the link highlighted below…


…you get on a page where you’ve more details about the update.


The updateId actually appears in the address bar. 😀

Now, to import the update into WSUS, I do:

$MSUfile = 'C:\Users\administrator\Downloads\Catalog\Update for Windows 7 for x64-based Systems (KB3125574)\AMD64-all-windows6.1-kb3125574-v4-x64_2dafb1d203c8964239af3048b5dd4b1264cd93b9.msu'
# Find the imported file
(Get-WsusServer).SearchUpdates('3125574') | fl *

The convenience update has been successfully imported and can now be approved.