Scroll Top

Query logs with PowerShell

Martin Handel

Spannende Frage von einem Kunden:

von welchen Rechnern aus meldet sich das Dienstkonto X an?

Das Dienstkonto X ist ein User im On-Premises AD und wird auf mehreren, leider in Summe unbekannten, Systemen als Dienstkonto verwendet (organisch gewachsen). Um jetzt die Systeme zu identifizieren, auf denen das Konto verwendet wird, soll herausgefunden werden, von welchen IPs dieses Konto authentifiziert wird. Über die abgehende IP-Adresse kann wiederum das System identifiziert werden (DNS, CMDB, Weißderkuckuck).

Die Ingredienzien: drei Domain Controller, kein zentrales Monitoring, eine unbekannte Anzahl an Systemen, welche das Dienstkonto nutzen und wenig Interesse manuell durch die Logs auf den DCs zu gehen.

Die Lösung: PowerShell!

Everyone wants a log
You’re gonna love it, log
Come on and get your log
Everyone needs a log
log log log

Log from Windows!

Die Mechanik per PowerShell ein Log abzufragen ist, ist immer die gleiche.

Toolset:

  • Windows PowerShell V 5.1 oder PowerShell V 6 (oder höher)
  • 16384 offene Zielports (TCP 135 + TCP 49152-65535)
  • Mitgliedschaft in “Event Log Readers” beim Zielsystem
  • Logname
  • sonstige Kriterien (EventID, Username, Zeitstempel etc.)

Aufbau eines Events

So ein Event aus einem Eventlog bei Microsoft Windows ist strukturiert aufgebaut. Hier beispielsweise das Event mit der ID 4624 aus dem Log Security.

Event Message:

Log Name:      Security
Source:        Microsoft-Windows-Security-Auditing
Date:          19/10/2023 8:44:25 AM
Event ID:      4624
Task Category: Logon
Level:         Information
Keywords:      Audit Success
User:          N/A
Computer:      DC01.CLOUD-HYBRID.DE
Description:
An account was successfully logged on.


Subject:
Security ID: SYSTEM
Account Name: DC01$
Account Domain: CH
Logon ID: 0x3E7


Logon Information:
Logon Type: 5
Restricted Admin Mode: -
Virtual Account: No
Elevated Token: Yes


Impersonation Level:Impersonation


New Logon:
Security ID: SYSTEM
Account Name: SYSTEM
Account Domain: NT AUTHORITY
Logon ID: 0x3E7
Linked Logon ID: 0x0
Network Account Name: -
Network Account Domain: -
Logon GUID: {00000000-0000-0000-0000-000000000000}


Process Information:
Process ID: 0x3b0
Process Name: C:\Windows\System32\services.exe


Network Information:
Workstation Name: -
Source Network Address: -
Source Port: -


Detailed Authentication Information:
Logon Process: Advapi  
Authentication Package: Negotiate
Transited Services: -
Package Name (NTLM only): -
Key Length: 0


This event is generated when a logon session is created. It is generated on the computer that was accessed.


The subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.


The logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).


The New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.


The network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.


The impersonation level field indicates the extent to which a process in the logon session can impersonate.


The authentication information fields provide detailed information about this specific logon request.
- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.
- Transited services indicate which intermediate services have participated in this logon request.
- Package name indicates which sub-protocol was used among the NTLM protocols.
- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.
Event Xml:
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
  <System>
    <Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}" />
    <EventID>4624</EventID>
    <Version>2</Version>
    <Level>0</Level>
    <Task>12544</Task>
    <Opcode>0</Opcode>
    <Keywords>0x8020000000000000</Keywords>
    <TimeCreated SystemTime="2023-10-19T06:44:25.8516715Z" />
    <EventRecordID>10259</EventRecordID>
    <Correlation />
    <Execution ProcessID="964" ThreadID="1128" />
    <Channel>Security</Channel>
    <Computer>DC01.CLOUD-HYBRID.DE</Computer>
    <Security />
  </System>
  <EventData>
    <Data Name="SubjectUserSid">S-1-5-18</Data>
    <Data Name="SubjectUserName">DC01$</Data>
    <Data Name="SubjectDomainName">CH</Data>
    <Data Name="SubjectLogonId">0x3e7</Data>
    <Data Name="TargetUserSid">S-1-5-18</Data>
    <Data Name="TargetUserName">SYSTEM</Data>
    <Data Name="TargetDomainName">NT AUTHORITY</Data>
    <Data Name="TargetLogonId">0x3e7</Data>
    <Data Name="LogonType">5</Data>
    <Data Name="LogonProcessName">Advapi  </Data>
    <Data Name="AuthenticationPackageName">Negotiate</Data>
    <Data Name="WorkstationName">-</Data>
    <Data Name="LogonGuid">{00000000-0000-0000-0000-000000000000}</Data>
    <Data Name="TransmittedServices">-</Data>
    <Data Name="LmPackageName">-</Data>
    <Data Name="KeyLength">0</Data>
    <Data Name="ProcessId">0x3b0</Data>
    <Data Name="ProcessName">C:\Windows\System32\services.exe</Data>
    <Data Name="IpAddress">-</Data>
    <Data Name="IpPort">-</Data>
    <Data Name="ImpersonationLevel">%%1833</Data>
    <Data Name="RestrictedAdminMode">-</Data>
    <Data Name="TargetOutboundUserName">-</Data>
    <Data Name="TargetOutboundDomainName">-</Data>
    <Data Name="VirtualAccount">%%1843</Data>
    <Data Name="TargetLinkedLogonId">0x0</Data>
    <Data Name="ElevatedToken">%%1842</Data>
  </EventData>
</Event>

Also irgendwie zwei Teile: eine Event Message und einmal Event XML.

Die Event Message enthält die für mich wesentlichen Informationen und wird aus dem XML-Teil gebildet.

Die Event XML enthält die strukturieren “Rohdaten”.

Filtern der Events

Das Filtern der Events via

(Get-WinEvent -LogName security -MaxEvents 50).where{ $_.Id -eq 4768 }

reicht hier nicht mehr aus.

Ich benötige mehr Filter-Parameter, wie zum Beispiel einen sAMAccoutName. Blöd, dass sowas nicht sofort verfügbar ist.

Tipp

Wenn man in der (Windows) PowerShell ‘Get-WinEvent -‘ schreibt und dann auf die Tab-Taste drückt provoziert man das Intellisense.

Get-WinEvent

/Tipp

XML-Filter

Ein XML-Filter ist genau das, was man nun hier braucht!

Der XML-Filter wird durch eine typisierte Variable mit dem Namen XMLFilter in der PowerShell hinterlegt: [System.Xml.XmlDocument]$XMLFilter

 [System.Xml.XmlDocument]$XMLFilter = @"
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">
*[EventData[Data[@Name='TargetUserName'] and (Data="$sAMAccountName")]]
and
*[System[(EventID='4768')]]
</Select>
</Query>
</QueryList>
"@

Visual Studio Code (VSC)

XMLFilter

Das Bild zeigt den Code im Visual Studio Code (VSC) mit einer anderen abgefragten Event ID (hier: 4768).

Der Grund, warum ich das als Bild zeige: ich war überrascht, wie die Code-Sturktur im VSC aussehen muss im Vergleich zur PowerShellISE.

XMLISE

Was bedeutet aber nun was in dem XMLFilter steht?

“[System.Xml.XmlDocument]$XMLFilter” ist die typisierte Variable “XMLFilter”
Dann kommt die XPath-Abfrage:
  • <QueryList> – öffnet die Abfrage
  • <Query Id=”0″ Path=”Security”> – definiert die Suche im Log “Security”
  • <Select Path=”Security”> – nimmt die logischen Elemente der Suche auf und verbindet diese per Operator
  • *[EventData[Data[@Name=’TargetUserName’] and (Data=”$sAMAccountName”)]] – navigiert den Baum “EventData” (hier 4) runter zu dem Element “TargetUserName” (hier 5) und fragt den Value nach der PowerShell-Variable $sAMAccountName ab.
  • *[System[(EventID=’4624′)]] – navigiert den Baum “System” (hier 2) runter zu dem Element “EventID” (hier 3)
XMLStructure

Events für Kerberos V5 Ticket Granting Tickets (TGTs)

Um die Events für die Anmeldung per Kerberos V5 zu auditieren benötige ich die richtige Event ID. Die richtige Event ID wird aber nicht automatisch generiert, ich muss vorab noch eine Überwachungsrichtlinie aktivieren, damit das Event auf einem DC generiert wird.

Dem geneigten Leser kann ich hier die Unterlagen der BSI (Bundesamt für Sicherheit in der Informationstechnik) empfehlen: APP.2.2: Active Directory (Edition 2021)

monitoring TGT

Unter “Account Logon” muss in der Überwachnungsrichtlinie “Audit Kerberos Authentication Service” mindestens der Erfolg / Success überwacht werden.

monitor TGT success and failure

Event ID 4768

Die für mich fragliche Event ID ist die 4768 welche im Log “Security” generiert wird.

4768 TGT request
4768 XML view
Log Name:      Security
Source:        Microsoft-Windows-Security-Auditing
Date:          14/12/2023 7:00:25 AM
Event ID:      4768
Task Category: Kerberos Authentication Service
Level:         Information
Keywords:      Audit Success
User:          N/A
Computer:      DC01.CLOUD-HYBRID.DE
Description:
A Kerberos authentication ticket (TGT) was requested.


Account Information:
Account Name: Administrator
Supplied Realm Name: ch
User ID: CH\Administrator


Service Information:
Service Name: krbtgt
Service ID: CH\krbtgt


Network Information:
Client Address: ::ffff:192.168.0.12
Client Port: 49724


Additional Information:
Ticket Options: 0x40810010
Result Code: 0x0
Ticket Encryption Type: 0x12
Pre-Authentication Type: 2


Certificate Information:
Certificate Issuer Name:
Certificate Serial Number:
Certificate Thumbprint:


Certificate information is only provided if a certificate was used for pre-authentication.


Pre-authentication types, ticket options, encryption types and result codes are defined in RFC 4120.

Event Xml:

<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
  <System>
    <Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}" />
    <EventID>4768</EventID>
    <Version>0</Version>
    <Level>0</Level>
    <Task>14339</Task>
    <Opcode>0</Opcode>
    <Keywords>0x8020000000000000</Keywords>
    <TimeCreated SystemTime="2023-12-14T06:00:25.9471286Z" />
    <EventRecordID>28108</EventRecordID>
    <Correlation />
    <Execution ProcessID="956" ThreadID="4608" />
    <Channel>Security</Channel>
    <Computer>DC01.CLOUD-HYBRID.DE</Computer>
    <Security />
  </System>
  <EventData>
    <Data Name="TargetUserName">Administrator</Data>
    <Data Name="TargetDomainName">ch</Data>
    <Data Name="TargetSid">S-1-5-21-1728524055-293110278-399330310-500</Data>
    <Data Name="ServiceName">krbtgt</Data>
    <Data Name="ServiceSid">S-1-5-21-1728524055-293110278-399330310-502</Data>
    <Data Name="TicketOptions">0x40810010</Data>
    <Data Name="Status">0x0</Data>
    <Data Name="TicketEncryptionType">0x12</Data>
    <Data Name="PreAuthType">2</Data>
    <Data Name="IpAddress">::ffff:192.168.0.12</Data>
    <Data Name="IpPort">49724</Data>
    <Data Name="CertIssuerName">
    </Data>
    <Data Name="CertSerialNumber">
    </Data>
    <Data Name="CertThumbprint">
    </Data>
  </EventData>
</Event>

 

XML-Filter für die 4768er Events

Der XML-Filter für die 4768er Events ist kein Hexenwerk:

        #XML Filter
        [System.Xml.XmlDocument]$XMLFilter = @"
        <QueryList>
        <Query Id="0" Path="Security">
            <Select Path="Security">
                *[EventData[Data[@Name='TargetUserName'] and (Data="Administrator")]]
                and
                *[System[(EventID='4768')]]
            </Select>
        </Query>
        </QueryList>
"@
Das variable Element ist der sAMAccoutnName, welcher unter “<Data Name=”TargetUserName”>Administrator</Data>” gespeichert wird.

Frisch ans Werk, das fertige PowerShell-Skript!

Bei dem PowerShell-Skript möchte ich auch noch später auf die Ergebnisse der damaligen Abfragen zugreifen. In Ermangelung einer besseren Idee lasse ich mir das ins Dateisystem als Log-Datei ausgeben: Q&D (Quick and Dirty).

Der Name der function: “Get-ADUserLogonIP”

function Get-ADUserLogonIP
Die Parameter: sAMAccountName (wer wird gesucht) und FolderPath (wohin soll die Logdatei geschrieben werden).
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [System.String]$sAMAccountName,
        [Parameter(Position = 1, Mandatory = $true)]
        [System.String]$FolderPath
    )
Der Begin-Block erstellt eine Liste der RWDCs (der Kunde betreibt keine RODCs), prüft ob der Username vorhanden ist, prüft ob der Ordner für die Logdatei vorhanden ist und baut den XML-Filter auf.
    begin {
        # Building list of RWDCs
        $RWDCs = (Get-ADDomain).ReplicaDirectoryServers


        # Does the account exist
        $AccountExist = Get-ADObject -Filter { samaccountname -eq $sAMAccountName }
        if ($null -eq $AccountExist){
            Write-Host -ForegroundColor Yellow "Specified ADAccount does not exist!"
            Break
        }


        # Does the path exist
        $pathCheck = Test-Path -Path $FolderPath
        if ($pathCheck -eq $false){
            Write-Host -ForegroundColor Yellow "The given folder path does not exist!"
            Break
        }


        # Building absolute file path
        $FileName = $sAMAccountName + "_" + $(Get-Date -Format "MMddyyyy-HHmm") + ".txt"
        $absolutePath = $FolderPath + "\" + $FileName


        #XML Filter
        [System.Xml.XmlDocument]$XMLFilter = @"
        <QueryList>
        <Query Id="0" Path="Security">
            <Select Path="Security">
                *[EventData[Data[@Name='TargetUserName'] and (Data="$sAMAccountName")]]
                and
                *[System[(EventID='4768')]]
            </Select>
        </Query>
        </QueryList>
"@
    }
Der Process-Block fragt nun jeden RWDC nach jenen Events ab, welche dem XML-Filter entsprechen. Diese Events werden sortiert (neueste zuerst) und der Inhalt eines Events, die Message, in eine Textdatei geschrieben.

Wiederkehrende IPs werden aussortiert.

    process {
        # Collecting the appropriate events from the read-write domain controllers and sorting the events by the time created (oldest first)
        $Message = $RWDCs.foreach{
            Get-WinEvent -ComputerName $_ -FilterXml $XMLFilter
        }
        $Message = $Message | Sort-Object -Property TimeCreated -Descending
        $Message.Message | Out-File -FilePath "$FolderPath\$FileName" -Append


        # Generating the list of IPs
        $ListOfIPs = ((Get-Content -Path $absolutePath | Select-String -Pattern "Client Address:") | Out-String).replace("Client Address:       ","").Replace("::ffff:","")
        $UniqueIPs = $ListOfIPs -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split " " | Sort-Object -Unique
    }

Zu guter Letzt der End-Block: hier werden die eindeutigen IPs durch Komata getrennt in der PowerShell-Konsole angezeigt.

    end {
        Write-Host $($UniqueIPs -join ", ")
    }

Das ganze Skript

<#
    .SYNOPSIS
        Retreives the source IP address while logon of an given user.


    .DESCRIPTION
        The event id 4768 records the event for a TGT request.
        In this event the source IP address is recorded.


        This PowerShell script retreives the events and isolates
        the source addresses, filtering them for unique instances.


        For each given user a log file is being created, summerizing
        the messages of the event 4768.


    .PARAMETER  sAMAccountName
        sAMAccountName is the sAMAccountName of a user to be
        searched for.
   
    .PARAMETER  FolderPath
        FolderPath is the root folder where the log file is being
        created.
       
    .EXAMPLE
        Get-ADUserLogonIP -sAMAccountName JohnDoe -Folder C:\temp
        This example shows how to retreive the source IP for the user "JohnDoe"
        while storing the log file in C:\temp folder


    .NOTES
        10/27/2023 - Version 1.0.0 - Martin Handl: initial


    .LINK
        https://iqunit.com
#>
function Get-ADUserLogonIP {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [System.String]$sAMAccountName,
        [Parameter(Position = 1, Mandatory = $true)]
        [System.String]$FolderPath
    )
   
    begin {
        # Building list of RWDCs
        $RWDCs = (Get-ADDomain).ReplicaDirectoryServers


        # Does the account exist
        $AccountExist = Get-ADObject -Filter { samaccountname -eq $sAMAccountName }
        if ($null -eq $AccountExist){
            Write-Host -ForegroundColor Yellow "Specified ADAccount does not exist!"
            Break
        }


        # Does the path exist
        $pathCheck = Test-Path -Path $FolderPath
        if ($pathCheck -eq $false){
            Write-Host -ForegroundColor Yellow "The given folder path does not exist!"
            Break
        }


        # Building absolute file path
        $FileName = $sAMAccountName + "_" + $(Get-Date -Format "MMddyyyy-HHmm") + ".txt"
        $absolutePath = $FolderPath + "\" + $FileName


        #XML Filter
        [System.Xml.XmlDocument]$XMLFilter = @"
        <QueryList>
        <Query Id="0" Path="Security">
            <Select Path="Security">
                *[EventData[Data[@Name='TargetUserName'] and (Data="$sAMAccountName")]]
                and
                *[System[(EventID='4768')]]
            </Select>
        </Query>
        </QueryList>
"@
    }
   
    process {
        # Collecting the appropriate events from the read-write domain controllers and sorting the events by the time created (oldest first)
        $Message = $RWDCs.foreach{
            Get-WinEvent -ComputerName $_ -FilterXml $XMLFilter
        }
        $Message = $Message | Sort-Object -Property TimeCreated -Descending
        $Message.Message | Out-File -FilePath "$FolderPath\$FileName" -Append


        # Generating the list of IPs
        $ListOfIPs = ((Get-Content -Path $absolutePath | Select-String -Pattern "Client Address:") | Out-String).replace("Client Address:       ","").Replace("::ffff:","")
        $UniqueIPs = $ListOfIPs -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split " " | Sort-Object -Unique
    }
   
    end {
        Write-Host $($UniqueIPs -join ", ")
    }
}
Log from Windows!
AuditUserLogonIPs.ps1
PS C:\Users\Administrator> . .\Documents\AuditUserLogonIPs.ps1
PS C:\Users\Administrator> Get-ADUserLogonIP -sAMAccountName Administrator -FolderPath C:\temp\
::1, 192.168.0.10, 192.168.0.11, 192.168.0.12
PS C:\Users\Administrator>

Nachtrag – 05.06.2024

Bei einem deutschsprachig installiertem AD muss nach einem anderen Begriff gesucht werden: “Clientadresse:” anstelle “Client Address:”, auch sind es etwas mehr Leerzeichen als im Englischen.

Daher unten die “deutsche” Version von dem PowerShell-Skript.

<#
.SYNOPSIS
Retreives the source IP address while logon of an given user.


.DESCRIPTION
The event id 4768 records the event for a TGT request.
        In this event the source IP address is recorded.


        This PowerShell script retreives the events and isolates
        the source addresses, filtering them for unique instances.


        For each given user a log file is being created, summerizing
        the messages of the event 4768.


.PARAMETER  sAMAccountName
sAMAccountName is the sAMAccountName of a user to be
        searched for.


    .PARAMETER  FolderPath
FolderPath is the root folder where the log file is being
        created.
        
.EXAMPLE
Get-ADUserLogonIP -sAMAccountName JohnDoe -Folder C:\temp
        This example shows how to retreive the source IP for the user "JohnDoe"
        while storing the log file in C:\temp folder


.NOTES
10/27/2023 - Version 1.0.0 - Martin Handl: initial


.LINK
IQunit IT GmbH – IT-Weiterbildungen & -Consulting aus Ulm
#> function Get-ADUserLogonIP {     [CmdletBinding()]     param (         [Parameter(Position = 0, Mandatory = $true)] [System.String]$sAMAccountName,         [Parameter(Position = 1, Mandatory = $true)] [System.String]$FolderPath     )          begin {         # Building list of RWDCs         $RWDCs = (Get-ADDomain).ReplicaDirectoryServers         # Does the account exist         $AccountExist = Get-ADObject -Filter { samaccountname -eq $sAMAccountName }         if ($null -eq $AccountExist){             Write-Host -ForegroundColor Yellow "Specified ADAccount does not exist!"             Break         }         # Does the path exist         $pathCheck = Test-Path -Path $FolderPath         if ($pathCheck -eq $false){             Write-Host -ForegroundColor Yellow "The given folder path does not exist!"             Break         }         # Building absolute file path         $FileName = $sAMAccountName + "_" + $(Get-Date -Format "MMddyyyy-HHmm") + ".txt"         $absolutePath = $FolderPath + "\" + $FileName         #XML Filter         [System.Xml.XmlDocument]$XMLFilter = @"         <QueryList>         <Query Id="0" Path="Security">             <Select Path="Security">                 *[EventData[Data[@Name='TargetUserName'] and (Data="$sAMAccountName")]]                 and                 *[System[(EventID='4768')]]             </Select>         </Query>         </QueryList> "@     }          process {         # Collecting the appropriate events from the read-write domain controllers and sorting the events by the time created (oldest first)         $Message = $RWDCs.foreach{             Get-WinEvent -ComputerName $_ -FilterXml $XMLFilter         }         $Message = $Message | Sort-Object -Property TimeCreated -Descending         $Message.Message | Out-File -FilePath "$FolderPath\$FileName" -Append         # Generating the list of IPs         $ListOfIPs = ((Get-Content -Path $absolutePath | Select-String -Pattern "Clientadresse:") | Out-String).replace("Clientadresse: ","").Replace("::ffff:","")         $UniqueIPs = $ListOfIPs -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split " " | Sort-Object -Unique     }          end {         Write-Host $($UniqueIPs -join ", ")     } }

Related Posts