From 4001c11fe46a764725d7d987cc117db4dff8853d Mon Sep 17 00:00:00 2001 From: Vincent LE TOUX Date: Sat, 9 May 2020 12:38:08 +0200 Subject: [PATCH] Added a bot mode to PingCastle --- Bot/Bot.cs | 220 ++++++++++++++++++ Bot/BotData.cs | 27 +++ Bot/BotStream.cs | 36 +++ .../ExportDataFromActiveDirectoryLive.cs | 56 +++-- PingCastle.csproj | 3 + Program.cs | 17 +- Tasks.cs | 14 +- 7 files changed, 358 insertions(+), 15 deletions(-) create mode 100644 Bot/Bot.cs create mode 100644 Bot/BotData.cs create mode 100644 Bot/BotStream.cs diff --git a/Bot/Bot.cs b/Bot/Bot.cs new file mode 100644 index 0000000..634fccc --- /dev/null +++ b/Bot/Bot.cs @@ -0,0 +1,220 @@ +using PingCastle.Data; +using PingCastle.Healthcheck; +using PingCastle.Rules; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Serialization; + +namespace PingCastle.Bot +{ + + + public class Bot + { + public void Run(string pipeName) + { + BotInputOutput input; + bool stop = false; + + XmlSerializer xs = new XmlSerializer(typeof(BotInputOutput)); + Console.WriteLine("Bot: hello"); + using (var pipe = BotStream.OpenPipeStream(pipeName)) + { + while (!stop) + { + try + { + var buffer = new byte[4]; + int read = pipe.Read(buffer, 0, 4); + if (read < 4) + return; + int count = BitConverter.ToInt32(buffer, 0); + var data = new byte[count]; + read = 0; + while (read < count) + { + int r = pipe.Read(data, read, count - read); + if (r == 0) + { + Console.WriteLine("Pipe shutdown"); + return; + } + read += r; + } + Console.WriteLine("Bot: message received"); + using (var ms = new MemoryStream(data)) + { + input = (BotInputOutput)xs.Deserialize(ms); + } + } + catch (Exception ex) + { + Console.WriteLine("Exception when reading the input " + ex.Message); + Console.WriteLine("StackTrace:" + ex.StackTrace); + return; + } + + + BotInputOutput output; + string order = GetItem(input, "Command"); + try + { + switch (order) + { + case "healthcheck": + output = RunHealthCheck(input); + break; + case "tohtml": + output = ToHtml(input); + break; + case "shutdown": + stop = true; + output = new BotInputOutput(); + output.Data = new List(); + AddData(output, "Status", "shutdown"); + break; + case null: + output = ExceptionOutput("Command not found"); + break; + default: + output = ExceptionOutput("Invalid command " + order); + break; + } + } + catch (Exception ex) + { + output = ExceptionOutput("Exception during the job " + ex.Message, ex.StackTrace); + Console.WriteLine("Exception:" + ex.Message); + Console.WriteLine("StackTrace:" + ex.StackTrace); + } + + Console.WriteLine("Writing data"); + + using (var ms = new MemoryStream()) + using (XmlWriter writer = XmlWriter.Create(ms)) + { + xs.Serialize(writer, output); + ms.Position = 0; + var buffer = ms.GetBuffer(); + var t = BitConverter.GetBytes((int)ms.Length); + pipe.Write(t, 0, 4); + pipe.Write(buffer, 0, (int)ms.Length); + Console.WriteLine("Bot: message sent"); + } + } + } + Console.WriteLine("Exiting"); + } + + private string GetItem(BotInputOutput input, string key) + { + foreach(var k in input.Data) + { + if (k.Key == key) + return k.Value; + } + return null; + } + + private BotInputOutput ExceptionOutput(string message, string stacktrace = null) + { + var o = new BotInputOutput(); + o.Data = new List(); + AddData(o, "Status", "Error"); + AddData(o, "Error", message); + if (!string.IsNullOrEmpty(stacktrace)) + { + AddData(o, "StackTrace", stacktrace); + } + return o; + } + + private BotInputOutput RunHealthCheck(BotInputOutput input) + { + try + { + var analyze = new HealthcheckAnalyzer(); + var parameters = new PingCastleAnalyzerParameters(); + parameters.Server = GetItem(input, "Server"); + var login = GetItem(input, "Login"); + var password = GetItem(input, "Password"); + if (!string.IsNullOrEmpty(login) && !string.IsNullOrEmpty(password)) + parameters.Credential = new System.Net.NetworkCredential(login, password); + var port = GetItem(input, "Port"); + if (!string.IsNullOrEmpty(port)) + parameters.Port = int.Parse(port); + var healthcheck = analyze.PerformAnalyze(parameters); + + var o = new BotInputOutput(); + o.Data = new List(); + AddData(o, "Status", "OK"); + AddData(o, "Target", parameters.Server); + + int riskId = 0; + foreach(var risk in healthcheck.RiskRules) + { + riskId++; + var rule = RuleSet.GetRuleFromID(risk.RiskId); + AddData(o, "Rationale_" + riskId, risk.Rationale); + AddData(o, "Title_" + riskId, rule.Title); + AddData(o, "Solution_" + riskId, rule.Solution); + AddData(o, "Points_" + riskId, risk.Points.ToString()); + AddData(o, "Documentation_" + riskId, rule.Documentation); + AddData(o, "TechnicalExplanation_" + riskId, rule.TechnicalExplanation); + foreach(var d in rule.Details) + { + AddData(o, "Detail_" + riskId, d); + } + } + + healthcheck.SetExportLevel(PingCastleReportDataExportLevel.Full); + var xmlreport = DataHelper.SaveAsXml(healthcheck, null, false); + AddData(o, "Report", xmlreport); + + return o; + } + catch (Exception ex) + { + Console.WriteLine("Exception:" + ex.Message); + Console.WriteLine("StackTrace:" + ex.StackTrace); + return ExceptionOutput("Exception during the healthcheck " + ex.Message, ex.StackTrace); + } + } + + private BotInputOutput ToHtml(BotInputOutput input) + { + try + { + var xml = GetItem(input, "Report"); + using (var ms = new MemoryStream(UnicodeEncoding.UTF8.GetBytes(xml))) + { + HealthcheckData healthcheckData = DataHelper.LoadXml(ms, "bot", null); + var endUserReportGenerator = PingCastleFactory.GetEndUserReportGenerator(); + var license = LicenseManager.Validate(typeof(Program), new Program()) as ADHealthCheckingLicense; + var report = endUserReportGenerator.GenerateReportFile(healthcheckData, license, healthcheckData.GetHumanReadableFileName()); + + var o = new BotInputOutput(); + o.Data = new List(); + AddData(o, "Status", "OK"); + AddData(o, "Report", report); + return o; + } + } + catch (Exception ex) + { + Console.WriteLine("Exception:" + ex.Message); + Console.WriteLine("StackTrace:" + ex.StackTrace); + return ExceptionOutput("Exception during the job " + ex.Message, ex.StackTrace); + } + } + + private static void AddData(BotInputOutput o, string key, string value) + { + o.Data.Add(new BotData(key, value)); + } + } +} diff --git a/Bot/BotData.cs b/Bot/BotData.cs new file mode 100644 index 0000000..9a5da4c --- /dev/null +++ b/Bot/BotData.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PingCastle.Bot +{ + public class BotInputOutput + { + public List Data { get; set; } + + } + public class BotData + { + public BotData() + { + + } + public BotData(string Key, string Value) : this() + { + this.Key = Key; + this.Value = Value; + } + + public string Key { get; set; } + public string Value { get; set; } + } +} diff --git a/Bot/BotStream.cs b/Bot/BotStream.cs new file mode 100644 index 0000000..124aaa3 --- /dev/null +++ b/Bot/BotStream.cs @@ -0,0 +1,36 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace PingCastle.Bot +{ + internal class BotStream + { + [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] + private static extern IntPtr CreateFile(String lpFileName, + UInt32 dwDesiredAccess, UInt32 dwShareMode, + IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, + UInt32 dwFlagsAndAttributes, + IntPtr hTemplateFile); + + + private const UInt32 GENERIC_READ = 0x80000000; + private const UInt32 GENERIC_WRITE = 0x40000000; + private const UInt32 OPEN_EXISTING = 3; + + public static FileStream OpenPipeStream(string pipeName) + { + Console.WriteLine("Opening pipe :" + pipeName); + IntPtr p = CreateFile(@"\\.\pipe\" + pipeName, GENERIC_READ + GENERIC_WRITE, 0, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero); + if (p.ToInt32() == -1) + { + throw new Win32Exception(); + } + return new FileStream(new Microsoft.Win32.SafeHandles.SafeFileHandle(p, true), FileAccess.ReadWrite); + } + } +} diff --git a/Graph/Export/ExportDataFromActiveDirectoryLive.cs b/Graph/Export/ExportDataFromActiveDirectoryLive.cs index 25d3591..1244a46 100644 --- a/Graph/Export/ExportDataFromActiveDirectoryLive.cs +++ b/Graph/Export/ExportDataFromActiveDirectoryLive.cs @@ -84,10 +84,36 @@ public GraphObjectReference ExportData(List UsersToInvestigate) Storage.InsertRelationOnHold(); Trace.WriteLine("Done"); DisplayAdvancement("- Export completed"); + DumpObjectReferenceOnTrace(); return objectReference; } - private void BuildDirectDelegationData() + private void DumpObjectReferenceOnTrace() + { + Trace.WriteLine("============================"); + Trace.WriteLine("Dump graph"); + Trace.WriteLine("============================"); + var s = (LiveDataStorage)Storage; + foreach(var id in s.nodes.Keys) + { + var node = s.nodes[id]; + if (s.relations.ContainsKey(id)) + { + var relations = s.relations[id]; + foreach(var rid in relations.Keys) + { + Trace.WriteLine(node.Name + " -> " + s.nodes[rid].Name + " [" + string.Join(",", relations[rid].Hint.ToArray()) + "]"); + } + } + else + { + Trace.WriteLine(node.Name + " -> "); + } + } + Trace.WriteLine("============================"); + } + + private void BuildDirectDelegationData() { if (domainInfo.ForestFunctionality < 2) return; @@ -136,24 +162,28 @@ private void ExportReportData(GraphObjectReference objectReference, List objectReference.Objects[typology].Remove(obj); } } - foreach (string user in UsersToInvestigate) + if (UsersToInvestigate != null) { - Trace.WriteLine("Working on " + user); - aditems = Search(user); - if (aditems.Count != 0) + foreach (string user in UsersToInvestigate) { - string userKey = user; - if (aditems[0].ObjectSid != null) + Trace.WriteLine("Working on " + user); + aditems = Search(user); + if (aditems.Count != 0) { - userKey = aditems[0].ObjectSid.Value; + string userKey = user; + if (aditems[0].ObjectSid != null) + { + userKey = aditems[0].ObjectSid.Value; + } + objectReference.Objects[Data.CompromiseGraphDataTypology.UserDefined].Add(new GraphSingleObject(userKey, user)); + RelationFactory.AnalyzeADObject(aditems[0]); + } + else + { + Trace.WriteLine("Unable to find the user: " + user); } - objectReference.Objects[Data.CompromiseGraphDataTypology.UserDefined].Add(new GraphSingleObject(userKey, user)); - RelationFactory.AnalyzeADObject(aditems[0]); } - else - Trace.WriteLine("Unable to find the user: " + user); } - AnalyzeMissingObjets(); } diff --git a/PingCastle.csproj b/PingCastle.csproj index 3a74593..4df8001 100644 --- a/PingCastle.csproj +++ b/PingCastle.csproj @@ -90,6 +90,9 @@ + + + diff --git a/Program.cs b/Program.cs index e3fd696..1bf3cfa 100644 --- a/Program.cs +++ b/Program.cs @@ -42,6 +42,7 @@ public class Program : IPingCastleLicenseInfo private bool PerformHealthCheckReloadReport; bool PerformHealthCheckGenerateDemoReports; bool PerformScanner = false; + bool PerformBot = false; Tasks tasks = new Tasks(); @@ -166,6 +167,10 @@ private void Run(string[] args) { if (!tasks.CartoTask(PerformHealthCheckGenerateDemoReports)) return; } + if (PerformBot) + { + if (!tasks.BotTask()) return; + } if (PerformHealthCheckReport) { if (!tasks.AnalysisTask()) return; @@ -311,6 +316,15 @@ private bool ParseCommandLine(string[] args) } tasks.apiKey = args[++i]; break; + case "--bot": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --bot is mandatory"); + return false; + } + tasks.botPipe = args[++i]; + PerformBot = true; + break; case "--carto": PerformCarto = true; break; @@ -689,7 +703,8 @@ private bool ParseCommandLine(string[] args) && !PerformScanner && !PerformGenerateKey && !PerformHealthCheckGenerateDemoReports && !PerformCarto && !PerformUploadAllReport - && !PerformHCRules) + && !PerformHCRules + && !PerformBot) { WriteInRed("You must choose at least one value among --healthcheck --hc-conso --advanced-export --advanced-report --nullsession --carto"); DisplayHelp(); diff --git a/Tasks.cs b/Tasks.cs index 31a4af0..35e4c01 100644 --- a/Tasks.cs +++ b/Tasks.cs @@ -61,6 +61,7 @@ public class Tasks public string apiEndpoint; public string apiKey; public bool AnalyzeReachableDomains; + public string botPipe; Dictionary xmlreports = new Dictionary(); Dictionary htmlreports = new Dictionary(); @@ -224,7 +225,7 @@ public bool CompleteTasks() return true; } - public bool AnalysisCheckTask(string server) + public bool AnalysisCheckTask(string server) { return true; } @@ -771,6 +772,17 @@ void UploadToWebsite(string filename, string filecontent) ); } + public bool BotTask() + { + return StartTask("Running Bot", + () => + { + var bot = new PingCastle.Bot.Bot(); + bot.Run(botPipe); + } + ); + } + // function used to encapsulate a task and to fail gracefully with an error message // return true is success; false in cas of failure delegate void TaskDelegate();