-
Notifications
You must be signed in to change notification settings - Fork 254
Expand file tree
/
Copy pathSharphound.cs
More file actions
494 lines (433 loc) · 20 KB
/
Sharphound.cs
File metadata and controls
494 lines (433 loc) · 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
// ---------------------------------------------------- //
// ______ __ __ __ //
// / __/ / ___ ________ / // /_ __ _____ ___/ / //
// _\ \/ _ \/ _ `/ __/ _ \/ _ / _ \/ // / _ \/ _ / //
// /___/_//_/\_,_/_/ / .__/_//_/\___/\_,_/_//_/\_,_/ //
// /_/ //
// app type : console //
// dotnet ver. : 462 //
// client ver : 3? //
// license : open....? //
//------------------------------------------------------//
// creational_pattern : Inherit from System.CommandLine //
// structural_pattern : Chain Of Responsibility //
// behavioral_pattern : inherit from SharpHound3 //
// ---------------------------------------------------- //
using System;
using System.Collections.Generic;
using System.DirectoryServices.ActiveDirectory;
using System.DirectoryServices.Protocols;
using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Sharphound.Client;
using Sharphound.Runtime;
using SharpHoundCommonLib;
using Timer = System.Timers.Timer;
namespace Sharphound
{
#region Reference Implementations
internal class BasicLogger : ILogger
{
private readonly int _verbosity;
public BasicLogger(int verbosity)
{
_verbosity = verbosity;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
WriteLevel(logLevel, state.ToString(), exception);
}
public bool IsEnabled(LogLevel logLevel)
{
return (int)logLevel >= _verbosity;
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
private void WriteLevel(LogLevel level, string message, Exception e = null)
{
if (IsEnabled(level))
Console.WriteLine(FormatLog(level, message, e));
}
private static string FormatLog(LogLevel level, string message, Exception e)
{
var time = DateTime.Now;
return $"{time:O}|{level.ToString().ToUpper()}|{message}{(e != null ? $"\n{e}" : "")}";
}
}
internal class SharpLinks : Links<IContext>
{
/// <summary>
/// Init and check defaults
/// </summary>
/// <param name="context"></param>
/// <param name="options"></param>
/// <returns></returns>
public IContext Initialize(IContext context, LDAPConfig options)
{
context.Logger.LogTrace("Entering initialize link");
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = new List<JsonConverter> {new KindConvertor()}
};
CommonLib.ReconfigureLogging(context.Logger);
//We've successfully parsed arguments, lets do some options post-processing.
var currentTime = DateTime.Now;
//var padString = new string('-', initString.Length);
context.Logger.LogInformation("Initializing SharpHound at {time} on {date}",
currentTime.ToShortTimeString(), currentTime.ToShortDateString());
// Check to make sure both LDAP options are set if either is set
if (options.Password != null && options.Username == null ||
options.Username != null && options.Password == null)
{
context.Logger.LogTrace("You must specify both LdapUsername and LdapPassword if using these options!");
context.Flags.IsFaulted = true;
return context;
}
//Check some loop options
if (!context.Flags.Loop) return context;
//If loop is set, ensure we actually set options properly
if (context.LoopDuration == TimeSpan.Zero)
{
context.Logger.LogTrace("Loop specified without a duration. Defaulting to 2 hours!");
context.LoopDuration = TimeSpan.FromHours(2);
}
if (context.LoopInterval == TimeSpan.Zero)
context.LoopInterval = TimeSpan.FromSeconds(30);
if (!context.Flags.NoOutput)
{
var filename = context.ResolveFileName(Path.GetRandomFileName(), "", false);
try
{
using (File.Create(filename))
{
}
File.Delete(filename);
}
catch (Exception e)
{
context.Logger.LogCritical("unable to write to target directory");
context.Flags.IsFaulted = true;
}
}
context.Logger.LogTrace("Exiting initialize link");
return context;
}
public IContext TestConnection(IContext context)
{
context.Logger.LogTrace("Entering TestConnection link");
//2. TestConnection()
// Initial LDAP connection test. Search for the well known administrator SID to make sure we can connect successfully.
var result = context.LDAPUtils.TestLDAPConfig(context.DomainName);
if (!result)
{
context.Logger.LogError("Unable to connect to LDAP, verify your credentials");
context.Flags.IsFaulted = true;
}
context.Flags.InitialCompleted = false;
context.Flags.NeedsCancellation = false;
context.Timer = null;
context.LoopEnd = DateTime.Now;
context.Logger.LogTrace("Exiting TestConnection link");
return context;
}
public IContext SetSessionUserName(string overrideUserName, IContext context)
{
context.Logger.LogTrace("Entering SetSessionUserName");
//3. SetSessionUserName()
// Set the current user name for session collection.
context.CurrentUserName = overrideUserName ?? WindowsIdentity.GetCurrent().Name.Split('\\')[1];
context.Logger.LogTrace("Exiting SetSessionUserName");
return context;
}
public IContext InitCommonLib(IContext context)
{
context.Logger.LogTrace("Entering InitCommonLib");
//4. Create our Cache/Initialize Common Lib
context.Logger.LogTrace("Getting cache path");
var path = context.GetCachePath();
context.Logger.LogTrace("Cache Path: {Path}", path);
Cache cache;
if (!File.Exists(path))
{
context.Logger.LogTrace("Cache file does not exist");
cache = null;
}
else
try
{
context.Logger.LogTrace("Loading cache from disk");
var json = File.ReadAllText(path);
cache = JsonConvert.DeserializeObject<Cache>(json, CacheContractResolver.Settings);
context.Logger.LogInformation("Loaded cache with stats: {stats}", cache?.GetCacheStats());
}
catch (Exception e)
{
context.Logger.LogError("Error loading cache: {exception}, creating new", e);
cache = null;
}
CommonLib.InitializeCommonLib(context.Logger, cache);
context.Logger.LogTrace("Exiting InitCommonLib");
return context;
}
public IContext GetDomainsForEnumeration(IContext context)
{
context.Logger.LogTrace("Entering GetDomainsForEnumeration");
if (context.Flags.SearchForest)
{
context.Logger.LogInformation("[SearchForest] Cross-domain enumeration may result in reduced data quality");
var forest = context.LDAPUtils.GetForest(context.DomainName);
if (forest == null)
{
context.Logger.LogError("Unable to contact forest to get domains for SearchForest");
context.Flags.IsFaulted = true;
return context;
}
context.Domains = (from Domain d in forest.Domains select d.Name).ToArray();
context.Logger.LogInformation("Domains for enumeration: {Domains}", JsonConvert.SerializeObject(context.Domains));
return context;
}
var domain = context.LDAPUtils.GetDomain(context.DomainName)?.Name ?? context.DomainName;
if (domain == null)
{
context.Logger.LogError("unable to resolve a domain to use, manually specify one or check spelling");
context.Flags.IsFaulted = true;
}
context.Domains = new[] { domain };
context.Logger.LogTrace("Exiting GetDomainsForEnumeration");
return context;
}
public IContext StartBaseCollectionTask(IContext context)
{
context.Logger.LogTrace("Entering StartBaseCollectionTask");
context.Logger.LogInformation("Flags: {flags}", context.ResolvedCollectionMethods.GetIndividualFlags());
//5. Start the collection
var task = new CollectionTask(context);
context.CollectionTask = task.StartCollection();
context.Logger.LogTrace("Exiting StartBaseCollectionTask");
return context;
}
public async Task<IContext> AwaitBaseRunCompletion(IContext context)
{
// 6. Wait for the collection to complete
await context.CollectionTask;
return context;
}
public async Task<IContext> AwaitLoopCompletion(IContext context)
{
await context.CollectionTask;
return context;
}
public IContext DisposeTimer(IContext context)
{
//14. Dispose the context.
context.Timer?.Dispose();
return context;
}
public IContext Finish(IContext context)
{
////16. And we're done!
var currTime = DateTime.Now;
context.Logger.LogInformation(
"SharpHound Enumeration Completed at {Time} on {Date}! Happy Graphing!", currTime.ToShortTimeString(),
currTime.ToShortDateString());
return context;
}
public IContext SaveCacheFile(IContext context)
{
if (context.Flags.MemCache)
return context;
// 15. Program exit started. Save the cache file
var cache = Cache.GetCacheInstance();
context.Logger.LogInformation("Saving cache with stats: {stats}", cache.GetCacheStats());
var serialized = JsonConvert.SerializeObject(cache);
using var stream =
new StreamWriter(context.GetCachePath());
stream.Write(serialized);
return context;
}
public IContext StartLoop(IContext context)
{
if (!context.Flags.Loop || context.CancellationTokenSource.IsCancellationRequested) return context;
context.ResolvedCollectionMethods = context.ResolvedCollectionMethods.GetLoopCollectionMethods();
context.Logger.LogInformation("Creating loop manager with methods {Methods}", context.ResolvedCollectionMethods);
var manager = new LoopManager(context);
context.Logger.LogInformation("Starting looping");
context.CollectionTask = manager.StartLooping();
return context;
}
public IContext StartLoopTimer(IContext context)
{
//If loop is set, set up our timer for the loop now
if (!context.Flags.Loop || context.CancellationTokenSource.IsCancellationRequested) return context;
context.LoopEnd = context.LoopEnd.AddMilliseconds(context.LoopDuration.TotalMilliseconds);
context.Timer = new Timer();
context.Timer.Elapsed += (_, _) =>
{
if (context.Flags.InitialCompleted)
context.CancellationTokenSource.Cancel();
else
context.Flags.NeedsCancellation = true;
};
context.Timer.Interval = context.LoopDuration.TotalMilliseconds;
context.Timer.AutoReset = false;
context.Timer.Start();
return context;
}
}
#endregion
#region Console Entrypoint
public class Program
{
public static async Task Main(string[] args)
{
var logger = new BasicLogger((int)LogLevel.Information);
logger.LogInformation("This version of SharpHound is compatible with the 4.3.1 Release of BloodHound");
try
{
var parser = new Parser(with =>
{
with.CaseInsensitiveEnumValues = true;
with.CaseSensitive = false;
with.HelpWriter = Console.Error;
});
var options = parser.ParseArguments<Options>(args);
await options.WithParsedAsync(async options =>
{
if (!options.ResolveCollectionMethods(logger, out var resolved, out var dconly)) return;
logger = new BasicLogger(options.Verbosity);
var flags = new Flags
{
Loop = options.Loop,
DumpComputerStatus = options.TrackComputerCalls,
NoRegistryLoggedOn = options.SkipRegistryLoggedOn,
ExcludeDomainControllers = options.ExcludeDCs,
SkipPortScan = options.SkipPortCheck,
SkipPasswordAgeCheck = options.SkipPasswordCheck,
DisableKerberosSigning = options.DisableSigning,
SecureLDAP = options.SecureLDAP,
InvalidateCache = options.RebuildCache,
NoZip = options.NoZip,
NoOutput = false,
Stealth = options.Stealth,
RandomizeFilenames = options.RandomFileNames,
MemCache = options.MemCache,
CollectAllProperties = options.CollectAllProperties,
DCOnly = dconly,
PrettyPrint = options.PrettyPrint,
SearchForest = options.SearchForest
};
var ldapOptions = new LDAPConfig
{
Port = options.LDAPPort,
DisableSigning = options.DisableSigning,
SSL = options.SecureLDAP,
AuthType = AuthType.Negotiate,
DisableCertVerification = options.DisableCertVerification
};
if (options.DomainController != null) ldapOptions.Server = options.DomainController;
if (options.LDAPUsername != null)
{
if (options.LDAPPassword == null)
{
logger.LogInformation("Prompting for interactive LDAPPassword");
StringBuilder passwordBuilder = new StringBuilder();
Console.Write("LDAPPassword: ");
while (true)
{
ConsoleKeyInfo key = Console.ReadKey(true);
if (key.Key == ConsoleKey.Enter)
break;
if (key.Key == ConsoleKey.Backspace)
{
// Don't allow user to backspace through prompt
if (passwordBuilder.Length > 0)
{
passwordBuilder.Length--;
Console.Write("\b \b");
}
continue;
}
passwordBuilder.Append(key.KeyChar);
Console.Write("*");
}
Console.WriteLine();
options.LDAPPassword = passwordBuilder.ToString();
}
ldapOptions.Username = options.LDAPUsername;
ldapOptions.Password = options.LDAPPassword;
}
IContext context = new BaseContext(logger, ldapOptions, flags)
{
DomainName = options.Domain,
CacheFileName = options.CacheName,
ZipFilename = options.ZipFilename,
SearchBase = options.DistinguishedName,
StatusInterval = options.StatusInterval,
RealDNSName = options.RealDNSName,
ComputerFile = options.ComputerFile,
OutputPrefix = options.OutputPrefix,
OutputDirectory = options.OutputDirectory,
Jitter = options.Jitter,
Throttle = options.Throttle,
LdapFilter = options.LdapFilter,
PortScanTimeout = options.PortCheckTimeout,
ResolvedCollectionMethods = resolved,
Threads = options.Threads,
LoopDuration = options.LoopDuration,
LoopInterval = options.LoopInterval,
ZipPassword = options.ZipPassword,
IsFaulted = false
};
var cancellationTokenSource = new CancellationTokenSource();
context.CancellationTokenSource = cancellationTokenSource;
// Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs eventArgs)
// {
// eventArgs.Cancel = true;
// cancellationTokenSource.Cancel();
// };
// Create new chain links
Links<IContext> links = new SharpLinks();
// Run our chain
context = links.Initialize(context, ldapOptions);
if (context.Flags.IsFaulted)
return;
context = links.TestConnection(context);
if (context.Flags.IsFaulted)
return;
context = links.SetSessionUserName(options.OverrideUserName, context);
context = links.InitCommonLib(context);
context = links.GetDomainsForEnumeration(context);
if (context.Flags.IsFaulted)
return;
context = links.StartBaseCollectionTask(context);
context = await links.AwaitBaseRunCompletion(context);
context = links.StartLoopTimer(context);
context = links.StartLoop(context);
context = await links.AwaitLoopCompletion(context);
context = links.SaveCacheFile(context);
links.Finish(context);
});
}
catch (Exception ex)
{
logger.LogError($"Error running SharpHound: {ex.Message}\n{ex.StackTrace}");
}
}
// Accessor function for the PS1 to work, do not change or remove
public static void InvokeSharpHound(string[] args)
{
Main(args).Wait();
}
}
#endregion
}