【问题标题】:How to prevent browser from sending NTLM credentials?如何防止浏览器发送 NTLM 凭据?
【发布时间】:2018-07-20 14:29:23
【问题描述】:

我正在一个网站上工作,我们想通过 Spring Security Kerberos 使用 Kerberos 身份验证。所以,我们不支持 NTLM。当用户发出未经身份验证的请求时,服务器将回复带有标头 WWW-Authenticate: Negotiate 的 HTTP 401。

问题: 对于某些用户/配置,浏览器将发送 NTLM 凭据。服务器不一定在 Windows 上运行,因此它无法处理 NTLM 凭据。

据我了解,“协商”的意思是“请尽可能向我发送 Kerberos,否则发送 NTLM”。是否有其他设置显示“仅向我发送 Kerberos”?或者有什么方法可以告诉浏览器网站只支持 Kerberos?

作为后续,为什么浏览器没有可用的 Kerberos?在这种情况下,他们登录到同一个域。也许他们的凭据已经过期?

【问题讨论】:

  • 您没有提及用户正在运行哪些浏览器,以及后端的目录服务类型:Active Directory、OpenLDAP、Red Hat IdM 还是其他?
  • 在这个级别上,我不确定这是否重要。但对于我的测试,我使用的是 IE 11 和 Chrome。目录服务是 Active Directory。 Web 服务器完全在我的控制之下,而不是 IIS。我将 Java 与 Spring Security Kerberos 一起使用。这是我看到的事件序列 1. 浏览器发送未经身份验证的请求 2. 服务器回复 HTTP 401 Unauthorized, WWW-Authenticate: Negotiate header。 3. 浏览器响应 Kerberos(好!)或 NTLM(坏!)。当浏览器与服务器位于同一台计算机上时,它始终使用 NTLM。但我希望它永远不要使用 NTLM。
  • 当浏览器与服务器在同一台计算机上时,Kerberos 将无法工作。请使用单独的机器进行测试。 Windows 具有环回检查安全功能,旨在帮助防止对本地计算机的反射攻击。因此,如果 FQDN 与本地计算机名称不匹配,Kerberos 身份验证将失败,并且将回退到 NTLM。要限制或阻止 NTLM,请查看以下内容:docs.microsoft.com/en-us/windows/security/threat-protection/…
  • 您对“协商”的理解是错误的。 “协商”是指客户端和服务器协商相互接受的机械类型(如果可用)。这可能是也可能不是 Kerberos。
  • 如果浏览器在步骤 3 中响应 NTLM,您是否尝试过使用“HTTP 401 Unauthorized, WWW-Authenticate: Negotiate + response token”拒绝?在我的情况下,IE 然后使用包装 Kerberos 令牌的 SPNEGO 令牌发出第三个请求。

标签: kerberos ntlm spring-security-kerberos mit-kerberos


【解决方案1】:

不应混淆 Kerberos 和 Spnego。虽然 Spnego 经常用于 Kerberos 身份验证,但 Spnego 并不总是意味着 Kerberos,甚至不代表 Kerberos。

Spnego 是一种允许客户端和服务器协商相互接受的机械类型(如果可用)的协议。

这可能是也可能不是 Kerberos,具体取决于客户端和服务器在协商过程中请求的子机制。 协商过程可能需要多次握手尝试。

以人类语言为例。如果我按优先顺序说英语、拉丁语和祖鲁语,而你说爱斯基摩语和祖鲁语,那么我们最终会说祖鲁语。

在我目前正在测试的设置中,使用 Internet Explorer 作为客户端,以及使用 JAAS + GSS 作为服务器的自定义 Java 应用程序服务器,我观察到与您的评论类似的行为:

  1. 浏览器发送未经身份验证的请求
  2. 服务器回复 HTTP 401 Unauthorized, WWW-Authenticate: Negotiate header。
  3. 浏览器要么以 Negotiate + NTLM 令牌响应(错误!)。

在我的情况下,游戏并没有就此结束,它继续如下:

  1. 服务器回复 HTTP 401 Unauthorized, WWW-Authenticate: Negotiate + GSS response token
  2. 浏览器响应 Negotiate + Spnego NegoTokenTarg 包装 Kerberos 令牌。
  3. 服务器解开 Kerberos 令牌;解码和验证客户端;以 HTTP 200 响应,WWW-Authenticate: Negotiate + GSS 响应令牌

即我不会阻止浏览器发送 NTLM 令牌,我的服务器只是继续协商另一轮,直到它获得 Kerberos 令牌。

作为一个附带问题:Internet Explorer 11 在步骤 3 中提供的令牌不完全符合 Spnego,它既不是 NegTokenInit 也不是 NetTokenTarg,而且 127 字节的长度显然太短而无法包装或包装一个 Kerberos 令牌。

您正在使用 Spring Security Kerberos,但在评论中您表示对其他库感兴趣,所以下面是我的基于 JGSS 的 Spnego 身份验证代码。

为简洁起见,我省略了 JAAS 设置,但所有这些都发生在 JAAS Subject.doAs() 特权上下文中。

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]);
}

请注意,此代码“按原样”显示,作为示例。它正在进行中,存在许多缺陷:

1) getTokenType() 使用解码后的令牌,而 extractKerberosToken 作用于编码后的令牌,两者都应该对解码后的令牌使用字节操作。

2) 基于长度的令牌拒绝有点太简单了。我计划添加更好的 NTLM 令牌识别....

3) 我没有真正的 GSS 上下文循环。如果我不喜欢客户呈现的内容,我会拒绝并关闭上下文。 对于来自客户端的任何后续握手尝试,我会打开一个新的 GSS 上下文。

【讨论】:

  • 在我的情况下,浏览器不会直接通过 Active Directory 进行身份验证。相反,当它接受带有 Authenticate-negotiate 标头的 401 响应时,它会弹出登录对话框。防止登录表单自动显示的任何提示?
猜你喜欢
  • 1970-01-01
  • 2015-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-19
  • 2018-12-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多