From 050a8e18b91e63aefca877b7d039f1de1aa5be62 Mon Sep 17 00:00:00 2001 From: LuemmelSec <58529760+LuemmelSec@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:50:06 +0100 Subject: [PATCH 1/2] Updated SID lookup Now using ldap queries rather than LSA lookups. Needed for running from non domain joined systems --- Certify/Commands/EnumCas.cs | 3 + Certify/Commands/EnumPkiObjects.cs | 3 + Certify/Commands/EnumTemplates.cs | 3 + Certify/Commands/ManageCa.cs | 4 ++ Certify/Commands/ManageTemplate.cs | 3 + Certify/Lib/LdapOperations.cs | 88 ++++++++++++++++++++++++++++++ Certify/Util/DisplayUtil.cs | 20 +++++++ 7 files changed, 124 insertions(+) diff --git a/Certify/Commands/EnumCas.cs b/Certify/Commands/EnumCas.cs index eab5116..363a6d7 100644 --- a/Certify/Commands/EnumCas.cs +++ b/Certify/Commands/EnumCas.cs @@ -60,6 +60,9 @@ public static int Execute(Options opts) var ldap = new LdapOperations(opts.Domain, opts.LdapServer); + // Set LdapOps for SID resolution on non-domain joined systems + DisplayUtil.LdapOps = ldap; + Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); List user_sids = null; diff --git a/Certify/Commands/EnumPkiObjects.cs b/Certify/Commands/EnumPkiObjects.cs index 917e402..e5115db 100644 --- a/Certify/Commands/EnumPkiObjects.cs +++ b/Certify/Commands/EnumPkiObjects.cs @@ -39,6 +39,9 @@ public static int Execute(Options opts) var ldap = new LdapOperations(opts.Domain, opts.LdapServer); + // Set LdapOps for SID resolution on non-domain joined systems + DisplayUtil.LdapOps = ldap; + Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); var pki_objects = ldap.GetPKIObjects(); diff --git a/Certify/Commands/EnumTemplates.cs b/Certify/Commands/EnumTemplates.cs index 3096857..2d038bf 100644 --- a/Certify/Commands/EnumTemplates.cs +++ b/Certify/Commands/EnumTemplates.cs @@ -76,6 +76,9 @@ public static int Execute(Options opts) var ldap = new LdapOperations(opts.Domain, opts.LdapServer); + // Set LdapOps for SID resolution on non-domain joined systems + DisplayUtil.LdapOps = ldap; + Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); if (!string.IsNullOrEmpty(opts.CertificateAuthority)) diff --git a/Certify/Commands/ManageCa.cs b/Certify/Commands/ManageCa.cs index 0e4a0d2..1c9a309 100644 --- a/Certify/Commands/ManageCa.cs +++ b/Certify/Commands/ManageCa.cs @@ -141,6 +141,10 @@ private static void PerformTemplateModifications(Options opts, string server, st var deleted_temps = template_pairs.RemoveAll(t => opts.ToggleTemplates.Contains(t.Item1, StringComparer.OrdinalIgnoreCase)); var ldap = new LdapOperations(opts.Domain, opts.LdapServer); + + // Set LdapOps for SID resolution on non-domain joined systems + DisplayUtil.LdapOps = ldap; + var ldap_temps = ldap.GetCertificateTemplates().Where(t => t.Name != null); var unidentified = add_templates.Where(t => !ldap_temps.Any(x => t.Equals(x.Name, StringComparison.OrdinalIgnoreCase))).ToList(); diff --git a/Certify/Commands/ManageTemplate.cs b/Certify/Commands/ManageTemplate.cs index 5aea5be..f652cd5 100644 --- a/Certify/Commands/ManageTemplate.cs +++ b/Certify/Commands/ManageTemplate.cs @@ -90,6 +90,9 @@ public static int Execute(Options opts) var ldap = new LdapOperations(opts.Domain, opts.LdapServer); + // Set LdapOps for SID resolution on non-domain joined systems + DisplayUtil.LdapOps = ldap; + Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); var template = ldap.GetCertificateTemplateEntry(opts.CertificateTemplate); diff --git a/Certify/Lib/LdapOperations.cs b/Certify/Lib/LdapOperations.cs index 38ac88c..48658a3 100644 --- a/Certify/Lib/LdapOperations.cs +++ b/Certify/Lib/LdapOperations.cs @@ -10,6 +10,8 @@ class LdapOperations { public string ConfigurationPath { get; } public string LdapServer { get; } + public string DefaultNamingContext { get; } + private string DomainRoot { get; } public LdapOperations() : this(null, null) @@ -27,12 +29,98 @@ public LdapOperations(string domain, string server) root_dse_path = $"LDAP://{domain}/RootDSE"; using (var root_dse = new DirectoryEntry(root_dse_path)) + { ConfigurationPath = $"{root_dse.Properties["configurationNamingContext"][0]}"; + DefaultNamingContext = $"{root_dse.Properties["defaultNamingContext"][0]}"; + } if (server == null) + { LdapServer = string.Empty; + DomainRoot = string.Empty; + } else + { LdapServer = $"{server}/"; + DomainRoot = $"LDAP://{server}/"; + } + } + + // Resolve SID to account name using LDAP query + // This works on non-domain joined systems with domain credentials + public string GetNameFromSid(string sid) + { + if (string.IsNullOrEmpty(sid)) + return null; + + try + { + // For well-known SIDs, try local resolution first + if (sid.StartsWith("S-1-5-32-") || sid.StartsWith("S-1-5-18") || sid.StartsWith("S-1-1-")) + { + try + { + var sid_object = new System.Security.Principal.SecurityIdentifier(sid); + return sid_object.Translate(typeof(System.Security.Principal.NTAccount)).ToString(); + } + catch + { + // Fall through to LDAP lookup + } + } + + // Use Global Catalog for forest-wide SID lookup + // GC:// searches across all domains in the forest + string gc_path; + if (string.IsNullOrEmpty(LdapServer)) + gc_path = "GC://"; + else + gc_path = $"GC://{LdapServer.TrimEnd('/')}"; + + using (var searcher = new DirectorySearcher(new DirectoryEntry(gc_path))) + { + searcher.Filter = $"(objectSid={ConvertSidToLdapFilter(sid)})"; + searcher.PropertiesToLoad.Add("sAMAccountName"); + searcher.PropertiesToLoad.Add("name"); + searcher.SearchScope = SearchScope.Subtree; + + var result = searcher.FindOne(); + if (result != null) + { + if (result.Properties.Contains("sAMAccountName")) + return result.Properties["sAMAccountName"][0].ToString(); + else if (result.Properties.Contains("name")) + return result.Properties["name"][0].ToString(); + } + } + } + catch + { + // Silently fail and return null + } + + return null; + } + + // Convert SID string to LDAP filter format + private string ConvertSidToLdapFilter(string sid) + { + try + { + var sid_object = new System.Security.Principal.SecurityIdentifier(sid); + var sid_bytes = new byte[sid_object.BinaryLength]; + sid_object.GetBinaryForm(sid_bytes, 0); + + var hex_string = ""; + foreach (byte b in sid_bytes) + hex_string += $"\\{b:x2}"; + + return hex_string; + } + catch + { + return sid; + } } public IEnumerable GetPKIObjects() diff --git a/Certify/Util/DisplayUtil.cs b/Certify/Util/DisplayUtil.cs index 72fc393..30736e2 100644 --- a/Certify/Util/DisplayUtil.cs +++ b/Certify/Util/DisplayUtil.cs @@ -13,6 +13,10 @@ namespace Certify.Lib { class DisplayUtil { + // LdapOperations instance for SID resolution + // Set this before calling Display methods for proper SID resolution on non-domain joined systems + public static LdapOperations LdapOps { get; set; } + public static void PrintPkiObjectControllers(Dictionary>> object_controllers, bool hide_admins) { foreach (var object_controller in object_controllers.OrderBy(o => GetUserNameFromSid(o.Key))) @@ -368,6 +372,22 @@ public static string GetUserSidString(string sid, int padding = 35) public static string GetUserNameFromSid(string sid) { + // Try LDAP-based resolution first (works on non-domain joined systems) + if (LdapOps != null) + { + try + { + var name = LdapOps.GetNameFromSid(sid); + if (!string.IsNullOrEmpty(name)) + return name; + } + catch + { + // Fall through to Translate() method + } + } + + // Fallback to local resolution (may fail on non-domain joined systems) try { var sid_object = new SecurityIdentifier(sid); From fc19ba9fa9cf40ec79e60c0246a153af24a0480a Mon Sep 17 00:00:00 2001 From: LuemmelSec <58529760+LuemmelSec@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:49:58 +0100 Subject: [PATCH 2/2] Enhance LDAP operations with domain extraction Added extraction of NetBIOS domain name from distinguished name. --- Certify/Lib/LdapOperations.cs | 58 +++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/Certify/Lib/LdapOperations.cs b/Certify/Lib/LdapOperations.cs index 48658a3..840b135 100644 --- a/Certify/Lib/LdapOperations.cs +++ b/Certify/Lib/LdapOperations.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.DirectoryServices; using System.Linq; @@ -82,15 +82,32 @@ public string GetNameFromSid(string sid) searcher.Filter = $"(objectSid={ConvertSidToLdapFilter(sid)})"; searcher.PropertiesToLoad.Add("sAMAccountName"); searcher.PropertiesToLoad.Add("name"); + searcher.PropertiesToLoad.Add("distinguishedName"); searcher.SearchScope = SearchScope.Subtree; var result = searcher.FindOne(); if (result != null) { + string accountName = null; + if (result.Properties.Contains("sAMAccountName")) - return result.Properties["sAMAccountName"][0].ToString(); + accountName = result.Properties["sAMAccountName"][0].ToString(); else if (result.Properties.Contains("name")) - return result.Properties["name"][0].ToString(); + accountName = result.Properties["name"][0].ToString(); + + if (!string.IsNullOrEmpty(accountName)) + { + // Extract domain from DN (DC=domain,DC=com -> DOMAIN) + if (result.Properties.Contains("distinguishedName")) + { + var dn = result.Properties["distinguishedName"][0].ToString(); + var domain = ExtractDomainFromDN(dn); + if (!string.IsNullOrEmpty(domain)) + return $"{domain}\\{accountName}"; + } + + return accountName; + } } } } @@ -102,6 +119,41 @@ public string GetNameFromSid(string sid) return null; } + // Extract NetBIOS domain name from Distinguished Name + // DC=contoso,DC=com -> CONTOSO + private string ExtractDomainFromDN(string dn) + { + if (string.IsNullOrEmpty(dn)) + return null; + + try + { + // Find the first DC= component + var dcIndex = dn.IndexOf("DC=", StringComparison.OrdinalIgnoreCase); + if (dcIndex == -1) + return null; + + // Extract all DC components + var dcPart = dn.Substring(dcIndex); + var dcComponents = new List(); + + foreach (var part in dcPart.Split(',')) + { + var trimmed = part.Trim(); + if (trimmed.StartsWith("DC=", StringComparison.OrdinalIgnoreCase)) + dcComponents.Add(trimmed.Substring(3)); + } + + // Return the first component (NetBIOS-style domain name) + // For full FQDN, you could return string.Join(".", dcComponents) + return dcComponents.Count > 0 ? dcComponents[0].ToUpper() : null; + } + catch + { + return null; + } + } + // Convert SID string to LDAP filter format private string ConvertSidToLdapFilter(string sid) {