Enabling PowerShell ScriptBlock and Transcript Logging
TL;DR Disabling LLMNR, mDNS, & NetBIOS, require SMB Signing, enabling Sysmon, and enabling both PowerShell Script Block and Transcript logging via a startup PS1.
This is essentially Part I in our mini series on Logging CLI Usage
Part I: PowerShell ScriptBlock & Transcript Logging
Part II: Logging common evasion TTPs
Part III: Whitelisting admins and flagging phishy CLI usage
Part IV: Transcript Logging WinRM Sessions
Background
We recently had a Red Team engagement at work and did better than I expected. The Red Team only got anywhere because we gave them initial access and one of our infrastructure folks screwed the proverbial pooch business process wise. In other words it wasn’t anything technical, [mis]configured, or an unpatched vulnerability.
However between recommendations the Red Team made and what we knew we needed to get around to eventually I made a quick and dirty list of ‘Best Practice 101’ things we hadn’t done yet.
- Disable LLMNR, mDNS, & NetBIOS
- Require SMB Signing
- Require AES [essentially disabling RC4]
- Prefer IPv4 over IPv6 [if you’re not using IPv6]
- Enable Sysmon
Most of this can be done in Group Policy settings, however I didn’t feel like clicking a bunch of buttons.
Why a PS1?
I’m a fan of write once, run anywhere. I’m also not a fan of GUIs. Hence I simply wrote a PS1 as I could
- Push it as a startup script via GPO
- Push it as a startup script via MECM
- Push it as a script via Intune
- Push it as a startup script on multiple enclaves
Additionally I could try it out and validate it worked in the test environment, aka the home lab, before using it at work, aka production.
PowerShell Logging
One of our smarter SIEM Guys remarked after the Red Team engagement that it would have been nice to know exactly what they ran and not just the results. I thought I’d heard of it before and consulted CW6 Google.
I may be preaching to the choir here, but there’s PowerShell Module & Script Block Logging … and then there’s PowerShell Transcript Logging. The first two log event IDs 4103 and 4104 respectively to the Windows Event Logs. Transcript Logging logs everything, including the commands ran and the resulting output, to *.txt files in the specified location.
Where to put Transcript Logs
I pointed clients to a share drive I created on the WEC. However please note that these logs do not have to be stored on a WEC. They are completely independent of Windows Event Logs, Windows Event Forwarding, and the WEC. They can be stored locally on each endpoint, on any share drive, etc.
However there are a few very strong arguments for placing them on a share drive and an argument of convenience for placing them on the WEC.
- If they are stored locally then an attacker who gains Local Amin will likely delete them.
- Querying them would be a PITA if they’re stored locally on each endpoint.
- An attacker would need to get Domain Admin or Local Admin on the WEC to delete or tamper with them.
- Event Logs are already collected on the WEC, so why not put the Transcript Logs there too.
- It’s easier to forward everything on to a SIEM, Azure Sentinel, etc if they’re stored together.
Pre-staging the Share
It’s critical to setup NTFS and share permissions on the share used to store Transcript logs rather carefully. This is because Domain Admins and Privileged Users are still users themselves. Therefore they are bound to accidentally type a plaintext password into their CLI eventually. If everyone can read the contents of that share then you would be essentially creating a [mis]config that is a much more serious security issue then you started with.
Get Rich’s stories in your inbox
Join Medium for free to get updates from this writer.
Therefore you want in the NTFS permissions
- No inheritance
- Administrators, and only Administrators, to have “FullControl”
- Authenticated Users & Domain Computers to have “CreateFiles,AppendData,WriteData,ReadAttributes”
- It doesn’t hurt to give Domain Users “CreateFiles,AppendData,WriteData,ReadAttributes”, but is not strictly necessary unless you changed something in the default AD.
In the Share permissions you want
- Administrators to have ‘FullAccess’
- Authenticated Users & Domain Computers to have ‘ChangeAccess’
Mere Domain Users can see the share, but when they attempt to read the data they get denied.
This is what that looks like in the PowerShell Transcript Logs.
I used this code block to create and setup the share to host the Transcript logs. TestWECII is simply the WEC’s name in my home lab, change that according to your environment.
Invoke-Command TestWECII -ScriptBlock {
$Path = "C:\PS_Logs"
if (-not(Test-Path $Path)) {New-Item $Path -ItemType Directory}
if (-not(Get-SmbShare -Name "PS_Logs$" -ErrorAction SilentlyContinue))
{New-SMBShare -Name "PS_Logs$" -Path $Path -FullAccess "Administrators" -ChangeAccess "Authenticated Users", "Domain Computers" -Description "PowerShell Transcription Drop Box" -EncryptData $true}
#Set the NTFS DACL so mere users can't read, modify, or delete the logs. Users require "CreateFiles,AppendData,WriteData,ReadAttributes"
$Acl = Get-Acl $Path
$Acl.SetAccessRuleProtection($true, $false) # Disable Inheritance, remove existing rules
$GroupSystem = New-Object System.Security.AccessControl.FileSystemAccessRule("SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$GroupAdmins = New-Object System.Security.AccessControl.FileSystemAccessRule("Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$GroupUsers = New-Object System.Security.AccessControl.FileSystemAccessRule("Authenticated Users", "CreateFiles,AppendData,WriteData,ReadAttributes", "ContainerInherit,ObjectInherit", "None", "Allow")
$GroupDomainUsers = New-Object System.Security.AccessControl.FileSystemAccessRule("Domain Users", "CreateFiles,AppendData,WriteData,ReadAttributes", "ContainerInherit,ObjectInherit", "None", "Allow")
$GroupComputers = New-Object System.Security.AccessControl.FileSystemAccessRule("Domain Computers", "CreateFiles,AppendData,WriteData,ReadAttributes", "ContainerInherit,ObjectInherit", "None", "Allow")
$Acl.AddAccessRule($GroupSystem)
$Acl.AddAccessRule($GroupAdmins)
$Acl.AddAccessRule($GroupUsers)
$Acl.AddAccessRule($GroupDomainUsers)
$Acl.AddAccessRule($GroupComputers)
Set-Acl -Path $Path -AclObject $Acl
}
# - - Run this if you made the share without adding the -EncryptDate $true like I did to encrypt it later
Invoke-Command TestWECII {if(-not((Get-SmbShare -Name PS_Logs$).EncryptData) -eq "$True") {Set-SmbShare -Name PS_Logs$ -EncryptData $true -Confirm:$false}}Applying PowerShell Logging via Group Policy
Then create a GPO with a startup script, drop this PS1 in the GPO’s folder on SYSVOL, and apply the GPO to the OU that you want to enable PowerShell Script Block and Transcript Logging on.
My startup PS1 for Windows Hardening also enables Sysmon, requires SMB Signing, and all that other stuff I listed at the beginning of this howto in a bullet list. This is just the snippet that enabled PowerShell Logging. I will post the entire PS1 on GitHub.
# - - Enable PowerShell logging - -
$PSLogPath = "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
if (-not(Test-Path $PSLogPath)) {New-Item -Path $psLogPath -Force}
# 1 = Enabled, 0 = Disabled
Set-ItemProperty -Path $PSLogPath -Name "EnableScriptBlockLogging" -Value 1 -Type DWord
Set-ItemProperty -Path $PSLogPath -Name "EnableScriptBlockInvocationLogging" -Value 1 -Type DWord
#Give the Event Log Readers group access to PowerShell logs
wevtutil sl "Microsoft-Windows-PowerShell/Operational" /ca:"O:BAG:SYD:(A;;0x1;;;SY)(A;;0x5;;;BA)(A;;0x1;;;S-1–5–32–573)"
# - - Run this in the startup PS1 on clients - -
#Enable Transcript
$NetworkPath = "\\TestWECII\PS_Logs$"
$RegPath = "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\Transcription"
if (-not(Test-Path $RegPath)) { New-Item -Path $RegPath -Force }
Set-ItemProperty -Path $RegPath -Name "EnableTranscripting" -Value 1 -Type DWord
Set-ItemProperty -Path $RegPath -Name "EnableInvocationHeader" -Value 1 -Type DWord
Set-ItemProperty -Path $RegPath -Name "OutputDirectory" -Value $NetworkPath -Type StringWhere this gets truly interesting
We are now logging everything anyone does in PowerShell, but what if we want to alert on suspicious behavior like someone running Rubeus, importing the Mimikatz PS1, running the AMSI Bypass, etc etc?
This method isn’t perfect given attacker’s ability to obfuscate but we can flag the low hanging fruit and attackers who don’t bother being sneaky via simply using Windows version of ‘grep’ against the entire folder of PowerShell Transcript Logs and checking them against a Dirty Word List.
Function Get-Dirty
{
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string]$DirtyWordPath, # Rename to avoid confusion with the list itself
[Parameter(Mandatory=$true, Position=1)]
[string]$LogPath
)
$Load the list from the path provided in the parameter
$DirtyWordList = Get-Content $DirtyWordPath
#Search all transcripts and return the result of the entire loop to the pipeline
$Results = Get-ChildItem -Path "$LogPath\*.txt" -Recurse | ForEach-Object {
$CurrentFile = $_
$Matches = Select-String -Path $CurrentFile.FullName -Pattern $DirtyWordList -Context 0,0
if ($Matches) {
$FileContent = Get-Content $CurrentFile.FullName
$UserLine = $FileContent[3]
$HostLine = $FileContent[6]
$StartTime = $FileContent[2] # Start time is usually line 2 (Index 1)
foreach ($Match in $Matches) {
[PSCustomObject]@{
Timestamp = ($StartTime).Split(':', 2)[1].Trim()
User = ($UserLine -replace 'Username: ', '').Trim()
Computer = ($HostLine -replace 'Computer: ', '').Trim()
DirtyWord = $Match.Pattern
Command = $Match.Line.Trim()
LogFile = $CurrentFile.Name
}
}
}
}
# Send the final collection to the function output
return $Results
}
# - - How to run it now - -
Write-Host "Example command to use this function: Get-Dirty -DirtyWordPath "C:\DirtyWords.txt" -LogPath "C:\PS_Logs" | Format-Table -AutoSize"
Write-Host "Use | Out-GridView instead of | Format-Table -Autosize IOT get output you can sort by time."Run the query via
Get-Dirty -DirtyWordPath "C:\DirtyWords.txt" -LogPath "C:\PS_Logs" | Format-Table -AutoSizeI intentionally generated some of these results to validate the query works by logging into a VM and running a couple simple commands an attacker might do like ‘. .\Invoke-Mimikatz.ps1’ and two versions of the AMSI Bypass.
Obviously you’ll get some false positives and you’ll flag some Admins who were simply thinking outside the box, but it gives you something to look into more whenever your Cyber folks have downtime.
Summary
Personally I find PowerShell Transcript Logging fascinating and admittedly I really should have test ran it years earlier. I now have a TON of work I need to do in the home lab testing out ideas like “what if an attacker uses Living Off the Land (LOL) to run PowerShell without PowerShell? Will Transcript and/or Script Block Logging still log it?” and “how can I write a function that white lists the usernames of our IT folks and then flags anyone else who does anything in PowerShell?”.
The second idea is really interesting because if a Public Affairs person or someone in HR runs PowerShell at all, let alone starts running common enumeration commands, then that’s a big red flag. It’s an indicator they either got phished or they’re an insider threat. Either way it’s well worth looking into to see if they were just bored, using PowerShell to calculate their mile pace on a half marathon, or something much worse.
At any rate after testing out, validating, and fine tuning all this at home I enabled PowerShell ScriptBlock and Transcript logging at work. The rest of the common hardening techniques are on the immediate ‘to do’ list at work.
They say that everyone has a test environment, but some also have a production. Our incredibly smart boss said earlier this week “but we don’t’ have a test environment”. He’s right, strictly at work and strictly speaking as “we”. However I do :p
I also have to tweak that startup PS1 a bit so it’s valid as an Intune Script. Our main enclave at work is no longer AD, Microsoft is pushing Entra ID/Intune/M365 heavily, my home lab is hybrid AD, and I don’t want to become a complete dinosaur who only knows AD and Group Policy.
One of the things that came out of that Red Team engagement was using a ‘Banned Word List’ for passwords. AD has no built in feature for this, but Entra ID does. One can enable this ‘on prem’ if, and only if, they’re running hybrid AD and put an Entra ID agent on their DCs. I understand it in theory, but I haven’t actually done it in the home lab yet.
Well that’s a project for the future.
References
Greater Visibility Through PowerShell Logging: https://cloud.google.com/blog/topics/threat-intelligence/greater-visibility/
Sysmon xml config: https://github.com/SwiftOnSecurity/sysmon-config
New AMSI Bypass: https://github.com/thesecguy45/Tools/blob/main/AMSI-Bypasses
AMSI Bypass over time: https://medium.com/@thesecguy/amsi-bypass-in-2025-bypassing-modern-av-edr-f8a46b91280e
The hero who gave me the right syntax to silently install Sysmon: https://jeffreyappel.nl/deploy-sysmon-and-collect-data-with-sentinel-and-the-ama-agent/#:~:text=Sysmon%20needs%20to%20be%20installed,the%20current%20configuration%20sysmon%20%2Dc








