How to prevent browser from sending NTLM credentials?
Asked Answered
P

1

8

I’m working on a site where we want to use Kerberos authentication using Spring Security Kerberos. So, we don’t support NTLM. When the user makes an unauthenticated request, the server will reply with an HTTP 401 with header WWW-Authenticate: Negotiate.

The problem: For some users/configurations, the browser will send NTLM credentials. The server is not necessarily running on Windows so it can’t handle the NTLM credentials.

As I understand, “Negotiate” means “please send me Kerberos if possible, or else send NTLM”. Is there a different setting that says “only send me Kerberos”? Or is there some way to tell the browsers the site only supports Kerberos?

As a follow-up, why would the browser not have Kerberos available? In this case they are logged in to the same domain. Maybe their credentials have expired?

Pammie answered 20/7, 2018 at 14:29 Comment(9)
You neglected to mention what browser(s) users are running, and what Directory Service type on the back-end: Active Directory, OpenLDAP, Red Hat IdM or some other?Viridity
At this level, I'm not sure it matters. But for my testing, I'm using IE 11 and Chrome. The directory service is Active Directory. The web server is entirely under my control, not IIS. I'm using Java with Spring Security Kerberos. This is the sequence of events I'm seeing 1. Browser sends an unauthenticated request 2. Server replies with HTTP 401 Unauthorized, WWW-Authenticate: Negotiate header. 3. Browser either responds with Kerberos (good!) or NTLM (bad!). When the browser is on the same computer as the server, it always uses NTLM. But I want it to never use NTLM.Pammie
When the browser is on the same computer as the server, Kerberos won't work. Please use separate machines for testing. Windows has a loopback check security feature that is designed to help prevent reflection attacks on the local computer. Therefore, Kerberos authentication fails if the FQDN does not match the local computer name and fallback to NTLM will occur. To restrict or block NTLM, look into the following: learn.microsoft.com/en-us/windows/security/threat-protection/…Viridity
Thanks! I didn't know that Kerberos would be blocked on other accounts on the server.Pammie
Your understanding of "Negotiate " is incorrect. "Negotiate" means client and server negotiate a mutually acceptable mech type (if available). That may or may not be Kerberos.Defect
If the browser responds with NTLM in step 3, have you tried rejecting with "HTTP 401 Unauthorized, WWW-Authenticate: Negotiate + response token"? In my case IE then makes a 3rd request with a SPNEGO token wrapping a Kerberos token.Defect
Thanks @FlyingSheep. I figured the protocol couldn't be that silly. Do you have more info on how to get the "response token" part of the server response should be, to tell the browser "please use Kerberos"? I am using Spring Security Kerberos, if that matters, but willing to use a different library.Pammie
@guitarsteve I am using plain old Java JAAS + GSS.Defect
@guitarsteve ...I use the incoming token to generate a response token. For me this works even if the incoming token smells like NTLM. The response token should contain the Kerberos OID first, indicating the Server wishes to use that mech. byte gssapiData[] = Base64.getDecoder().decode(authBody); byte token[] = gssContext.acceptSecContext(gssapiData, offset, gssapiData.length); String responseToken = Base64.getEncoder().encodeToString(token); resp.status(401); resp.header(“WWW-Authenticate", "Negotiate "+responseToken);Defect
D
7

Kerberos and Spnego should not be confused. Though Spnego is often used for Kerberos authentication, Spnego does not always mean Kerberos, or even a preference for Kerberos.

Spnego is a protocol that allows client and server to negotiate a mutually acceptable mech type (if available).

That may or may not be Kerberos depending on the sub-mechanisms requested by the client and server during the negotiation process. The Negotiation process may take several handshake attempts.

Using human languages as an example. If I speak English, Latin and Zulu, in that order of preference, and you speak Eskimau and Zulu, then we will end up speaking Zulu.

In the setup that I am currently testing, with Internet Explorer as a client, and a custom Java Application Server using JAAS + GSS as the Server I observe similar behavour to that in your comment:

  1. Browser sends an unauthenticated request
  2. Server replies with HTTP 401 Unauthorized, WWW-Authenticate: Negotiate header.
  3. Browser either responds with Negotiate + NTLM token (bad!).

In my case the game does not end there, it continues as follows:

  1. Server replies with HTTP 401 Unauthorized, WWW-Authenticate: Negotiate + GSS response token
  2. Browser responds with Negotiate + Spnego NegoTokenTarg wrapping a Kerberos Token.
  3. Server unwraps the Kerberos Token; decodes, and authenticates the client; responds with HTTP 200, WWW-Authenticate: Negotiate + GSS response token

i.e. I don't prevent the browser sending an NTLM token, my Server just continues negotiation for another round until it gets a Kerberos Token.

As a side issue: the token provided by Internet Explorer 11 at step 3. above is not properly Spnego compliant, it is neither a NegTokenInit, nor a NetTokenTarg, and at 127 bytes long is clearly much too short to be or wrap a Kerberos token.

You are using Spring Security Kerberos, but in a comment you indicate an interest in other libraries, so below is my JGSS based Spnego authentication code.

For brevity I leave out the JAAS setup, but all this takes place in a JAAS Subject.doAs() privileged context.

public static final String NEGOTIATE =    "Negotiate ";
public static final String AUTHORIZATION = "Authorization";
public static final String WWWAUTHENTICATE = "WWW-Authenticate";
public static final int HTTP_OK = 200;
public static final int HTTP_GOAWAY = 401; //Unauthorized
public static final String SPNEGOOID = "1.3.6.1.5.5.2";
public static final String KRB5OID = "1.2.840.113554.1.2.2";

public void spnegoAuthenticate(Request req, Response resp, Service http) {

    GSSContext gssContext = null;
    String kerberosUser = null; 
    String auth =req.headers("Authorization");
    if ( auth != null && auth.startsWith(NEGOTIATE )) {
        //smells like an SPNEGO request, so get the token from the http headers
        String authBody = auth.substring(NEGOTIATE.length());
        int offset =0;

        // As GSS cannot directly process Spnego NegTokenInit and NegTokenTarg, preprocess and extract native Kerberos token.
        authBody = preProcessToken(authBody);

        try {     
            byte gssapiData[] = Base64.getDecoder().decode(authBody);

            gssContext = initGSSContext(SPNEGOOID, KRB5OID);
            byte token[] = gssContext.acceptSecContext(gssapiData, offset, gssapiData.length);

            if (gssapiData.length > 128) {
                //extract the Kerberos User. The Execute/Login service will compare this with the user in the message body.
                kerberosUser = gssContext.getSrcName().toString();
                resp.status(HTTP_OK);
            } else {
                //Is too short to be a kerberos token (or to wrap one), so don't try and extract the user.
                //This could be a first pass from an SPNEGO enabled Web-browser. Maybe NTLM?
                resp.status(HTTP_GOAWAY);
            }

            String responseToken = Base64.getEncoder().encodeToString(token);
            if (responseToken != null && responseToken.length() > 0) {
                resp.header(WWWAUTHENTICATE, NEGOTIATE + responseToken);    
            }         
        } catch (GSSException e) {
            // Something went wrong fishing the token from the http headers
            http.halt(401, "Go Away! This is a privileged route, and you ain't privileged!"+"\r\n");    
        } finally {
            try {
                gssContext.dispose();
            } catch (GSSException e) {
                //error handling here
            }
        }
    } else {
        //This is either not a SPNEGO request, or is the first pass without token 
        resp.header(WWWAUTHENTICATE, NEGOTIATE.trim()); //set header to suggest negotiation
        http.halt(HTTP_GOAWAY, "Go Away! This is a privileged route, and you ain't privileged! Only come back when you are."+"\r\n");
    }
}

private String preProcessToken(String authBody) {
    String tag = getTokenType(authBody); 
    if (tag.equals("60")) {
        // is a standard "application constructed" token. Kerberos tokens seem to start with "YI.."
    } else if (tag.equals("A0")) {
        // is a Spnego NegTokenInit, starting with "oA.." to "oP.."
        authBody=extractKerberosToken(authBody);
    } else if (tag.equals("A1")) {
        // is a Spnego NegTokenTarg, starting with "oQ.." to "oZ.."
        authBody=extractKerberosToken(authBody);
    } else {
        // some other unexpected token.
        // TODO: generate error
    }
    return authBody;
}

private String extractKerberosToken(String authBody) {
    return authBody.substring(authBody.indexOf("YI", 2));
}

private String getTokenType(String authBody) {
    return String.format("%02X",    Base64.getDecoder().decode(authBody.substring(0,2))[0]);
}

Note this code is presented "as-is", as an example. It is work-in-progress and has a number of flaws:

1) getTokenType() uses the decoded token, but extractKerberosToken works on the encoded token, both should use byte operations on the decoded token.

2) Token rejection based on length is a little too simple. I plan to add better NTLM token identification....

3) I don't have a true GSS context loop. If I don't like what the client presents, I reject and close the context. For any following handshake attempts from the client I open a new GSS context.

Defect answered 4/8, 2018 at 16:22 Comment(2)
In my case, browser doesn't authenticate with Active Directory directly. Instead, it pops up login dialog when it takes the 401 response with Authenticate-negotiate header. Any tips for preventing the automatic display of the login form?Durst
Hi, I’m hitting this very same issue but I’m using Windows API. AcceptSecurityContext errors with SEC_E_INSUFFICIENT_MEMORY when using the "Kerberos" security package. Have you heard of a method to generate a response token with Windows API to be able to do Step 4?Fayfayal

© 2022 - 2024 — McMap. All rights reserved.