Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Certify/Commands/EnumCas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> user_sids = null;
Expand Down
3 changes: 3 additions & 0 deletions Certify/Commands/EnumPkiObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions Certify/Commands/EnumTemplates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions Certify/Commands/ManageCa.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions Certify/Commands/ManageTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
142 changes: 141 additions & 1 deletion Certify/Lib/LdapOperations.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Linq;
Expand All @@ -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)
Expand All @@ -27,12 +29,150 @@ 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.PropertiesToLoad.Add("distinguishedName");
searcher.SearchScope = SearchScope.Subtree;

var result = searcher.FindOne();
if (result != null)
{
string accountName = null;

if (result.Properties.Contains("sAMAccountName"))
accountName = result.Properties["sAMAccountName"][0].ToString();
else if (result.Properties.Contains("name"))
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;
}
}
}
}
catch
{
// Silently fail and return null
}

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<string>();

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)
{
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<PKIObject> GetPKIObjects()
Expand Down
20 changes: 20 additions & 0 deletions Certify/Util/DisplayUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, List<Tuple<string, string>>> object_controllers, bool hide_admins)
{
foreach (var object_controller in object_controllers.OrderBy(o => GetUserNameFromSid(o.Key)))
Expand Down Expand Up @@ -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);
Expand Down