Session Enumeration With NetSessionEnum API

Within an Active Directory environment, authenticated users are able to determine who is logged into a remote system.

This is achieved by using the NetSessionEnum function of the NET_API:

NET_API_STATUS NET_API_FUNCTION NetSessionEnum(
LMSTR servername,
LMSTR UncClientName,
LMSTR username,
DWORD level,
LPBYTE *bufptr,
DWORD prefmaxlen,
LPDWORD entriesread,
LPDWORD totalentries,
LPDWORD resume_handle
);

The level DWORD determines the information returned from the query, and dictates the permission level required.

Level 10 can be queried using a standard Active Directory Authenticated account:

“Return the name of the computer, name of the user, and active and idle times for the session. The bufptr parameter points to an array of SESSION_INFO_10 structures.”

Note, that as of Windows Server 2016 querying NetSessionEnum requires administrator access 🙁

We can query NetSessionEnum in C# using PInvoke;

using System;
using System.Runtime.InteropServices;

namespace EnumerateSessions
{
    internal class Program
    {
        static void Main(string[] args)
        {
            SessionEnumeration("DC01.bordergate.local");
        }

        static void SessionEnumeration(string TargetHostname)
        {
            IntPtr pSessionInfo;
            IntPtr pResumeHandle = IntPtr.Zero;
            UInt32 entriesRead, totalEntries;
            int c = 0;
            string serveraddr = TargetHostname;
            var netStatus = Win32.NetSessionEnum( serveraddr,null,null,10, out pSessionInfo,  Win32.MAX_PREFERRED_LENGTH, out entriesRead,out totalEntries, ref pResumeHandle
            );
            try
            {
                if (netStatus != Win32.NET_API_STATUS.NERR_Success)
                {
                    Console.WriteLine("Error connecting to domain controller!");
                    throw new InvalidOperationException(netStatus.ToString());
                }
                for (int i = 0; i < entriesRead; i++)
                {
                    var pCurrentSessionInfo = new IntPtr(pSessionInfo.ToInt32() + (Win32.SESSION_INFO_10.SIZE_OF * i));
                    var s = (Win32.SESSION_INFO_10)Marshal.PtrToStructure(pCurrentSessionInfo, typeof(Win32.SESSION_INFO_10));
                    Console.WriteLine(s.sesi10_cname.ToString() + " " + s.sesi10_username.ToString());
                    c++;
                }
                Console.ReadLine();
            }
            finally
            {
                Win32.NetApiBufferFree(pSessionInfo);
            }
        }

        internal class Win32
        {
            [DllImport("netapi32.dll", SetLastError = true)]
            public static extern NET_API_STATUS NetSessionEnum(
            [MarshalAs(UnmanagedType.LPTStr)] string serverName,
            string uncClientName,
            string userName,
            UInt32 level,
            out IntPtr bufPtr,
            int prefMaxLen,
            out UInt32 entriesRead,
            out UInt32 totalEntries,
            ref IntPtr resume_handle
    );

            [DllImport("netapi32.dll")]
            public static extern uint NetApiBufferFree(IntPtr Buffer);

            public const int MAX_PREFERRED_LENGTH = -1;

            public enum NET_API_STATUS : uint
            {
                NERR_Success = 0,
                NERR_InvalidComputer = 2351,
                NERR_NotPrimary = 2226,
                NERR_SpeGroupOp = 2234,
                NERR_LastAdmin = 2452,
                NERR_BadPassword = 2203,
                NERR_PasswordTooShort = 2245,
                NERR_UserNotFound = 2221,
                ERROR_ACCESS_DENIED = 5,
                ERROR_NOT_ENOUGH_MEMORY = 8,
                ERROR_INVALID_PARAMETER = 87,
                ERROR_INVALID_NAME = 123,
                ERROR_INVALID_LEVEL = 124,
                ERROR_MORE_DATA = 234,
                ERROR_SESSION_CREDENTIAL_CONFLICT = 1219,
                BADNETPATH = 53
            }

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
            public struct SESSION_INFO_10
            {
                public static readonly int SIZE_OF = Marshal.SizeOf(typeof(SESSION_INFO_10));
                public string sesi10_cname;
                public string sesi10_username;
                public uint sesi10_time;
                public uint sesi10_idle_time;
            }

        }

    }
}

The code will list the users logged into the host, and their source addresses;

EnumerateSessions.exe
\\192.168.1.164 alice
\\[::1] Administrator
\\192.168.1.97  bob

I’ve create an example application to query this information available from a remote host available here:

https://github.com/bordergate/bgsession

C:\Users\user\AppData\Local\Packages\Microsoft.Office.OneNote_8wekyb3d8bbwe\TempState\msohtmlclip\clip_image001.png

This type of information can be useful to attackers to identify logged in administrator accounts that can be targeted with credential stealing attacks such as pass the ticket.

From a blue team perspective, this information can also be useful. For instance if suspicious activity is detected in an Active Directory user account, the account can be locked however the users session may persist. Querying active sessions remotely can allow the blue team to determine where active sessions are active so they can be manually killed.

Prevention

Changing permissions of the NetSessionEnum registry key:

HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/LanmanServer/DefaultSecurity/SrvsvcSessionInfo

Can be done to remove the authenticated users group SID (S-1-5-11). A PowerShell script has been released to automate making the change:

https://gallery.technet.microsoft.com/Net-Cease-Blocking-Net-1e8dcb5b