// Copyright 2017 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/ntlm/ntlm_client.h" #include #include "base/containers/span.h" #include "base/strings/string_util.h" #include "build/build_config.h" #include "net/ntlm/ntlm.h" #include "net/ntlm/ntlm_buffer_reader.h" #include "net/ntlm/ntlm_buffer_writer.h" #include "net/ntlm/ntlm_test_data.h" #include "testing/gtest/include/gtest/gtest.h" namespace net::ntlm { namespace { std::vector GenerateAuthMsg(const NtlmClient& client, base::span challenge_msg) { return client.GenerateAuthenticateMessage( test::kNtlmDomain, test::kUser, test::kPassword, test::kHostnameAscii, reinterpret_cast(test::kChannelBindings), test::kNtlmSpn, test::kClientTimestamp, test::kClientChallenge, challenge_msg); } std::vector GenerateAuthMsg(const NtlmClient& client, const NtlmBufferWriter& challenge_writer) { return GenerateAuthMsg(client, challenge_writer.GetBuffer()); } bool GetAuthMsgResult(const NtlmClient& client, const NtlmBufferWriter& challenge_writer) { return !GenerateAuthMsg(client, challenge_writer).empty(); } bool ReadBytesPayload(NtlmBufferReader* reader, base::span buffer) { SecurityBuffer sec_buf; return reader->ReadSecurityBuffer(&sec_buf) && (sec_buf.length == buffer.size()) && reader->ReadBytesFrom(sec_buf, buffer); } // Reads bytes from a payload and assigns them to a string. This makes // no assumptions about the underlying encoding. bool ReadStringPayload(NtlmBufferReader* reader, std::string* str) { SecurityBuffer sec_buf; if (!reader->ReadSecurityBuffer(&sec_buf)) return false; if (!reader->ReadBytesFrom( sec_buf, base::as_writable_bytes(base::make_span( base::WriteInto(str, sec_buf.length + 1), sec_buf.length)))) { return false; } return true; } // Reads bytes from a payload and assigns them to a string16. This makes // no assumptions about the underlying encoding. This will fail if there // are an odd number of bytes in the payload. bool ReadString16Payload(NtlmBufferReader* reader, std::u16string* str) { SecurityBuffer sec_buf; if (!reader->ReadSecurityBuffer(&sec_buf) || (sec_buf.length % 2 != 0)) return false; std::vector raw(sec_buf.length); if (!reader->ReadBytesFrom(sec_buf, raw)) return false; #if defined(ARCH_CPU_BIG_ENDIAN) for (size_t i = 0; i < raw.size(); i += 2) { std::swap(raw[i], raw[i + 1]); } #endif str->assign(reinterpret_cast(raw.data()), raw.size() / 2); return true; } void MakeV2ChallengeMessage(size_t target_info_len, std::vector* out) { static const size_t kChallengeV2HeaderLen = 56; // Leave room for the AV_PAIR header and the EOL pair. size_t server_name_len = target_info_len - kAvPairHeaderLen * 2; // See [MS-NLP] Section 2.2.1.2. NtlmBufferWriter challenge(kChallengeV2HeaderLen + target_info_len); ASSERT_TRUE(challenge.WriteMessageHeader(MessageType::kChallenge)); ASSERT_TRUE( challenge.WriteSecurityBuffer(SecurityBuffer(0, 0))); // target name ASSERT_TRUE(challenge.WriteFlags(NegotiateFlags::kTargetInfo)); ASSERT_TRUE(challenge.WriteZeros(kChallengeLen)); // server challenge ASSERT_TRUE(challenge.WriteZeros(8)); // reserved ASSERT_TRUE(challenge.WriteSecurityBuffer( SecurityBuffer(kChallengeV2HeaderLen, target_info_len))); // target info ASSERT_TRUE(challenge.WriteZeros(8)); // version ASSERT_EQ(kChallengeV2HeaderLen, challenge.GetCursor()); ASSERT_TRUE(challenge.WriteAvPair( AvPair(TargetInfoAvId::kServerName, std::vector(server_name_len, 'a')))); ASSERT_TRUE(challenge.WriteAvPairTerminator()); ASSERT_TRUE(challenge.IsEndOfBuffer()); *out = challenge.Pass(); } } // namespace TEST(NtlmClientTest, SimpleConstructionV1) { NtlmClient client(NtlmFeatures(false)); ASSERT_FALSE(client.IsNtlmV2()); ASSERT_FALSE(client.IsEpaEnabled()); ASSERT_FALSE(client.IsMicEnabled()); } TEST(NtlmClientTest, VerifyNegotiateMessageV1) { NtlmClient client(NtlmFeatures(false)); std::vector result = client.GetNegotiateMessage(); ASSERT_EQ(kNegotiateMessageLen, result.size()); ASSERT_EQ(0, memcmp(test::kExpectedNegotiateMsg, result.data(), kNegotiateMessageLen)); } TEST(NtlmClientTest, MinimalStructurallyValidChallenge) { NtlmClient client(NtlmFeatures(false)); NtlmBufferWriter writer(kMinChallengeHeaderLen); ASSERT_TRUE(writer.WriteBytes(base::make_span(test::kMinChallengeMessage) .subspan<0, kMinChallengeHeaderLen>())); ASSERT_TRUE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, MinimalStructurallyValidChallengeZeroOffset) { NtlmClient client(NtlmFeatures(false)); // The spec (2.2.1.2) states that the length SHOULD be 0 and the offset // SHOULD be where the payload would be if it was present. This is the // expected response from a compliant server when no target name is sent. // In reality the offset should always be ignored if the length is zero. // Also implementations often just write zeros. uint8_t raw[kMinChallengeHeaderLen]; memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen); // Modify the default valid message to overwrite the offset to zero. ASSERT_NE(0x00, raw[16]); raw[16] = 0x00; NtlmBufferWriter writer(kMinChallengeHeaderLen); ASSERT_TRUE(writer.WriteBytes(raw)); ASSERT_TRUE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, ChallengeMsgTooShort) { NtlmClient client(NtlmFeatures(false)); // Fail because the minimum size valid message is 32 bytes. NtlmBufferWriter writer(kMinChallengeHeaderLen - 1); ASSERT_TRUE(writer.WriteBytes(base::make_span(test::kMinChallengeMessage) .subspan<0, kMinChallengeHeaderLen - 1>())); ASSERT_FALSE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, ChallengeMsgNoSig) { NtlmClient client(NtlmFeatures(false)); // Fail because the first 8 bytes don't match "NTLMSSP\0" uint8_t raw[kMinChallengeHeaderLen]; memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen); // Modify the default valid message to overwrite the last byte of the // signature. ASSERT_NE(0xff, raw[7]); raw[7] = 0xff; NtlmBufferWriter writer(kMinChallengeHeaderLen); ASSERT_TRUE(writer.WriteBytes(raw)); ASSERT_FALSE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, ChallengeMsgWrongMessageType) { NtlmClient client(NtlmFeatures(false)); // Fail because the message type should be MessageType::kChallenge // (0x00000002) uint8_t raw[kMinChallengeHeaderLen]; memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen); // Modify the message type. ASSERT_NE(0x03, raw[8]); raw[8] = 0x03; NtlmBufferWriter writer(kMinChallengeHeaderLen); ASSERT_TRUE(writer.WriteBytes(raw)); ASSERT_FALSE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, ChallengeWithNoTargetName) { NtlmClient client(NtlmFeatures(false)); // The spec (2.2.1.2) states that the length SHOULD be 0 and the offset // SHOULD be where the payload would be if it was present. This is the // expected response from a compliant server when no target name is sent. // In reality the offset should always be ignored if the length is zero. // Also implementations often just write zeros. uint8_t raw[kMinChallengeHeaderLen]; memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen); // Modify the default valid message to overwrite the offset to zero. ASSERT_NE(0x00, raw[16]); raw[16] = 0x00; NtlmBufferWriter writer(kMinChallengeHeaderLen); ASSERT_TRUE(writer.WriteBytes(raw)); ASSERT_TRUE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, Type2MessageWithTargetName) { NtlmClient client(NtlmFeatures(false)); // One extra byte is provided for target name. uint8_t raw[kMinChallengeHeaderLen + 1]; memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen); // Put something in the target name. raw[kMinChallengeHeaderLen] = 'Z'; // Modify the default valid message to indicate 1 byte is present in the // target name payload. ASSERT_NE(0x01, raw[12]); ASSERT_EQ(0x00, raw[13]); ASSERT_NE(0x01, raw[14]); ASSERT_EQ(0x00, raw[15]); raw[12] = 0x01; raw[14] = 0x01; NtlmBufferWriter writer(kChallengeHeaderLen + 1); ASSERT_TRUE(writer.WriteBytes(raw)); ASSERT_TRUE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, NoTargetNameOverflowFromOffset) { NtlmClient client(NtlmFeatures(false)); uint8_t raw[kMinChallengeHeaderLen]; memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen); // Modify the default valid message to claim that the target name field is 1 // byte long overrunning the end of the message message. ASSERT_NE(0x01, raw[12]); ASSERT_EQ(0x00, raw[13]); ASSERT_NE(0x01, raw[14]); ASSERT_EQ(0x00, raw[15]); raw[12] = 0x01; raw[14] = 0x01; NtlmBufferWriter writer(kMinChallengeHeaderLen); ASSERT_TRUE(writer.WriteBytes(raw)); // The above malformed message could cause an implementation to read outside // the message buffer because the offset is past the end of the message. // Verify it gets rejected. ASSERT_FALSE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, NoTargetNameOverflowFromLength) { NtlmClient client(NtlmFeatures(false)); // Message has 1 extra byte of space after the header for the target name. // One extra byte is provided for target name. uint8_t raw[kMinChallengeHeaderLen + 1]; memcpy(raw, test::kMinChallengeMessage, kMinChallengeHeaderLen); // Put something in the target name. raw[kMinChallengeHeaderLen] = 'Z'; // Modify the default valid message to indicate 2 bytes are present in the // target name payload (however there is only space for 1). ASSERT_NE(0x02, raw[12]); ASSERT_EQ(0x00, raw[13]); ASSERT_NE(0x02, raw[14]); ASSERT_EQ(0x00, raw[15]); raw[12] = 0x02; raw[14] = 0x02; NtlmBufferWriter writer(kMinChallengeHeaderLen + 1); ASSERT_TRUE(writer.WriteBytes(raw)); // The above malformed message could cause an implementation // to read outside the message buffer because the length is // longer than available space. Verify it gets rejected. ASSERT_FALSE(GetAuthMsgResult(client, writer)); } TEST(NtlmClientTest, Type3UnicodeWithSessionSecuritySpecTest) { NtlmClient client(NtlmFeatures(false)); std::vector result = GenerateAuthMsg(client, test::kChallengeMsgV1); ASSERT_FALSE(result.empty()); ASSERT_EQ(std::size(test::kExpectedAuthenticateMsgSpecResponseV1), result.size()); ASSERT_EQ(0, memcmp(test::kExpectedAuthenticateMsgSpecResponseV1, result.data(), result.size())); } TEST(NtlmClientTest, Type3WithoutUnicode) { NtlmClient client(NtlmFeatures(false)); std::vector result = GenerateAuthMsg( client, base::make_span(test::kMinChallengeMessageNoUnicode) .subspan<0, kMinChallengeHeaderLen>()); ASSERT_FALSE(result.empty()); NtlmBufferReader reader(result); ASSERT_TRUE(reader.MatchMessageHeader(MessageType::kAuthenticate)); // Read the LM and NTLM Response Payloads. uint8_t actual_lm_response[kResponseLenV1]; uint8_t actual_ntlm_response[kResponseLenV1]; ASSERT_TRUE(ReadBytesPayload(&reader, actual_lm_response)); ASSERT_TRUE(ReadBytesPayload(&reader, actual_ntlm_response)); ASSERT_EQ(0, memcmp(test::kExpectedLmResponseWithV1SS, actual_lm_response, kResponseLenV1)); ASSERT_EQ(0, memcmp(test::kExpectedNtlmResponseWithV1SS, actual_ntlm_response, kResponseLenV1)); std::string domain; std::string username; std::string hostname; ASSERT_TRUE(ReadStringPayload(&reader, &domain)); ASSERT_EQ(test::kNtlmDomainAscii, domain); ASSERT_TRUE(ReadStringPayload(&reader, &username)); ASSERT_EQ(test::kUserAscii, username); ASSERT_TRUE(ReadStringPayload(&reader, &hostname)); ASSERT_EQ(test::kHostnameAscii, hostname); // The session key is not used in HTTP. Since NTLMSSP_NEGOTIATE_KEY_EXCH // was not sent this is empty. ASSERT_TRUE(reader.MatchEmptySecurityBuffer()); // Verify the unicode flag is not set and OEM flag is. NegotiateFlags flags; ASSERT_TRUE(reader.ReadFlags(&flags)); ASSERT_EQ(NegotiateFlags::kNone, flags & NegotiateFlags::kUnicode); ASSERT_EQ(NegotiateFlags::kOem, flags & NegotiateFlags::kOem); } TEST(NtlmClientTest, ClientDoesNotDowngradeSessionSecurity) { NtlmClient client(NtlmFeatures(false)); std::vector result = GenerateAuthMsg(client, base::make_span(test::kMinChallengeMessageNoSS) .subspan<0, kMinChallengeHeaderLen>()); ASSERT_FALSE(result.empty()); NtlmBufferReader reader(result); ASSERT_TRUE(reader.MatchMessageHeader(MessageType::kAuthenticate)); // Read the LM and NTLM Response Payloads. uint8_t actual_lm_response[kResponseLenV1]; uint8_t actual_ntlm_response[kResponseLenV1]; ASSERT_TRUE(ReadBytesPayload(&reader, actual_lm_response)); ASSERT_TRUE(ReadBytesPayload(&reader, actual_ntlm_response)); // The important part of this test is that even though the // server told the client to drop session security. The client // DID NOT drop it. ASSERT_EQ(0, memcmp(test::kExpectedLmResponseWithV1SS, actual_lm_response, kResponseLenV1)); ASSERT_EQ(0, memcmp(test::kExpectedNtlmResponseWithV1SS, actual_ntlm_response, kResponseLenV1)); std::u16string domain; std::u16string username; std::u16string hostname; ASSERT_TRUE(ReadString16Payload(&reader, &domain)); ASSERT_EQ(test::kNtlmDomain, domain); ASSERT_TRUE(ReadString16Payload(&reader, &username)); ASSERT_EQ(test::kUser, username); ASSERT_TRUE(ReadString16Payload(&reader, &hostname)); ASSERT_EQ(test::kHostname, hostname); // The session key is not used in HTTP. Since NTLMSSP_NEGOTIATE_KEY_EXCH // was not sent this is empty. ASSERT_TRUE(reader.MatchEmptySecurityBuffer()); // Verify the unicode and session security flag is set. NegotiateFlags flags; ASSERT_TRUE(reader.ReadFlags(&flags)); ASSERT_EQ(NegotiateFlags::kUnicode, flags & NegotiateFlags::kUnicode); ASSERT_EQ(NegotiateFlags::kExtendedSessionSecurity, flags & NegotiateFlags::kExtendedSessionSecurity); } // ------------------------------------------------ // NTLM V2 specific tests. // ------------------------------------------------ TEST(NtlmClientTest, SimpleConstructionV2) { NtlmClient client(NtlmFeatures(true)); ASSERT_TRUE(client.IsNtlmV2()); ASSERT_TRUE(client.IsEpaEnabled()); ASSERT_TRUE(client.IsMicEnabled()); } TEST(NtlmClientTest, VerifyNegotiateMessageV2) { NtlmClient client(NtlmFeatures(true)); std::vector result = client.GetNegotiateMessage(); ASSERT_FALSE(result.empty()); ASSERT_EQ(std::size(test::kExpectedNegotiateMsg), result.size()); ASSERT_EQ(0, memcmp(test::kExpectedNegotiateMsg, result.data(), result.size())); } TEST(NtlmClientTest, VerifyAuthenticateMessageV2) { // Generate the auth message from the client based on the test challenge // message. NtlmClient client(NtlmFeatures(true)); std::vector result = GenerateAuthMsg(client, test::kChallengeMsgFromSpecV2); ASSERT_FALSE(result.empty()); ASSERT_EQ(std::size(test::kExpectedAuthenticateMsgSpecResponseV2), result.size()); ASSERT_EQ(0, memcmp(test::kExpectedAuthenticateMsgSpecResponseV2, result.data(), result.size())); } TEST(NtlmClientTest, VerifyAuthenticateMessageInResponseToChallengeWithoutTargetInfoV2) { // Test how the V2 client responds when the server sends a challenge that // does not contain target info. eg. Windows 2003 and earlier do not send // this. See [MS-NLMP] Appendix B Item 8. These older Windows servers // support NTLMv2 but don't send target info. Other implementations may // also be affected. NtlmClient client(NtlmFeatures(true)); std::vector result = GenerateAuthMsg(client, test::kChallengeMsgV1); ASSERT_FALSE(result.empty()); ASSERT_EQ(std::size(test::kExpectedAuthenticateMsgToOldV1ChallegeV2), result.size()); ASSERT_EQ(0, memcmp(test::kExpectedAuthenticateMsgToOldV1ChallegeV2, result.data(), result.size())); } // When the challenge message's target info is maximum size, adding new AV_PAIRs // to the response will overflow SecurityBuffer. Test that we handle this. TEST(NtlmClientTest, AvPairsOverflow) { { NtlmClient client(NtlmFeatures(/*enable_NTLMv2=*/true)); std::vector short_challenge; ASSERT_NO_FATAL_FAILURE(MakeV2ChallengeMessage(0xfff, &short_challenge)); EXPECT_FALSE(GenerateAuthMsg(client, short_challenge).empty()); } { NtlmClient client(NtlmFeatures(/*enable_NTLMv2=*/true)); std::vector long_challenge; ASSERT_NO_FATAL_FAILURE(MakeV2ChallengeMessage(0xffff, &long_challenge)); EXPECT_TRUE(GenerateAuthMsg(client, long_challenge).empty()); } } } // namespace net::ntlm