diff --git a/README.md b/README.md index 490b4c0e..e6355b9c 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ The key incantations are: `-a` Skips file enumeration, just gives you a list of listable shares on the target hosts. +`-g` Skips file enumeration, just gives you a list of shares and folders on the target hosts. (combine with `-w` for the most useful results) + `-u` Makes Snaffler pull a list of account names from AD, choose the ones that look most-interesting, and then use them in a search rule. `-d` Domain to search for computers to search for shares on to search for files in. Easy. @@ -74,12 +76,16 @@ The key incantations are: `-z` Path to a config file that defines all of the above, and much much more! See below for more details. Give it `-z generate` to generate a sample config file called `.\default.toml`. -`-t` Type of log you would like to output. Currently supported options are plain and JSON. Defaults to plain. +`-t` Type of log you would like to output. Currently supported options are plain, JSON, and HTML. Defaults to plain. `-x` Max number of threads to use. Don't set it below 4 or shit will break. `-p` Path to a directory full of .toml formatted rules. Snaffler will load all of these in place of the default ruleset. +`-w` Log everything (currently just logs directories walked but not marked for snaffling) + +`-q` Disable coloring of console output + ## What does any of this log output mean? Hopefully this annotated example will help: diff --git a/SnaffCore/Classifiers/DirClassifier.cs b/SnaffCore/Classifiers/DirClassifier.cs index 34eb5fd2..564a1484 100644 --- a/SnaffCore/Classifiers/DirClassifier.cs +++ b/SnaffCore/Classifiers/DirClassifier.cs @@ -1,4 +1,7 @@ -using SnaffCore.Concurrency; +using System.IO; +using SnaffCore.Classifiers.EffectiveAccess; +using SnaffCore.Concurrency; +using static SnaffCore.Config.Options; namespace SnaffCore.Classifiers { @@ -20,6 +23,11 @@ public DirResult ClassifyDir(string dir) Triage = ClassifierRule.Triage, ScanDir = true, }; + + DirectoryInfo dirInfo = new DirectoryInfo(dir); + EffectivePermissions effPerms = new EffectivePermissions(MyOptions.CurrentUser); + dirResult.RwStatus = effPerms.CanRw(dirInfo); + // check if it matches TextClassifier textClassifier = new TextClassifier(ClassifierRule); TextResult textResult = textClassifier.TextMatch(dir); @@ -30,6 +38,10 @@ public DirResult ClassifyDir(string dir) { case MatchAction.Discard: dirResult.ScanDir = false; + if (MyOptions.LogEverything) + { + Mq.DirResult(dirResult); + } return dirResult; case MatchAction.Snaffle: dirResult.Triage = ClassifierRule.Triage; @@ -40,6 +52,12 @@ public DirResult ClassifyDir(string dir) return null; } } + + if (MyOptions.LogEverything) + { + Mq.DirResult(dirResult); + } + return dirResult; } } @@ -48,6 +66,7 @@ public class DirResult { public bool ScanDir { get; set; } public string DirPath { get; set; } + public RwStatus RwStatus { get; set; } public Triage Triage { get; set; } } } diff --git a/SnaffCore/Classifiers/EffectiveAccess.cs b/SnaffCore/Classifiers/EffectiveAccess.cs index 834f572a..af6b95f5 100644 --- a/SnaffCore/Classifiers/EffectiveAccess.cs +++ b/SnaffCore/Classifiers/EffectiveAccess.cs @@ -1,5 +1,10 @@  +using System; +using System.IO; +using System.Security.AccessControl; +using System.Security.Principal; + namespace SnaffCore.Classifiers.EffectiveAccess { public class RwStatus @@ -7,6 +12,84 @@ public class RwStatus public bool CanRead { get; set; } public bool CanWrite { get; set; } public bool CanModify { get; set; } + + public override string ToString() + { + char[] rwChars = { '-', '-', '-' }; + + if (CanRead) rwChars[0] = 'R'; + if (CanWrite) rwChars[1] = 'W'; + if (CanModify) rwChars[2] = 'M'; + + return new string(rwChars); + } } + public class EffectivePermissions + { + private readonly string _username; + + public EffectivePermissions(string username) + { + _username = username; + } + + public RwStatus CanRw(AuthorizationRuleCollection acl) + { + RwStatus rwStatus = new RwStatus(); + + try + { + foreach (FileSystemAccessRule rule in acl) + { + if (rule.IdentityReference.Value.Equals(_username, StringComparison.OrdinalIgnoreCase)) + { + if ((rwStatus.CanRead != true) && (((rule.FileSystemRights & FileSystemRights.Read) == FileSystemRights.Read) || + ((rule.FileSystemRights & FileSystemRights.ReadAndExecute) == FileSystemRights.ReadAndExecute) || + ((rule.FileSystemRights & FileSystemRights.ReadData) == FileSystemRights.ReadData) || + ((rule.FileSystemRights & FileSystemRights.ListDirectory) == FileSystemRights.ListDirectory))) + { + rwStatus.CanRead = true; + } + if ((rwStatus.CanWrite != true) && (((rule.FileSystemRights & FileSystemRights.Write) == FileSystemRights.Write) || + ((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || + ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || + ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || + ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions) || + ((rule.FileSystemRights & FileSystemRights.AppendData) == FileSystemRights.AppendData) || + ((rule.FileSystemRights & FileSystemRights.WriteData) == FileSystemRights.WriteData) || + ((rule.FileSystemRights & FileSystemRights.CreateFiles) == FileSystemRights.CreateFiles) || + ((rule.FileSystemRights & FileSystemRights.CreateDirectories) == FileSystemRights.CreateDirectories))) + { + rwStatus.CanWrite = true; + } + if ((rwStatus.CanModify != true) && (((rule.FileSystemRights & FileSystemRights.Modify) == FileSystemRights.Modify) || + ((rule.FileSystemRights & FileSystemRights.FullControl) == FileSystemRights.FullControl) || + ((rule.FileSystemRights & FileSystemRights.TakeOwnership) == FileSystemRights.TakeOwnership) || + ((rule.FileSystemRights & FileSystemRights.ChangePermissions) == FileSystemRights.ChangePermissions))) + { + rwStatus.CanModify = true; + } + } + } + } + catch (UnauthorizedAccessException) { } + + return rwStatus; + } + + public RwStatus CanRw(FileInfo fileInfo) + { + FileSecurity fileSecurity = fileInfo.GetAccessControl(); + AuthorizationRuleCollection acl = fileSecurity.GetAccessRules(true, true, typeof(NTAccount)); + return CanRw(acl); + } + + public RwStatus CanRw(DirectoryInfo dirInfo) + { + DirectorySecurity dirSecurity = dirInfo.GetAccessControl(); + AuthorizationRuleCollection acl = dirSecurity.GetAccessRules(true, true, typeof(NTAccount)); + return CanRw(acl); + } + } } diff --git a/SnaffCore/Classifiers/FileResult.cs b/SnaffCore/Classifiers/FileResult.cs index 4865eea4..72752786 100644 --- a/SnaffCore/Classifiers/FileResult.cs +++ b/SnaffCore/Classifiers/FileResult.cs @@ -14,19 +14,8 @@ public class FileResult public FileResult(FileInfo fileInfo) { - //EffectivePermissions effPerms = new EffectivePermissions(MyOptions.CurrentUser); - - // get an aggressively simplified version of the file's ACL - //this.RwStatus = effPerms.CanRw(fileInfo); - try - { - File.OpenRead(fileInfo.FullName); - this.RwStatus = new RwStatus() { CanRead = true, CanModify = false, CanWrite = false }; - } - catch (Exception e) - { - this.RwStatus = new RwStatus() { CanModify = false, CanRead = false, CanWrite = false }; - } + EffectivePermissions effPerms = new EffectivePermissions(MyOptions.CurrentUser); + this.RwStatus = effPerms.CanRw(fileInfo); // nasty debug this.FileInfo = fileInfo; @@ -56,58 +45,5 @@ public void SnaffleFile(FileInfo fileInfo, string snafflePath) Directory.CreateDirectory(snaffleDirPath); File.Copy(sourcePath, (Path.Combine(snafflePath, cleanedPath)), true); } - - /* - public static EffectivePermissions.RwStatus CanRw(FileInfo fileInfo) - { - BlockingMq Mq = BlockingMq.GetMq(); - - try - { - EffectivePermissions.RwStatus rwStatus = new EffectivePermissions.RwStatus { CanWrite = false, CanRead = false, CanModify = false }; - EffectivePermissions effPerms = new EffectivePermissions(); - string dir = fileInfo.DirectoryName; - - // we hard code this otherwise it tries to do some madness where it uses RPC with a share server to check file access, then fails if you're not admin on that host. - string hostname = "localhost"; - - string whoami = WindowsIdentity.GetCurrent().Name; - - string[] accessStrings = effPerms.GetEffectivePermissions(fileInfo, whoami); - - string[] readRights = new string[] { "Read", "ReadAndExecute", "ReadData", "ListDirectory" }; - string[] writeRights = new string[] { "Write", "Modify", "FullControl", "TakeOwnership", "ChangePermissions", "AppendData", "WriteData", "CreateFiles", "CreateDirectories" }; - string[] modifyRights = new string[] { "Modify", "FullControl", "TakeOwnership", "ChangePermissions" }; - - foreach (string access in accessStrings) - { - if (access == "FullControl") - { - rwStatus.CanModify = true; - rwStatus.CanRead = true; - rwStatus.CanWrite = true; - } - if (readRights.Contains(access)){ - rwStatus.CanRead = true; - } - if (writeRights.Contains(access)) - { - rwStatus.CanWrite = true; - } - if (modifyRights.Contains(access)) - { - rwStatus.CanModify = true; - } - } - - return rwStatus; - } - catch (Exception e) - { - Mq.Error(e.ToString()); - return new EffectivePermissions.RwStatus { CanWrite = false, CanRead = false }; ; - } - } - */ } } \ No newline at end of file diff --git a/SnaffCore/Classifiers/ShareClassifier.cs b/SnaffCore/Classifiers/ShareClassifier.cs index 4e6e7d15..153d4eec 100644 --- a/SnaffCore/Classifiers/ShareClassifier.cs +++ b/SnaffCore/Classifiers/ShareClassifier.cs @@ -1,4 +1,5 @@ -using SnaffCore.Concurrency; +using SnaffCore.Classifiers.EffectiveAccess; +using SnaffCore.Concurrency; using System; using System.IO; using static SnaffCore.Config.Options; @@ -32,11 +33,18 @@ public bool ClassifyShare(string share) // in this context snaffle means 'send a report up the queue, and scan the share further' if (IsShareReadable(share)) { + // is this supposed to be here? + DirectoryInfo shareInfo = new DirectoryInfo(share); + + EffectivePermissions effPerms = new EffectivePermissions(MyOptions.CurrentUser); + RwStatus rwStatus = effPerms.CanRw(shareInfo); + ShareResult shareResult = new ShareResult() { Triage = ClassifierRule.Triage, Listable = true, - SharePath = share + SharePath = share, + RwStatus = rwStatus }; Mq.ShareResult(shareResult); } @@ -76,9 +84,7 @@ public class ShareResult public string SharePath { get; set; } public string ShareComment { get; set; } public bool Listable { get; set; } - public bool RootWritable { get; set; } - public bool RootReadable { get; set; } - public bool RootModifyable { get; set; } + public RwStatus RwStatus { get; set; } public Triage Triage { get; set; } = Triage.Gray; } } \ No newline at end of file diff --git a/SnaffCore/Concurrency/SnafflerMessage.cs b/SnaffCore/Concurrency/SnafflerMessage.cs index 943c70af..93b1b051 100644 --- a/SnaffCore/Concurrency/SnafflerMessage.cs +++ b/SnaffCore/Concurrency/SnafflerMessage.cs @@ -1,8 +1,20 @@ using SnaffCore.Classifiers; using System; +using System.ComponentModel; +using System.Text.RegularExpressions; +using static SnaffCore.Config.Options; namespace SnaffCore.Concurrency { + public struct SnafflerMessageComponents + { + public string DateTime; + public string Type; + public string Message; + public string Triage; + public string Permissions; + } + public class SnafflerMessage { public DateTime DateTime { get; set; } @@ -11,5 +23,211 @@ public class SnafflerMessage public FileResult FileResult { get; set; } public ShareResult ShareResult { get; set; } public DirResult DirResult { get; set; } + + private static int _typePaddingLength; + private static int _triagePaddingLength; + private static string _hostString; + + private static int GetTypePaddingLength() + { + if (_typePaddingLength == 0) + { + int typePaddingLength = 0; + var enumValues = Enum.GetValues(typeof(SnafflerMessageType)); + + foreach (var enumValue in enumValues) + { + var fieldInfo = typeof(SnafflerMessageType).GetField(enumValue.ToString()); + var descriptionAttribute = (DescriptionAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute)); + string readableName = descriptionAttribute == null ? enumValue.ToString() : descriptionAttribute.Description; + + if (readableName.Length > typePaddingLength) + { + typePaddingLength = readableName.Length; + } + } + + _typePaddingLength = typePaddingLength; + } + + return _typePaddingLength; + } + + private static int GetTriagePaddingLength() + { + if (_triagePaddingLength == 0) + { + int triagePaddingLength = 0; + var enumValues = Enum.GetValues(typeof(Triage)); + + foreach (var enumValue in enumValues) + { + var fieldInfo = typeof(Triage).GetField(enumValue.ToString()); + var descriptionAttribute = (DescriptionAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute)); + string readableName = descriptionAttribute == null ? enumValue.ToString() : descriptionAttribute.Description; + + if (readableName.Length > triagePaddingLength) + { + triagePaddingLength = readableName.Length; + } + } + + _triagePaddingLength = triagePaddingLength; + } + + return _triagePaddingLength; + } + + private static string GetHostString() + { + if (string.IsNullOrWhiteSpace(_hostString)) + { + _hostString = "[" + System.Security.Principal.WindowsIdentity.GetCurrent().Name + "@" + System.Net.Dns.GetHostName() + "]"; + } + + return _hostString; + } + + // get a more user friendly name for a type variant if provided + private string GetUserReadableType() + { + var typeVariantField = Type.GetType().GetField(Type.ToString()); + var typeVariantAttribute = (DescriptionAttribute)Attribute.GetCustomAttribute(typeVariantField, typeof(DescriptionAttribute)); + return typeVariantAttribute == null ? Type.ToString() : typeVariantAttribute.Description; + } + + private static String BytesToString(long byteCount) + { + string[] suf = { "B", "kB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB + if (byteCount == 0) + return "0" + suf[0]; + long bytes = Math.Abs(byteCount); + int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); + double num = Math.Round(bytes / Math.Pow(1024, place), 1); + return (Math.Sign(byteCount) * num) + suf[place]; + } + + public SnafflerMessageComponents ToStringComponents() + { + return ToStringComponents(true, true); + } + + public SnafflerMessageComponents ToStringComponents(bool includeTriage, bool includePermissions) + { + + // this is everything that stays the same between message types + string dateTime = String.Format("{1}{0}{2:u}", MyOptions.Separator, GetHostString(), DateTime.ToUniversalTime()); + string readableType = GetUserReadableType(); + string formattedMessage = Message; + + string triageString = ""; + string permissionsString = ""; + + switch (Type) + { + case SnafflerMessageType.FileResult: + string fileResultTemplate = MyOptions.LogTSV ? "{1}{0}{2}{0}{3}{0}{4}{0}{5}{0}{6}" : "{1}{0}<{2}|{3}|{4}|{5:u}>{0}{6}"; + + try + { + string matchedclassifier = FileResult.MatchedRule.RuleName; + DateTime modifiedStamp = FileResult.FileInfo.LastWriteTime.ToUniversalTime(); + + string matchedstring = ""; + + long fileSize = FileResult.FileInfo.Length; + + string fileSizeString; + + // TSV output will probably be machine-consumed. Don't pretty it up. + if (MyOptions.LogTSV) + { + fileSizeString = fileSize.ToString(); + } + else + { + fileSizeString = BytesToString(fileSize); + } + + string filepath = FileResult.FileInfo.FullName; + + string matchcontext = ""; + if (FileResult.TextResult != null) + { + matchedstring = FileResult.TextResult.MatchedStrings[0]; + matchcontext = FileResult.TextResult.MatchContext; + matchcontext = Regex.Replace(matchcontext, @"\r\n?|\n", "\\n"); // Replace newlines with \n for consistent log lines + } + + triageString = FileResult.MatchedRule.Triage.ToString(); + permissionsString = FileResult.RwStatus.ToString(); + + formattedMessage = string.Format(fileResultTemplate, MyOptions.Separator, filepath, matchedclassifier, matchedstring, fileSizeString, modifiedStamp, matchcontext); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + Console.WriteLine(FileResult.FileInfo.FullName); + } + break; + case SnafflerMessageType.DirResult: + triageString = DirResult.Triage.ToString(); + permissionsString = DirResult.RwStatus.ToString(); + formattedMessage = DirResult.DirPath; + break; + case SnafflerMessageType.ShareResult: + string shareResultTemplate = MyOptions.LogTSV ? "{1}{0}{2}" : "{1}{2}"; + + triageString = ShareResult.Triage.ToString(); + permissionsString = ShareResult.RwStatus.ToString(); + + // this lets us do conditional formatting, we don't want angled brackets around a blank comment + string shareComment = ShareResult.ShareComment; + if (ShareResult.ShareComment.Length > 0) { + shareComment = MyOptions.Separator + "<" + shareComment + ">"; + } + + formattedMessage = string.Format(shareResultTemplate, MyOptions.Separator, ShareResult.SharePath, shareComment); + break; + } + + return new SnafflerMessageComponents + { + DateTime = dateTime, + Type = readableType, + Message = formattedMessage, + Triage = triageString, + Permissions = permissionsString, + }; + } + + public static string StringFromComponents(SnafflerMessageComponents components) + { + return StringFromComponents(components, true, true); + } + + public static string StringFromComponents(SnafflerMessageComponents components, bool includeTriage, bool includePermissions) + { + string paddedType = ("[" + components.Type + "]"); + if (!MyOptions.LogTSV) paddedType = paddedType.PadRight(GetTypePaddingLength() + 2); + + string triageString = ""; + if (includeTriage && components.Triage.Length > 0) + { + triageString = ("[" + components.Triage + "]"); + if (!MyOptions.LogTSV) triageString = triageString.PadRight(GetTriagePaddingLength() + 2); + triageString += MyOptions.Separator; + } + + string permissionsString = (includePermissions && components.Permissions.Length > 0) ? String.Format(MyOptions.LogTSV ? "{1}{0}" : "[{1}]{0}", MyOptions.Separator, components.Permissions) : ""; + + return String.Format("{1}{0}{2}{0}{3}{4}{0}{5}", MyOptions.Separator, components.DateTime, paddedType, triageString, permissionsString, components.Message); + } + + public override string ToString() + { + SnafflerMessageComponents components = ToStringComponents(); + return SnafflerMessage.StringFromComponents(components); + } } } \ No newline at end of file diff --git a/SnaffCore/Concurrency/SnafflerMessageType.cs b/SnaffCore/Concurrency/SnafflerMessageType.cs index ac3c0a78..c70432b0 100644 --- a/SnaffCore/Concurrency/SnafflerMessageType.cs +++ b/SnaffCore/Concurrency/SnafflerMessageType.cs @@ -1,10 +1,15 @@ -namespace SnaffCore.Concurrency +using System.ComponentModel; + +namespace SnaffCore.Concurrency { public enum SnafflerMessageType { Error, + [Description("Share")] ShareResult, + [Description("Dir")] DirResult, + [Description("File")] FileResult, Finish, Info, diff --git a/SnaffCore/Config/Options.cs b/SnaffCore/Config/Options.cs index 78c66282..9016db80 100644 --- a/SnaffCore/Config/Options.cs +++ b/SnaffCore/Config/Options.cs @@ -7,7 +7,8 @@ namespace SnaffCore.Config public enum LogType { Plain = 0, - JSON = 1 + JSON = 1, + HTML = 2 } public partial class Options @@ -23,6 +24,9 @@ public partial class Options public bool ScanSysvol { get; set; } = true; public bool ScanNetlogon { get; set; } = true; public bool ScanFoundShares { get; set; } = true; + public bool ScanFoundFiles { get; set; } = true; + public bool LogEverything { get; set; } = false; + public bool NoColorLogs { get; set; } = false; public int InterestLevel { get; set; } = 0; public bool DfsOnly { get; set; } = false; public bool DfsShareDiscovery { get; set; } = false; diff --git a/SnaffCore/ShareFind/ShareFinder.cs b/SnaffCore/ShareFind/ShareFinder.cs index 4d1395c1..0732a765 100644 --- a/SnaffCore/ShareFind/ShareFinder.cs +++ b/SnaffCore/ShareFind/ShareFinder.cs @@ -19,7 +19,7 @@ public class ShareFinder private BlockingMq Mq { get; set; } private BlockingStaticTaskScheduler TreeTaskScheduler { get; set; } private TreeWalker TreeWalker { get; set; } - //private EffectivePermissions effectivePermissions { get; set; } = new EffectivePermissions(MyOptions.CurrentUser); + private EffectivePermissions EffectivePermissions { get; set; } = new EffectivePermissions(MyOptions.CurrentUser); public ShareFinder() { @@ -94,7 +94,8 @@ internal void GetComputerShares(string computer) { Listable = true, SharePath = shareName, - ShareComment = hostShareInfo.shi1_remark.ToString() + ShareComment = hostShareInfo.shi1_remark.ToString(), + RwStatus = new RwStatus() }; // Try to find this computer+share in the list of DFS targets @@ -159,26 +160,13 @@ internal void GetComputerShares(string computer) // Share is readable, report as green (the old default/min of the Triage enum ) shareResult.Triage = Triage.Green; - try - { - DirectoryInfo dirInfo = new DirectoryInfo(shareResult.SharePath); - - //EffectivePermissions.RwStatus rwStatus = effectivePermissions.CanRw(dirInfo); - - shareResult.RootModifyable = false; - shareResult.RootWritable = false; - shareResult.RootReadable = true; + DirectoryInfo dirInfo = new DirectoryInfo(shareResult.SharePath); + RwStatus rwStatus = EffectivePermissions.CanRw(dirInfo); + shareResult.RwStatus = rwStatus; - /* - if (rwStatus.CanWrite || rwStatus.CanModify) - { - triage = Triage.Yellow; - } - */ - } - catch (System.UnauthorizedAccessException e) + if (rwStatus.CanWrite || rwStatus.CanModify) { - Mq.Error("Failed to get permissions on " + shareResult.SharePath); + shareResult.Triage = Triage.Yellow; } if (MyOptions.ScanFoundShares) diff --git a/SnaffCore/TreeWalk/TreeWalker.cs b/SnaffCore/TreeWalk/TreeWalker.cs index c3cf3fc3..3be741e4 100644 --- a/SnaffCore/TreeWalk/TreeWalker.cs +++ b/SnaffCore/TreeWalk/TreeWalker.cs @@ -27,52 +27,55 @@ public void WalkTree(string currentDir) { // Walks a tree checking files and generating results as it goes. - if (!Directory.Exists(currentDir)) - { - return; - } + if (!Directory.Exists(currentDir)) + { + return; + } - try + if (MyOptions.ScanFoundFiles) { - string[] files = Directory.GetFiles(currentDir); - // check if we actually like the files - foreach (string file in files) + try { - FileTaskScheduler.New(() => + string[] files = Directory.GetFiles(currentDir); + // check if we actually like the files + foreach (string file in files) { - try + FileTaskScheduler.New(() => { - FileScanner.ScanFile(file); - } - catch (Exception e) - { - Mq.Error("Exception in FileScanner task for file " + file); - Mq.Trace(e.ToString()); - } - }); + try + { + FileScanner.ScanFile(file); + } + catch (Exception e) + { + Mq.Error("Exception in FileScanner task for file " + file); + Mq.Trace(e.ToString()); + } + }); + } + } + catch (UnauthorizedAccessException) + { + //Mq.Trace(e.ToString()); + //continue; + } + catch (DirectoryNotFoundException) + { + //Mq.Trace(e.ToString()); + //continue; + } + catch (IOException) + { + //Mq.Trace(e.ToString()); + //continue; + } + catch (Exception e) + { + Mq.Degub(e.ToString()); + //continue; } } - catch (UnauthorizedAccessException) - { - //Mq.Trace(e.ToString()); - //continue; - } - catch (DirectoryNotFoundException) - { - //Mq.Trace(e.ToString()); - //continue; - } - catch (IOException) - { - //Mq.Trace(e.ToString()); - //continue; - } - catch (Exception e) - { - Mq.Degub(e.ToString()); - //continue; - } - + try { string[] subDirs = Directory.GetDirectories(currentDir); diff --git a/Snaffler/Config.cs b/Snaffler/Config.cs index 0a7e9a57..b0fd4d8d 100644 --- a/Snaffler/Config.cs +++ b/Snaffler/Config.cs @@ -100,13 +100,19 @@ private static Options ParseImpl(string[] args) SwitchArgument dfsArg = new SwitchArgument('f', "dfs", "Limits Snaffler to finding file shares via DFS, for \"OPSEC\" reasons.", false); SwitchArgument findSharesOnlyArg = new SwitchArgument('a', "sharesonly", "Stops after finding shares, doesn't walk their filesystems.", false); + SwitchArgument findFoldersOnlyArg = new SwitchArgument('g', "foldersonly", + "Stops after finding folders, doesn't scan files.", false); + SwitchArgument logEverything = new SwitchArgument('w', "logeverything", + "Log everything.", false); + SwitchArgument noColorLogs = new SwitchArgument('q', "nocolor", + "Do not color logs.", false); ValueArgument compExclusionArg = new ValueArgument('k', "exclusions", "Path to a file containing a list of computers to exclude from scanning."); ValueArgument compTargetArg = new ValueArgument('n', "comptarget", "List of computers in a file(e.g C:\targets.txt), a single Computer (or comma separated list) to target."); ValueArgument ruleDirArg = new ValueArgument('p', "rulespath", "Path to a directory full of toml-formatted rules. Snaffler will load all of these in place of the default ruleset."); ValueArgument logType = new ValueArgument('t', "logtype", "Type of log you would like to output. Currently supported options are plain and JSON. Defaults to plain."); ValueArgument timeOutArg = new ValueArgument('e', "timeout", "Interval between status updates (in minutes) also acts as a timeout for AD data to be gathered via LDAP. Turn this knob up if you aren't getting any computers from AD when you run Snaffler through a proxy or other slow link. Default = 5"); - // list of letters i haven't used yet: gnqw + // list of letters i haven't used yet: CommandLineParser.CommandLineParser parser = new CommandLineParser.CommandLineParser(); parser.Arguments.Add(timeOutArg); @@ -127,6 +133,9 @@ private static Options ParseImpl(string[] args) parser.Arguments.Add(tsvArg); parser.Arguments.Add(dfsArg); parser.Arguments.Add(findSharesOnlyArg); + parser.Arguments.Add(findFoldersOnlyArg); + parser.Arguments.Add(logEverything); + parser.Arguments.Add(noColorLogs); parser.Arguments.Add(maxThreadsArg); parser.Arguments.Add(compTargetArg); parser.Arguments.Add(ruleDirArg); @@ -173,6 +182,10 @@ private static Options ParseImpl(string[] args) { parsedConfig.LogType = LogType.JSON; } + else if (logType.Value.ToLower() == "html") + { + parsedConfig.LogType = LogType.HTML; + } else { Mq.Info("Invalid type argument passed (" + logType.Value + ") defaulting to plaintext"); @@ -258,6 +271,21 @@ private static Options ParseImpl(string[] args) { parsedConfig.ScanFoundShares = false; } + if (findFoldersOnlyArg.Parsed) + { + parsedConfig.ScanFoundFiles = false; + } + + if (logEverything.Parsed) + { + parsedConfig.LogEverything = true; + } + + if (noColorLogs.Parsed) + { + parsedConfig.NoColorLogs = true; + } + if (maxThreadsArg.Parsed) { parsedConfig.MaxThreads = maxThreadsArg.Value; diff --git a/Snaffler/SnaffleRunner.cs b/Snaffler/SnaffleRunner.cs index 4d01882d..69ef8299 100644 --- a/Snaffler/SnaffleRunner.cs +++ b/Snaffler/SnaffleRunner.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using System.Text.RegularExpressions; using System.Threading; +using System.ComponentModel; +using System.Web; namespace Snaffler { @@ -21,26 +23,8 @@ public class SnaffleRunner private LogLevel LogLevel { get; set; } private Options Options { get; set; } - private string _hostString; - - private string fileResultTemplate { get; set; } - private string shareResultTemplate { get; set; } - private string dirResultTemplate { get; set; } - - private string hostString() - { - if (string.IsNullOrWhiteSpace(_hostString)) - { - _hostString = "[" + System.Security.Principal.WindowsIdentity.GetCurrent().Name + "@" + System.Net.Dns.GetHostName() + "]"; - } - - return _hostString; - } - public void Run(string[] args) { - // prime the hoststring lazy instantiator - hostString(); // print the thing PrintBanner(); // set up the message queue for operation @@ -60,27 +44,12 @@ public void Run(string[] args) return; } - // set up the TSV output if the flag is set - if (Options.LogTSV) - { - fileResultTemplate = "{0}" + Options.Separator + "{1}" + Options.Separator + "{2}" + Options.Separator + "{3}" + Options.Separator + "{4}" + Options.Separator + "{5}" + Options.Separator + "{6}" + Options.Separator + "{7:u}" + Options.Separator + "{8}" + Options.Separator + "{9}"; - shareResultTemplate = "{0}" + Options.Separator + "{1}" + Options.Separator + "{2}"; - dirResultTemplate = "{0}" + Options.Separator + "{1}"; - } - // otherwise just do the normal thing - else - { - // treat all as strings except LastWriteTime {6} - fileResultTemplate = "{{{0}}}<{1}|{2}{3}{4}|{5}|{6}|{7:u}>({8}) {9}"; - shareResultTemplate = "{{{0}}}<{1}>({2}) {3}"; - dirResultTemplate = "{{{0}}}({1})"; - } //------------------------------------------ // set up new fangled logging //------------------------------------------ LoggingConfiguration nlogConfig = new LoggingConfiguration(); nlogConfig.Variables["encoding"] = "utf8"; - ColoredConsoleTarget logconsole = null; + TargetWithLayoutHeaderAndFooter logconsole = null; FileTarget logfile = null; ParseLogLevelString(Options.LogLevelString); @@ -88,58 +57,54 @@ public void Run(string[] args) // Targets where to log to: File and Console if (Options.LogToConsole) { - logconsole = new ColoredConsoleTarget("logconsole") + if (Options.NoColorLogs) { - DetectOutputRedirected = true, - UseDefaultRowHighlightingRules = false, - WordHighlightingRules = + logconsole = new ConsoleTarget("logconsole"); + } + else + { + logconsole = new ColoredConsoleTarget("logconsole") { - new ConsoleWordHighlightingRule("{Green}", ConsoleOutputColor.DarkGreen, - ConsoleOutputColor.White), - new ConsoleWordHighlightingRule("{Yellow}", ConsoleOutputColor.DarkYellow, - ConsoleOutputColor.White), - new ConsoleWordHighlightingRule("{Red}", ConsoleOutputColor.DarkRed, - ConsoleOutputColor.White), - new ConsoleWordHighlightingRule("{Black}", ConsoleOutputColor.Black, - ConsoleOutputColor.White), - - new ConsoleWordHighlightingRule("[Trace]", ConsoleOutputColor.DarkGray, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Degub]", ConsoleOutputColor.Gray, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Info]", ConsoleOutputColor.White, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Error]", ConsoleOutputColor.Magenta, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Fatal]", ConsoleOutputColor.Red, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[File]", ConsoleOutputColor.Green, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule("[Share]", ConsoleOutputColor.Yellow, - ConsoleOutputColor.Black), - new ConsoleWordHighlightingRule - { - CompileRegex = true, - Regex = @"<.*\|.*\|.*\|.*?>", - ForegroundColor = ConsoleOutputColor.Cyan, - BackgroundColor = ConsoleOutputColor.Black - }, - new ConsoleWordHighlightingRule + DetectOutputRedirected = true, + UseDefaultRowHighlightingRules = false, + WordHighlightingRules = { - CompileRegex = true, - Regex = @"^\d\d\d\d-\d\d\-\d\d \d\d:\d\d:\d\d [\+-]\d\d:\d\d ", - ForegroundColor = ConsoleOutputColor.DarkGray, - BackgroundColor = ConsoleOutputColor.Black - }, - new ConsoleWordHighlightingRule - { - CompileRegex = true, - Regex = @"\((?:[^\)]*\)){1}", - ForegroundColor = ConsoleOutputColor.DarkMagenta, - BackgroundColor = ConsoleOutputColor.Black + new ConsoleWordHighlightingRule("[Green]", ConsoleOutputColor.Green, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Yellow]", ConsoleOutputColor.Yellow, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Red]", ConsoleOutputColor.Red, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Black]", ConsoleOutputColor.White, + ConsoleOutputColor.Black), + + new ConsoleWordHighlightingRule("[Trace]", ConsoleOutputColor.DarkGray, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Degub]", ConsoleOutputColor.Gray, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Info]", ConsoleOutputColor.White, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Error]", ConsoleOutputColor.Magenta, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Fatal]", ConsoleOutputColor.Red, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[File]", ConsoleOutputColor.DarkCyan, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Share]", ConsoleOutputColor.Cyan, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule("[Dir]", ConsoleOutputColor.Blue, + ConsoleOutputColor.Black), + new ConsoleWordHighlightingRule + { + CompileRegex = true, + Regex = @"<.*?>", + ForegroundColor = ConsoleOutputColor.Cyan, + BackgroundColor = ConsoleOutputColor.Black + } } - } - }; + }; + } + if (LogLevel == LogLevel.Warn) { nlogConfig.AddRule(LogLevel.Warn, LogLevel.Warn, logconsole); @@ -185,6 +150,10 @@ public void Run(string[] args) }; logfile.Layout = jsonLayout; } + else if (Options.LogType == LogType.HTML) + { + logfile.Layout = "${longdate}${event-properties:htmlFields:objectPath=DateTime}${level}${event-properties:htmlFields:objectPath=Type}${event-properties:htmlFields:objectPath=Triage}${event-properties:htmlFields:objectPath=Permissions}${event-properties:htmlFields:objectPath=Message}"; + } } // Apply config @@ -211,6 +180,10 @@ public void Run(string[] args) exit = true; } } + + if (Options.LogType == LogType.JSON) FixJSONOutput(); + if (Options.LogType == LogType.HTML) FixHTMLOutput(); + return; } catch (Exception e) @@ -239,14 +212,7 @@ private bool HandleOutput() BlockingMq Mq = BlockingMq.GetMq(); foreach (SnafflerMessage message in Mq.Q.GetConsumingEnumerable()) { - if (Options.LogType == LogType.Plain) - { - ProcessMessage(message); - } - else if (Options.LogType == LogType.JSON) - { - ProcessMessageJSON(message); - } + ProcessMessage(message); // catch terminating messages and bail out of the master 'while' loop if ((message.Type == SnafflerMessageType.Fatal) || (message.Type == SnafflerMessageType.Finish)) @@ -259,203 +225,69 @@ private bool HandleOutput() private void ProcessMessage(SnafflerMessage message) { - // standardized time formatting, UTC - string datetime = String.Format("{1}{0}{2:u}{0}", Options.Separator, hostString(), message.DateTime.ToUniversalTime()); + Logger messageLogger = Logger; + string formattedMessageString; - switch (message.Type) + switch (Options.LogType) { - case SnafflerMessageType.Trace: - Logger.Trace(datetime + "[Trace]" + Options.Separator + message.Message); - break; - case SnafflerMessageType.Degub: - Logger.Debug(datetime + "[Degub]" + Options.Separator + message.Message); + case LogType.Plain: + formattedMessageString = message.ToString(); break; - case SnafflerMessageType.Info: - Logger.Info(datetime + "[Info]" + Options.Separator + message.Message); - break; - case SnafflerMessageType.FileResult: - Logger.Warn(datetime + "[File]" + Options.Separator + FileResultLogFromMessage(message)); - break; - case SnafflerMessageType.DirResult: - Logger.Warn(datetime + "[Dir]" + Options.Separator + DirResultLogFromMessage(message)); - break; - case SnafflerMessageType.ShareResult: - Logger.Warn(datetime + "[Share]" + Options.Separator + ShareResultLogFromMessage(message)); - break; - case SnafflerMessageType.Error: - Logger.Error(datetime + "[Error]" + Options.Separator + message.Message); + case LogType.JSON: + formattedMessageString = message.ToString(); + messageLogger = messageLogger.WithProperty("SnafflerMessage", message); break; - case SnafflerMessageType.Fatal: - Logger.Fatal(datetime + "[Fatal]" + Options.Separator + message.Message); - if (Debugger.IsAttached) - { - Console.ReadKey(); - } + case LogType.HTML: + SnafflerMessageComponents components = message.ToStringComponents(); + + formattedMessageString = SnafflerMessage.StringFromComponents(components); + messageLogger = messageLogger.WithProperty("htmlFields", new { components.DateTime, components.Type, components.Triage, components.Permissions, Message = HttpUtility.HtmlEncode(components.Message) }); break; - case SnafflerMessageType.Finish: - Logger.Info("Snaffler out."); - - if (Debugger.IsAttached) - { - Console.WriteLine("Press any key to exit."); - Console.ReadKey(); - } + default: + // this should be unreachable but whatever + formattedMessageString = message.ToString(); break; } - } - - private void ProcessMessageJSON(SnafflerMessage message) - { - // standardized time formatting, UTC - string datetime = String.Format("{1}{0}{2:u}{0}", Options.Separator, hostString(), message.DateTime.ToUniversalTime()); switch (message.Type) { case SnafflerMessageType.Trace: - //Logger.Trace(message); - Logger.Trace(datetime + "[Trace]" + Options.Separator + message.Message, message); + messageLogger.Trace(formattedMessageString); break; case SnafflerMessageType.Degub: - //Logger.Debug(message); - Logger.Debug(datetime + "[Degub]" + Options.Separator + message.Message, message); + messageLogger.Debug(formattedMessageString); break; case SnafflerMessageType.Info: - //Logger.Info(message); - Logger.Info(datetime + "[Info]" + Options.Separator + message.Message, message); + messageLogger.Info(formattedMessageString); break; case SnafflerMessageType.FileResult: - //Logger.Warn(message); - Logger.Warn(datetime + "[File]" + Options.Separator + FileResultLogFromMessage(message), message); - break; case SnafflerMessageType.DirResult: - //Logger.Warn(message); - Logger.Warn(datetime + "[Dir]" + Options.Separator + DirResultLogFromMessage(message), message); - break; case SnafflerMessageType.ShareResult: - //Logger.Warn(message); - Logger.Warn(datetime + "[Share]" + Options.Separator + ShareResultLogFromMessage(message), message); + messageLogger.Warn(formattedMessageString); break; case SnafflerMessageType.Error: - //Logger.Error(message); - Logger.Error(datetime + "[Error]" + Options.Separator + message.Message, message); + messageLogger.Error(formattedMessageString); break; case SnafflerMessageType.Fatal: - //Logger.Fatal(message); - Logger.Fatal(datetime + "[Fatal]" + Options.Separator + message.Message, message); + messageLogger.Fatal(formattedMessageString); if (Debugger.IsAttached) { Console.ReadKey(); } break; case SnafflerMessageType.Finish: - Logger.Info("Snaffler out."); - + messageLogger.Info("Snaffler out."); + if (Debugger.IsAttached) { Console.WriteLine("Press any key to exit."); Console.ReadKey(); } - if (Options.LogType == LogType.JSON) - { - Logger.Info("Normalising output, please wait..."); - FixJSONOutput(); - } - break; - } - } - - public string ShareResultLogFromMessage(SnafflerMessage message) - { - string sharePath = message.ShareResult.SharePath; - string triage = message.ShareResult.Triage.ToString(); - string shareComment = message.ShareResult.ShareComment; - string rwString = ""; - if (message.ShareResult.RootReadable) - { - rwString = rwString + "R"; - } - if (message.ShareResult.RootWritable) - { - rwString = rwString + "W"; - } - if (message.ShareResult.RootModifyable) - { - rwString = rwString + "M"; + break; } - - return string.Format(shareResultTemplate, triage, sharePath, rwString, shareComment); } - public string DirResultLogFromMessage(SnafflerMessage message) - { - string sharePath = message.DirResult.DirPath; - string triage = message.DirResult.Triage.ToString(); - return string.Format(dirResultTemplate, triage, sharePath); - } - - public string FileResultLogFromMessage(SnafflerMessage message) - { - try - { - string matchedclassifier = message.FileResult.MatchedRule.RuleName; - string triageString = message.FileResult.MatchedRule.Triage.ToString(); - DateTime modifiedStamp = message.FileResult.FileInfo.LastWriteTime.ToUniversalTime(); - - string canread = ""; - if (message.FileResult.RwStatus.CanRead) - { - canread = "R"; - } - - string canwrite = ""; - if (message.FileResult.RwStatus.CanWrite) - { - canwrite = "W"; - } - - string canmodify = ""; - if (message.FileResult.RwStatus.CanModify) - { - canmodify = "M"; - } - - string matchedstring = ""; - - long fileSize = message.FileResult.FileInfo.Length; - - string fileSizeString; - - // TSV output will probably be machine-consumed. Don't pretty it up. - if (Options.LogTSV) - { - fileSizeString = fileSize.ToString(); - } - else - { - fileSizeString = BytesToString(fileSize); - } - - string filepath = message.FileResult.FileInfo.FullName; - - string matchcontext = ""; - if (message.FileResult.TextResult != null) - { - matchedstring = message.FileResult.TextResult.MatchedStrings[0]; - matchcontext = message.FileResult.TextResult.MatchContext; - matchcontext = Regex.Replace(matchcontext, @"\r\n?|\n", "\\n"); // Replace newlines with \n for consistent log lines - } - - return string.Format(fileResultTemplate, triageString, matchedclassifier, canread, canwrite, canmodify, matchedstring, fileSizeString, modifiedStamp, - filepath, matchcontext); - } - catch (Exception e) - { - Console.WriteLine(e.ToString()); - Console.WriteLine(message.FileResult.FileInfo.FullName); - return ""; - } - } private void ParseLogLevelString(string logLevelString) { switch (logLevelString.ToLower()) @@ -488,17 +320,6 @@ private void ParseLogLevelString(string logLevelString) } } - private static String BytesToString(long byteCount) - { - string[] suf = { "B", "kB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB - if (byteCount == 0) - return "0" + suf[0]; - long bytes = Math.Abs(byteCount); - int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); - double num = Math.Round(bytes / Math.Pow(1024, place), 1); - return (Math.Sign(byteCount) * num) + suf[place]; - } - public void WriteColor(string textToWrite, ConsoleColor fgColor) { Console.ForegroundColor = fgColor; @@ -579,5 +400,32 @@ private void FixJSONOutput() //Delete the temporary file. File.Delete(Options.LogFilePath + ".tmp"); } + + private void FixHTMLOutput() + { + //Rename the log file temporarily + File.Move(Options.LogFilePath, Options.LogFilePath + ".tmp"); + + //Prepare the normalised file + using (StreamWriter file = new StreamWriter(Options.LogFilePath)) + { + //Write the start of the surrounding template that we need + file.Write("Snaffler Logs
"); + + //Open the original file + using (FileStream sourceStream = new FileStream(Options.LogFilePath + ".tmp", FileMode.Open, FileAccess.Read)) + using (StreamReader sourceReader = new StreamReader(sourceStream)) + { + //Write the original content + file.Write(sourceReader.ReadToEnd()); + } + + //Write the end of the surrounding template that we need + file.Write("
TimestampDateTimeLevelTypeTriagePermissionsMessage
"); + } + + //Delete the temporary file + File.Delete(Options.LogFilePath + ".tmp"); + } } } diff --git a/Snaffler/Snaffler.csproj b/Snaffler/Snaffler.csproj index f84fdba9..e49e62a3 100644 --- a/Snaffler/Snaffler.csproj +++ b/Snaffler/Snaffler.csproj @@ -63,6 +63,7 @@ +