Improved NTLM authentication API and implementation

This commit is contained in:
Tal Aloni 2017-01-13 23:34:37 +02:00
parent 6e52e002a6
commit 345f4ae444
7 changed files with 125 additions and 177 deletions

View file

@ -34,7 +34,8 @@ namespace SMBLibrary.Authentication
/// <summary>
/// NegotiateLanManagerKey and NegotiateExtendedSecurity are mutually exclusive
/// If both are set then NegotiateLanManagerKey must be ignored
/// If both are set then NegotiateLanManagerKey must be ignored.
/// NTLM v2 requires this flag to be set.
/// </summary>
NegotiateExtendedSecurity = 0x80000, // NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
NegotiateIdentify = 0x100000, // NTLMSSP_NEGOTIATE_IDENTIFY

View file

@ -13,13 +13,9 @@ namespace SMBLibrary.Server
{
public interface INTLMAuthenticationProvider
{
// CIFS style NTLM
byte[] GenerateServerChallenge();
User Authenticate(string accountNameToAuth, byte[] lmResponse, byte[] ntlmResponse);
// SSPI style NTLM
byte[] GetChallengeMessageBytes(byte[] negotiateMessageBytes);
User Authenticate(byte[] authenticateMessageBytes);
ChallengeMessage GetChallengeMessage(NegotiateMessage negotiateMessage);
bool Authenticate(AuthenticateMessage authenticateMessage);
/// <summary>
/// Permit access to this user via the guest user account if the normal authentication process fails.

View file

@ -28,7 +28,7 @@ namespace SMBLibrary.Server
/// <summary>
/// LM v1 / NTLM v1
/// </summary>
public User AuthenticateV1(string accountNameToAuth, byte[] serverChallenge, byte[] lmResponse, byte[] ntlmResponse)
private User AuthenticateV1(string accountNameToAuth, byte[] serverChallenge, byte[] lmResponse, byte[] ntlmResponse)
{
for (int index = 0; index < this.Count; index++)
{
@ -56,7 +56,7 @@ namespace SMBLibrary.Server
/// <summary>
/// LM v1 / NTLM v1 Extended Security
/// </summary>
public User AuthenticateV1Extended(string accountNameToAuth, byte[] serverChallenge, byte[] lmResponse, byte[] ntlmResponse)
private User AuthenticateV1Extended(string accountNameToAuth, byte[] serverChallenge, byte[] lmResponse, byte[] ntlmResponse)
{
for (int index = 0; index < this.Count; index++)
{
@ -80,7 +80,7 @@ namespace SMBLibrary.Server
/// <summary>
/// LM v2 / NTLM v2
/// </summary>
public User AuthenticateV2(string domainNameToAuth, string accountNameToAuth, byte[] serverChallenge, byte[] lmResponse, byte[] ntlmResponse)
private User AuthenticateV2(string domainNameToAuth, string accountNameToAuth, byte[] serverChallenge, byte[] lmResponse, byte[] ntlmResponse)
{
for (int index = 0; index < this.Count; index++)
{
@ -112,13 +112,13 @@ namespace SMBLibrary.Server
return null;
}
public byte[] GenerateServerChallenge()
private byte[] GenerateServerChallenge()
{
new Random().NextBytes(m_serverChallenge);
return m_serverChallenge;
}
public ChallengeMessage GetChallengeMessage(byte[] negotiateMessageBytes)
public ChallengeMessage GetChallengeMessage(NegotiateMessage negotiateMessage)
{
byte[] serverChallenge = GenerateServerChallenge();
@ -138,42 +138,29 @@ namespace SMBLibrary.Server
return message;
}
public byte[] GetChallengeMessageBytes(byte[] negotiateMessageBytes)
public bool Authenticate(AuthenticateMessage message)
{
ChallengeMessage message = GetChallengeMessage(negotiateMessageBytes);
return message.GetBytes();
}
if ((message.NegotiateFlags & NegotiateFlags.NegotiateAnonymous) > 0)
{
return this.EnableGuestLogin;
}
public User Authenticate(byte[] authenticateMessageBytes)
{
AuthenticateMessage message = new AuthenticateMessage(authenticateMessageBytes);
return Authenticate(message);
}
public User Authenticate(AuthenticateMessage message)
{
User user;
if ((message.NegotiateFlags & NegotiateFlags.NegotiateExtendedSecurity) > 0)
{
user = AuthenticateV1Extended(message.UserName, m_serverChallenge, message.LmChallengeResponse, message.NtChallengeResponse);
if (user == null)
{
// NTLM v2:
user = AuthenticateV2(message.DomainName, message.UserName, m_serverChallenge, message.LmChallengeResponse, message.NtChallengeResponse);
}
}
else
{
user = AuthenticateV1(message.UserName, m_serverChallenge, message.LmChallengeResponse, message.NtChallengeResponse);
}
if (user == null)
{
// NTLM v2
user = AuthenticateV2(message.DomainName, message.UserName, m_serverChallenge, message.LmChallengeResponse, message.NtChallengeResponse);
}
return user;
}
public User Authenticate(string accountNameToAuth, byte[] lmResponse, byte[] ntlmResponse)
{
return AuthenticateV1(accountNameToAuth, m_serverChallenge, lmResponse, ntlmResponse);
return (user != null);
}
public bool FallbackToGuest(string userName)

View file

@ -18,7 +18,7 @@ namespace SMBLibrary.Server.SMB1
/// </summary>
public class NegotiateHelper
{
internal static NegotiateResponseNTLM GetNegotiateResponse(SMB1Header header, NegotiateRequest request, byte[] serverChallenge)
internal static NegotiateResponseNTLM GetNegotiateResponse(SMB1Header header, NegotiateRequest request, INTLMAuthenticationProvider users)
{
NegotiateResponseNTLM response = new NegotiateResponseNTLM();
@ -37,7 +37,8 @@ namespace SMBLibrary.Server.SMB1
ServerCapabilities.LargeWrite;
response.SystemTime = DateTime.UtcNow;
response.ServerTimeZone = (short)-TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now).TotalMinutes;
response.Challenge = serverChallenge;
ChallengeMessage challengeMessage = users.GetChallengeMessage(CreateNegotiateMessage());
response.Challenge = challengeMessage.ServerChallenge;
response.DomainName = String.Empty;
response.ServerName = String.Empty;
@ -67,5 +68,13 @@ namespace SMBLibrary.Server.SMB1
return response;
}
private static NegotiateMessage CreateNegotiateMessage()
{
NegotiateMessage negotiateMessage = new NegotiateMessage();
negotiateMessage.NegotiateFlags = NegotiateFlags.NegotiateUnicode | NegotiateFlags.NegotiateOEM | NegotiateFlags.RequestTarget | NegotiateFlags.NegotiateSign | NegotiateFlags.NegotiateSeal | NegotiateFlags.NegotiateLanManagerKey | NegotiateFlags.NegotiateNTLMKey | NegotiateFlags.NegotiateAlwaysSign | NegotiateFlags.NegotiateVersion | NegotiateFlags.Negotiate128 | NegotiateFlags.Negotiate56;
negotiateMessage.Version = Authentication.Version.Server2003;
return negotiateMessage;
}
}
}

View file

@ -24,10 +24,11 @@ namespace SMBLibrary.Server.SMB1
// The PrimaryDomain field in the request is used to determine with domain controller should authenticate the user credentials,
// However, the domain controller itself does not use this field.
// See: http://msdn.microsoft.com/en-us/library/windows/desktop/aa378749%28v=vs.85%29.aspx
User user;
AuthenticateMessage message = CreateAuthenticateMessage(request.AccountName, request.OEMPassword, request.UnicodePassword);
bool loginSuccess;
try
{
user = users.Authenticate(request.AccountName, request.OEMPassword, request.UnicodePassword);
loginSuccess = users.Authenticate(message);
}
catch (EmptyPasswordNotAllowedException)
{
@ -35,9 +36,9 @@ namespace SMBLibrary.Server.SMB1
return new ErrorResponse(CommandName.SMB_COM_SESSION_SETUP_ANDX);
}
if (user != null)
if (loginSuccess)
{
ushort? userID = state.AddConnectedUser(user.AccountName);
ushort? userID = state.AddConnectedUser(message.UserName);
if (!userID.HasValue)
{
header.Status = NTStatus.STATUS_TOO_MANY_SESSIONS;
@ -46,7 +47,7 @@ namespace SMBLibrary.Server.SMB1
header.UID = userID.Value;
response.PrimaryDomain = request.PrimaryDomain;
}
else if (users.FallbackToGuest(user.AccountName))
else if (users.FallbackToGuest(message.UserName))
{
ushort? userID = state.AddConnectedUser("Guest");
if (!userID.HasValue)
@ -98,23 +99,25 @@ namespace SMBLibrary.Server.SMB1
MessageTypeName messageType = AuthenticationMessageUtils.GetMessageType(messageBytes);
if (messageType == MessageTypeName.Negotiate)
{
byte[] challengeMessageBytes = users.GetChallengeMessageBytes(messageBytes);
NegotiateMessage negotiateMessage = new NegotiateMessage(messageBytes);
ChallengeMessage challengeMessage = users.GetChallengeMessage(negotiateMessage);
if (isRawMessage)
{
response.SecurityBlob = challengeMessageBytes;
response.SecurityBlob = challengeMessage.GetBytes();
}
else
{
response.SecurityBlob = GSSAPIHelper.GetGSSTokenResponseBytesFromNTLMSSPMessage(challengeMessageBytes);
response.SecurityBlob = GSSAPIHelper.GetGSSTokenResponseBytesFromNTLMSSPMessage(challengeMessage.GetBytes());
}
header.Status = NTStatus.STATUS_MORE_PROCESSING_REQUIRED;
}
else // MessageTypeName.Authenticate
{
User user;
AuthenticateMessage authenticateMessage = new AuthenticateMessage(messageBytes);
bool loginSuccess;
try
{
user = users.Authenticate(messageBytes);
loginSuccess = users.Authenticate(authenticateMessage);
}
catch (EmptyPasswordNotAllowedException)
{
@ -122,9 +125,9 @@ namespace SMBLibrary.Server.SMB1
return new ErrorResponse(CommandName.SMB_COM_SESSION_SETUP_ANDX);
}
if (user != null)
if (loginSuccess)
{
ushort? userID = state.AddConnectedUser(user.AccountName);
ushort? userID = state.AddConnectedUser(authenticateMessage.UserName);
if (!userID.HasValue)
{
header.Status = NTStatus.STATUS_TOO_MANY_SESSIONS;
@ -132,7 +135,7 @@ namespace SMBLibrary.Server.SMB1
}
header.UID = userID.Value;
}
else if (users.FallbackToGuest(user.AccountName))
else if (users.FallbackToGuest(authenticateMessage.UserName))
{
ushort? userID = state.AddConnectedUser("Guest");
if (!userID.HasValue)
@ -154,5 +157,16 @@ namespace SMBLibrary.Server.SMB1
return response;
}
private static AuthenticateMessage CreateAuthenticateMessage(string accountNameToAuth, byte[] lmResponse, byte[] ntlmResponse)
{
AuthenticateMessage authenticateMessage = new AuthenticateMessage();
authenticateMessage.NegotiateFlags = NegotiateFlags.NegotiateUnicode | NegotiateFlags.NegotiateOEM | NegotiateFlags.RequestTarget | NegotiateFlags.NegotiateSign | NegotiateFlags.NegotiateSeal | NegotiateFlags.NegotiateLanManagerKey | NegotiateFlags.NegotiateNTLMKey | NegotiateFlags.NegotiateAlwaysSign | NegotiateFlags.NegotiateVersion | NegotiateFlags.Negotiate128 | NegotiateFlags.Negotiate56;
authenticateMessage.UserName = accountNameToAuth;
authenticateMessage.LmChallengeResponse = lmResponse;
authenticateMessage.NtChallengeResponse = ntlmResponse;
authenticateMessage.Version = Authentication.Version.Server2003;
return authenticateMessage;
}
}
}

View file

@ -64,8 +64,7 @@ namespace SMBLibrary.Server
}
else
{
byte[] serverChallenge = m_users.GenerateServerChallenge();
return NegotiateHelper.GetNegotiateResponse(header, request, serverChallenge);
return NegotiateHelper.GetNegotiateResponse(header, request, m_users);
}
}
else

View file

@ -29,158 +29,97 @@ namespace SMBLibrary.Server.Win32
}
}
public byte[] GenerateServerChallenge()
public ChallengeMessage GetChallengeMessage(NegotiateMessage negotiateMessage)
{
NegotiateMessage negotiateMessage = new NegotiateMessage();
negotiateMessage.NegotiateFlags = NegotiateFlags.NegotiateUnicode | NegotiateFlags.NegotiateOEM | NegotiateFlags.RequestTarget | NegotiateFlags.NegotiateSign | NegotiateFlags.NegotiateSeal | NegotiateFlags.NegotiateLanManagerKey | NegotiateFlags.NegotiateNTLMKey | NegotiateFlags.NegotiateAlwaysSign | NegotiateFlags.NegotiateVersion | NegotiateFlags.Negotiate128 | NegotiateFlags.Negotiate56;
negotiateMessage.Version = Authentication.Version.Server2003;
byte[] negotiateMessageBytes = negotiateMessage.GetBytes();
byte[] challengeMessageBytes = SSPIHelper.GetType2Message(negotiateMessageBytes, out m_serverContext);
ChallengeMessage challengeMessage = new ChallengeMessage(challengeMessageBytes);
m_serverChallenge = challengeMessage.ServerChallenge;
return m_serverChallenge;
}
public byte[] GetChallengeMessageBytes(byte[] negotiateMessageBytes)
{
byte[] challengeMessageBytes = SSPIHelper.GetType2Message(negotiateMessageBytes, out m_serverContext);
ChallengeMessage message = new ChallengeMessage(challengeMessageBytes);
m_serverChallenge = message.ServerChallenge;
return challengeMessageBytes;
return challengeMessage;
}
/// <summary>
/// Note: The 'limitblankpassworduse' (Under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa)
/// will cause AcceptSecurityContext to return SEC_E_LOGON_DENIED when the correct password is blank.
/// Authenticate will return false when the password is correct in these cases:
/// 1. The correct password is blank and 'limitblankpassworduse' is set to 1.
/// 2. The user is listed in the "Deny access to this computer from the network" list.
/// </summary>
public User Authenticate(string accountNameToAuth, byte[] lmResponse, byte[] ntlmResponse)
public bool Authenticate(AuthenticateMessage message)
{
if (accountNameToAuth == String.Empty ||
(String.Equals(accountNameToAuth, "Guest", StringComparison.InvariantCultureIgnoreCase) && IsPasswordEmpty(lmResponse, ntlmResponse) && this.EnableGuestLogin))
if ((message.NegotiateFlags & NegotiateFlags.NegotiateAnonymous) > 0)
{
int guestIndex = IndexOf("Guest");
if (guestIndex >= 0)
{
return this[guestIndex];
}
return null;
return this.EnableGuestLogin;
}
int index = IndexOf(accountNameToAuth);
if (index >= 0)
// AuthenticateType3Message is not reliable when 'limitblankpassworduse' is set to 1 and the user has an empty password set.
// Note: Windows LogonUser API calls will be listed in the security event log.
if (!AreEmptyPasswordsAllowed() &&
IsPasswordEmpty(message) &&
LoginAPI.HasEmptyPassword(message.UserName))
{
// We should not spam the security event log, and should call the Windows LogonUser API
// just to verify the user has a blank password.
if (!AreEmptyPasswordsAllowed() &&
IsPasswordEmpty(lmResponse, ntlmResponse) &&
LoginAPI.HasEmptyPassword(accountNameToAuth))
if (FallbackToGuest(message.UserName))
{
return false;
}
else
{
throw new EmptyPasswordNotAllowedException();
}
AuthenticateMessage authenticateMessage = new AuthenticateMessage();
authenticateMessage.NegotiateFlags = NegotiateFlags.NegotiateUnicode | NegotiateFlags.NegotiateOEM | NegotiateFlags.RequestTarget | NegotiateFlags.NegotiateSign | NegotiateFlags.NegotiateSeal | NegotiateFlags.NegotiateLanManagerKey | NegotiateFlags.NegotiateNTLMKey | NegotiateFlags.NegotiateAlwaysSign | NegotiateFlags.NegotiateVersion | NegotiateFlags.Negotiate128 | NegotiateFlags.Negotiate56;
authenticateMessage.UserName = accountNameToAuth;
authenticateMessage.LmChallengeResponse = lmResponse;
authenticateMessage.NtChallengeResponse = ntlmResponse;
authenticateMessage.Version = Authentication.Version.Server2003;
byte[] authenticateMessageBytes = authenticateMessage.GetBytes();
bool success = SSPIHelper.AuthenticateType3Message(m_serverContext, authenticateMessageBytes);
if (success)
{
return this[index];
}
}
return null;
}
/// <summary>
/// Note: The 'limitblankpassworduse' (Under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa)
/// will cause AcceptSecurityContext to return SEC_E_LOGON_DENIED when the correct password is blank.
/// </summary>
public User Authenticate(byte[] authenticateMessageBytes)
{
AuthenticateMessage message = new AuthenticateMessage(authenticateMessageBytes);
if ((message.NegotiateFlags & NegotiateFlags.NegotiateAnonymous) > 0 ||
(String.Equals(message.UserName, "Guest", StringComparison.InvariantCultureIgnoreCase) && IsPasswordEmpty(message) && this.EnableGuestLogin))
{
int guestIndex = IndexOf("Guest");
if (guestIndex >= 0)
{
return this[guestIndex];
}
return null;
}
int index = IndexOf(message.UserName);
if (index >= 0)
{
// We should not spam the security event log, and should call the Windows LogonUser API
// just to verify the user has a blank password.
if (!AreEmptyPasswordsAllowed() &&
IsPasswordEmpty(message) &&
LoginAPI.HasEmptyPassword(message.UserName))
{
throw new EmptyPasswordNotAllowedException();
}
bool success = SSPIHelper.AuthenticateType3Message(m_serverContext, authenticateMessageBytes);
if (success)
{
return this[index];
}
}
return null;
}
public bool IsPasswordEmpty(byte[] lmResponse, byte[] ntlmResponse)
{
// Special case for anonymous authentication
// Windows NT4 SP6 will send 1 null byte OEMPassword and 0 bytes UnicodePassword for anonymous authentication
if (lmResponse.Length == 0 || ByteUtils.AreByteArraysEqual(lmResponse, new byte[] { 0x00 }) || ntlmResponse.Length == 0)
{
return true;
}
byte[] emptyPasswordLMv1Response = NTAuthentication.ComputeLMv1Response(m_serverChallenge, String.Empty);
if (ByteUtils.AreByteArraysEqual(emptyPasswordLMv1Response, lmResponse))
{
return true;
}
byte[] emptyPasswordNTLMv1Response = NTAuthentication.ComputeNTLMv1Response(m_serverChallenge, String.Empty);
if (ByteUtils.AreByteArraysEqual(emptyPasswordNTLMv1Response, ntlmResponse))
{
return true;
}
return false;
byte[] messageBytes = message.GetBytes();
bool success = SSPIHelper.AuthenticateType3Message(m_serverContext, messageBytes);
return success;
}
public bool IsPasswordEmpty(AuthenticateMessage message)
{
// Special case for anonymous authentication, see [MS-NLMP] 3.3.1 - NTLM v1 Authentication
// See [MS-NLMP] 3.3.1 - NTLM v1 Authentication
// Special case for anonymous authentication:
if (message.LmChallengeResponse.Length == 1 || message.NtChallengeResponse.Length == 0)
{
return true;
}
byte[] clientChallenge = ByteReader.ReadBytes(message.LmChallengeResponse, 0, 8);
byte[] emptyPasswordNTLMv1Response = NTAuthentication.ComputeNTLMv1ExtendedSecurityResponse(m_serverChallenge, clientChallenge, String.Empty);
if (ByteUtils.AreByteArraysEqual(emptyPasswordNTLMv1Response, message.NtChallengeResponse))
if ((message.NegotiateFlags & NegotiateFlags.NegotiateExtendedSecurity) > 0)
{
return true;
}
// NTLM v1 extended security:
byte[] clientChallenge = ByteReader.ReadBytes(message.LmChallengeResponse, 0, 8);
byte[] emptyPasswordNTLMv1Response = NTAuthentication.ComputeNTLMv1ExtendedSecurityResponse(m_serverChallenge, clientChallenge, String.Empty);
if (ByteUtils.AreByteArraysEqual(emptyPasswordNTLMv1Response, message.NtChallengeResponse))
{
return true;
}
if (message.NtChallengeResponse.Length > 24)
// NTLM v2:
byte[] _LMv2ClientChallenge = ByteReader.ReadBytes(message.LmChallengeResponse, 16, 8);
byte[] emptyPasswordLMv2Response = NTAuthentication.ComputeLMv2Response(m_serverChallenge, _LMv2ClientChallenge, String.Empty, message.UserName, message.DomainName);
if (ByteUtils.AreByteArraysEqual(emptyPasswordLMv2Response, message.LmChallengeResponse))
{
return true;
}
if (message.NtChallengeResponse.Length > 24)
{
NTLMv2ClientChallengeStructure clientChallengeStructure = new NTLMv2ClientChallengeStructure(message.NtChallengeResponse, 16);
byte[] clientChallengeStructurePadded = clientChallengeStructure.GetBytesPadded();
byte[] emptyPasswordNTLMv2Response = NTAuthentication.ComputeNTLMv2Response(m_serverChallenge, clientChallengeStructurePadded, String.Empty, message.UserName, message.DomainName);
if (ByteUtils.AreByteArraysEqual(emptyPasswordNTLMv2Response, message.NtChallengeResponse))
{
return true;
}
}
}
else
{
NTLMv2ClientChallengeStructure clientChallengeStructure = new NTLMv2ClientChallengeStructure(message.NtChallengeResponse, 16);
byte[] clientChallengeStructurePadded = clientChallengeStructure.GetBytesPadded();
byte[] emptyPasswordNTLMv2Response = NTAuthentication.ComputeNTLMv2Response(m_serverChallenge, clientChallengeStructurePadded, String.Empty, message.UserName, message.DomainName);
if (ByteUtils.AreByteArraysEqual(emptyPasswordNTLMv2Response, message.NtChallengeResponse))
// NTLM v1:
byte[] emptyPasswordLMv1Response = NTAuthentication.ComputeLMv1Response(m_serverChallenge, String.Empty);
if (ByteUtils.AreByteArraysEqual(emptyPasswordLMv1Response, message.LmChallengeResponse))
{
return true;
}
byte[] emptyPasswordNTLMv1Response = NTAuthentication.ComputeNTLMv1Response(m_serverChallenge, String.Empty);
if (ByteUtils.AreByteArraysEqual(emptyPasswordNTLMv1Response, message.NtChallengeResponse))
{
return true;
}
@ -195,13 +134,16 @@ namespace SMBLibrary.Server.Win32
}
/// <summary>
/// We immitate Windows, Guest logins are disabled when the guest account has password set
/// We immitate Windows, Guest logins are disabled in any of these cases:
/// 1. The Guest account is disabled.
/// 2. The Guest account has password set.
/// 3. The Guest account is listed in the "deny access to this computer from the network" list.
/// </summary>
private bool EnableGuestLogin
{
get
{
return (IndexOf("Guest") >= 0) && LoginAPI.HasEmptyPassword("Guest");
return LoginAPI.ValidateUserPassword("Guest", String.Empty, LogonType.Network);
}
}