Analyze RDP Disconnection Logs using PowerShell

The PowerShell script is designed to extract information about Remote Desktop Protocol (RDP) local session manager events from the Windows event logs on a RDS host and save it to a CSV file.

Script: Get events with EventID 40 from Microsoft-Windows-TerminalServices-LocalSessionManager/Operational Event Log

$RDPAuths = Get-WinEvent -LogName 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'-FilterXPath '<QueryList><Query Id="0"><Select>*[System[EventID=40]]</Select></Query></QueryList>'

[xml[]]$xml = $RDPAuths | ForEach-Object { $_.ToXml() }

$EventData = $xml.Event | ForEach-Object {
    [PSCustomObject]@{
        TimeCreated = (Get-Date $_.System.TimeCreated.SystemTime -Format 'yyyy-MM-dd hh:mm:ss K')
        Session     = $_.UserData.EventXML.Session
        Reason      = $_.UserData.EventXML.Reason
        EventID     = $_.System.EventID
        User        = $_.UserData.EventXML.User
        SessionID   = $_.UserData.EventXML.SessionID
        Address     = $_.UserData.EventXML.Address
    }
}

$EventData | Export-Csv -Path c:\rdlog-LSM-Operational.csv -Encoding ASCII

Here is an explanation of how the code works.

  1. First, the script uses the Get-WinEvent cmdlet to retrieve events from the ‘Microsoft-Windows-TerminalServices-LocalSessionManager/Operational’ log. This log contains information about RDP disconnections. The events are stored in the $RDPAuths variable.
  2. The $RDPAuths variable is then piped to the ForEach-Object cmdlet to convert each event to XML format using the ToXml() method. The resulting array of XML objects is stored in the $xml variable.
  3. Next, the script uses the ForEach-Object cmdlet again to iterate over the $xml.Event array. For each event, a new custom object is created using the [PSCustomObject]@{} syntax. This custom object contains the following properties: TimeCreatedSessionReasonEventIDUserSessionID, and Address. These properties are extracted from the event XML data using dot notation.
  4. Finally, the custom objects are piped to the Export-Csv cmdlet, which saves the objects as a CSV file at the specified path (c:\rdlog-LSM-Operational.csv) with ASCII encoding.

This PowerShell script is useful for extracting and analyzing RDP local session manager events, such as monitoring for disconnect reasons. The resulting CSV file provides a simple, convenient way to view and analyze the RDP events.

RDS Session Host Server Disconnect Codes

RDS server client disconnect codeDisconnect reason
0x00000001The disconnection was initiated by an administrative tool on the server in another session.
0x00000002The disconnection was due to a forced logoff initiated by an administrative tool on the server in another session.
0x00000003The idle session limit timer on the server has elapsed.
0x00000004The active session limit timer on the server has elapsed.
0x00000005Another user connected to the server, forcing the disconnection of the current connection.
0x00000006The server ran out of available memory resources.
0x00000007The server denied the connection.
0x00000009The user cannot connect to the server due to insufficient access privileges.
0x0000000A (10)The server does not accept saved user credentials and requires that the user enter their credentials for each connection.
0x0000000B (11)The disconnection was initiated by the user disconnecting his or her session on the server or by an administrative tool on the server.
0x0000000C (12)The disconnection was initiated by the user logging off his or her session on the server.

Extended Disconnect Reason Codes

Reference: https://learn.microsoft.com/en-us/windows/win32/termserv/extendeddisconnectreasoncode

Extended ReasonCode
NoInfo0
APIInitiatedDisconnect1
APIInitiatedLogoff2
ServerIdleTimeout3
ServerLogonTimeout4
ReplacedByOtherConnection5
OutOfMemory6
ServerDeniedConnection7
ServerDeniedConnectionFips8
ServerInsufficientPrivileges9
ServerFreshCredsRequired10
RpcInitiatedDisconnectByUser11
LogoffByUser2
LicenseInternal256
LicenseNoLicenseServer257
LicenseNoLicense258
LicenseErrClientMsg259
LicenseHwidDoesntMatchLicense260
LicenseErrClientLicense261
LicenseCantFinishProtocol262
LicenseClientEndedProtocol263
LicenseErrClientEncryption264
LicenseCantUpgradeLicense265
LicenseNoRemoteConnections266
LicenseCreatingLicStoreAccDenied267
RdpEncInvalidCredentials768
ProtocolRangeStart4096
ProtocolRangeEnd32767

Could not establish trust relationship for the SSL/TLS secure channel.

Working with some older Cisco ASA devices, I’m trying to access the ASDM interface. The browser isn’t giving me luck, so I turned to PowerShell to help me, but I get the following error when trying an Invoke-WebRequest to grab the asdm.jnlp file I need.

The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

Eh, ok. My first thought was to somehow avoid a certificate check but I did not see a native way of doing this with Invoke-WebRequest (at least from an old Server 2008 box with PowerShell v4.0).

StackOverflow to the rescue. Here’s the solution that worked for me.

if (-not("dummy" -as [type])) {
    add-type -TypeDefinition @"
using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

public static class Dummy {
    public static bool ReturnTrue(object sender,
        X509Certificate certificate,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors) { return true; }

    public static RemoteCertificateValidationCallback GetDelegate() {
        return new RemoteCertificateValidationCallback(Dummy.ReturnTrue);
    }
}
"@
}

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = [dummy]::GetDelegate()

Now I can add on my Invoke-WebRequest and everything works.

Get Enabled AD Users with Last Logon Time and Organizational Unit Information

This PowerShell script retrieves information about enabled Active Directory (AD) users, including their SAM account name, last logon time, and organizational unit (OU). The script makes use of several cmdlets and concepts that are common in PowerShell, including filtering, selecting, sorting, and transforming data.

Get-ADUser -Filter * -Properties lastLogon |
    Where-Object { $_.Enabled -eq $True } |
    Select-Object samaccountname, @{
        Name="lastLogon";
        Expression={[datetime]::FromFileTime($_.lastLogon)}
    }, @{
        Name="OU";
        Expression={( $_.distinguishedname -split ',' )[1].Split('=')[1]}
    } |
    Sort-Object OU |
    Where-Object { $_.OU -notmatch "CN=" }

Here is a detailed explanation of each part of the code:

  1. Get-ADUser cmdlet:

The Get-ADUser cmdlet is used to retrieve information about AD user objects. The -Filter parameter is used to specify that I want to retrieve all user objects, and the -Properties parameter is used to specify that I want to retrieve the lastLogon property.

  1. Where-Object cmdlet:

The Where-Object cmdlet is used to filter the results of the Get-ADUser cmdlet based on the Enabled property. In this case, I want to retrieve only those users that have their Enabled property set to $True.

  1. Select-Object cmdlet:

The Select-Object cmdlet is used to select specific properties from the filtered results. In this case, I want to select the samaccountnamelastLogon, and OU properties. The @{Name="lastLogon";Expression={[datetime]::FromFileTime($_.lastLogon)}} expression is used to convert the lastLogon property from a file time format to a more readable date/time format. The @{Name="OU";Expression={( $_.distinguishedname -split ',' )[1].Split('=')[1]}} expression is used to extract the name of the OU from the DistinguishedName property.

  1. Sort-Object cmdlet:

The Sort-Object cmdlet is used to sort the selected results based on the distinguishedname property. In this case, I want to sort the results in ascending order by the distinguishedname property.

  1. Where-Object cmdlet:

The final Where-Object cmdlet is used to further filter the sorted results based on the distinguishedname property. In this case, I want to retrieve only those results where the distinguishedname property does not match the string “CN=”.

Writing a portscan utility in .NET

I’m working on a side project that is a portscan utility written in VB.net. Here’s my progress so far, and it is working.

There’s some way to go on this little project. I think I can optimize it further, clean up the code, and fix my logic for many of the options. See an earlier post about handling command line arguments I wrote which is what class I use for handling the arguments.

Check out my C port scanner which is significantly faster (65k ports in < 30 seconds).

Handling Commandline Arguments in VB.NET

Handling commandline arguments in VB.NET (or really anything) can be tricky. I came across the following class the other day while looking for a drop-in solution to a RAD I was developing.

Here’s what my usage shows for my application, thanks to the .NET class.

portscan.exe

portscan 0.9.2 by Rich Kreider

Usage: portscan -h <host> -s <startport> -e <endport> -f -d -t <timeoutms>

Options:
        -h      Host.  IP address or hostname.
        -s      Start port.  Begin scan at the specified port.
        -e      End port.  End scan at the specified port (max 65535).
        -f      Fast scan.  Scan without a timeout.  May be unreliable.
        -t      Timeout.  Timeout in ms to confirm port open (not used with -f)
        -p      Single port.  Scan single port.  Ignores -e and -s.
Examples:
        Normal Scan:
                portscan -h 192.168.1.50
        Normal Scan Custom Port Range:
                portscan -h 192.168.1.50 -s 1 -e 1024
        Fast Scan All Ports:
                portscan -h 192.168.1.50 -f
        Normal Scan Single Port:
                portscan -h 192.168.1.50 -p 80

InputArguments.vb

Public Class InputArguments
#Region "fields & properties"
    Public Const DEFAULT_KEY_LEADING_PATTERN As String = "-"

    Protected _parsedArguments As New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase)
    Protected ReadOnly _keyLeadingPattern As String

    Default Public Property Item(key As String) As String
        Get
            Return GetValue(key)
        End Get
        Set(ByVal value As String)
            If key IsNot Nothing Then
                _parsedArguments(key) = value
            End If
        End Set
    End Property
    Public ReadOnly Property KeyLeadingPattern() As String
        Get
            Return _keyLeadingPattern
        End Get
    End Property
#End Region

#Region "public methods"
    Public Sub New(args() As String, pkeyLeadingPattern As String)
        _keyLeadingPattern = If(Not String.IsNullOrEmpty(pkeyLeadingPattern), pkeyLeadingPattern, DEFAULT_KEY_LEADING_PATTERN)

        If args IsNot Nothing AndAlso args.Length > 0 Then
            Parse(args)
        End If
    End Sub
    Public Sub New(args() As String)
        Me.New(args, Nothing)
    End Sub

    Public Function Contains(key As String) As Boolean
        Dim adjustedKey As String = Nothing
        Return ContainsKey(key, adjustedKey)
    End Function

    Public Overridable Function GetPeeledKey(key As String) As String
        Return If(IsKey(key), key.Substring(_keyLeadingPattern.Length), key)
    End Function
    Public Overridable Function GetDecoratedKey(ByVal key As String) As String
        Return If(Not IsKey(key), (_keyLeadingPattern & key), key)
    End Function
    Public Overridable Function IsKey(str As String) As Boolean
        Return str.StartsWith(_keyLeadingPattern)
    End Function
#End Region

#Region "internal methods"
    Protected Overridable Sub Parse(args() As String)
        For index As Integer = 0 To args.Length - 1
            If args(index) Is Nothing Then
                Continue For
            End If

            Dim key As String = Nothing
            Dim val As String = Nothing

            If IsKey(args(index)) Then
                key = args(index)

                If index + 1 < args.Length AndAlso Not IsKey(args(index + 1)) Then
                    val = args(index + 1)
                    index += 1
                End If
            Else
                val = args(index)
            End If

            ' adjustment
            If key Is Nothing Then
                key = val
                val = Nothing
            End If
            _parsedArguments(key) = val
        Next
    End Sub

    Protected Overridable Function GetValue(key As String) As String
        Dim adjustedKey As String = Nothing
        If ContainsKey(key, adjustedKey) Then
            Return _parsedArguments(adjustedKey)
        End If

        Return Nothing
    End Function

    Protected Overridable Function ContainsKey(ByVal key As String, <Runtime.InteropServices.Out()> ByRef adjustedKey As String) As Boolean
        adjustedKey = key

        If _parsedArguments.ContainsKey(key) Then
            Return True
        End If

        If IsKey(key) Then
            Dim peeledKey As String = GetPeeledKey(key)
            If _parsedArguments.ContainsKey(peeledKey) Then
                adjustedKey = peeledKey
                Return True
            End If
            Return False
        End If

        Dim decoratedKey As String = GetDecoratedKey(key)
        If _parsedArguments.ContainsKey(decoratedKey) Then
            adjustedKey = decoratedKey
            Return True
        End If
        Return False
    End Function
#End Region
End Class

To make use of the class, I have the following code sample.

Module Module1

Public ReadOnly Property CommandLineArguments As String()
        Get
            Return Environment.GetCommandLineArgs
        End Get
End Property

Dim host as String

Sub Main() {
    Dim arguments As New InputArguments(CommandLineArguments)

    If arguments.Contains("-h") Then
        host = arguments.Item("-h")
    End If
}

End Module

Supplying portscan.exe -h 127.0.0.1 results in the host variable being assigned the commandline argument value of 127.0.0.1.