Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 113 additions & 32 deletions src/CommonLib/Processors/GPOLocalGroupProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.XPath;
using Microsoft.Extensions.Logging;
Expand All @@ -13,6 +14,38 @@
using SharpHoundCommonLib.OutputTypes;

namespace SharpHoundCommonLib.Processors {
/// <summary>
/// Owns state shared by GPOLocalGroupProcessor instances and gives that state an explicit lifetime.
/// </summary>
public sealed class GPOLocalGroupProcessorContext : IDisposable {
private readonly GPOLocalGroupProcessor.ActionCache _actionCache = new();
private int _disposed;

/// <summary>
/// Creates a <see cref="GPOLocalGroupProcessor"/> that shares its GPO action cache with other
/// processors created by this context.
/// </summary>
public GPOLocalGroupProcessor CreateGPOLocalGroupProcessor(ILdapUtils utils, ILogger log = null) {
if (Volatile.Read(ref _disposed) != 0) {
throw new ObjectDisposedException(nameof(GPOLocalGroupProcessorContext));
}

return new GPOLocalGroupProcessor(utils, _actionCache, log);
}

/// <summary>
/// Clears the shared processor state. Processors created by this context must not
/// be used after the context is disposed.
/// </summary>
public void Dispose() {
if (Interlocked.Exchange(ref _disposed, 1) != 0) {
return;
}

_actionCache.Dispose();
}
}

public class GPOLocalGroupProcessor {
private static readonly Regex KeyRegex = new(@"(.+?)\s*=(.*)", RegexOptions.Compiled);

Expand All @@ -30,8 +63,6 @@ public class GPOLocalGroupProcessor {
private static readonly Regex ExtractRid =
new(@"S-1-5-32-([0-9]{3})", RegexOptions.Compiled | RegexOptions.IgnoreCase);

private static readonly ConcurrentDictionary<string, List<GroupAction>> GpoActionCache = new();

private static readonly Dictionary<string, LocalGroupRids> ValidGroupNames =
new(StringComparer.OrdinalIgnoreCase) {
{ "Administrators", LocalGroupRids.Administrators },
Expand All @@ -43,9 +74,46 @@ public class GPOLocalGroupProcessor {
private readonly ILogger _log;

private readonly ILdapUtils _utils;
private readonly ActionCache _actionCache;

internal sealed class ActionCache : IDisposable {
private readonly ConcurrentDictionary<string, Lazy<Task<List<GroupAction>>>> _buildTasks =
new(StringComparer.OrdinalIgnoreCase);
private int _disposed;

public Lazy<Task<List<GroupAction>>> GetOrAddBuildTask(string distinguishedName,
Func<Lazy<Task<List<GroupAction>>>> buildTaskFactory) {
ThrowIfDisposed();
return _buildTasks.GetOrAdd(distinguishedName, _ => buildTaskFactory());
}

public void RemoveBuildTask(string distinguishedName, Lazy<Task<List<GroupAction>>> buildTask) {
ThrowIfDisposed();
((ICollection<KeyValuePair<string, Lazy<Task<List<GroupAction>>>>>)_buildTasks).Remove(
new KeyValuePair<string, Lazy<Task<List<GroupAction>>>>(distinguishedName, buildTask));
}

public void Dispose() {
if (Interlocked.Exchange(ref _disposed, 1) != 0) {
return;
}

_buildTasks.Clear();
}

public GPOLocalGroupProcessor(ILdapUtils utils, ILogger log = null) {
private void ThrowIfDisposed() {
if (Volatile.Read(ref _disposed) != 0) {
throw new ObjectDisposedException(nameof(GPOLocalGroupProcessorContext));
}
}
}

public GPOLocalGroupProcessor(ILdapUtils utils, ILogger log = null) : this(utils, new ActionCache(), log) {
}

internal GPOLocalGroupProcessor(ILdapUtils utils, ActionCache actionCache, ILogger log = null) {
_utils = utils;
_actionCache = actionCache;
_log = log ?? Logging.LogProvider.CreateLogger("GPOLocalGroupProc");
}

Expand Down Expand Up @@ -124,36 +192,22 @@ public async Task<ResultingGPOChanges> ReadGPOLocalGroups(string gpLink, string
foreach (var rid in Enum.GetValues(typeof(LocalGroupRids))) data[(LocalGroupRids)rid] = new GroupResults();

foreach (var linkDn in orderedLinks) {
if (!GpoActionCache.TryGetValue(linkDn.ToLower(), out var actions)) {
actions = new List<GroupAction>();

var gpoDomain = Helpers.DistinguishedNameToDomain(linkDn);
var result = await _utils.Query(new LdapQueryParameters() {
LDAPFilter = new LdapFilter().AddAllObjects().GetFilter(),
SearchScope = SearchScope.Base,
Attributes = [LDAPProperties.GPCFileSYSPath, LDAPProperties.Flags],
SearchBase = linkDn,
DomainName = gpoDomain
}).DefaultIfEmpty(LdapResult<IDirectoryObject>.Fail()).FirstOrDefaultAsync();

if (!result.IsSuccess) {
continue;
}

if (!result.Value.TryGetProperty(LDAPProperties.GPCFileSYSPath, out var filePath) ||
// Filter out GPOs that are disabled or the computer configuration is disabled
(result.Value.TryGetProperty(LDAPProperties.Flags, out var flags) && flags is "2" or "3")) {
GpoActionCache.TryAdd(linkDn, actions);
continue;
}

//Add the actions for each file. The GPO template file actions will override the XML file actions
await foreach (var item in ProcessGPOXmlFile(filePath, gpoDomain)) actions.Add(item);
await foreach (var item in ProcessGPOTemplateFile(filePath, gpoDomain)) actions.Add(item);
var buildTask = _actionCache.GetOrAddBuildTask(linkDn,
() => new Lazy<Task<List<GroupAction>>>(() => BuildGPOActionCache(linkDn),
LazyThreadSafetyMode.ExecutionAndPublication));
List<GroupAction> actions;
try {
actions = await buildTask.Value;
} catch {
_actionCache.RemoveBuildTask(linkDn, buildTask);
throw;
}

//Cache the actions for this GPO for later
GpoActionCache.TryAdd(linkDn.ToLower(), actions);
// Query failures are not cached so a later attempt can retry the GPO.
if (actions == null) {
_actionCache.RemoveBuildTask(linkDn, buildTask);
continue;
}

//If there are no actions, then we can move on from this GPO
if (actions.Count == 0)
Expand Down Expand Up @@ -248,6 +302,33 @@ public async Task<ResultingGPOChanges> ReadGPOLocalGroups(string gpLink, string
return ret;
}

private async Task<List<GroupAction>> BuildGPOActionCache(string linkDn) {
var actions = new List<GroupAction>();
var gpoDomain = Helpers.DistinguishedNameToDomain(linkDn);
var result = await _utils.Query(new LdapQueryParameters() {
LDAPFilter = new LdapFilter().AddAllObjects().GetFilter(),
SearchScope = SearchScope.Base,
Attributes = [LDAPProperties.GPCFileSYSPath, LDAPProperties.Flags],
SearchBase = linkDn,
DomainName = gpoDomain
}).DefaultIfEmpty(LdapResult<IDirectoryObject>.Fail()).FirstOrDefaultAsync();

if (!result.IsSuccess) {
return null;
}

if (!result.Value.TryGetProperty(LDAPProperties.GPCFileSYSPath, out var filePath) ||
// Filter out GPOs that are disabled or the computer configuration is disabled
(result.Value.TryGetProperty(LDAPProperties.Flags, out var flags) && flags is "2" or "3")) {
return actions;
}

//Add the actions for each file. The GPO template file actions will override the XML file actions
await foreach (var item in ProcessGPOXmlFile(filePath, gpoDomain)) actions.Add(item);
await foreach (var item in ProcessGPOTemplateFile(filePath, gpoDomain)) actions.Add(item);
return actions;
}

/// <summary>
/// Parses a GPO GptTmpl.inf file and pulls group membership changes out
/// </summary>
Expand Down Expand Up @@ -576,4 +657,4 @@ internal enum LocalGroupRids {
PSRemote = 580
}
}
}
}
91 changes: 77 additions & 14 deletions src/CommonLib/Processors/PortScanner.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,90 @@
using System;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharpHoundRPC.PortScanner;

namespace SharpHoundCommonLib.Processors {
/// <summary>
/// Owns state shared by PortScanner instances and gives that state an explicit lifetime.
/// </summary>
public sealed class PortScannerContext : IDisposable {
private readonly PortScanner.ScanCache _scanCache = new();
private int _disposed;

/// <summary>
/// Creates a <see cref="PortScanner"/> that shares its scan cache with other scanners
/// created by this context.
/// </summary>
public PortScanner CreatePortScanner(ILogger log = null, int maxTimeout = 10000) {
if (Volatile.Read(ref _disposed) != 0) {
throw new ObjectDisposedException(nameof(PortScannerContext));
}

return new PortScanner(_scanCache, log, maxTimeout);
}

/// <summary>
/// Clears the shared scanner state. Scanners created by this context must not
/// be used after the context is disposed.
/// </summary>
public void Dispose() {
if (Interlocked.Exchange(ref _disposed, 1) != 0) {
return;
}

_scanCache.Dispose();
}
}

public class PortScanner : IPortScanner {
private static readonly ConcurrentDictionary<PingCacheKey, bool> PortScanCache = new();
private readonly ILogger _log;
private readonly AdaptiveTimeout _adaptiveTimeout;
private readonly ScanCache _scanCache;

public PortScanner() : this(null) {
internal sealed class ScanCache : IDisposable {
private readonly ConcurrentDictionary<PingCacheKey, bool> _portScanCache = new();
private int _disposed;

public bool TryGet(PingCacheKey key, out bool status) {
ThrowIfDisposed();
return _portScanCache.TryGetValue(key, out status);
}

public void Add(PingCacheKey key, bool status) {
ThrowIfDisposed();
_portScanCache.TryAdd(key, status);
}

public void Dispose() {
if (Interlocked.Exchange(ref _disposed, 1) != 0) {
return;
}

_portScanCache.Clear();
}

private void ThrowIfDisposed() {
if (Volatile.Read(ref _disposed) != 0) {
throw new ObjectDisposedException(nameof(PortScannerContext));
}
}
}

public PortScanner() : this((ILogger)null) {

}

public PortScanner(ILogger log = null, int maxTimeout = 10000) {
public PortScanner(ILogger log = null, int maxTimeout = 10000) : this(
new ScanCache(), log, maxTimeout) {
}

internal PortScanner(ScanCache scanCache, ILogger log = null, int maxTimeout = 10000) {
_scanCache = scanCache;
_log = log ?? Logging.LogProvider.CreateLogger("PortScanner");
_adaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMilliseconds(maxTimeout), _log);
_adaptiveTimeout = new AdaptiveTimeout(TimeSpan.FromMilliseconds(maxTimeout), _log);
}

/// <summary>
Expand All @@ -35,7 +102,7 @@ public virtual async Task<bool> CheckPort(string hostname, int port = 445,
HostName = hostname
};

if (PortScanCache.TryGetValue(key, out var status)) {
if (_scanCache.TryGet(key, out var status)) {
_log.LogTrace("Port scan cache hit for {HostName}:{Port}: {Status}", hostname, port, status);
return status;
}
Expand All @@ -48,12 +115,12 @@ public virtual async Task<bool> CheckPort(string hostname, int port = 445,
if (throwError) {
throw new TimeoutException(ca.Error);
}
PortScanCache.TryAdd(key, false);
_scanCache.Add(key, false);
return false;
}

_log.LogTrace("CheckPort Succeeded for {HostName}:{Port}", hostname, port);
PortScanCache.TryAdd(key, true);
_scanCache.Add(key, true);
return true;
}
catch (Exception e) {
Expand All @@ -63,16 +130,12 @@ public virtual async Task<bool> CheckPort(string hostname, int port = 445,
throw;
}

PortScanCache.TryAdd(key, false);
_scanCache.Add(key, false);
return false;
}
}

public static void ClearCache() {
PortScanCache.Clear();
}

private class PingCacheKey {
internal class PingCacheKey {
internal string HostName { get; set; }
internal int Port { get; set; }

Expand All @@ -94,4 +157,4 @@ public override int GetHashCode() {
}
}
}
}
}
Loading
Loading