Skip to content

Commit 26001a3

Browse files
authored
NTLM Unit Tests (#194)
* chore: add unit tests for NTLM * chore: add unit tests for NTLM * chore: fix SmbProcessorTest.cs * chore: using internal instead of public
1 parent 4abd7c4 commit 26001a3

8 files changed

Lines changed: 282 additions & 15 deletions

src/CommonLib/Ntlm/HttpNtlmAuthenticationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private async Task<string[]> GetSupportedNtlmAuthSchemesAsync(Uri url) {
5757
return ExtractAuthSchemes(getResponse);
5858
}
5959

60-
private string[] ExtractAuthSchemes(HttpResponseMessage response) {
60+
internal string[] ExtractAuthSchemes(HttpResponseMessage response) {
6161
if (response.StatusCode == HttpStatusCode.OK) {
6262
throw new AuthNotRequiredException(
6363
"Authorization was not solicited when enumerating Authentication schemes");

src/CommonLib/Processors/CAEnrollmentProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private async Task<IEnumerable<APIResult<CAEnrollmentEndpoint>>>
107107
return endpoints;
108108
}
109109

110-
private (Uri httpUrl, Uri httpsUrl) BuildEnrollmentUrls(CAEnrollmentEndpointType type) {
110+
internal (Uri httpUrl, Uri httpsUrl) BuildEnrollmentUrls(CAEnrollmentEndpointType type) {
111111
switch (type) {
112112
case CAEnrollmentEndpointType.WebEnrollmentApplication:
113113
return (new Uri($"http://{_caDnsHostname}/certsrv/"),

src/CommonLib/Processors/DCLdapProcessor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public virtual async Task<bool> TestLdapsPort() {
125125
return await _scanner.CheckPort(_ldapSslEndpoint.Host, _ldapSslEndpoint.Port, _portScanTimeout);
126126
}
127127

128-
public virtual async Task<SharpHoundRPC.Result<bool>> CheckIsNtlmSigningRequired() {
128+
public async Task<SharpHoundRPC.Result<bool>> CheckIsNtlmSigningRequired() {
129129
try {
130130
var options = new LdapAuthOptions() {
131131
Signing = false
@@ -147,7 +147,7 @@ public virtual async Task<bool> TestLdapsPort() {
147147
// 3) Correct bindings to ensure NTLM auth is enabled
148148
// However, as of right now we only do #2. We can't do #1 right now since the
149149
// Window's SSPI APIs (InitSecurityContext) always add channel bindings.
150-
public virtual async Task<SharpHoundRPC.Result<bool>> CheckIsChannelBindingDisabled() {
150+
public async Task<SharpHoundRPC.Result<bool>> CheckIsChannelBindingDisabled() {
151151
try {
152152
// 1) Can we connect with *invalid* bindings
153153

@@ -171,7 +171,7 @@ public virtual async Task<bool> TestLdapsPort() {
171171
/// <param name="endpoint"></param>
172172
/// <param name="options"></param>
173173
/// <returns></returns>
174-
private async Task<bool> Authenticate(Uri endpoint, LdapAuthOptions options) {
174+
protected internal virtual async Task<bool> Authenticate(Uri endpoint, LdapAuthOptions options) {
175175
var host = endpoint.Host;
176176
var auth = new NtlmAuthenticationHandler($"LDAP/{host.ToUpper()}") {
177177
Options = options
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Net.Sockets;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Moq;
10+
using SharpHoundCommonLib;
11+
using SharpHoundCommonLib.OutputTypes;
12+
using SharpHoundCommonLib.Processors;
13+
using SharpHoundRPC;
14+
using Xunit;
15+
using Xunit.Abstractions;
16+
17+
namespace CommonLibTest {
18+
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
19+
public class CAEnrollmentProcessorTest : IDisposable {
20+
21+
private readonly ITestOutputHelper _testOutputHelper;
22+
23+
public CAEnrollmentProcessorTest(ITestOutputHelper testOutputHelper) {
24+
_testOutputHelper = testOutputHelper;
25+
}
26+
27+
public void Dispose() {
28+
}
29+
30+
// TODO: Has error "The requested security protocol is not supported
31+
// [Theory]
32+
// [MemberData(nameof(BuildEnrollmentUrlsData))]
33+
// public async Task CAEnrollmentProcessor_BuildEnrollmentUrls(CAEnrollmentEndpointType type, Uri expectedhttpUrl, Uri expectedhttpsUrl)
34+
// {
35+
// var processor = new CAEnrollmentProcessor("primary.testlab.local", "primary-dc-ca");
36+
// (Uri httpUrl, Uri httpsUrl) = processor.BuildEnrollmentUrls(type);
37+
//
38+
// Assert.Equal(httpUrl, expectedhttpUrl);
39+
// Assert.Equal(httpsUrl, expectedhttpsUrl);
40+
// }
41+
//
42+
// public static IEnumerable<object[]> BuildEnrollmentUrlsData =>
43+
// new List<object[]>
44+
// {
45+
// new object[]
46+
// {
47+
// CAEnrollmentEndpointType.WebEnrollmentApplication,
48+
// new Uri("http://primary.testlab.local/certsrv/"),
49+
// new Uri("https://primary.testlab.local/certsrv/")
50+
// },
51+
// new object[]
52+
// {
53+
// CAEnrollmentEndpointType.EnrollmentWebService,
54+
// new Uri("http://primary.testlab.local/primary-dc-ca_CES_Kerberos/service.svc"),
55+
// new Uri("https://primary.testlab.local/primary-dc-ca_CES_Kerberos/service.svc")
56+
// }
57+
// };
58+
}
59+
}

test/unit/DCLdapProcessorTest.cs

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics.CodeAnalysis;
4+
using System.Runtime.CompilerServices;
45
using System.Threading.Tasks;
56
using Moq;
67
using SharpHoundCommonLib;
@@ -21,19 +22,70 @@ public DCLdapProcessorTest(ITestOutputHelper testOutputHelper) {
2122

2223
public void Dispose() {
2324
}
25+
26+
[Fact]
27+
public async Task DCLdapProcessor_Scan() {
28+
var mockProcessor = new Mock<DCLdapProcessor>(It.IsAny<int>(), "primary.testlab.local", null);
29+
30+
mockProcessor.Setup(x => x.Authenticate(It.IsAny<Uri>(), It.IsAny<LdapAuthOptions>())).ReturnsAsync(false);
31+
32+
mockProcessor.Setup(x => x.TestLdapPort()).ReturnsAsync(true);
33+
mockProcessor.Setup(x => x.TestLdapsPort()).ReturnsAsync(true);
34+
35+
var processor = mockProcessor.Object;
36+
var receivedStatus = new List<CSVComputerStatus>();
37+
processor.ComputerStatusEvent += async status => {
38+
receivedStatus.Add(status);
39+
};
40+
var results = await processor.Scan("primary.testlab.local", TimeSpan.FromMinutes(2));
41+
42+
Assert.Equal(2, receivedStatus.Count);
43+
var status = receivedStatus[0];
44+
Assert.Equal(CSVComputerStatus.StatusSuccess, status.Status);
45+
status = receivedStatus[1];
46+
Assert.Equal(CSVComputerStatus.StatusSuccess, status.Status);
47+
Assert.True(results.HasLdap);
48+
Assert.True(results.HasLdaps);
49+
Assert.True(results.IsSigningRequired.Result);
50+
Assert.False(results.IsChannelBindingDisabled.Result);
51+
}
52+
53+
[Fact]
54+
public async Task DCLdapProcessor_Scan_Failed() {
55+
var mockProcessor = new Mock<DCLdapProcessor>(It.IsAny<int>(), "primary.testlab.local", null);
56+
57+
mockProcessor.Setup(x => x.Authenticate(It.IsAny<Uri>(), It.IsAny<LdapAuthOptions>())).Throws(new Exception("Error"));
58+
59+
mockProcessor.Setup(x => x.TestLdapPort()).ReturnsAsync(true);
60+
mockProcessor.Setup(x => x.TestLdapsPort()).ReturnsAsync(true);
61+
62+
var processor = mockProcessor.Object;
63+
var receivedStatus = new List<CSVComputerStatus>();
64+
processor.ComputerStatusEvent += async status => {
65+
receivedStatus.Add(status);
66+
};
67+
var results = await processor.Scan("primary.testlab.local", TimeSpan.FromMinutes(2));
68+
69+
Assert.Equal(2, receivedStatus.Count);
70+
var status = receivedStatus[0];
71+
Assert.Contains("CheckIsNtlmSigningRequired failed: System.Exception: Error", status.Status);
72+
status = receivedStatus[1];
73+
Assert.Contains("CheckIsNtlmSigningRequired failed: System.Exception: Error", status.Status);
74+
Assert.True(results.HasLdap);
75+
Assert.True(results.HasLdaps);
76+
Assert.False(results.IsSigningRequired.Result);
77+
Assert.False(results.IsChannelBindingDisabled.Result);
78+
Assert.False(results.IsSigningRequired.Collected);
79+
Assert.False(results.IsChannelBindingDisabled.Collected);
80+
}
2481

2582
[Fact]
2683
public async Task DCLdapProcessor_CheckScan_Timeout() {
2784
var mockProcessor = new Mock<DCLdapProcessor>(2, "primary.testlab.local", null);
2885

29-
mockProcessor.Setup(x => x.CheckIsNtlmSigningRequired()).ReturnsAsync(() => {
86+
mockProcessor.Setup(x => x.Authenticate(It.IsAny<Uri>(), It.IsAny<LdapAuthOptions>())).ReturnsAsync(() => {
3087
Task.Delay(100).Wait();
31-
return NtStatus.StatusAccessDenied;
32-
});
33-
34-
mockProcessor.Setup(x => x.CheckIsChannelBindingDisabled()).ReturnsAsync(() => {
35-
Task.Delay(100).Wait();
36-
return NtStatus.StatusAccessDenied;
88+
return false;
3789
});
3890

3991
mockProcessor.Setup(x => x.TestLdapPort()).ReturnsAsync(true);
@@ -51,7 +103,30 @@ public async Task DCLdapProcessor_CheckScan_Timeout() {
51103
Assert.Equal("Timeout", status.Status);
52104
status = receivedStatus[1];
53105
Assert.Equal("Timeout", status.Status);
106+
Assert.Equal("Timeout",results.IsSigningRequired.FailureReason);
107+
Assert.Equal("Timeout",results.IsChannelBindingDisabled.FailureReason);
108+
}
109+
110+
[Fact]
111+
public async Task DCLdapProcessor_CheckIsNtlmSigningRequired()
112+
{
113+
var mockProcessor = new Mock<DCLdapProcessor>(It.IsAny<int>(), "primary.testlab.local", null);
114+
mockProcessor.Setup(x => x.Authenticate(It.IsAny<Uri>(), It.IsAny<LdapAuthOptions>())).ReturnsAsync(false);
115+
var processor = mockProcessor.Object;
116+
var result = await processor.CheckIsNtlmSigningRequired();
117+
Assert.True(result.IsSuccess);
118+
Assert.True(result.Value);
119+
}
120+
121+
[Fact]
122+
public async Task DCLdapProcessor_CheckIsNtlmSigningRequired_Exception()
123+
{
124+
var mockProcessor = new Mock<DCLdapProcessor>(It.IsAny<int>(), "primary.testlab.local", null);
125+
mockProcessor.Setup(x => x.Authenticate(It.IsAny<Uri>(), It.IsAny<LdapAuthOptions>())).Throws(new Exception("Error"));
126+
var processor = mockProcessor.Object;
127+
var result = await processor.CheckIsNtlmSigningRequired();
128+
Assert.True(result.IsFailed);
129+
Assert.Contains("CheckIsNtlmSigningRequired failed: System.Exception: Error", result.Error);
54130
}
55-
56131
}
57132
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Net.Http.Headers;
7+
using System.Net.Sockets;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Moq;
11+
using SharpHoundCommonLib;
12+
using SharpHoundCommonLib.Ntlm;
13+
using SharpHoundCommonLib.OutputTypes;
14+
using SharpHoundCommonLib.Processors;
15+
using SharpHoundRPC;
16+
using Xunit;
17+
using Xunit.Abstractions;
18+
19+
namespace CommonLibTest {
20+
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
21+
public class HttpNtlmAuthenticationServiceTest : IDisposable {
22+
23+
private readonly ITestOutputHelper _testOutputHelper;
24+
25+
public HttpNtlmAuthenticationServiceTest(ITestOutputHelper testOutputHelper) {
26+
_testOutputHelper = testOutputHelper;
27+
}
28+
29+
public void Dispose() {
30+
}
31+
32+
[Fact]
33+
public async Task HttpNtlmAuthenticationService_ExtractAuthSchemes_AuthNotRequiredException()
34+
{
35+
var service = new HttpNtlmAuthenticationService(new HttpClientFactory(), null);
36+
var httpResponseMessage = new HttpResponseMessage
37+
{
38+
StatusCode = HttpStatusCode.OK,
39+
};
40+
41+
var ex = Assert.Throws<AuthNotRequiredException>(() => service.ExtractAuthSchemes(httpResponseMessage));
42+
43+
Assert.Equal(ex.Message, "Authorization was not solicited when enumerating Authentication schemes");
44+
}
45+
46+
[Fact]
47+
public async Task HttpNtlmAuthenticationService_ExtractAuthSchemes_HttpForbiddenException()
48+
{
49+
var service = new HttpNtlmAuthenticationService(new HttpClientFactory(), null);
50+
var httpResponseMessage = new HttpResponseMessage
51+
{
52+
StatusCode = HttpStatusCode.Forbidden,
53+
};
54+
55+
var ex = Assert.Throws<HttpForbiddenException>(() => service.ExtractAuthSchemes(httpResponseMessage));
56+
57+
Assert.Equal(ex.Message, "Forbidden when enumerating Auth schemes");
58+
}
59+
60+
[Fact]
61+
public async Task HttpNtlmAuthenticationService_ExtractAuthSchemes_HttpServerErrorException()
62+
{
63+
var service = new HttpNtlmAuthenticationService(new HttpClientFactory(), null);
64+
var httpResponseMessage = new HttpResponseMessage
65+
{
66+
StatusCode = HttpStatusCode.InternalServerError,
67+
};
68+
69+
var ex = Assert.Throws<HttpServerErrorException>(() => service.ExtractAuthSchemes(httpResponseMessage));
70+
71+
Assert.Equal(ex.Message, "Server Error when enumerating Auth schemes");
72+
}
73+
74+
[Fact]
75+
public async Task HttpNtlmAuthenticationService_ExtractAuthSchemes_Success()
76+
{
77+
var service = new HttpNtlmAuthenticationService(new HttpClientFactory(), null);
78+
var httpResponseMessage = new HttpResponseMessage();
79+
httpResponseMessage.StatusCode = HttpStatusCode.Accepted;
80+
httpResponseMessage.Headers.WwwAuthenticate.Add(
81+
new AuthenticationHeaderValue("NTLM", "realm=localhost"));
82+
httpResponseMessage.Headers.WwwAuthenticate.Add(
83+
new AuthenticationHeaderValue("Negotiate", "realm=localhost"));
84+
httpResponseMessage.Headers.WwwAuthenticate.Add(
85+
new AuthenticationHeaderValue("Basic", "realm=localhost"));
86+
87+
var result = service.ExtractAuthSchemes(httpResponseMessage);
88+
89+
Assert.Equal(result[0], "NTLM");
90+
Assert.Equal(result[1], "Negotiate");
91+
}
92+
}
93+
}

test/unit/SmbProcessorTest.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public void Dispose() {
2525

2626
[Fact]
2727
public async Task SmbProcessor_TestTimeout() {
28-
28+
2929
var mockSmbScanner = new Mock<ISmbScanner>();
3030
mockSmbScanner
3131
.Setup(x => x.ScanHost(It.IsAny<string>(), It.IsAny<int>()))
@@ -34,7 +34,6 @@ public async Task SmbProcessor_TestTimeout() {
3434
return NtStatus.StatusAccessDenied;
3535
});
3636

37-
3837
var mockProcessor = new SmbProcessor(2, mockSmbScanner.Object);
3938
var receivedStatus = new List<CSVComputerStatus>();
4039
mockProcessor.ComputerStatusEvent += async status => receivedStatus.Add(status);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.IO;
5+
using System.Net.Sockets;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Moq;
9+
using SharpHoundCommonLib;
10+
using SharpHoundCommonLib.OutputTypes;
11+
using SharpHoundCommonLib.Processors;
12+
using SharpHoundRPC;
13+
using Xunit;
14+
using Xunit.Abstractions;
15+
16+
namespace CommonLibTest {
17+
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
18+
public class WebClientServiceProcessorTest : IDisposable {
19+
20+
private readonly ITestOutputHelper _testOutputHelper;
21+
22+
public WebClientServiceProcessorTest(ITestOutputHelper testOutputHelper) {
23+
_testOutputHelper = testOutputHelper;
24+
}
25+
26+
public void Dispose() {
27+
}
28+
29+
[Fact]
30+
public async Task WebClientServiceProcessorTest_TestPathExists()
31+
{
32+
var processor = new WebClientServiceProcessor();
33+
34+
var result = await processor.IsWebClientRunning("primary.testlab.local");
35+
36+
Assert.True(result.Collected);
37+
Assert.False(result.Result);
38+
}
39+
40+
}
41+
}

0 commit comments

Comments
 (0)