489 lines
22 KiB
PowerShell
489 lines
22 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
|
|
A PowerShell module for hunt teaming via Windows event logs
|
|
.DESCRIPTION
|
|
|
|
DeepBlueCLI can automatically determine events that are typically triggered during a majority of successful breaches, including use of malicious command lines including PowerShell.
|
|
.Example
|
|
|
|
Process local Windows security event log:
|
|
.\DeepBlue.ps1
|
|
.\DeepBlue.ps1 -log security
|
|
.Example
|
|
Process local Windows system event log:
|
|
|
|
.\DeepBlue.ps1 -log system
|
|
.\DeepBlue.ps1 "" system
|
|
.Example
|
|
Process evtx file:
|
|
|
|
.\DeepBlue.ps1 .\evtx\new-user-security.evtx
|
|
.\DeepBlue.ps1 -file .\evtx\new-user-security.evtx
|
|
.LINK
|
|
https://github.com/sans-blue-team/DeepBlueCLI
|
|
|
|
#>
|
|
|
|
# DeepBlueCLI 0.3 Beta
|
|
# Eric Conrad, Backshore Communications, LLC
|
|
# deepblue <at> backshore <dot> net
|
|
# Twitter: @eric_conrad
|
|
# http://ericconrad.com
|
|
#
|
|
|
|
param ([string]$file=$env:file,[string]$log=$env:log)
|
|
|
|
function Main {
|
|
$text="" # Temporary scratch pad variable to hold output text
|
|
$minlength=1000 # Minimum length of command line to alert
|
|
# Load cmd match regexes from csv file, ignore comments
|
|
$regexes = Get-Content ".\regexes.txt" | Select-String '^[^#]' | ConvertFrom-Csv
|
|
# Load cmd whitelist regexes from csv file, ignore comments
|
|
$whitelist = Get-Content ".\whitelist.txt" | Select-String '^[^#]' | ConvertFrom-Csv
|
|
$logname=Check-Options $file $log
|
|
"Processing the " + $logname + " log..."
|
|
$filter=Create-Filter $file $logname
|
|
$failedlogons=0 # Count of failed logons (Security event 4625)
|
|
$maxfailedlogons=100 # Alert after this many failed logons
|
|
# Get the events:
|
|
try{
|
|
$events = iex "Get-WinEvent $filter -ErrorAction Stop"
|
|
}
|
|
catch {
|
|
Write-Host "Get-WinEvent $filter -ErrorAction Stop"
|
|
Write-Host "Get-WinEvent error: " $_.Exception.Message "`n"
|
|
Write-Host "Exiting...`n"
|
|
exit
|
|
}
|
|
ForEach ($event in $events) {
|
|
$output="" # Final output text string
|
|
$eventXML = [xml]$event.ToXml()
|
|
if ($logname -eq "Security"){
|
|
if ($event.id -eq 4688){
|
|
# A new process has been created. (Command Line Logging)
|
|
$commandline=$eventXML.Event.EventData.Data[8]."#text"
|
|
$output += (Check-Command $commandline $minlength $regexes $whitelist 0)
|
|
}
|
|
ElseIf ($event.id -eq 4720){
|
|
# A user account was created.
|
|
$username=$eventXML.Event.EventData.Data[0]."#text"
|
|
$securityid=$eventXML.Event.EventData.Data[2]."#text"
|
|
$output += " New user created: $username`n"
|
|
$output += " - User SID: $securityid`n"
|
|
}
|
|
ElseIf(($event.id -eq 4728) -or ($event.id -eq 4732)){
|
|
# A member was added to a security-enabled (global|local) group.
|
|
$groupname=$eventXML.Event.EventData.Data[2]."#text"
|
|
# Check if group is Administrators, may later expand to all groups
|
|
if ($groupname -eq "Administrators"){
|
|
$username=$eventXML.Event.EventData.Data[0]."#text"
|
|
$securityid=$eventXML.Event.EventData.Data[1]."#text"
|
|
switch ($event.id){
|
|
4728 {$output += " User added to global $groupname group`n"}
|
|
4732 {$output += " User added to local $groupname group`n"}
|
|
}
|
|
$output += " - Username: $username`n"
|
|
$output += " - User SID: $securityid`n"
|
|
}
|
|
}
|
|
ElseIf($event.id -eq 4625){
|
|
# An account failed to log on.
|
|
# Requires auditing logon failures
|
|
# https://technet.microsoft.com/en-us/library/cc976395.aspx
|
|
$username=$eventXML.Event.EventData.Data[5]."#text"
|
|
$failedlogons += 1
|
|
}
|
|
}
|
|
ElseIf ($logname -eq "System"){
|
|
if ($event.id -eq 7045){
|
|
# A service was installed in the system.
|
|
$servicename=$eventXML.Event.EventData.Data[0]."#text"
|
|
# Check for suspicious service name
|
|
$text = (Check-Regex $servicename $regexes 1)
|
|
if ($text){
|
|
$output += " Service created, service name: $servicename`n"
|
|
$output += $text
|
|
}
|
|
# Check for suspicious cmd
|
|
$commandline=$eventXML.Event.EventData.Data[1]."#text"
|
|
$output += (Check-Command $commandline $minlength $regexes $whitelist 1)
|
|
}
|
|
ElseIf ($event.id -eq 7030){
|
|
# The ... service is marked as an interactive service. However, the system is configured
|
|
# to not allow interactive services. This service may not function properly.
|
|
$servicename=$eventXML.Event.EventData.Data."#text"
|
|
$output += " Interactive service warning, service name: $servicename`n"
|
|
# Check for suspicious service name
|
|
$output += (Check-Regex $servicename $regexes 1)
|
|
}
|
|
ElseIf ($event.id -eq 7036){
|
|
# The ... service entered the stopped|running state.
|
|
$servicename=$eventXML.Event.EventData.Data[0]."#text"
|
|
$text = (Check-Regex $servicename $regexes 1)
|
|
if ($text){
|
|
$output += " " + $event.Message + "`n"
|
|
$output += $text
|
|
}
|
|
}
|
|
}
|
|
ElseIf ($logname -eq "Application"){
|
|
if (($event.id -eq 2) -and ($event.Providername -eq "EMET")){
|
|
# EMET Block
|
|
$output += " EMET Block`n"
|
|
if ($event.Message){
|
|
# EMET Message is a blob of text that looks like this:
|
|
#########################################################
|
|
# EMET detected HeapSpray mitigation and will close the application: iexplore.exe
|
|
#
|
|
# HeapSpray check failed:
|
|
# Application : C:\Program Files (x86)\Internet Explorer\iexplore.exe
|
|
# User Name : WIN-CV6AHH1BNU9\Instructor
|
|
# Session ID : 1
|
|
# PID : 0xBA8 (2984)
|
|
# TID : 0x9E8 (2536)
|
|
# Module : mshtml.dll
|
|
# Address : 0x6FBA7512, pull out relevant parts
|
|
$array = $event.message -split '\n' # Split each line of the message into an array
|
|
$message = $array[0]
|
|
$application = Remove-Spaces($array[3])
|
|
$username = Remove-Spaces($array[4])
|
|
$output += " - Message: $message`n"
|
|
$output += " - $application`n"
|
|
$output += " - $username`n"
|
|
}
|
|
Else{
|
|
# If the message is blank: EMET is not installed locally.
|
|
# This occurs when parsing remote event logs sent from systems with EMET installed
|
|
$output += " Warning: EMET Message field is blank. Install EMET locally to see full details of this alert"
|
|
}
|
|
}
|
|
}
|
|
ElseIf ($logname -eq "Applocker"){
|
|
if ($event.id -eq 8004){
|
|
# ...was prevented from running.
|
|
$output += " Applocker block: " + $event.message
|
|
}
|
|
}
|
|
ElseIf ($logname -eq "PowerShell"){
|
|
#$event.pd
|
|
if ($event.id -eq 4103){
|
|
$pscommand= $eventXML.Event.EventData.Data[2]."#text"
|
|
if ($pscommand -Match "Host Application"){
|
|
# Multiline replace, remove everything before "Host Application = "
|
|
$pscommand = $pscommand -Replace "(?ms)^.*Host.Application = ",""
|
|
# Remove every line after the "Host Application = " line.
|
|
$pscommand = $pscommand -Replace "(?ms)`n.*$",""
|
|
$output += (Check-Command $pscommand $minlength $regexes $whitelist 0)
|
|
}
|
|
}
|
|
ElseIf ($event.id -eq 4104){
|
|
# This section requires PowerShell command logging, which is not the default with
|
|
# event 4104 (logs the script block but not the command that launched it).
|
|
#
|
|
# Add the following to \Windows\System32\WindowsPowerShell\v1.0\profile.ps1
|
|
# $LogCommandHealthEvent = $true
|
|
# $LogCommandLifecycleEvent = $true
|
|
#
|
|
# See the following for more information:
|
|
#
|
|
# https://logrhythm.com/blog/powershell-command-line-logging/
|
|
# http://hackerhurricane.blogspot.com/2014/11/i-powershell-logging-what-everyone.html
|
|
#
|
|
# Thank you: @heinzarelli and @HackerHurricane
|
|
#
|
|
# The command's path is $eventxml.Event.EventData.Data[4]
|
|
# Blank path means it was run as a commandline. CLI parsing is *much* simpler than
|
|
# script parsing.
|
|
# This ignores scripts and grabs PowerShell CLIs
|
|
if (-not ($eventxml.Event.EventData.Data[4]."#text")){
|
|
$pscommand=$eventXML.Event.EventData.Data[2]."#text"
|
|
$output += (Check-Command $pscommand 500 $regexes $whitelist 0)
|
|
}
|
|
}
|
|
}
|
|
ElseIf ($logname -eq "Sysmon"){
|
|
#@{logname="Microsoft-Windows-Sysmon/Operational";id=1} | %{$_.Properties[11].Value}| sort -Unique
|
|
#get-winevent @{logname="Microsoft-Windows-Sysmon/Operational";id=1} | % {$_.Properties[11].Value}| Sort-Object -unique
|
|
#Get-WinEvent @{logname="Microsoft-Windows-Sysmon/Operational";id=7}|fl
|
|
# Check command lines
|
|
if ($event.id -eq 1){
|
|
#get-winevent @{logname="Microsoft-Windows-Sysmon/Operational";id=1} | % {$_.Properties[4].Value}
|
|
$commandline=$eventXML.Event.EventData.Data[4]."#text"
|
|
# Remove "Command Line: " from the $commandline
|
|
#$commandline= $commandline -Replace "^Command Line:",""
|
|
#$commandline
|
|
$output += (Check-Command $commandline $minlength $regexes $whitelist 0)
|
|
}
|
|
# Check for unsigned EXEs/DLLs:
|
|
ElseIf ($event.id -eq 7){
|
|
if ($eventXML.Event.EventData.Data[6]."#text" -eq "false"){
|
|
$image=$eventXML.Event.EventData.Data[3]."#text"
|
|
$imageload=$eventXML.Event.EventData.Data[4]."#text"
|
|
$hash=$eventXML.Event.EventData.Data[5]."#text"
|
|
$pscommand= " - Image: " + $image + "`r`n"
|
|
$pscommand+= " - ImageLoaded: " + $imageload + "`r`n"
|
|
#$pscommand+= " - Hash: " + $hash + "`r`n"
|
|
# Multiple hashes may be logged, we want SHA1. Remove everything through "SHA1="
|
|
$sha1= $hash -Replace "(?ms)^.*SHA1=",""
|
|
# Split the string on commas, grab field 0
|
|
$sha1=$sha1.Split(",")[0]
|
|
$hashfile=".\hashes\$sha1"
|
|
if (-not (Test-Path $hashfile)){
|
|
# Hash file doesn't exist, create it
|
|
$csv=$image+","+$imageload
|
|
$csv | Set-Content $hashfile
|
|
}
|
|
#$pscommand+= $eventXML.Event.EventData.Data[6]."#text" + "`r`n"
|
|
#$pscommand+= $eventXML.Event.EventData.Data[7]."#text" + "`r`n"
|
|
#$pscommand+= $eventXML.Event.EventData.Data[8]."#text" + "`r`n"
|
|
$output+= " Unsigned image:`r`n"
|
|
$output+= $pscommand
|
|
}
|
|
}
|
|
}
|
|
if ($output){
|
|
$event.TimeCreated
|
|
$output
|
|
""
|
|
}
|
|
}
|
|
if ($failedlogons -gt $maxfailedlogons){
|
|
"High number of failed logons in the security event log: " + $failedlogons
|
|
}
|
|
}
|
|
|
|
function Check-Options($file, $log)
|
|
{
|
|
$log_error="Unknown and/or unsupported log type"
|
|
$logname=""
|
|
# Checks the command line options, return logname to parse
|
|
if($file -eq ""){ # No filename provided, parse local logs
|
|
if(($log -eq "") -or ($log -eq "Security")){ # Parse the security log if no log was selected
|
|
$logname="Security"
|
|
}
|
|
ElseIf ($log -eq "System"){
|
|
$logname="System"
|
|
}
|
|
ElseIf ($log -eq "Application"){
|
|
$logname="Application"
|
|
}
|
|
ElseIf ($log -eq "Sysmon"){
|
|
$logname="Sysmon"
|
|
}
|
|
ElseIf ($log -eq "Powershell"){
|
|
$logname="Powershell"
|
|
}
|
|
Else{
|
|
write-host $log_error
|
|
exit 1
|
|
}
|
|
}
|
|
else{ # Filename provided, check if it exists:
|
|
if (Test-Path $file){ # File exists. Todo: verify it is an evtx file.
|
|
# Get-WinEvent will generate this error for non-evtx files: "...file does not appear to be a valid log file.
|
|
# Specify only .evtx, .etl, or .evt filesas values of the Path parameter."
|
|
#
|
|
# Check the LogName of the first event
|
|
try{
|
|
$event=Get-WinEvent -path $file -max 1 -ErrorAction Stop
|
|
}
|
|
catch
|
|
{
|
|
Write-Host "Get-WinEvent error: " $_.Exception.Message "`n"
|
|
Write-Host "Exiting...`n"
|
|
exit
|
|
}
|
|
switch ($event.LogName){
|
|
"Security" {$logname="Security"}
|
|
"System" {$logname="System"}
|
|
"Application" {$logname="Application"}
|
|
"Microsoft-Windows-AppLocker*" {$logname="Applocker"}
|
|
"Microsoft-Windows-PowerShell/Operational" {$logname="Powershell"}
|
|
"Microsoft-Windows-Sysmon/Operational" {$logname="Sysmon"}
|
|
default {"Logic error 3, should not reach here...";Exit 1}
|
|
}
|
|
}
|
|
else{ # Filename does not exist, exit
|
|
Write-host "Error: no such file. Exiting..."
|
|
exit 1
|
|
}
|
|
}
|
|
return $logname
|
|
}
|
|
|
|
function Create-Filter($file, $logname)
|
|
{
|
|
# Return the Get-Winevent filter
|
|
#
|
|
$sys_events="7030,7036,7045"
|
|
$sec_events="4688,4720,4728,4732,4625"
|
|
$app_events="2"
|
|
$applocker_events="8003,8004,8006,8007"
|
|
$powershell_events="4103,4104"
|
|
$sysmon_events="1,7"
|
|
if ($file -ne ""){
|
|
switch ($logname){
|
|
"Security" {$filter="@{path=""$file"";ID=$sec_events}"}
|
|
"System" {$filter="@{path=""$file"";ID=$sys_events}"}
|
|
"Application" {$filter="@{path=""$file"";ID=$app_events}"}
|
|
"Applocker" {$filter="@{path=""$file"";ID=$applocker_events}"}
|
|
"Powershell" {$filter="@{path=""$file"";ID=$powershell_events}"}
|
|
"Sysmon" {$filter="@{path=""$file"";ID=$sysmon_events}"}
|
|
default {"Logic error 1, should not reach here...";Exit 1}
|
|
}
|
|
}
|
|
else{
|
|
switch ($logname){
|
|
"Security" {$filter="@{Logname=""Security"";ID=$sec_events}"}
|
|
"System" {$filter="@{Logname=""System"";ID=$sys_events}"}
|
|
"Application" {$filter="@{Logname=""Application"";ID=$app_events}"}
|
|
"Applocker" {$filter="@{logname=""Microsoft-Windows-AppLocker"";ID=$applocker_events}"}
|
|
"Powershell" {$filter="@{logname=""Microsoft-Windows-PowerShell/Operational"";ID=$powershell_events}"}
|
|
"Sysmon" {$filter="@{logname=""Microsoft-Windows-Sysmon/Operational"";ID=$sysmon_events}"}
|
|
default {"Logic error 2, should not reach here...";Exit 1}
|
|
}
|
|
}
|
|
return $filter
|
|
}
|
|
|
|
|
|
function Check-Command($commandline,$minlength,$regexes,$whitelist,$servicecmd){
|
|
$text=""
|
|
$base64=""
|
|
# Check to see if command is whitelisted
|
|
foreach ($entry in $whitelist) {
|
|
if ($commandline -Match $entry.regex) {
|
|
# Command is whitelisted, return nothing
|
|
return
|
|
}
|
|
}
|
|
#$cmdlength=$commandline.length
|
|
#if ($cmdlength -gt $minlength){
|
|
if ($commandline.length -gt $minlength){
|
|
$text += " - Long Command Line: greater than $minlength bytes`n"
|
|
}
|
|
$text += (Check-Obfu $commandline)
|
|
$text += (Check-Regex $commandline $regexes 0)
|
|
# Check for base64 encoded function, decode and print if found
|
|
# This section is highly use case specific, other methods of base64 encoding and/or compressing may evade these checks
|
|
if ($commandline -Match "\-enc.*[A-Za-z0-9/+=]{100}"){
|
|
$base64= $commandline -Replace "^.* \-Enc(odedCommand)? ",""
|
|
}
|
|
ElseIf ($commandline -Match ":FromBase64String\("){
|
|
$base64 = $commandline -Replace "^.*:FromBase64String\(\'*",""
|
|
$base64 = $base64 -Replace "\'.*$",""
|
|
}
|
|
if ($base64){
|
|
if ($commandline -Match "Compression.GzipStream.*Decompress"){
|
|
# Metasploit-style compressed and base64-encoded function. Uncompress it.
|
|
$decoded=New-Object IO.MemoryStream(,[Convert]::FromBase64String($base64))
|
|
$uncompressed=(New-Object IO.StreamReader(((New-Object IO.Compression.GzipStream($decoded,[IO.Compression.CompressionMode]::Decompress))),[Text.Encoding]::ASCII)).ReadToEnd()
|
|
$text += " Decoded/decompressed Base64:" + $uncompressed
|
|
$text += " - Base64-encoded and compressed function`n"
|
|
}
|
|
else{
|
|
$decoded = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($base64))
|
|
$text += " Decoded Base64:" + $decoded + "`n"
|
|
$text += " - Base64-encoded function`n"
|
|
$text += (Check-Obfu $decoded)
|
|
$text += (Check-Regex $decoded $regexes 0)
|
|
#foreach ($regex in $regexes){
|
|
# if ($regex.Type -eq 0) { # Image Path match
|
|
# if ($decoded -Match $regex.regex) {
|
|
# $text += " - " + $regex.String + "`n"
|
|
# }
|
|
# }
|
|
#}
|
|
}
|
|
}
|
|
if ($text){
|
|
if ($servicecmd){
|
|
return " Service File Name: $commandline`n" + $text
|
|
}
|
|
Else{
|
|
return " Command Line: $commandline`n" + $text
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function Check-Regex($string,$regexes,$type){
|
|
$regextext="" # Local variable for return output
|
|
foreach ($regex in $regexes){
|
|
if ($regex.Type -eq $type) { # Type is 0 for Commands, 1 for services. Set in regexes.csv
|
|
if ($string -Match $regex.regex) {
|
|
$regextext += " - " + $regex.String + "`n"
|
|
}
|
|
}
|
|
}
|
|
return $regextext
|
|
}
|
|
|
|
function Check-Obfu($string){
|
|
# Check how many special characters are in the command. Inspired by Invoke-Obfuscation: https://twitter.com/danielhbohannon/status/778268820242825216
|
|
# There are many ways to do this, including regex. Need a way that doesn't kill the CPU. This works, but isn't super concise. There is probably a
|
|
# better way.
|
|
#
|
|
$obfutext="" # Local variable for return output
|
|
$maxchars=25
|
|
#$obfuchars = "\+", "\'", "\}", "\{"
|
|
#foreach ($char in $obfuchars){
|
|
#
|
|
# I tried to loop through the characters (as the two commented lines above show, but
|
|
# hit problems of variable interpolation. I am probably making a simple mistake.
|
|
# If you can get the above loop working, please email deepblue at backshore dot net.
|
|
# I will repay in an adult beverage
|
|
#
|
|
# In the meantime, this is ugly, but works
|
|
$string2 = $string -replace "`'"
|
|
# Compare the length
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars ' characters`n"
|
|
}
|
|
$string2 = $string -replace "`{"
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars { characters`n"
|
|
}
|
|
$string2 = $string -replace "`}"
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars } characters`n"
|
|
}
|
|
$string2 = $string -replace ","
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars , characters`n"
|
|
}
|
|
$string2 = $string -replace "!"
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars ! characters`n"
|
|
}
|
|
$string2 = $string -replace "%"
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars % characters`n"
|
|
}
|
|
$string2 = $string -replace "&"
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars & characters`n"
|
|
}
|
|
$string2 = $string -replace ">"
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars > characters`n"
|
|
}
|
|
$string2 = $string -replace "`""
|
|
if (($string.length - $string2.length) -gt $maxchars){
|
|
$obfutext += " - Possible command obfuscation: greater than $maxchars double quotes`n"
|
|
}
|
|
return $obfutext
|
|
#}
|
|
}
|
|
|
|
function Remove-Spaces($string){
|
|
# Changes this: Application : C:\Program Files (x86)\Internet Explorer\iexplore.exe
|
|
# to this: Application: C:\Program Files (x86)\Internet Explorer\iexplore.exe
|
|
$string = $string.trim() -Replace "\s+:",":"
|
|
return $string
|
|
}
|
|
|
|
. Main
|
|
|