Skip to content

Commit

Permalink
Proper NTLM2 challenge support
Browse files Browse the repository at this point in the history
  • Loading branch information
ethomson committed Mar 14, 2019
1 parent cd331db commit 813560c
Show file tree
Hide file tree
Showing 6 changed files with 622 additions and 394 deletions.
23 changes: 15 additions & 8 deletions src/main/java/com/microsoft/tfs/tools/poxy/Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Base64;
import java.util.HashMap;
Expand Down Expand Up @@ -180,7 +181,15 @@ else if (!connectionHeaderRead)
if (options.isAuthenticationRequired() &&
!handleAuthentication(request, response))
{
keepAlive = HeaderUtils.isConnectionKeepAlive(response.getHeaders());
if (HeaderUtils.isConnectionKeepAlive(response.getHeaders()))
{
keepAlive = true;
}
else if (HeaderUtils.isConnectionClose(response.getHeaders()))
{
keepAlive = false;
}

continue;
}

Expand Down Expand Up @@ -309,7 +318,7 @@ else if (ntlmChallenge != null && message.getType() == 3)
String username = responseMessage.getUsername();
String password = options.getProxyCredentials(username);

if (NTLM.verifyResponse(username, null, password, ntlmChallenge, responseMessage))
if (password != null && NTLM.verifyResponse(username, null, password, ntlmChallenge, responseMessage))
{
authenticated = true;
return true;
Expand Down Expand Up @@ -344,29 +353,27 @@ else if (options.getAuthenticationType() == AuthenticationType.Basic &&
if (challengeMessage != null)
{
responseHeaders.add(new Header(Constants.PROXY_AUTHENTICATE_HEADER, "NTLM " + challengeMessage));
responseHeaders.add(new Header(Constants.CONNECTION_HEADER, Constants.CONNECTION_KEEP_ALIVE));
}
else
{
responseHeaders.add(new Header(Constants.PROXY_AUTHENTICATE_HEADER, "NTLM"));
responseHeaders.add(new Header(Constants.CONNECTION_HEADER, Constants.CONNECTION_CLOSE));
}
}
else if (options.getAuthenticationType() == AuthenticationType.Basic)
{
responseHeaders.add(new Header(Constants.PROXY_AUTHENTICATE_HEADER, "Basic realm=\"Proxy\""));
responseHeaders.add(new Header(Constants.CONNECTION_HEADER, Constants.CONNECTION_CLOSE));
}

String responseHtml = "<html><head><title>Proxy Authentication Required</title></head><body><p>Proxy Authentication Required</p></body></html>";
byte[] responseBytes = responseHtml.getBytes(StandardCharsets.US_ASCII);

responseHeaders.add(new Header(Constants.CONTENT_TYPE_HEADER, Constants.CONTENT_TYPE_TEXT_HTML));
responseHeaders.add(new Header(Constants.CONTENT_LENGTH_HEADER, Integer.toString(responseHtml.length())));
responseHeaders.add(new Header(Constants.CONTENT_LENGTH_HEADER, Integer.toString(responseBytes.length)));
responseHeaders.add(new Header(Constants.CONTENT_TYPE_HEADER, Constants.CONTENT_TYPE_TEXT_HTML + "; charset=iso-8859-1"));

response.writeHeaders(responseHeaders);
response.endHeaders();

response.writeLine(responseHtml);
response.getStream().write(responseBytes);
response.flush();
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public abstract class HeaderUtils
*/
public static final HeaderFilter NEVER_TRANSMIT_FILTER = new HeaderFilter(new String[]
{
"trailer", "upgrade"
"trailer", "upgrade", "via"
});

/*
Expand Down
55 changes: 34 additions & 21 deletions src/main/java/com/microsoft/tfs/tools/poxy/NTLM.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.Provider;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;

import javax.crypto.Cipher;
Expand All @@ -22,33 +23,49 @@ public class NTLM

public static NTLMMessage.Type2Message createChallenge(NTLMMessage.Type1Message negotiate) throws Exception
{
int flags = NTLMMessage.FLAG_NEGOTIATE_OEM |
(negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_UNICODE) |
NTLMMessage.FLAG_NEGOTIATE_NTLM;
int flags = 0;

if ((negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_UNICODE) == NTLMMessage.FLAG_NEGOTIATE_UNICODE)
{
flags |= NTLMMessage.FLAG_NEGOTIATE_UNICODE;
}
else if ((negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_OEM) == NTLMMessage.FLAG_NEGOTIATE_OEM)
{
flags |= NTLMMessage.FLAG_NEGOTIATE_OEM;
}
else
{
throw new Exception("Unknown charset");
}

final String hostname = Utils.getHostname();

flags |= (negotiate.getFlags() & NTLMMessage.FLAG_REQUEST_TARGET);
flags |= (negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_NTLM);
flags |= (negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_ALWAYS_SIGN);
flags |= (negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_EXTENDED_SESSIONSECURITY);
flags |= (negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_128);
flags |= (negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_56);

flags |= NTLMMessage.FLAG_TARGET_TYPE_SERVER;
flags |= NTLMMessage.FLAG_NEGOTIATE_TARGET_INFO;
final NTLMMessage.TargetInformation targetInfo = new NTLMMessage.TargetInformation(hostname, hostname, hostname, hostname, new Date());

flags |= (negotiate.getFlags() & NTLMMessage.FLAG_NEGOTIATE_VERSION);
final NTLMMessage.Version version = new NTLMMessage.Version((byte)0, (byte)5, (short)42);

final byte[] challenge = new byte[8];
ThreadLocalRandom.current().nextBytes(challenge);

return new NTLMMessage.Type2Message(flags, challenge, "poxyproxy", null);
return new NTLMMessage.Type2Message(flags, challenge, hostname, targetInfo, version);
}

public static boolean verifyResponse(String username, String domain, String password, NTLMMessage.Type2Message challenge, NTLMMessage.Type3Message response) throws Exception
{
// If we doesn't care about the domain, just use the web user's
domain = domain != null ? domain : response.getDomain();

// Some clients (notably firefox) like to send an NTLM2 Session Response here
// instead of a full-fledged NTLM2 response.
if ((response.getFlags() & NTLMMessage.NTLM_NEGOTIATE_NTLM2_SIGN_AND_SEAL) == NTLMMessage.NTLM_NEGOTIATE_NTLM2_SIGN_AND_SEAL)
{
return (verifyNTLM2SessionResponse(username, domain, password, challenge, response));
}
else if(!allowLM)
{
return (verifyNTLM2Response(username, domain, password, challenge, response));
}

return (verifyLMResponse(username, domain, password, challenge, response) && verifyNTLMResponse(username, domain, password, challenge, response));
return (verifyNTLM2Response(username, domain, password, challenge, response));
}

private static boolean verifyLMResponse(String username, String domain, String password, NTLMMessage.Type2Message challenge, NTLMMessage.Type3Message response) throws Exception
Expand Down Expand Up @@ -119,7 +136,7 @@ private static boolean verifyNTLM2Response(String username, String domain, Strin
return false;
}

if ((challenge.getFlags() & NTLMMessage.NTLM_NEGOTIATE_TARGET_INFO) == NTLMMessage.NTLM_NEGOTIATE_TARGET_INFO)
if ((challenge.getFlags() & NTLMMessage.FLAG_NEGOTIATE_TARGET_INFO) == NTLMMessage.FLAG_NEGOTIATE_TARGET_INFO)
{
if (challenge.getTargetInformation() == null)
{
Expand Down Expand Up @@ -152,10 +169,6 @@ private static boolean verifyNTLM2Response(String username, String domain, Strin
if (responseData[0] != 1 || responseData[1] != 1 || responseData[2] != 0 || responseData[3] != 0)
return false;

// Ensure the target information is the same as provided by the server
if (targetInfoLen > 0 && !arrayEquals(responseData, 28, challenge.getTargetInformation(), 0, targetInfoLen))
return false;

// Get the NTLM2 hash
byte[] ntlmHash = ntlm2Hash(username, password, domain);

Expand Down
Loading

0 comments on commit 813560c

Please sign in to comment.