Last active
May 25, 2023 18:48
-
-
Save JoeDibley/fd93a9c5b3d45dbd8cbfdd003ddc1bd1 to your computer and use it in GitHub Desktop.
Checks the username of logged in users to the Kerberos Tickets. Whilst not the best it is a potential local based detection for things like Pass-the-Ticket, Golden Ticket type attacks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
This is comprised of 3 functions: | |
1. Get-Sessions: Get Session information from WMI. | |
Reused some code from https://github.com/tmmtsmith/Powershell/blob/master/Get-LoggedOnUsers.ps1 for the regex matching | |
2. Return-SessionTickets: Retrieves Kerberos Session Tickets. This is a klist wrapper to return PSCustomObjects | |
This code is mostly all from the GetKerbTix.ps1 script in the Technet Script Center | |
https://gallery.technet.microsoft.com/scriptcenter/List-All-Cached-Kerberos-5ba41829 | |
3. Invoke-PtTCheck: Perform username matching to see if the username logged on has different username to Kerberos tickets registered for their session | |
This is not by any means a full detection for Pass-the-Ticket or Golden Ticket but can just show 1 simple way | |
in which a blue team could go about checking for something like this on a compromised client. | |
USAGE | |
1. Dot source the script | |
PS> . Invoke-PtTCheck.ps1 | |
2. Run Invoke-PtTCheck | |
#> | |
function Get-Sessions | |
{ | |
param | |
( | |
[parameter(Mandatory = $false)] | |
[String[]] | |
$computerName = "localhost" | |
) | |
Write-Verbose -Message "Executing Win32_LoggedOnUser on $Computer" | |
$logon_users = Get-WmiObject win32_loggedonuser -ComputerName $ComputerName | |
if ($logon_users) | |
{ | |
Write-Verbose -Message "Found $($logon_users.count) Logon Sessions on $Computer" | |
foreach ($Logon_User in $Logon_Users) | |
{ | |
# Ensure Antecedent Matches the Domain and Name format expected. PowerShell Automatically stores matches in $Matches variable. | |
$Logon_User.antecedent -match $regexa | Out-Null | |
#Create Domain\Username format | |
$username = $matches[1] + "\" + $matches[2] | |
# Ensure Dependent matches the expected logonID format | |
$Logon_User.dependent -match $regexd | Out-Null | |
# Extract Logon ID from matches | |
$session = $matches[1] | |
# Put Logon ID in the Session Hex Format which is used by klist | |
$sessionHex = ('0x{0:X}' -f [int]$session) | |
# Create and output objects | |
$Object = New-Object -TypeName psobject | |
$Object | Add-Member -MemberType NoteProperty -Name "Session" -Value $sessionHex | |
$Object | Add-Member -MemberType NoteProperty -Name "Username" -Value $username | |
$Object | |
} | |
} | |
} | |
function Return-SessionTickets | |
{ | |
param | |
( | |
[parameter(Mandatory = $false)] | |
$SessionID = $null, | |
[Switch]$TimeOutput | |
) | |
$OS = Get-WmiObject win32_operatingsystem | |
if ($SessionID -eq $null) | |
{ | |
$TicketsArray = klist.exe tickets | |
} | |
else | |
{ | |
$TicketsArray = klist.exe tickets -li $sessionID | |
if ($TicketsArray -like "*Error calling API LsaCallAuthenticationPackage*") | |
{ | |
$SessionID = "0:$sessionID" | |
$TicketsArray = klist.exe tickets -li $sessionID | |
} | |
} | |
$Counter = 0 | |
$TicketsObject = New-Object PSObject | |
foreach ($line in $TicketsArray) | |
{ | |
if ($line -match "^#\d") | |
{ | |
$Ticket = New-Object PSObject | |
$Number = $Line.Split('>')[0] | |
$Line1 = $Line.Split('>')[1] | |
$TicketNumber = "Ticket " + $Number | |
$Client = $Line1; $Client = $Client.Replace('Client:', ''); $Client = $Client.Substring(2) | |
$Server = $TicketsArray[$Counter + 1]; $Server = $Server.Replace('Server:', ''); $Server = $Server.substring(2) | |
$KerbTicketEType = $TicketsArray[$Counter + 2]; $KerbTicketEType = $KerbTicketEType.Replace('KerbTicket Encryption Type:', ''); $KerbTicketEType = $KerbTicketEType.substring(2) | |
$TickFlags = $TicketsArray[$Counter + 3]; $TickFlags = $TickFlags.Replace('Ticket Flags', ''); $TickFlags = $TickFlags.substring(2) | |
$StartTime = $TicketsArray[$Counter + 4]; $StartTime = $StartTime.Replace('Start Time:', ''); $StartTime = $StartTime.substring(2) | |
$EndTime = $TicketsArray[$Counter + 5]; $EndTime = $EndTime.Replace('End Time:', ''); $EndTime = $EndTime.substring(4) | |
$RenewTime = $TicketsArray[$Counter + 6]; $RenewTime = $RenewTime.Replace('Renew Time:', ''); $RenewTime = $RenewTime.substring(2) | |
$SessionKey = $TicketsArray[$Counter + 7]; $SessionKey = $SessionKey.Replace('Session Key Type:', ''); $SessionKey = $SessionKey.substring(2) | |
# Convert Start and End Times to actual dates | |
[datetime]$StartTime_Obj = $StartTime.TrimEnd('(local)') | |
[datetime]$EndTime_Obj = $EndTime.TrimEnd('(local)') | |
[datetime]$RenewTime_Obj = $RenewTime.TrimEnd('(local)') | |
# Calculate the Service Ticket Lifetime | |
$Ticket_Lifetime = New-TimeSpan -Start $StartTime_Obj -End $EndTime_Obj | |
$Renew_Lifetime = New-TimeSpan -Start $StartTime_Obj -End $RenewTime_Obj | |
# Get Service From SPN | |
$Service = $Server.split('/')[0] | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "TicketNumber" -Value $TicketNumber | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "Client" -Value $Client | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "Server" -Value $Server | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "KerbTicketEncryptionType" -Value $KerbTicketEType | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "TicketFlags" -Value $TickFlags | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "StartTime" -Value $StartTime_Obj | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "EndTime" -Value $EndTime_Obj | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "RenewTime" -Value $RenewTime_Obj | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "SessionKeyType" -Value $SessionKey | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "TicketLifetime" -Value $Ticket_Lifetime | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "Service" -Value $Service | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "RenewLifetime" -Value $Renew_Lifetime | |
if ($OS.BuildNumber -ge 9200) | |
{ | |
$CacheFlags = $TicketsArray[$Counter + 8]; $CacheFlags = $CacheFlags.Replace('Cache Flags:', ''); $CacheFlags = $CacheFlags.substring(2) | |
$KDCCalled = $TicketsArray[$Counter + 9]; $KDCCalled = $KDCCalled.Replace('Kdc Called:', ''); $KDCCalled = $KDCCalled.substring(2) | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "Cache Flags" -Value $CacheFlags | |
Add-Member -InputObject $Ticket -MemberType NoteProperty -Name "KDC Called" -Value $KDCCalled | |
} | |
[array]$Tickets += $Ticket | |
} | |
$Counter++ | |
} | |
If ($TimeOutput) | |
{ | |
return $Tickets | select-object Client, Service, StartTime, EndTime, RenewTime, Ticketlifetime, RenewLifetime | Sort-Object -Descending TicketLifetime | Format-Table | |
} | |
else | |
{ | |
return $Tickets | |
} | |
} | |
function Invoke-PtTCheck | |
{ | |
param | |
( | |
[parameter(Mandatory = $false)] | |
[string[]]$ComputerName = "localhost" # For when / if it can support multiple hosts | |
) | |
BEGIN | |
{ | |
# Regex Matches to get session info | |
$regexa = '.+Domain="(.+)",Name="(.+)"$' | |
$regexd = '.+LogonId="(\d+)"$' | |
} | |
PROCESS | |
{ | |
foreach ($Computer in $ComputerName) | |
{ | |
[pscustomobject]$sessions = Get-Sessions -computerName $Computer | |
foreach ($Session in $sessions) | |
{ | |
# Use Wrapper Function for Klist to search for tickets for the use | |
$Session_Tickets = Return-SessionTickets -sessionID $Session.Session | |
if ($Session_Tickets) | |
{ | |
# Get Unique Client (Username) from Ticket FORMAT = Username @ FQDN Domain | |
$Unique_Username_for_session = $Session_Tickets.Client | Select-Object -Unique | |
$Unique_Username_for_session_Formatted = $Unique_Username_for_session.split(" ")[0] | |
Write-Verbose -Message "Unique Username in $sessionHex`: $Unique_Username_for_session_Formatted" | |
# Due to weird formatting and potential for different netbios vs full dns name | |
# of domain just check to see if the username in the session ticket is on the right of the username field | |
if ($Session.username.split('\')[-1] -eq $Unique_Username_for_session_Formatted) | |
{ | |
$Session | Add-Member -MemberType NoteProperty -Name "IsPtT" -Value $False | |
$Session | Add-Member -MemberType NoteProperty -Name "PtTUser" -Value $null | |
} | |
elseif ($Session.Username.split('\')[0] -eq $Unique_Username_for_session_Formatted.trimend('$')) | |
{ | |
# This is to match computer name in the domain field to the computername in Kerberos tickets for SYSTEM / Local Service / Network Service Services | |
$Session | Add-Member -MemberType NoteProperty -Name "IsPtT" -Value $False | |
$Session | Add-Member -MemberType NoteProperty -Name "PtTUser" -Value $null | |
} | |
else | |
{ | |
$Session | Add-Member -MemberType NoteProperty -Name "IsPtT" -Value $true | |
$Session | Add-Member -MemberType NoteProperty -Name "PtTUser" -Value $Unique_Username_for_session_Formatted | |
} | |
} | |
else | |
{ | |
Write-Verbose -Message "No session tickets for user: $($Session.Username)" | |
$Session | Add-Member -MemberType NoteProperty -Name "IsPtT" -Value $false | |
$Session | Add-Member -MemberType NoteProperty -Name "PtTUser" -Value $null | |
} | |
[array] $sessionOutput += $Session | |
} | |
} | |
} | |
END | |
{ | |
$sessionOutput | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment