using System.Globalization; using System.Net; using System.Security.Principal; using DSInternals.Common; using DSInternals.Common.Cryptography; using DSInternals.Common.Data; using DSInternals.Common.Exceptions; using DSInternals.Common.Kerberos; using DSInternals.Common.Schema; using DSInternals.Replication.Interop; using DSInternals.Replication.Model; namespace DSInternals.Replication; /// /// Provides methods for connecting to a domain controller and replicating directory objects. /// public class DirectoryReplicationClient : IDisposable, IKdsRootKeyResolver { /// /// Service principal name (SPN) of the destination server. /// private const string ServicePrincipalNameFormat = "ldap/{0}"; /// /// Named pipe used for DRSR communication. /// private const string DrsNamedPipeName = @"\pipe\lsass"; /// /// Prefix of the configuration naming context. /// private const string ConfigurationNamingContextPrefix = "CN=Configuration,DC="; /// /// Prefix of the schema naming context. /// private const string SchemaNamingContextPrefix = "CN=Schema,CN=Configuration,DC="; /// /// Identifier of Windows Server 2000 dcpromo. /// private static readonly Guid DcPromoGuid2k = new("6abec3d1-3054-41c8-a362-5a0c5b7d5d71"); /// /// Identifier of Windows Server 2003+ dcpromo. /// private static readonly Guid DcPromoGuid2k3 = new("6afab99c-6e26-464a-975f-f58f105218bc"); /// /// Non-DC client identifier. /// private static readonly Guid NtdsApiClientGuid = new("e24d201a-4fd6-11d1-a3da-0000f875ae0d"); private bool _isFullSchemaLoaded = false; private RpcBinding _rpcBinding; private DrsConnection _drsConnection; private IKdsRootKeyResolver _rootKeyResolver; private readonly Lazy<(string DomainNamingContext, string DNSDomainName, string NetBIOSDomainName)> _domainInfo; private readonly Lazy _namingContexts; private readonly Lazy _secretDecryptor; private EventHandler _sessionKeyChangedHandler; /// /// The domain naming context of the connected server. /// public string DomainNamingContext => _domainInfo.Value.DomainNamingContext; /// /// The DNS domain name of the connected server. /// public string DNSDomainName => _domainInfo.Value.DNSDomainName; /// /// The NetBIOS domain name of the connected server. /// public string NetBIOSDomainName => _domainInfo.Value.NetBIOSDomainName; /// /// The naming contexts (partitions) hosted by the connected server. /// public string[] NamingContexts => _namingContexts.Value; /// /// The configuration naming context of the connected server. /// public string ConfigurationNamingContext { get { // TODO: It would be more elegant to load the ConfigNC based on its GUID. return this.NamingContexts. Where(context => context.StartsWith(ConfigurationNamingContextPrefix, StringComparison.InvariantCultureIgnoreCase)). First(); } } /// /// The schema naming context of the connected server. /// public string SchemaNamingContext { get { return this.NamingContexts. Where(context => context.StartsWith(SchemaNamingContextPrefix, StringComparison.InvariantCultureIgnoreCase)). First(); } } /// /// Initializes a new instance of the class. /// /// The FQDN or IP address of the domain controller. /// The credentials to use for authentication. public DirectoryReplicationClient(string server, NetworkCredential credential = null) { ArgumentException.ThrowIfNullOrWhiteSpace(server); var schema = BaseSchema.Create(); this._rpcBinding = new RpcBinding(server, RpcProtseq.ncacn_ip_tcp); string spn = String.Format(CultureInfo.InvariantCulture, ServicePrincipalNameFormat, server); this._rpcBinding.AuthenticateAs(spn, credential, RpcAuthenticationLevel.PacketPrivacy, RpcAuthenticationType.Negotiate); this._drsConnection = new DrsConnection(this._rpcBinding.DangerousGetHandle(), NtdsApiClientGuid, schema); // The replication client can fetch root keys. Cache them for performance. this._rootKeyResolver = new KdsRootKeyCache(this); // Lazily fetch domain info, naming contexts, and the secret decryptor on first access. this._domainInfo = new Lazy<(string, string, string)>(this.LoadDomainInfo); this._namingContexts = new Lazy(() => this._drsConnection.ListNamingContexts()); this._secretDecryptor = new Lazy(() => { var decryptor = new ReplicationSecretDecryptor(this._drsConnection.SessionKey); // The RPC session key can be renegotiated mid-replication. Forward those changes to the // decryptor so it always tries the current key (while retaining the previous ones). this._sessionKeyChangedHandler = (sender, e) => decryptor.ChangeSessionKey(e.SessionKey); this._drsConnection.SessionKeyChanged += this._sessionKeyChangedHandler; return decryptor; }); } /// /// Gets the replication cursors for the specified naming context. /// /// The naming context to retrieve replication cursors for. /// An array of replication cursors. public ReplicationCursor[] GetReplicationCursors(string namingContext) { ArgumentException.ThrowIfNullOrWhiteSpace(namingContext); return this._drsConnection.GetReplicationCursors(namingContext); } /// /// Retrieves all accounts from the current domain partition. /// /// Optional progress reporter invoked after each replication cycle. /// The set of properties to retrieve for each account. /// Token used to cooperatively cancel the replication between cycles. /// An enumerable collection of directory service accounts. public IEnumerable GetAccounts(IProgress progress = null, AccountPropertySets propertySets = AccountPropertySets.All, CancellationToken cancellationToken = default) { string domainNamingContext = this.DomainNamingContext; return ReplicateAllObjects(domainNamingContext, progress, cancellationToken) .Select(dsObject => AccountFactory.CreateAccount(dsObject, this.NetBIOSDomainName, _secretDecryptor.Value, _rootKeyResolver, propertySets)) .Where(account => account != null); // CreateAccount returns null for other object types } /// /// Retrieves all directory objects from the specified naming context. /// /// Partition to replicate. /// Optional progress reporter invoked after each replication cycle. /// Token used to cooperatively cancel the replication between cycles. /// An enumerable collection of directory service objects. public IEnumerable ReplicateAllObjects(string namingContext, IProgress progress = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(namingContext); ReplicationCookie currentCookie = new(namingContext); ReplicationResult result; int processedObjectCount = 0; do { // Check for cancellation between replication cycles (the native DRS call itself is not interruptible). cancellationToken.ThrowIfCancellationRequested(); // Perform one replication cycle result = this._drsConnection.ReplicateAllObjects(currentCookie); // Report replication progress processedObjectCount += result.Objects.Count; progress?.Report(new ReplicationProgress(result.Cookie, processedObjectCount, result.TotalObjectCount)); // Pass-through the returned objects foreach (var obj in result.Objects) { yield return obj; } // Update the position of the replication cursor currentCookie = result.Cookie; } while (result.HasMoreData); } /// /// Retrieves a single account by its object GUID. /// /// The object GUID of the account to retrieve. /// The set of properties to retrieve for the account. /// An enumerable collection of directory service accounts. /// Thrown when the object is not an account. public DSAccount GetAccount(Guid objectGuid, AccountPropertySets propertySets = AccountPropertySets.All) { ReplicaObject obj = this._drsConnection.ReplicateSingleObject(objectGuid); var account = AccountFactory.CreateAccount(obj, this.NetBIOSDomainName, _secretDecryptor.Value, _rootKeyResolver, propertySets); if (account == null) { // If the target object is not an account, CreateAccount returns null throw new DirectoryObjectOperationException("The object is not an account.", objectGuid); } return account; } /// /// Retrieves a single account by its distinguished name. /// /// The distinguished name of the account to retrieve. /// The set of properties to retrieve for the account. /// An enumerable collection of directory service accounts. /// Thrown when the object is not an account. public DSAccount GetAccount(string distinguishedName, AccountPropertySets propertySets = AccountPropertySets.All) { ReplicaObject obj = this._drsConnection.ReplicateSingleObject(distinguishedName); return AccountFactory.CreateAccount(obj, this.NetBIOSDomainName, _secretDecryptor.Value, _rootKeyResolver, propertySets) ?? throw new DirectoryObjectOperationException("The object is not an account.", distinguishedName); } /// /// Retrieves a single account by its NT account name. /// /// The NT account name of the account to retrieve. /// The set of properties to retrieve for the account. /// An enumerable collection of directory service accounts. public DSAccount GetAccount(NTAccount accountName, AccountPropertySets propertySets = AccountPropertySets.All) { Guid objectGuid = this._drsConnection.ResolveGuid(accountName); return this.GetAccount(objectGuid, propertySets); } /// /// Retrieves a single account by its security identifier (SID). /// /// The security identifier (SID) of the account to retrieve. /// The set of properties to retrieve for the account. /// An enumerable collection of directory service accounts. public DSAccount GetAccount(SecurityIdentifier sid, AccountPropertySets propertySets = AccountPropertySets.All) { Guid objectGuid = this._drsConnection.ResolveGuid(sid); return this.GetAccount(objectGuid, propertySets); } /// /// Retrieves a single trusted-domain object by name from the current domain. /// /// The trust object name (CN) to retrieve. /// The trusted-domain object. public TrustedDomain GetTrustedDomain(string name) { return this.GetTrustedDomain(name, this.DomainNamingContext, this.DNSDomainName); } /// /// Retrieves a single trusted-domain object by name. /// /// The trust object name (CN) to retrieve. /// The DNS name of the domain containing the trust object. /// The trusted-domain object. /// Thrown when the object is not a trusted-domain object. public TrustedDomain GetTrustedDomain(string name, string domain) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentException.ThrowIfNullOrWhiteSpace(domain); string domainNamingContext = DistinguishedName.GetDNFromDNSName(domain).ToString(); return this.GetTrustedDomain(name, domainNamingContext, domain); } /// /// Retrieves a single trusted-domain object by name. /// /// The trust object name (CN) to retrieve. /// The domain naming context containing the trust object. /// The DNS domain name containing the trust object. /// The trusted-domain object. /// Thrown when the object is not a trusted-domain object. public TrustedDomain GetTrustedDomain(string name, string domainNamingContext, string dnsDomainName) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentException.ThrowIfNullOrWhiteSpace(domainNamingContext); ArgumentException.ThrowIfNullOrWhiteSpace(dnsDomainName); // TODO: validate the input against injection attacks. // TODO: Consider using the DistinguishedName class for DN concatenation. string trustDN = $"CN={name},CN=System,{domainNamingContext}"; var trustObject = this._drsConnection.ReplicateSingleObject(trustDN); return new TrustedDomain( trustObject, dnsDomainName, this.NetBIOSDomainName, _secretDecryptor.Value); } /// /// Retrieves the KDS root key with the specified identifier. /// /// The identifier of the KDS root key to retrieve. /// Whether to suppress the not found exception. /// public KdsRootKey? GetKdsRootKey(Guid rootKeyId, bool suppressNotFoundException = false) { // Derive the full path to the object // Example: CN=4dd60361-9394-492a-b11d-51a955f02b06,CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration,DC=contoso,DC=com string rootKeyDN = KdsRootKey.GetDistinguishedName(rootKeyId, this.ConfigurationNamingContext); try { var rootKeyObject = _drsConnection.ReplicateSingleObject(rootKeyDN); return new KdsRootKey(rootKeyObject); } catch (DirectoryObjectNotFoundException) { if (suppressNotFoundException) { return null; } else { throw; } } } /// /// Retrieves all DPAPI backup keys from the specified domain partition. /// /// The distinguished name of the domain partition. /// An enumerable collection of DPAPI backup keys. public IEnumerable GetDPAPIBackupKeys(string domainNamingContext) { // TODO: Split this function into RSA and Legacy Part so that exception in one of them does not crash the whole process // Fetch the legacy pointer first, because there is a higher chance that it is present than the RSA one. string legacyPointerDN = DPAPIBackupKey.GetPreferredLegacyKeyPointerDN(domainNamingContext); var legacyPointer = this.GetLSASecret(legacyPointerDN); yield return legacyPointer; string legacyKeyDN = DPAPIBackupKey.GetKeyDN(legacyPointer.KeyId, domainNamingContext); var legacyKey = this.GetLSASecret(legacyKeyDN); yield return legacyKey; string rsaPointerDN = DPAPIBackupKey.GetPreferredRSAKeyPointerDN(domainNamingContext); var rsaPointer = this.GetLSASecret(rsaPointerDN); yield return rsaPointer; string rsaKeyDN = DPAPIBackupKey.GetKeyDN(rsaPointer.KeyId, domainNamingContext); var rsaKey = this.GetLSASecret(rsaKeyDN); yield return rsaKey; } /// /// Retrieves a single LSA secret by its distinguished name. /// /// The distinguished name of the LSA secret. /// The DPAPI backup key. private DPAPIBackupKey GetLSASecret(string distinguishedName) { var secretObj = this._drsConnection.ReplicateSingleObject(distinguishedName); return new DPAPIBackupKey(secretObj, _secretDecryptor.Value); } /// /// Writes the NGC public key to the specified account. /// /// The GUID of the account to write the key to. /// The NGC public key to write. public void WriteNgcKey(Guid objectGuid, byte[] publicKey) { string distinguishedName = this._drsConnection.ResolveDistinguishedName(objectGuid); this.WriteNgcKey(distinguishedName, publicKey); } /// /// Writes the NGC public key to the specified account. /// /// The name of the account to write the key to. /// The NGC public key to write. public void WriteNgcKey(NTAccount accountName, byte[] publicKey) { string distinguishedName = this._drsConnection.ResolveDistinguishedName(accountName); this.WriteNgcKey(distinguishedName, publicKey); } /// /// Writes the NGC public key to the specified account. /// /// The security identifier of the account to write the key to. /// The NGC public key to write. public void WriteNgcKey(SecurityIdentifier sid, byte[] publicKey) { string distinguishedName = this._drsConnection.ResolveDistinguishedName(sid); this.WriteNgcKey(distinguishedName, publicKey); } /// /// Writes the NGC public key to the specified account. /// /// The distinguished name of the account to write the key to. /// The NGC public key to write. public void WriteNgcKey(string accountDN, byte[] publicKey) { this._drsConnection.WriteNgcKey(accountDN, publicKey); } /// /// Adds the SID history of a source principal to a destination principal through MS-DRSR. /// /// Name of the source domain (FQDN or NetBIOS). /// Name of the source principal in the source domain. Not used when contains . /// Name of the source domain controller (PDC). /// Required unless contains or . /// Credentials for the source domain. /// Name of the destination domain (FQDN or NetBIOS). /// Name of the destination principal in the destination domain. Not used when contains . /// Behavior flags for the operation. public void AddSidHistory( string sourceDomain = null, string sourcePrincipal = null, string sourceDomainController = null, NetworkCredential sourceCredential = null, string destinationDomain = null, string destinationPrincipal = null, AddSidHistoryOptions flags = AddSidHistoryOptions.None) { this._drsConnection.AddSidHistory( sourceDomain, sourcePrincipal, sourceDomainController, sourceCredential, destinationDomain, destinationPrincipal, flags); } /// /// Verifies whether the RPC channel is secure. /// public void AddSidHistory() { this.AddSidHistory(flags: AddSidHistoryOptions.CheckSecureChannel); } /// /// Adds SID history within the same domain and deletes the source object. /// /// Distinguished name of the source principal. /// Distinguished name of the destination principal. /// Name of the source domain controller (PDC). public void AddSidHistory(string sourcePrincipal, string destinationPrincipal, string? sourceDomainController = null) { this.AddSidHistory( sourcePrincipal: sourcePrincipal, sourceDomainController: sourceDomainController, destinationPrincipal: destinationPrincipal, flags: AddSidHistoryOptions.DeleteSourceObject); } /// /// Adds SID history across forests. /// /// Name of the source domain (FQDN or NetBIOS). /// Name of the source principal in the source domain. /// Name of the destination domain (FQDN or NetBIOS). /// Name of the destination principal in the destination domain. /// Name of the source domain controller (PDC), if specified. /// Credentials for the source domain, if specified. public void AddSidHistory( string sourceDomain, string sourcePrincipal, string destinationDomain, string destinationPrincipal, string sourceDomainController = null, NetworkCredential sourceCredential = null) { this.AddSidHistory( sourceDomain: sourceDomain, sourcePrincipal: sourcePrincipal, sourceDomainController: sourceDomainController, sourceCredential: sourceCredential, destinationDomain: destinationDomain, destinationPrincipal: destinationPrincipal, flags: AddSidHistoryOptions.None); } /// /// Replicates the entire schema partition. /// /// Optional progress reporter invoked after each replication cycle. /// Token used to cooperatively cancel the replication between cycles. public void FetchFullSchema(IProgress progress = null, CancellationToken cancellationToken = default) { if (_isFullSchemaLoaded) { // Full schema only needs to be replicated once. return; } // Create a blank schema representation ReplicationSchema schema = new(); // Replicate the entire schema partition ReplicationCookie currentCookie = new(SchemaNamingContext); ReplicationResult result; int processedObjectCount = 0; do { // Check for cancellation between replication cycles (the native DRS call itself is not interruptible). cancellationToken.ThrowIfCancellationRequested(); // Perform one replication cycle result = this._drsConnection.ReplicateAllObjects(currentCookie); // Report replication progress processedObjectCount += result.Objects.Count; progress?.Report(new ReplicationProgress(result.Cookie, processedObjectCount, result.TotalObjectCount)); // Merge the prefix tables if (result.PrefixTable != null) { schema.PrefixTable.Add(result.PrefixTable); } // Try to add the object to the schema if it is an attribute or class definition foreach (var schemaObject in result.Objects) { schema.AddSchemaObject(schemaObject); } // Update the position of the replication cursor currentCookie = result.Cookie; } while (result.HasMoreData); _drsConnection.UpdateSchemaCache(schema); _isFullSchemaLoaded = true; } /// /// Releases all resources used by the . /// public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!disposing) { return; } if (this._drsConnection != null) { if (this._sessionKeyChangedHandler != null) { this._drsConnection.SessionKeyChanged -= this._sessionKeyChangedHandler; this._sessionKeyChangedHandler = null; } this._drsConnection.Dispose(); this._drsConnection = null; } if (this._rpcBinding != null) { this._rpcBinding.Dispose(); this._rpcBinding = null; } } /// /// Loads the domain naming context, DNS domain name, and NetBIOS domain name of the connected server. /// private (string DomainNamingContext, string DNSDomainName, string NetBIOSDomainName) LoadDomainInfo() { // These is no direct way of retrieving current DC's domain info, so we are using a combination of 3 calls. // We first retrieve FSMO roles. The PDC emulator lies in the same domain as the current server. var fsmoRoles = _drsConnection.ListRoles(); // We need the DC object of the PDC Emulator. It is the parent of the NTDS Settings object. string pdcEmulator = new DistinguishedName(fsmoRoles.PdcEmulator).Parent.ToString(); // Get the PDC account object from the domain partition. var pdcInfo = _drsConnection.ListInfoForServer(pdcEmulator); string pdcAccountDN = pdcInfo.ServerReference; // Get the PDC Emulator's domain naming context. string domainNamingContext = new DistinguishedName(pdcAccountDN).RootNamingContext.ToString(); string dnsDomainName = new DistinguishedName(domainNamingContext).GetDnsName(); // Get the PDC Emulator's NetBIOS account name and extract the domain part. NTAccount pdcAccount = _drsConnection.ResolveAccountName(pdcAccountDN); string netBIOSDomainName = pdcAccount.NetBIOSDomainName(); return (domainNamingContext, dnsDomainName, netBIOSDomainName); } #region IKdsRootKeyResolver /// /// Gets the KDS root key with the specified identifier. /// /// The identifier of the KDS root key. /// The KDS root key, or null if not found. KdsRootKey? IKdsRootKeyResolver.GetKdsRootKey(Guid id) => this.GetKdsRootKey(id, suppressNotFoundException: true); /// /// Gets a value indicating whether the resolver supports looking up all root keys. /// bool IKdsRootKeyResolver.SupportsLookupAll => false; /// /// Gets a value indicating whether the resolver supports looking up root keys by effective time. /// bool IKdsRootKeyResolver.SupportsLookupByEffectiveTime => false; /// /// Gets the KDS root key that was effective at the specified time. /// /// Search by effective time is not supported by the MS-DRSR protocol. KdsRootKey? IKdsRootKeyResolver.GetKdsRootKey(DateTime effectiveTime) => throw new NotSupportedException("Search by effective time is not supported by the MS-DRSR protocol."); /// /// Gets all KDS root keys. /// /// Search by class type is not supported by the MS-DRSR protocol. IEnumerable IKdsRootKeyResolver.GetKdsRootKeys() => throw new NotSupportedException("Search by class type is not supported by the MS-DRSR protocol."); #endregion IKdsRootKeyResolver }