Skip to content

Instantly share code, notes, and snippets.

@FriedrichWeinmann
Created November 7, 2023 16:22
Show Gist options
  • Save FriedrichWeinmann/e3443c586eb872aa95f0b883a16505e7 to your computer and use it in GitHub Desktop.
Save FriedrichWeinmann/e3443c586eb872aa95f0b883a16505e7 to your computer and use it in GitHub Desktop.
Copies the SID from a principal in one domain into the SID history of a principal in another.
function Import-ADPrincipalSID {
<#
.SYNOPSIS
Copies the SID from a principal in one domain into the SID history of a principal in another.
.DESCRIPTION
Copies the SID from a principal in one domain into the SID history of a principal in another.
In order for this to work, some prerequisites must be met:
- The credentials used for both sides must be direct members of the Domain Admins group. No "Equivalent permissions" or anything like that.
- There must be a trust between the domains, at least the source domain must trust the destination domain.
- The principal being migrated must be of the same type (user to user, domain-local group to domain-local group, ...)
- The DCs in both domains must have "Account Management" auditing enabled
- The source domain must have a group named "<sourcedomain NetBIOSName>$$$". E.g.: "CONTOSO$$$"
- The destination Domain must be able to reach the source domain.
This tool uses PowerShell remoting to connect to the destination DC to execute its task (as it must be executed on a domain controller).
This defaults to the PDC Emulator if a domain name is offered to the -Server parameter.
.PARAMETER Server
The destination domain (or a DC from it)
.PARAMETER FromServer
The source domain (or a DC from it)
.PARAMETER FromCredential
Credentials to use to connect to the source domain.
Must be a direct Domain Admins member in the source domain.
.PARAMETER Credential
Credentials to use to connect to the destination domain.
Must be a direct Domain Admins member in the destination domain.
Uses the current account by default (which then must be a domain admin)
.PARAMETER Identity
Sam Account Name of the principal (user/group/...) in the destination domain.
.PARAMETER OldIdentity
Sam Account Name of the principal (user/group/...) in the source domain.
.EXAMPLE
PS C:\> Import-ADPrincipalSID -Server contoso.com -FromServer fabrikam.org -FromCredential $cred -Identity mm -OldIdentity mm
Migrates the SID & SID History of the account "mm" from fabrikam.org to contoso.com
.EXAMPLE
PS C:\> Import-Csv .\users-to-migrate.csv | Import-ADPrincipalSID -Server contoso.com -FromServer fabrikam.org -FromCredential $cred
Migrates all users in "users-to-migrate.csv" from fabrikam.org to contoso.com.
The CSV must have two columns, "Identity" and "OldIdentity".
.EXAMPLE
PS C:\> Get-Content .\users-to-migrate.txt | Import-ADPrincipalSID -Server contoso.com -FromServer fabrikam.org -FromCredential $cred
Migrates all users in "users-to-migrate.txt" from fabrikam.org to contoso.com.
All users in the text file must have the same SAMAccountName in both domains.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]
$Server,
[Parameter(Mandatory = $true)]
[string]
$FromServer,
[Parameter(Mandatory = $true)]
[PSCredential]
$FromCredential,
[PSCredential]
$Credential,
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string]
$Identity,
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string]
$OldIdentity
)
begin {
#region Remote Scriptblocks
$sourceCode = {
#region Code
$source = @'
using System;
using System.ComponentModel;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace DSApi {
public static class Native
{
[DllImport("Ntdsapi.dll", SetLastError = true)]
public static extern int DsBind(
string DomainControllerName,
string DnsDomainName,
out IntPtr Connection
);
[DllImport("Ntdsapi.dll", SetLastError = true)]
public static extern int DsUnBind(
IntPtr Connection
);
[DllImport("Ntdsapi.dll", SetLastError = true)]
public static extern int DsBindWithCred(
string DomainControllerName,
string DnsDomainName,
IntPtr AuthHandle,
out IntPtr Connection
);
[DllImport("Ntdsapi.dll", SetLastError = true)]
public static extern int DsMakePasswordCredentials(
string User,
string Domain,
string Password,
out IntPtr AuthHandle
);
[DllImport("Ntdsapi.dll", SetLastError = true)]
public static extern int DsFreePasswordCredentials(
IntPtr AuthHandle
);
[DllImport("Ntdsapi.dll", SetLastError = true)]
public static extern int DsAddSidHistory(
IntPtr SessionHandle,
uint Flags,
string SrcDomain,
string SrcPrincipal,
string SrcDomainController,
IntPtr SrcCredentialHandle,
string DstDomain,
string DstPrincipal
);
}
public class DSSession : IDisposable
{
public PSCredential Credential;
private IntPtr _Session;
private IntPtr _Credential;
public void Authenticate()
{
if (null == Credential)
throw new ArgumentException("No Credentials provided to authenticate with!");
if (IntPtr.Zero != _Session)
Native.DsUnBind(_Session);
if (IntPtr.Zero != _Credential)
Native.DsFreePasswordCredentials(_Credential);
string user = Credential.UserName;
string domain = Environment.GetEnvironmentVariable("UserDNSDomain");
if (Regex.IsMatch(user, "^\\w+\\\\\\w+$"))
{
string[] parts = user.Split('\\');
domain = parts[0];
user = parts[1];
}
int result = Native.DsMakePasswordCredentials(user, domain, Credential.GetNetworkCredential().Password, out _Credential);
if (result != 0)
throw new Win32Exception(result);
}
public void Connect(string DnsDomainName, string DomainControllerName)
{
if (_Session != IntPtr.Zero)
Native.DsUnBind(_Session);
int result = Native.DsBindWithCred(DomainControllerName, DnsDomainName, _Credential, out _Session);
if (result != 0)
throw new Win32Exception(result);
}
public IntPtr GetSessionPointer()
{
return _Session;
}
public IntPtr GetCredentialPointer()
{
return _Credential;
}
public void Dispose()
{
if (IntPtr.Zero != _Session)
Native.DsUnBind(_Session);
if (IntPtr.Zero != _Credential)
Native.DsFreePasswordCredentials(_Credential);
}
}
public static class DirectoryTools
{
public static void ImportSIDHistory(DSSession SourceSession, DSSession DestinationSession, string SourceDomain, string SourceDC, string SourcePrincipal, string DestinationDomain, string DestinationPrincipal)
{
int result = Native.DsAddSidHistory(
DestinationSession.GetSessionPointer(),
0,
SourceDomain,
SourcePrincipal,
SourceDC,
SourceSession.GetCredentialPointer(),
DestinationDomain,
DestinationPrincipal
);
if (result != 0)
throw new Win32Exception(result);
}
}
}
'@
Add-Type -TypeDefinition $source
#endregion Code
}
$code = {
param (
$FromDomain,
$FromServer,
$ToDomain,
$FromIdentity,
$ToIdentity
)
$result = [PSCustomObject]@{
FromDomain = $FromDomain
ToDomain = $ToDomain
FromIdentity = $FromIdentity
ToIdentity = $ToIdentity
Success = $true
Message = ''
Error = $null
}
try {
$srcSession = [DSApi.DSSession]::new()
$srcSession.Credential = $fromCred
$srcSession.Authenticate()
}
catch {
$result.Success = $false
$result.Error = $_
$result.Message = 'Error connecting to source domain. Ensure the credentials provided are valid and the domain can be reached!'
$result
return
}
$dstSession = [DSApi.DSSession]::new()
$dstSession.Connect($ToDomain, "")
try { [DSApi.DirectoryTools]::ImportSIDHistory($srcSession, $dstSession, $FromDomain, $FromServer, $FromIdentity, $ToDomain, $ToIdentity) }
catch {
$result.Success = $false
$result.Error = $_
$result.Message = 'Failed to perform SID History import. Ensure both accounts are direct members in the domain admins, both identities are of the same type, both domains have "Account Management" auditing enabled for their domain controllers and the source domain has a group named <NetBIOSDomainName>$$$ (e.g.: "CONTOSO$$$").'
}
$srcSession.Dispose()
$dstSession.Dispose()
$result
}
#endregion Remote Scriptblocks
#region Resolve Domains, Servers and perform prep
# Target Domain & Server
$param = @{ Server = $Server }
if ($Credential) { $param.Credential = $Credential }
$domain = Get-ADDomain @param
if ($domain.DnsRoot -eq $Server) { $Server = $domain.PdcEmulator }
# Source Domain & Server
$oldParam = @{ Server = $FromServer }
if ($FromCredential) { $oldParam.Credential = $FromCredential }
$oldDomain = Get-ADDomain @oldParam
$oldServer = $FromServer
if ($FromServer -in $oldDomain.DnsRoot, $oldDomain.NetBIOSDomainName) { $oldServer = $oldDomain.PDCEmulator }
# PSRemoting Session
$remoteParam = @{ ComputerName = $Server }
if ($Credential) { $remoteParam.Credential = $Credential }
try { $pssession = New-PSSession @remoteParam -ErrorAction Stop }
catch {
Write-Warning "Failed to connect to destination domain controller $Server : $_"
throw
}
Invoke-Command -Session $pssession -ScriptBlock $sourceCode
Invoke-Command -Session $pssession -ScriptBlock { $script:fromCred = $using:FromCredential }
#endregion Resolve Domains, Servers and perform prep
}
process {
Invoke-Command -Session $pssession -ScriptBlock $code -ArgumentList @(
$oldDomain.DnsRoot
$oldServer
$domain.DnsRoot
$OldIdentity
$Identity
)
}
end {
Remove-PSSession -Session $pssession
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment