-
Notifications
You must be signed in to change notification settings - Fork 55
Expand file tree
/
Copy pathHttpNtlmAuthenticationService.cs
More file actions
209 lines (168 loc) · 8.7 KB
/
HttpNtlmAuthenticationService.cs
File metadata and controls
209 lines (168 loc) · 8.7 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
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Authentication;
using System.Threading.Tasks;
namespace SharpHoundCommonLib.Ntlm;
/// <summary>
/// This has been implemented as a bespoke service in order to allow us to change channel bindings. This is not possible with the built in NTLM functions
/// This service uses HTTP to authenticate over NTLM to computers. During the authentication process you can specify channel binding settings which is important
/// for our workflow.
/// </summary>
public class HttpNtlmAuthenticationService {
private readonly ILogger _logger;
private readonly INtlmHttpClientFactory _ntlmHttpClientFactory;
private readonly AdaptiveTimeout _getSupportedNTLMAuthSchemesAdaptiveTimeout;
private readonly AdaptiveTimeout _ntlmAuthAdaptiveTimeout;
private readonly AdaptiveTimeout _authWithChannelBindingAdaptiveTimeout;
public HttpNtlmAuthenticationService(INtlmHttpClientFactory ntlmHttpClientFactory, ILogger logger = null) {
_logger = logger ?? Logging.LogProvider.CreateLogger(nameof(HttpNtlmAuthenticationService));
_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)));
}
public async Task EnsureRequiresAuth(Uri url, bool? useBadChannelBindings) {
if (url == null)
throw new ArgumentException("Url property is null");
if (useBadChannelBindings == null && url.Scheme == "https")
throw new ArgumentException("When using HTTPS, useBadChannelBindings must be set");
var supportedAuthSchemes = await GetSupportedNtlmAuthSchemesAsync(url);
_logger.LogDebug("Supported NTLM auth schemes for {Url}: {AuthSchemes}. UseBadChannelBindings: {UseBadChannelBinding}. UseBadChannelBindings is null: {UseBadChannelBindingsIsNull}",
url, string.Join(",", supportedAuthSchemes), useBadChannelBindings ?? false, !useBadChannelBindings.HasValue);
foreach (var authScheme in supportedAuthSchemes) {
if (useBadChannelBindings == null) {
await AuthWithBadChannelBindingsAsync(url, authScheme);
} else {
if ((bool)useBadChannelBindings) {
await AuthWithBadChannelBindingsAsync(url, authScheme);
} else {
await AuthWithChannelBindingAsync(url, authScheme);
}
}
// If we've got here, everything has worked and it's accessible, so return
return;
}
}
private async Task<string[]> GetSupportedNtlmAuthSchemesAsync(Uri url) {
var httpClient = _ntlmHttpClientFactory.CreateUnauthenticatedClient();
using var getRequest = new HttpRequestMessage(HttpMethod.Get, url);
var result = await _getSupportedNTLMAuthSchemesAdaptiveTimeout.ExecuteWithTimeout(async (timeoutToken) => {
var getResponse = await httpClient.SendAsync(getRequest, timeoutToken);
return ExtractAuthSchemes(getResponse);
});
if (result.IsSuccess)
return result.Value;
else
throw new TimeoutException($"Timeout getting supported NTLM auth schemes for {url}");
}
internal string[] ExtractAuthSchemes(HttpResponseMessage response) {
if (response.StatusCode == HttpStatusCode.OK) {
throw new AuthNotRequiredException(
"Authorization was not solicited when enumerating Authentication schemes");
}
// We expect to get an Unauthorized. If not, something is off
if (response.StatusCode != HttpStatusCode.Unauthorized) {
if (response.StatusCode == HttpStatusCode.Forbidden) {
throw new HttpForbiddenException("Forbidden when enumerating Auth schemes");
}
if (response.StatusCode == HttpStatusCode.InternalServerError) {
throw new HttpServerErrorException("Server Error when enumerating Auth schemes");
}
// Use .NET's exceptions to make things easy
response.EnsureSuccessStatusCode();
}
if (response.Headers.WwwAuthenticate == null) {
throw new InvalidOperationException("WWW-Authenticate header is missing");
}
var schemes = response.Headers.WwwAuthenticate
.Select(header => header.Scheme)
.Where(scheme => scheme == "NTLM" || scheme == "Negotiate")
.Distinct()
.ToArray();
return schemes;
}
private async Task AuthWithBadChannelBindingsAsync(Uri url, string authScheme, NtlmAuthenticationHandler ntlmAuth = null) {
var httpClient = _ntlmHttpClientFactory.CreateUnauthenticatedClient();
var transport = new HttpTransport(httpClient, url, authScheme, _logger);
var ntlmAuthHandler = ntlmAuth ?? new NtlmAuthenticationHandler($"HTTP/{url.Host}");
var result = await _ntlmAuthAdaptiveTimeout.ExecuteWithTimeout((timeoutToken) => ntlmAuthHandler.PerformNtlmAuthenticationAsync(transport, timeoutToken));
if (!result.IsSuccess) {
throw new TimeoutException($"Timeout during NTLM authentication for {url} with {authScheme}");
}
var response = (HttpResponseMessage)result.Value;
if (response.StatusCode == HttpStatusCode.OK) {
return;
}
if (response.StatusCode == HttpStatusCode.Unauthorized) {
throw new HttpUnauthorizedException(
$"401 Unauthorized when accessing {url} with {authScheme} and no signing");
}
if (response.StatusCode == HttpStatusCode.Forbidden) {
// Indicates the path exists but is inaccessible.
// Common cause: trying to access CES (which requires HTTPS by default) over HTTP
throw new HttpForbiddenException($"403 Forbidden when accessing {url} with {authScheme} and no signing");
}
if (response.StatusCode == HttpStatusCode.InternalServerError) {
var body = await response.Content.ReadAsStringAsync();
if (body.Contains("ExtendedProtectionPolicy.PolicyEnforcement"))
throw new ExtendedProtectionMisconfiguredException(
$"EPA misconfigured at {url} with {authScheme} and no signing");
}
response.EnsureSuccessStatusCode();
}
private async Task<bool> AuthWithChannelBindingAsync(Uri url, string authScheme) {
using var client = _ntlmHttpClientFactory.CreateAuthenticatedHttpClient(url, authScheme);
var result = await _authWithChannelBindingAdaptiveTimeout.ExecuteWithTimeout(async (timeoutToken) => {
try {
HttpResponseMessage response = await client.GetAsync(url, timeoutToken);
return response.StatusCode == HttpStatusCode.OK;
}
catch (AuthenticationException ex) {
_logger.LogWarning(ex, $"Authentication failed for {url} with {authScheme}");
return false;
}
});
if (result.IsSuccess)
return result.Value;
else
throw new TimeoutException($"Timeout during channel binding authentication for {url} with {authScheme}");
}
}
[Serializable]
internal class HttpUnauthorizedException : Exception {
public HttpUnauthorizedException() {
}
public HttpUnauthorizedException(string message) : base(message) {
}
}
[Serializable]
internal class ExtendedProtectionMisconfiguredException : Exception {
public ExtendedProtectionMisconfiguredException() {
}
public ExtendedProtectionMisconfiguredException(string message) : base(message) {
}
}
[Serializable]
internal class HttpForbiddenException : Exception {
public HttpForbiddenException() {
}
public HttpForbiddenException(string message) : base(message) {
}
}
[Serializable]
internal class HttpServerErrorException : Exception {
public HttpServerErrorException() {
}
public HttpServerErrorException(string message) : base(message) {
}
}
[Serializable]
internal class AuthNotRequiredException : Exception {
public AuthNotRequiredException() {
}
public AuthNotRequiredException(string message) : base(message) {
}
}