From 8f6c43496ce399bb17872558485a5c7040e60f9f Mon Sep 17 00:00:00 2001 From: Michael Cuomo Date: Fri, 3 Apr 2026 08:32:27 -0400 Subject: [PATCH 1/2] feat: move away from process level schannel settings --- src/CommonLib/Ntlm/HttpClientFactory.cs | 35 ----------- .../Ntlm/HttpNtlmAuthenticationService.cs | 23 ++----- src/CommonLib/Ntlm/NtlmHttpClientFactory.cs | 62 +++++++++++++++++++ .../Processors/CAEnrollmentProcessor.cs | 12 ++-- .../unit/HttpNtlmAuthenticationServiceTest.cs | 8 +-- 5 files changed, 77 insertions(+), 63 deletions(-) delete mode 100644 src/CommonLib/Ntlm/HttpClientFactory.cs create mode 100644 src/CommonLib/Ntlm/NtlmHttpClientFactory.cs diff --git a/src/CommonLib/Ntlm/HttpClientFactory.cs b/src/CommonLib/Ntlm/HttpClientFactory.cs deleted file mode 100644 index 74b8e6f5b..000000000 --- a/src/CommonLib/Ntlm/HttpClientFactory.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; - -namespace SharpHoundCommonLib.Ntlm; - -public interface IHttpClientFactory { - HttpClient CreateUnauthenticatedClient(); - HttpClient CreateAuthenticatedHttpClient(Uri Url, string authPackage = "Kerberos"); -} - -public class HttpClientFactory : IHttpClientFactory { - public HttpClient CreateUnauthenticatedClient() { - var handler = new HttpClientHandler { - ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true, - UseDefaultCredentials = false - }; - - return new HttpClient(handler); - } - - public HttpClient CreateAuthenticatedHttpClient(Uri Url, string authPackage = "Kerberos") { - var handler = new HttpClientHandler { - Credentials = new CredentialCache() { - { Url, authPackage, CredentialCache.DefaultNetworkCredentials } - }, - - PreAuthenticate = true, - ServerCertificateCustomValidationCallback = - (httpRequestMessage, cert, cetChain, policyErrors) => { return true; }, - }; - - return new HttpClient(handler); - } -} \ No newline at end of file diff --git a/src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs b/src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs index cd0a7b935..6eca3cd0b 100644 --- a/src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs +++ b/src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs @@ -15,14 +15,14 @@ namespace SharpHoundCommonLib.Ntlm; /// public class HttpNtlmAuthenticationService { private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; + private readonly INtlmHttpClientFactory _ntlmHttpClientFactory; private readonly AdaptiveTimeout _getSupportedNTLMAuthSchemesAdaptiveTimeout; private readonly AdaptiveTimeout _ntlmAuthAdaptiveTimeout; private readonly AdaptiveTimeout _authWithChannelBindingAdaptiveTimeout; - public HttpNtlmAuthenticationService(IHttpClientFactory httpClientFactory, ILogger logger = null) { + public HttpNtlmAuthenticationService(INtlmHttpClientFactory ntlmHttpClientFactory, ILogger logger = null) { _logger = logger ?? Logging.LogProvider.CreateLogger(nameof(HttpNtlmAuthenticationService)); - _httpClientFactory = httpClientFactory; + _ntlmHttpClientFactory = ntlmHttpClientFactory; _getSupportedNTLMAuthSchemesAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(GetSupportedNtlmAuthSchemesAsync))); _ntlmAuthAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(NtlmAuthenticationHandler.PerformNtlmAuthenticationAsync))); _authWithChannelBindingAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(AuthWithBadChannelBindingsAsync))); @@ -57,7 +57,7 @@ public async Task EnsureRequiresAuth(Uri url, bool? useBadChannelBindings) { } private async Task GetSupportedNtlmAuthSchemesAsync(Uri url) { - var httpClient = _httpClientFactory.CreateUnauthenticatedClient(); + var httpClient = _ntlmHttpClientFactory.CreateUnauthenticatedClient(); using var getRequest = new HttpRequestMessage(HttpMethod.Get, url); var result = await _getSupportedNTLMAuthSchemesAdaptiveTimeout.ExecuteWithTimeout(async (timeoutToken) => { @@ -105,7 +105,7 @@ internal string[] ExtractAuthSchemes(HttpResponseMessage response) { } private async Task AuthWithBadChannelBindingsAsync(Uri url, string authScheme, NtlmAuthenticationHandler ntlmAuth = null) { - var httpClient = _httpClientFactory.CreateUnauthenticatedClient(); + var httpClient = _ntlmHttpClientFactory.CreateUnauthenticatedClient(); var transport = new HttpTransport(httpClient, url, authScheme, _logger); var ntlmAuthHandler = ntlmAuth ?? new NtlmAuthenticationHandler($"HTTP/{url.Host}"); @@ -143,18 +143,7 @@ private async Task AuthWithBadChannelBindingsAsync(Uri url, string authScheme, N } private async Task AuthWithChannelBindingAsync(Uri url, string authScheme) { - var handler = new HttpClientHandler { - ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true, - }; - - var credentialCache = new CredentialCache { - { url, authScheme, CredentialCache.DefaultNetworkCredentials } - }; - - handler.Credentials = credentialCache; - handler.PreAuthenticate = true; - - using var client = new HttpClient(handler); + using var client = _ntlmHttpClientFactory.CreateAuthenticatedHttpClient(url, authScheme); var result = await _authWithChannelBindingAdaptiveTimeout.ExecuteWithTimeout(async (timeoutToken) => { try { diff --git a/src/CommonLib/Ntlm/NtlmHttpClientFactory.cs b/src/CommonLib/Ntlm/NtlmHttpClientFactory.cs new file mode 100644 index 000000000..cb5e917d6 --- /dev/null +++ b/src/CommonLib/Ntlm/NtlmHttpClientFactory.cs @@ -0,0 +1,62 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Security.Authentication; + +namespace SharpHoundCommonLib.Ntlm; + +public interface INtlmHttpClientFactory { + HttpClient CreateUnauthenticatedClient(); + HttpClient CreateAuthenticatedHttpClient(Uri Url, string authPackage = "Kerberos"); +} + +public class NtlmHttpClientFactory : INtlmHttpClientFactory { + private readonly SslProtocols _sslProtocols; + + /// + /// Creates an HttpClientFactory whose handlers will negotiate TLS using OS/framework defaults. + /// + public NtlmHttpClientFactory() : this(SslProtocols.None) { } + + /// + /// Creates an HttpClientFactory whose handlers will restrict TLS negotiation to the specified protocols. + /// Use this overload when a specific set of legacy protocols must be supported for a target service, + /// rather than setting process-wide. + /// + /// + /// The SSL/TLS protocols to allow. Pass to defer to OS/framework defaults. + /// + public NtlmHttpClientFactory(SslProtocols sslProtocols) { + _sslProtocols = sslProtocols; + } + + public HttpClient CreateUnauthenticatedClient() { + var handler = new HttpClientHandler { + ServerCertificateCustomValidationCallback = + (httpRequestMessage, cert, cetChain, policyErrors) => true, + UseDefaultCredentials = false + }; + + if (_sslProtocols != SslProtocols.None) + handler.SslProtocols = _sslProtocols; + + return new HttpClient(handler); + } + + public HttpClient CreateAuthenticatedHttpClient(Uri Url, string authPackage = "Kerberos") { + var handler = new HttpClientHandler { + Credentials = new CredentialCache() { + { Url, authPackage, CredentialCache.DefaultNetworkCredentials } + }, + + PreAuthenticate = true, + ServerCertificateCustomValidationCallback = + (httpRequestMessage, cert, cetChain, policyErrors) => true, + }; + + if (_sslProtocols != SslProtocols.None) + handler.SslProtocols = _sslProtocols; + + return new HttpClient(handler); + } +} \ No newline at end of file diff --git a/src/CommonLib/Processors/CAEnrollmentProcessor.cs b/src/CommonLib/Processors/CAEnrollmentProcessor.cs index 753e33215..4bdd943fd 100644 --- a/src/CommonLib/Processors/CAEnrollmentProcessor.cs +++ b/src/CommonLib/Processors/CAEnrollmentProcessor.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Http; using System.Net.Sockets; +using System.Security.Authentication; using System.Threading.Tasks; namespace SharpHoundCommonLib.Processors { @@ -18,13 +19,10 @@ public class CAEnrollmentProcessor { private readonly string _caName; private readonly ILogger _logger; - public CAEnrollmentProcessor(string caDnsHostname, string caName, ILogger log = null) { - ServicePointManager.SecurityProtocol |= - SecurityProtocolType.Ssl3 - | SecurityProtocolType.Tls12 - | SecurityProtocolType.Tls11 - | SecurityProtocolType.Tls; + private const SslProtocols CaEnrollmentSslProtocols = + SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; + public CAEnrollmentProcessor(string caDnsHostname, string caName, ILogger log = null) { _caDnsHostname = caDnsHostname; _caName = caName; _logger = log ?? Logging.LogProvider.CreateLogger("CAEnrollmentProcessor"); @@ -126,7 +124,7 @@ private async Task>> private async Task> GetNtlmEndpoint(Uri url, bool? useBadChannelBinding, CAEnrollmentEndpointType type, CAEnrollmentEndpointScanResult scanResult) { var authService = new HttpNtlmAuthenticationService( - new HttpClientFactory() + new NtlmHttpClientFactory(CaEnrollmentSslProtocols) ); var output = new CAEnrollmentEndpoint(url, type, scanResult); diff --git a/test/unit/HttpNtlmAuthenticationServiceTest.cs b/test/unit/HttpNtlmAuthenticationServiceTest.cs index e78d654df..f5825e138 100644 --- a/test/unit/HttpNtlmAuthenticationServiceTest.cs +++ b/test/unit/HttpNtlmAuthenticationServiceTest.cs @@ -25,7 +25,7 @@ public void Dispose() { [Fact] public void HttpNtlmAuthenticationService_ExtractAuthSchemes_AuthNotRequiredException() { - var service = new HttpNtlmAuthenticationService(new HttpClientFactory(), null); + var service = new HttpNtlmAuthenticationService(new NtlmHttpClientFactory(), null); var httpResponseMessage = new HttpResponseMessage { StatusCode = HttpStatusCode.OK, }; @@ -37,7 +37,7 @@ public void HttpNtlmAuthenticationService_ExtractAuthSchemes_AuthNotRequiredExce [Fact] public void HttpNtlmAuthenticationService_ExtractAuthSchemes_HttpForbiddenException() { - var service = new HttpNtlmAuthenticationService(new HttpClientFactory(), null); + var service = new HttpNtlmAuthenticationService(new NtlmHttpClientFactory(), null); var httpResponseMessage = new HttpResponseMessage { StatusCode = HttpStatusCode.Forbidden, }; @@ -49,7 +49,7 @@ public void HttpNtlmAuthenticationService_ExtractAuthSchemes_HttpForbiddenExcept [Fact] public void HttpNtlmAuthenticationService_ExtractAuthSchemes_HttpServerErrorException() { - var service = new HttpNtlmAuthenticationService(new HttpClientFactory(), null); + var service = new HttpNtlmAuthenticationService(new NtlmHttpClientFactory(), null); var httpResponseMessage = new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError, }; @@ -61,7 +61,7 @@ public void HttpNtlmAuthenticationService_ExtractAuthSchemes_HttpServerErrorExce [Fact] public void HttpNtlmAuthenticationService_ExtractAuthSchemes_Success() { - var service = new HttpNtlmAuthenticationService(new HttpClientFactory(), null); + var service = new HttpNtlmAuthenticationService(new NtlmHttpClientFactory(), null); var httpResponseMessage = new HttpResponseMessage(); httpResponseMessage.StatusCode = HttpStatusCode.Accepted; httpResponseMessage.Headers.WwwAuthenticate.Add( From 830a9aac953d36f6fa708ba9ec6882209c3cae1b Mon Sep 17 00:00:00 2001 From: Michael Cuomo Date: Wed, 8 Apr 2026 14:12:32 -0400 Subject: [PATCH 2/2] chore: coderabbit nits --- src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs | 4 ++-- src/CommonLib/Processors/CAEnrollmentProcessor.cs | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs b/src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs index 6eca3cd0b..ac36bcae8 100644 --- a/src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs +++ b/src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs @@ -22,7 +22,7 @@ public class HttpNtlmAuthenticationService { public HttpNtlmAuthenticationService(INtlmHttpClientFactory ntlmHttpClientFactory, ILogger logger = null) { _logger = logger ?? Logging.LogProvider.CreateLogger(nameof(HttpNtlmAuthenticationService)); - _ntlmHttpClientFactory = ntlmHttpClientFactory; + _ntlmHttpClientFactory = ntlmHttpClientFactory ?? throw new ArgumentNullException(nameof(ntlmHttpClientFactory)); _getSupportedNTLMAuthSchemesAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(GetSupportedNtlmAuthSchemesAsync))); _ntlmAuthAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(NtlmAuthenticationHandler.PerformNtlmAuthenticationAsync))); _authWithChannelBindingAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(AuthWithBadChannelBindingsAsync))); @@ -206,4 +206,4 @@ public AuthNotRequiredException() { public AuthNotRequiredException(string message) : base(message) { } -} \ No newline at end of file +} diff --git a/src/CommonLib/Processors/CAEnrollmentProcessor.cs b/src/CommonLib/Processors/CAEnrollmentProcessor.cs index 4bdd943fd..a45c12afb 100644 --- a/src/CommonLib/Processors/CAEnrollmentProcessor.cs +++ b/src/CommonLib/Processors/CAEnrollmentProcessor.cs @@ -19,8 +19,9 @@ public class CAEnrollmentProcessor { private readonly string _caName; private readonly ILogger _logger; - private const SslProtocols CaEnrollmentSslProtocols = - SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; + // TLS1.3 is not available in .Net Framework 4.7.2, but the enum can still be assigned. + private const SslProtocols CaEnrollmentSslProtocols = + SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | (SslProtocols)12288; public CAEnrollmentProcessor(string caDnsHostname, string caName, ILogger log = null) { _caDnsHostname = caDnsHostname; @@ -46,7 +47,7 @@ await Task.WhenAll( } catch (Exception ex) { _logger.LogError(ex, "An error occurred while scanning enrollment endpoints"); } - + endpoints = TagEndpoints(endpoints).ToList(); return endpoints; @@ -57,7 +58,7 @@ private IEnumerable> TagEndpoints(IEnumerable> GetNtlmEndpoint(Uri url, boo } } } -} \ No newline at end of file +}