0x00 Preface

---

In domain penetration, after obtaining domain controller privileges, it is necessary to acquire domain users' login information, including their login IP addresses and timestamps. The common method involves examining the domain controller's login logs (Event ID 4624). However, manually filtering domain users' login IP addresses and timestamps from these logs (Event ID 4624) is time-consuming due to excessive irrelevant data and the need for repeated judgments. Therefore, we need to develop a program to automate this function.

In practical use, to adapt to various environments, support for local and multiple remote login protocols is also required. This article will share my implementation approach, open-source two tools, and document the details.

0x01 Introduction

---

This article will cover the following topics:

  • Implementation via EventLogSession
  • Implementation via WMI
  • Open-source code

0x02 Implementation via EventLogSession

---

Research indicates that EventLogSession not only supports parsing local log content but also enables remote log parsing via RPC. The following details the development specifics of EventLogSession.

1. Output log content for Event ID 4624

C Sharp implementation code:

using System;
using System.Diagnostics.Eventing.Reader;
namespace Test1
{
class Program
{
static void Main(string[] args)
{
var session = new EventLogSession();
string LogName = "Security";
string XPathQuery = "*[System/EventID=4624]";
EventLogQuery eventLogQuery = new EventLogQuery(LogName, PathType.LogName, XPathQuery)
{
Session = session,
TolerateQueryErrors = true,
ReverseDirection = true
};
using (EventLogReader eventLogReader = new EventLogReader(eventLogQuery))
{
eventLogReader.Seek(System.IO.SeekOrigin.Begin, 0);
do
{
EventRecord eventData = eventLogReader.ReadEvent();
if (eventData == null)
break;
Console.WriteLine(eventData.FormatDescription());
eventData.Dispose();
} while (true);
}
}
}
}

The above code can query local logs and output the complete content of the logs.

2. XML Format Parsing

To facilitate content extraction, you can choose to convert the output content to XML format.

Key code:

Console.WriteLine(eventData.ToXml());

Output content example:

4624101254400x8020000000000000393748Securitydc1.test.comS-1-0-0--0x0S-1-5-21-254706111-4049838133-2416586677-2102test1TEST0xa222fb53NtLmSsp NTLM{00000000-0000-0000-0000-000000000000}-NTLM V21280x0-192.168.1.345624%%1833

From the XML format, you can directly extract the EventRecordID. Key code:

XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(eventData.ToXml());
XmlNodeList recordid = xmldoc.GetElementsByTagName("EventRecordID");
Console.WriteLine(recordid[0].InnerText);

To extract TargetUserName, first retrieve the content of Data, then perform a filtering operation. Key code:

XmlNodeList data = xmldoc.GetElementsByTagName("Data");
foreach (XmlNode value in data)
{
if (value.OuterXml.Contains("TargetUserName"))
{
Console.WriteLine(value.InnerText);
}
}

Here we need to filter out the following attributes in total:

  • TargetUserSid
  • TargetDomainName
  • TargetUserName
  • IpAddress

During character matching, since the format is fixed, we can obtain the corresponding attributes from fixed offset positions to avoid multiple judgments and improve query efficiency.

Key code:

XmlNodeList data = xmldoc.GetElementsByTagName("Data");
String targetUserSid = data[4].InnerText;
String targetDomainName = data[6].InnerText;
String targetUserName = data[5].InnerText;
String ipAddress = data[18].InnerText;

3. Filtering criteria

To filter out valid login information, length checks are performed on targetUserSid and ipAddress. targetUserSid length must be greater than 9, and ipAddress length must be greater than 8.

Key code:

XmlNodeList data = xmldoc.GetElementsByTagName("Data");
String targetUserSid = data[4].InnerText;
String targetDomainName = data[6].InnerText;
String targetUserName = data[5].InnerText;
String ipAddress = data[18].InnerText;
if (targetUserSid.Length > 9 && ipAddress.Length > 8)
{
Console.WriteLine(targetUserSid);
Console.WriteLine(targetDomainName);
Console.WriteLine(targetUserName);
Console.WriteLine(ipAddress);
}

4. Support filtering logs within a specified time range

Can be achieved by modifying search conditions, key code:

string XPathQuery = "(Event/System/EventID=4624) and Event/System/TimeCreated/@SystemTime >= '2022-01-26T02:30:39' and Event/System/TimeCreated/@SystemTime <= '2022-01-26T02:31:00'";

Thus, the implementation code for parsing local login logs via EventLogSession is as follows:

using System;
using System.Diagnostics.Eventing.Reader;
using System.Xml;
namespace Test1
{
class Program
{
static void Main(string[] args)
{
var session = new EventLogSession();
string LogName = "Security";
string XPathQuery = "(Event/System/EventID=4624) and Event/System/TimeCreated/@SystemTime >= '2022-01-26T02:30:39' and Event/System/TimeCreated/@SystemTime <= '2022-01-26T02:31:00'";

EventLogQuery eventLogQuery = new EventLogQuery(LogName, PathType.LogName, XPathQuery)
{
Session = session,
TolerateQueryErrors = true,
ReverseDirection = true
};
int flagTotal = 0;
int flagExist = 0;
using (EventLogReader eventLogReader = new EventLogReader(eventLogQuery))
{
eventLogReader.Seek(System.IO.SeekOrigin.Begin, 0);
do
{
EventRecord eventData = eventLogReader.ReadEvent();
if (eventData == null)
break;
flagTotal++;
XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(eventData.ToXml());
XmlNodeList recordid = xmldoc.GetElementsByTagName("EventRecordID");
XmlNodeList data = xmldoc.GetElementsByTagName("Data");
String targetUserSid = data[4].InnerText;
String targetDomainName = data[6].InnerText;
String targetUserName = data[5].InnerText;
String ipAddress = data[18].InnerText;
if (targetUserSid.Length > 9 && ipAddress.Length > 8)
{
Console.WriteLine("[+] EventRecordID: " + recordid[0].InnerText);
Console.WriteLine(" TimeCreated : " + eventData.TimeCreated);
Console.WriteLine(" UserSid: " + targetUserSid);
Console.WriteLine(" DomainName: " + targetDomainName);
Console.WriteLine(" UserName: " + targetUserName);
Console.WriteLine(" IpAddress: " + ipAddress);
flagExist++;
}
eventData.Dispose();
} while (true);
Console.WriteLine("Total: " + flagTotal + ", Exist: " + flagExist);
}
}

}
}

5. Support remote login

Key code:

String server = "192.168.1.1";
String domain = "TEST";
String user = "Administrator";
String password = "Password@123";
SecureString securePwd = new SecureString();
foreach (char c in password)
{
securePwd.AppendChar(c);
}
var session = new EventLogSession(server, domain, user, securePwd, SessionAuthentication.Negotiate);

Integrate the above code to obtain the final code, which has been uploaded to GitHub at the following address:

An open-source project

The code supports the following features:

  • Can be compiled using csc.exe, supports versions 3.5 and 4.0
  • Supports local and remote log parsing, remote log parsing uses RPC method
  • Supports conditional judgment, can filter by specified date
  • Automatically extracts information: EventRecordID, TimeCreated, UserSid, DomainName, UserName, and IpAddress

0x03 Implementation via WMI

---

1. WMI syntax testing

Query syntax and property names can be studied with wbemtest. For usage of wbemtest, refer to the previous article 'Penetration Basics - Usage of WMIC'

Querying Security logs requires running wbemtest with administrator privileges

Click Query...

Query all Security logs:

Select * from Win32_NTLogEvent Where Logfile = 'Security'

Query all logs with Eventid=4672:

Select * from Win32_NTLogEvent Where Logfile = 'Security' AND EventCode = 4624

Query the log with EventRecordID=113438:

Select * from Win32_NTLogEvent Where Logfile = 'Security' AND RecordNumber = 113438

Based on the above information, we can easily write the wmic query command:

wmic /namespace:\\root\cimv2 path win32_ntlogevent where "Logfile='Security' AND RecordNumber = 113438"

Filter out the information we need:

wmic /namespace:\\root\cimv2 path win32_ntlogevent where "Logfile='Security' AND EventCode = 4624" get RecordNumber,TimeGenerated,Message

RecordNumber corresponds to EventRecordID, TimeGenerated is the log creation time, and the following content needs to be filtered from the Message:

  • Security ID
  • Account Domain
  • Account Name
  • Source Network Address

The filtering function can be implemented via batch processing or PowerShell, but considering compatibility and the uniformity of subsequent interface utilization, C Sharp is also chosen here for implementation

2. Output the log content where RecordNumber=131

Implementation code:

using System;
using System.Management;
namespace Test2
{
class Program
{
static void Main(string[] args)
{
String queryString = "SELECT * FROM Win32_NTLogEvent Where Logfile = 'Security' AND RecordNumber = 131";
ManagementScope s = new ManagementScope("root\\CIMV2");
SelectQuery q = new SelectQuery(queryString);
ManagementObjectSearcher mos = new ManagementObjectSearcher(s, q);
foreach (ManagementObject o in mos.Get())
{
PropertyDataCollection searcherProperties = o.Properties;
foreach (PropertyData sp in searcherProperties)
{
Console.WriteLine("Name = {0, -20}, Value = {1, -20}", sp.Name, sp.Value);
}
}
}
}
}

From the output results, it is found to be identical to the execution results of the wmic command:

RecordNumber corresponds to EventRecordID, TimeGenerated is the log creation time, and the following content needs to be filtered from the Message:

  • Security ID
  • Account Domain
  • Account Name
  • Source Network Address

3. Filtering Criteria

To filter out valid login information, length checks are performed on targetUserSid and ipAddress: targetUserSid length must be greater than 9, and ipAddress length must be greater than 8

Key Code:

String Message = o.GetPropertyValue("Message").ToString();
int pos1 = Message.LastIndexOf("Security ID");
int pos2 = Message.LastIndexOf("Account Name");
int pos3 = Message.LastIndexOf("Account Domain");
int pos4 = Message.LastIndexOf("Logon ID");
int pos5 = Message.LastIndexOf("Source Network Address");
int pos6 = Message.LastIndexOf("Source Port");
int length1 = pos2 - pos1 - 16;
int length2 = pos4 - pos3 - 20;
int length3 = pos3 - pos2 - 17;
int length4 = pos6 - pos5 - 27;
if (length1 < 0 || length2 < 0 || length3 < 0 || length4 < 0)
continue;
String targetUserSid = Message.Substring(pos1+14, length1);
String targetDomainName = Message.Substring(pos3 + 17, length2);
String targetUserName = Message.Substring(pos2 + 15, length3);
String ipAddress = Message.Substring(pos5 + 24, length4);
if (targetUserSid.Length > 9 && ipAddress.Length > 8)
{
Console.WriteLine("[+] EventRecordID: " + o.GetPropertyValue("RecordNumber"));
Console.WriteLine(" TimeCreated : " + o.GetPropertyValue("TimeGenerated"));
Console.WriteLine(" UserSid: " + targetUserSid);
Console.WriteLine(" DomainName: " + targetDomainName);
Console.WriteLine(" UserName: " + targetUserName);
Console.WriteLine(" IpAddress: " + ipAddress);
}

4. Support filtering logs within specified time periods

Can be achieved by modifying search conditions, key code:

String queryString = "SELECT * FROM Win32_NTLogEvent Where Logfile = 'Security' AND EventCode = 4624 AND TimeGenerated>=20210526 AND TimeGenerated<=20220426";

Thus, the implementation code for parsing local login logs through WMI is as follows:

using System;
using System.Management;
namespace Test2
{
class Program
{
static void Main(string[] args)
{
String queryString = "SELECT * FROM Win32_NTLogEvent Where Logfile = 'Security' AND EventCode = 4624 AND TimeGenerated>=20210526 AND TimeGenerated<=20220426";
ManagementScope s = new ManagementScope("root\\CIMV2");
SelectQuery q = new SelectQuery(queryString);
ManagementObjectSearcher mos = new ManagementObjectSearcher(s, q);
int flagTotal = 0;
int flagExist = 0;
foreach (ManagementObject o in mos.Get())
{
flagTotal++;
String Message = o.GetPropertyValue("Message").ToString();
int pos1 = Message.LastIndexOf("Security ID");
int pos2 = Message.LastIndexOf("Account Name");
int pos3 = Message.LastIndexOf("Account Domain");
int pos4 = Message.LastIndexOf("Logon ID");
int pos5 = Message.LastIndexOf("Source Network Address");
int pos6 = Message.LastIndexOf("Source Port");
int length1 = pos2 - pos1 - 16;
int length2 = pos4 - pos3 - 20;
int length3 = pos3 - pos2 - 17;
int length4 = pos6 - pos5 - 27;
if (length1 < 0 || length2 < 0 || length3 < 0 || length4 < 0)
continue;
String targetUserSid = Message.Substring(pos1+14, length1);
String targetDomainName = Message.Substring(pos3 + 17, length2);
String targetUserName = Message.Substring(pos2 + 15, length3);
String ipAddress = Message.Substring(pos5 + 24, length4);
{
Console.WriteLine("[+] EventRecordID: " + o.GetPropertyValue("RecordNumber"));
Console.WriteLine(" TimeCreated : " + o.GetPropertyValue("TimeGenerated"));
Console.WriteLine(" UserSid: " + targetUserSid);
Console.WriteLine(" DomainName: " + targetDomainName);
Console.WriteLine(" UserName: " + targetUserName);
Console.WriteLine(" IpAddress: " + ipAddress);
flagExist++;
}
}
Console.WriteLine("Total: " + flagTotal + ", Exist: " + flagExist);
}
}
}

5. Supports remote login

Key code:

var opt = new ConnectionOptions(); ;
opt.Username = "TEST\\Administrator";
opt.Password = "Password@123";
ManagementScope s = new ManagementScope("\\\\192.168.1.1\\root\\CIMV2", opt);

Integrating the above code yields the final code, which has been uploaded to GitHub at the following address:

An open-source project

The code supports the following features:

  • Compilable using csc.exe, supports versions 3.5 and 4.0
  • Supports local and remote log parsing; remote log parsing uses WMI
  • Supports conditional judgments for filtering by specified dates
  • Automatically extracts information: EventRecordID, TimeCreated, UserSid, DomainName, UserName, and IpAddress

0x04 Summary

---

This article details the implementation for obtaining domain user login information, open-sourcing two tools: SharpGetUserLoginIPRPC.cs and SharpGetUserLoginIPWMI.cs. In terms of communication efficiency, RPC is faster than WMI.