Skip to content

Commit

Permalink
Implemented Windows LAPS parser and fixed Views
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelGrafnetter committed Feb 2, 2025
1 parent 3f20c6e commit b140e7a
Show file tree
Hide file tree
Showing 22 changed files with 709 additions and 559 deletions.
45 changes: 43 additions & 2 deletions Src/DSInternals.Common/Data/LAPS/LapsClearTextPassword.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,62 @@
using Newtonsoft.Json;
using System.Globalization;
using System;
using System.Text;
using Newtonsoft.Json;

namespace DSInternals.Common.Data
{
/// <example>
/// {"n":"Administrator","t":"1d8161b41c41cde","p":"A6a3#7%eb!57be4a4B95Z43394ba956de69e5d8975#$8a6d)4f82da6ad500HGx"}
/// </example>
/// <seealso>https://learn.microsoft.com/en-us/windows-server/identity/laps/laps-technical-reference</seealso>
public class LapsClearTextPassword
{
[JsonProperty("n")]
public string AccountName;

[JsonProperty("t")]
public string UpdateTimestamp;
public string UpdateTimestampString;

[JsonProperty("p")]
public string Password;

[JsonIgnore()]
public DateTime? UpdateTimestamp
{
get
{
if(String.IsNullOrEmpty(UpdateTimestampString))
{
return null;
}

return DateTime.FromFileTimeUtc(long.Parse(UpdateTimestampString, NumberStyles.HexNumber, CultureInfo.InvariantCulture));
}
set
{
if(value.HasValue)
{
UpdateTimestampString = value.Value.ToFileTimeUtc().ToString("x");
}
else
{
UpdateTimestampString = null;
}
}
}

public static LapsClearTextPassword Parse(string json)
{
Validator.AssertNotNull(json, nameof(json));
return JsonConvert.DeserializeObject<LapsClearTextPassword>(json);
}

public static LapsClearTextPassword Parse(byte[] binaryJson)
{
Validator.AssertNotNull(binaryJson, nameof(binaryJson));

string json = Encoding.UTF8.GetString(binaryJson);
return Parse(json);
}
}
}
6 changes: 1 addition & 5 deletions Src/DSInternals.Common/Data/LAPS/LapsPasswordInformation.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DSInternals.Common.Data
{
Expand All @@ -29,7 +25,7 @@ public LapsPasswordInformation(string computerName, LapsClearTextPassword passwo
this.ComputerName = computerName;
this.Account = password.AccountName;
this.Password = password.Password;
// TODO: this.PasswordUpdateTime = password.UpdateTimestamp;
this.PasswordUpdateTime = password.UpdateTimestamp;
this.ExpirationTimestamp = expiration;
this.Source = LapsPasswordSource.CleartextPassword;
this.DecryptionStatus = LapsDecryptionStatus.NotApplicable;
Expand Down
7 changes: 5 additions & 2 deletions Src/DSInternals.Common/Data/Principals/AccountPropertySets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ public enum AccountPropertySets : short
LMHashHistory,
PasswordHashHistory = NTHashHistory | LMHashHistory,
SupplementalCredentials,
Secrets = PasswordHashes | PasswordHashHistory | SupplementalCredentials,
KeyCredentials,
RoamedCredentials,
LAPS,
All = DistinguishedName | GenericInformation | SecurityDescriptor | PasswordHashes | PasswordHashHistory | SupplementalCredentials | KeyCredentials | RoamedCredentials | LAPS,
WindowsLAPS,
LegacyLAPS,
LAPS = WindowsLAPS | LegacyLAPS,
All = DistinguishedName | GenericInformation | SecurityDescriptor | Secrets | KeyCredentials | RoamedCredentials | LAPS,
Default = All
}
}
52 changes: 24 additions & 28 deletions Src/DSInternals.Common/Data/Principals/DSAccount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ public DSAccount(DirectoryObject dsObject, string netBIOSDomainName, DirectorySe
this.LoadAccountInfo(dsObject, netBIOSDomainName, propertySets);

// Hashes and Supplemental Credentials
this.LoadSecrets(dsObject, pek, propertySets);
if (pek != null)
{
// Only continue if we have a decryption key
this.LoadSecrets(dsObject, pek, propertySets);
}

if (propertySets.HasFlag(AccountPropertySets.KeyCredentials))
{
Expand Down Expand Up @@ -195,9 +199,9 @@ public DateTime? LastLogonDate
}

/// <summary>
/// Gets the Nullable DateTime that specifies the last date and time that the password was set for this account.
/// Gets the date and time when the password was set for this account.
/// </summary>
public DateTime? LastPasswordSet
public DateTime? PasswordLastSet
{
get;
private set;
Expand Down Expand Up @@ -326,24 +330,24 @@ public KeyCredential[] KeyCredentials

protected void LoadAccountInfo(DirectoryObject dsObject, string netBIOSDomainName, AccountPropertySets propertySets)
{
// SamAccountName
// SamAccountName:
dsObject.ReadAttribute(CommonDirectoryAttributes.SAMAccountName, out string samAccountName);
this.SamAccountName = samAccountName;

// LogonName
// LogonName (DOMAIN\SamAccountName):
if (!string.IsNullOrEmpty(samAccountName))
{
this.LogonName = new NTAccount(netBIOSDomainName, samAccountName).ToString();
}

// Service Principal Name(s)
// Service Principal Name(s):
dsObject.ReadAttribute(CommonDirectoryAttributes.ServicePrincipalName, out string[] spn);
this.ServicePrincipalName = spn;

// Guid:
// ObjectGuid:
this.Guid = dsObject.Guid;

// Sid:
// ObjectSid:
this.Sid = dsObject.Sid;

// UAC:
Expand All @@ -363,15 +367,19 @@ protected void LoadAccountInfo(DirectoryObject dsObject, string netBIOSDomainNam
// Note: The value is stored as int in the DB, but the documentation says that it is an unsigned int
this.SupportedEncryptionTypes = (SupportedEncryptionTypes?)numericSupportedEncryptionTypes;

// PrimaryGroupId:
dsObject.ReadAttribute(CommonDirectoryAttributes.PrimaryGroupId, out int? groupId);
this.PrimaryGroupId = groupId.Value;

if (propertySets.HasFlag(AccountPropertySets.DistinguishedName))
{
// DN:
// Note: DN loading from the DB involves one or more seeks.
this.DistinguishedName = dsObject.DistinguishedName;
}

if(propertySets.HasFlag(AccountPropertySets.SecurityDescriptor))
{
// Security Descriptor:
// Note: Security descriptor loading from the DB involves a seek and binary data parsing.
dsObject.ReadAttribute(CommonDirectoryAttributes.SecurityDescriptor, out RawSecurityDescriptor securityDescriptor);
this.SecurityDescriptor = securityDescriptor;
}
Expand All @@ -386,10 +394,6 @@ protected void LoadAccountInfo(DirectoryObject dsObject, string netBIOSDomainNam
dsObject.ReadAttribute(CommonDirectoryAttributes.UserPrincipalName, out string upn);
this.UserPrincipalName = upn;

// PrimaryGroupId:
dsObject.ReadAttribute(CommonDirectoryAttributes.PrimaryGroupId, out int? groupId);
this.PrimaryGroupId = groupId.Value;

// LastLogon:
dsObject.ReadAttribute(CommonDirectoryAttributes.LastLogon, out DateTime? lastLogon, false);
this.LastLogon = lastLogon;
Expand All @@ -400,7 +404,7 @@ protected void LoadAccountInfo(DirectoryObject dsObject, string netBIOSDomainNam

// PwdLastSet
dsObject.ReadAttribute(CommonDirectoryAttributes.PasswordLastSet, out DateTime? pwdLastSet, false);
this.LastPasswordSet = pwdLastSet;
this.PasswordLastSet = pwdLastSet;

// Description
dsObject.ReadAttribute(CommonDirectoryAttributes.Description, out string description);
Expand All @@ -410,12 +414,6 @@ protected void LoadAccountInfo(DirectoryObject dsObject, string netBIOSDomainNam

protected void LoadSecrets(DirectoryObject dsObject, DirectorySecretDecryptor pek, AccountPropertySets propertySets)
{
if (pek == null)
{
// Do not continue if we do not have a decryption key
return;
}

if (propertySets.HasFlag(AccountPropertySets.LMHash))
{
// LM Hash:
Expand Down Expand Up @@ -482,18 +480,16 @@ protected void LoadKeyCredentials(DirectoryObject dsObject)
byte[][] keyCredentialBlobs;
dsObject.ReadLinkedValues(CommonDirectoryAttributes.KeyCredentialLink, out keyCredentialBlobs);

// Parse the blobs and combine them into one array.
var credentials = new List<KeyCredential>();

if (keyCredentialBlobs != null)
{
foreach (var blob in keyCredentialBlobs)
// Parse the blobs and combine them into one array.
this.KeyCredentials = new KeyCredential[keyCredentialBlobs.Length];

for(int i = 0; i < keyCredentialBlobs.Length; i++)
{
credentials.Add(new KeyCredential(blob, this.DistinguishedName));
this.KeyCredentials[i] = new KeyCredential(keyCredentialBlobs[i], this.DistinguishedName);
}
}

this.KeyCredentials = credentials.ToArray();
}
}
}
45 changes: 36 additions & 9 deletions Src/DSInternals.Common/Data/Principals/DSComputer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ public DSComputer(DirectoryObject dsObject, string netBIOSDomainName, DirectoryS
this.LoadGenericComputerAccountInfo(dsObject);
}

if (propertySets.HasFlag(AccountPropertySets.LAPS))
if (propertySets.HasFlag(AccountPropertySets.LegacyLAPS))
{
this.LoadLAPS(dsObject);
this.LoadLegacyLAPS(dsObject);
}

if (propertySets.HasFlag(AccountPropertySets.WindowsLAPS))
{
this.LoadWindowsLAPS(dsObject);
}
}

Expand Down Expand Up @@ -105,12 +110,10 @@ protected void LoadGenericComputerAccountInfo(DirectoryObject dsObject)
this.OperatingSystemServicePack = operatingSystemServicePack;
}

protected void LoadLAPS(DirectoryObject dsObject)
protected void LoadLegacyLAPS(DirectoryObject dsObject)
{
LapsPasswordInformation legacyLapsPassword = null;
LapsPasswordInformation lapsPassword = null;

// Legacy LAPS
dsObject.ReadAttribute(CommonDirectoryAttributes.LAPSPasswordExpirationTime, out DateTime? legacyExpirationTime, false);

if (legacyExpirationTime != null)
Expand All @@ -125,17 +128,31 @@ protected void LoadLAPS(DirectoryObject dsObject)
}
}

// Windows LAPS
if (legacyLapsPassword != null)
{
if(this.LapsPasswords == null)
{
this.LapsPasswords = new List<LapsPasswordInformation>();
}

this.LapsPasswords.Add(legacyLapsPassword);
}
}

protected void LoadWindowsLAPS(DirectoryObject dsObject)
{
LapsPasswordInformation lapsPassword = null;

dsObject.ReadAttribute(CommonDirectoryAttributes.WindowsLapsPasswordExpirationTime, out DateTime? expirationTime, false);

if (expirationTime != null)
{
// Read msLAPS-Password
dsObject.ReadAttribute(CommonDirectoryAttributes.WindowsLapsPassword, out string lapsJson);
dsObject.ReadAttribute(CommonDirectoryAttributes.WindowsLapsPassword, out byte[] binaryLapsJson);

if (!String.IsNullOrEmpty(lapsJson))
if (binaryLapsJson != null && binaryLapsJson.Length > 0)
{
LapsClearTextPassword passwordInfo = LapsClearTextPassword.Parse(lapsJson);
LapsClearTextPassword passwordInfo = LapsClearTextPassword.Parse(binaryLapsJson);
lapsPassword = new LapsPasswordInformation(this.SamAccountName, passwordInfo, expirationTime);
}

Expand All @@ -151,6 +168,16 @@ protected void LoadLAPS(DirectoryObject dsObject)
// Read msLAPS-EncryptedDSRMPasswordHistory
// TODO: dsObject.ReadAttribute(CommonDirectoryAttributes.WindowsLapsEncryptedDsrmPasswordHistory, out byte[][] encryptedDsrmPasswordHistory);
}

if (lapsPassword != null)
{
if (this.LapsPasswords == null)
{
this.LapsPasswords = new List<LapsPasswordInformation>();
}

this.LapsPasswords.Add(lapsPassword);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using DSInternals.Common.Data;
using System;
using System.Management.Automation;
using DSInternals.Common.Data;
using DSInternals.DataStore;
using DSInternals.PowerShell.Properties;
using System;
using System.Collections.Generic;
using System.Management.Automation;

namespace DSInternals.PowerShell.Commands
{
[Cmdlet(VerbsCommon.Get, "ADDBAccount")]
[OutputType(typeof(DSAccount))]
[OutputType(typeof(DSAccount), typeof(DSUser), typeof(DSComputer))]
public class GetADDBAccountCommand : ADDBPrincipalCommandBase
{
#region Constants
Expand Down
14 changes: 7 additions & 7 deletions Src/DSInternals.PowerShell/DSInternals.PowerShell.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,6 @@
<Content Include="License.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Views\DSInternals.GenericComputerAccountInfo.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Views\DSInternals.Hash.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down Expand Up @@ -254,15 +250,19 @@
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Views\DSInternals.GenericUserAccountInfo.format.ps1xml">
<Content Include="Views\DSInternals.BitLockerRecoveryInformation.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Views\DSInternals.LapsPasswordInformation.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Views\DSInternals.BitLockerRecoveryInformation.format.ps1xml">
<Content Include="Views\DSInternals.DSComputer.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Views\DSInternals.LAPSCredential.format.ps1xml">
<Content Include="Views\DSInternals.DSUser.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
Expand Down
6 changes: 3 additions & 3 deletions Src/DSInternals.PowerShell/DSInternals.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ FormatsToProcess = 'Views\DSInternals.AzureADUser.format.ps1xml',
'Views\DSInternals.Kerberos.format.ps1xml',
'Views\DSInternals.KeyCredential.format.ps1xml',
'Views\DSInternals.FidoKeyMaterial.format.ps1xml',
'Views\DSInternals.GenericUserAccountInfo.format.ps1xml',
'Views\DSInternals.GenericComputerAccountInfo.format.ps1xml',
'Views\DSInternals.BitLockerRecoveryInformation.format.ps1xml',
'Views\DSInternals.LAPSCredential.format.ps1xml',
'Views\DSInternals.LapsPasswordInformation.format.ps1xml',
'Views\DSInternals.DSAccount.format.ps1xml',
'Views\DSInternals.DSAccount.ExportViews.format.ps1xml',
'Views\DSInternals.DSUser.format.ps1xml',
'Views\DSInternals.DSComputer.format.ps1xml',
'Views\DSInternals.GroupManagedServiceAccount.format.ps1xml',
'Views\DSInternals.PasswordQualityTestResult.format.ps1xml',
'Views\DSInternals.KdsRootKey.format.ps1xml',
Expand Down
Loading

0 comments on commit b140e7a

Please sign in to comment.