Program.cs
1 using System; 2 using System.Threading; 3 using System.Collections.Generic; 4 using System.Collections; 5 using System.DirectoryServices; 6 using System.DirectoryServices.ActiveDirectory; 7 8 namespace SharpLDAPMonitor 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 15 var parsed = ArgumentParser.Parse(args); 16 Int32 delayInSeconds = 1; 17 String SearchBase = null; 18 String username = null; 19 String password = null; 20 String connectionString = "LDAP://{0}:{1}"; 21 Int32 PageSize = 5000; 22 DirectoryEntry ldapConnection; 23 Dictionary<string, ResultPropertyCollection> results_before = null; 24 Dictionary<string, ResultPropertyCollection> results_after = null; 25 Logger logger = null; 26 27 if (parsed.Arguments.ContainsKey("/logfile")) 28 { 29 logger = new Logger(parsed.Arguments["/logfile"], parsed.Arguments.ContainsKey("/debug")); 30 } 31 else 32 { 33 logger = new Logger(null, parsed.Arguments.ContainsKey("/debug")); 34 } 35 36 37 logger.WriteLine("[+]======================================================"); 38 logger.WriteLine("[+] Sharp LDAP live monitor v1.3 @podalirius_ "); 39 logger.WriteLine("[+]======================================================"); 40 logger.WriteLine(""); 41 42 // Display help 43 if (parsed.Arguments.ContainsKey("/help") || parsed.Arguments.ContainsKey("/h") || parsed.Arguments.Count == 0) 44 { 45 logger.WriteLine("Required"); 46 logger.WriteLine(" /dcip:<1.1.1.1> LDAP host to target, most likely the domain controller."); 47 48 logger.WriteLine("\nOptional"); 49 logger.WriteLine(" /user:<username> User to authenticate as."); 50 logger.WriteLine(" /pass:<password> Password of the account."); 51 logger.WriteLine(" /ldaps Use LDAPS instead of LDAP."); 52 logger.WriteLine(" /searchbase Sets the LDAP search base."); 53 logger.WriteLine(" /delay:<int> Delay between two queries in seconds (default: 1)."); 54 logger.WriteLine(" /randomize Randomize delay between two queries, between 1 and 5 seconds."); 55 logger.WriteLine(" /pagesize Sets the LDAP page size to use in queries (default: 5000)."); 56 logger.WriteLine(" /ignoreuserlogons Ignores user logon events."); 57 logger.WriteLine(" /debug Debug mode."); 58 59 logger.WriteLine("\nUsage: ldapmonitor.exe /user:DOMAIN\\User /pass:MyP@ssw0rd123! /dcip:192.168.1.1"); 60 Environment.Exit(-1); 61 } 62 63 Boolean ignoreuserlogons = parsed.Arguments.ContainsKey("/ignoreuserlogons"); 64 65 // Time delay 66 if (!(parsed.Arguments.ContainsKey("/randomize"))) 67 { 68 if (parsed.Arguments.ContainsKey("/delay")) 69 { 70 delayInSeconds = Int32.Parse(parsed.Arguments["/delay"]); 71 } 72 else 73 { 74 delayInSeconds = 1; 75 } 76 } 77 78 // Handle target host 79 if (!parsed.Arguments.ContainsKey("/dcip")) 80 { 81 logger.WriteLine("[!] /dcip parameter is required."); 82 Environment.Exit(-1); 83 } 84 85 // Handle LDAPS connection switch 86 if (!parsed.Arguments.ContainsKey("/ldaps")) 87 { 88 connectionString = String.Format(connectionString, parsed.Arguments["/dcip"], "389"); 89 } 90 else 91 { 92 connectionString = String.Format(connectionString, parsed.Arguments["/dcip"], "636"); 93 } 94 95 // Handle pagesize for LDAP responses 96 if (parsed.Arguments.ContainsKey("/pagesize")) 97 { 98 PageSize = Int32.Parse(parsed.Arguments["/pagesize"]); 99 } 100 else 101 { 102 PageSize = 5000; 103 } 104 105 // Handle 106 if (parsed.Arguments.ContainsKey("/searchbase")) 107 { 108 SearchBase = parsed.Arguments["/searchbase"]; 109 } 110 111 // Use the provided credentials or the current session 112 if (parsed.Arguments.ContainsKey("/user") && parsed.Arguments.ContainsKey("/pass")) 113 { 114 logger.WriteLine("[+] Using the following credentials:"); 115 logger.WriteLine(" | Target: " + connectionString); 116 logger.WriteLine(" | User: '" + parsed.Arguments["/user"] + "'"); 117 logger.WriteLine(" | Pass: '" + parsed.Arguments["/pass"] + "'"); 118 username = parsed.Arguments["/user"]; 119 password = parsed.Arguments["/pass"]; 120 } 121 else 122 { 123 logger.WriteLine("[+] Using the current session"); 124 logger.WriteLine(" | Host: " + connectionString); 125 } 126 127 try 128 { 129 // Get RootDSE infos (to get list of namingContexts) 130 DirectoryEntry rootDSE = new System.DirectoryServices.DirectoryEntry(String.Format("{0}/RootDSE", connectionString), username, password, System.DirectoryServices.AuthenticationTypes.Secure); 131 List<String> namingContexts = new List<string>(); 132 foreach (String nc in rootDSE.Properties["namingContexts"]) { namingContexts.Add(nc); } 133 134 // First query 135 logger.Debug("Performing initial query ..."); 136 results_before = QueryAllNamingContextsOrSearchBase(namingContexts, connectionString, SearchBase, username, password, PageSize, logger); 137 138 logger.WriteLine("\n[>] Listening for LDAP changes ..."); 139 140 while (true) 141 { 142 // Update query 143 results_after = QueryAllNamingContextsOrSearchBase(namingContexts, connectionString, SearchBase, username, password, PageSize, logger); 144 145 // Diff 146 diff(results_before, results_after, connectionString, logger, ignoreuserlogons); 147 results_before = results_after; 148 149 logger.Debug("Waiting " + delayInSeconds + " second."); 150 151 if (parsed.Arguments.ContainsKey("/randomize")) 152 { 153 Random rnd = new Random(); 154 delayInSeconds = rnd.Next(1, 5); 155 } 156 Thread.Sleep(delayInSeconds * 1000); 157 } 158 } 159 catch (System.Runtime.InteropServices.COMException e) 160 { 161 logger.WriteLine("\n"); 162 logger.Warning("Error: (0x" + e.ErrorCode.ToString("X8") + ") " + e.Message); 163 } 164 } 165 166 static Dictionary<string, ResultPropertyCollection> QueryAllNamingContextsOrSearchBase(List<String> namingContexts, String connectionString, String SearchBase, String Username, String Password, int PageSize, Logger logger) 167 { 168 DirectoryEntry ldapConnection; 169 DirectorySearcher ldapSearcher; 170 Dictionary<string, ResultPropertyCollection> results = new Dictionary<string, ResultPropertyCollection>(); 171 172 if (SearchBase != null) 173 { 174 logger.Debug(String.Format("Using SearchBase: {0}", SearchBase)); 175 ldapConnection = new System.DirectoryServices.DirectoryEntry(String.Format("{0}/{1}", connectionString, SearchBase), Username, Password, System.DirectoryServices.AuthenticationTypes.Secure); 176 ldapSearcher = new DirectorySearcher(ldapConnection); 177 ldapSearcher.Filter = "(objectClass=*)"; 178 179 foreach (SearchResult item in ldapSearcher.FindAll()) 180 { 181 if (!(results.ContainsKey(item.Path))) 182 { 183 results[item.Path] = item.Properties; 184 } 185 else 186 { 187 logger.Debug(String.Format("[debug] key already exists: {0} (this shouldn't be possible)", item.Path)); 188 } 189 } 190 191 return results; 192 } 193 else 194 { 195 foreach (String nc in namingContexts) { 196 logger.Debug(String.Format("Using namingContext as search base: {0}", SearchBase)); 197 ldapConnection = new System.DirectoryServices.DirectoryEntry(String.Format("{0}/{1}", connectionString, nc), Username, Password, System.DirectoryServices.AuthenticationTypes.Secure); 198 ldapSearcher = new DirectorySearcher(ldapConnection); 199 ldapSearcher.Filter = "(objectClass=*)"; 200 201 foreach(SearchResult item in ldapSearcher.FindAll()) { 202 if (!(results.ContainsKey(item.Path))) 203 { 204 results[item.Path] = item.Properties; 205 } 206 else 207 { 208 logger.Debug(String.Format("[debug] key already exists: {0} (this shouldn't be possible)", item.Path)); 209 } 210 } 211 } 212 return results; 213 } 214 } 215 216 /* static void InitLdapConnection() 217 { 218 = new DirectoryEntry(connectionString, username, password, System.DirectoryServices.AuthenticationTypes.Secure); 219 logger.Debug("Authentication successful!"); 220 } 221 */ 222 static void diff(Dictionary<string, ResultPropertyCollection> dict_results_before, Dictionary<string, ResultPropertyCollection> dict_results_after, String connectionString, Logger logger, Boolean ignoreuserlogons) 223 { 224 List<String> ignore_keys = new List<String>(); 225 if (ignoreuserlogons) 226 { 227 ignore_keys.Add("lastlogon"); 228 ignore_keys.Add("logoncount"); 229 } 230 231 String dateprompt = "[" + DateTime.UtcNow.ToString("yyyy/MM/dd hh:mm:ss") + "] "; 232 233 // Get created and deleted entries, and common_keys 234 List<String> common_keys = new List<String>(); 235 foreach (String key in dict_results_before.Keys) 236 { 237 if (dict_results_after.ContainsKey(key)) { common_keys.Add(key); } 238 else { logger.WriteLine(dateprompt + "'" + key.Replace(connectionString + "/", "") + "' was deleted."); } 239 } 240 foreach (String key in dict_results_after.Keys) 241 { 242 if (!dict_results_before.ContainsKey(key)) { logger.WriteLine(dateprompt + "'" + key.Replace(connectionString + "/", "") + "' was added."); } 243 } 244 245 List<Tuple<string, string, Object, Object>> attrs_diff = new List<Tuple<string, string, Object, Object>>(); 246 247 // Iterate over all the common keys 248 foreach (String path in common_keys) 249 { 250 attrs_diff.Clear(); 251 252 // Convert into dictionnaries 253 Dictionary<String, Object> dict_direntry_before = new Dictionary<String, Object>(); 254 Dictionary<String, Object> dict_direntry_after = new Dictionary<String, Object>(); 255 256 foreach (DictionaryEntry prop in dict_results_before[path]) 257 { 258 if (!(ignore_keys.Contains(prop.Key.ToString().ToLower()))) 259 { 260 dict_direntry_before.Add(prop.Key.ToString(), dict_results_before[path][prop.Key.ToString()][0]); 261 } 262 }; 263 foreach (DictionaryEntry prop in dict_results_after[path]) 264 { 265 if (!(ignore_keys.Contains(prop.Key.ToString().ToLower()))) 266 { 267 dict_direntry_after.Add(prop.Key.ToString(), dict_results_after[path][prop.Key.ToString()][0]); 268 } 269 }; 270 271 // Store different values 272 foreach (String pname in dict_direntry_after.Keys) 273 { 274 if (dict_direntry_after.ContainsKey(pname) && dict_direntry_before.ContainsKey(pname)) 275 { 276 if (!(dict_direntry_after[pname].ToString() == dict_direntry_before[pname].ToString())) 277 { 278 Tuple<string, string, Object, Object> diff = new Tuple<string, string, Object, Object>(path, pname, dict_direntry_after[pname], dict_direntry_before[pname]); 279 attrs_diff.Add(diff); 280 } 281 } 282 else if (dict_direntry_after.ContainsKey(pname) && !dict_direntry_before.ContainsKey(pname)) 283 { 284 Tuple<string, string, Object, Object> diff = new Tuple<string, string, Object, Object>(path, pname, dict_direntry_after[pname], null); 285 attrs_diff.Add(diff); 286 } 287 else if (!dict_direntry_after.ContainsKey(pname) && dict_direntry_before.ContainsKey(pname)) 288 { 289 Tuple<string, string, Object, Object> diff = new Tuple<string, string, Object, Object>(path, pname, null, dict_direntry_before[pname]); 290 attrs_diff.Add(diff); 291 } 292 } 293 294 // Show results 295 if (attrs_diff.ToArray().Length != 0) 296 { 297 logger.WriteLine(dateprompt + path.Replace(connectionString + "/", "")); 298 299 foreach (Tuple<string, string, Object, Object> t in attrs_diff) 300 { 301 if ((t.Item4 != null) && (t.Item3 != null)) 302 { 303 logger.WriteLine(" | Attribute " + t.Item2 + " changed from '" + t.Item4 + "' to '" + t.Item3 + "'"); 304 } 305 else if ((t.Item4 == null) && (t.Item3 != null)) 306 { 307 logger.WriteLine(" | Attribute " + t.Item2 + " = '" + t.Item3 + "' was created."); 308 } 309 else if ((t.Item4 != null) && (t.Item3 == null)) 310 { 311 logger.WriteLine(" | Attribute " + t.Item2 + " = '" + t.Item4 + "' was deleted."); 312 } 313 } 314 } 315 } 316 } 317 } 318 }