使用 GSSManager 验证 Kerberos 工单

2022-09-05 00:29:33

我有以下代码:

public static void main(String args[]){
    try {
        //String ticket = "Negotiate YIGCBg...==";
        //byte[] kerberosTicket = ticket.getBytes();
        byte[] kerberosTicket = Base64.decode("YIGCBg...==");
        GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);
        context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length);
        String user = context.getSrcName().toString();
        context.dispose();
    } catch (GSSException e) {
        e.printStackTrace();
    } catch (Base64DecodingException e) {
        e.printStackTrace();
    }
}

当然,它失败了。这是例外情况:

GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)

我不知道我应该做些什么来解决这个问题。老实说,我并不真正了解Kerberos。

我通过发送带有相应标头的401(以“协商”作为值)获得了此票证。浏览器立即再次发出相同的请求,标头包含此票证。WWW-Authenticateauthorization

我希望我能验证票证并确定用户是谁。

我需要密钥表文件吗?如果是这样,我将使用什么凭据运行此命令?我正在尝试使用 Kerberos 票证对网站进行身份验证。凭据是否为 IIS 中的凭据?

我错过了什么?


更新 1从Michael-O的回复中,我做了更多的谷歌搜索并找到了这篇文章,这使我找到了这篇文章

在 上,我发现 .table 31.3.6.1.5.5.2 SPNEGO

现在,我已按照第一篇文章中的示例将其添加到我的凭据中。这是我的代码:

public static void main(String args[]){
    try {            
        Oid mechOid = new Oid("1.3.6.1.5.5.2");

        GSSManager manager = GSSManager.getInstance();

        GSSCredential myCred = manager.createCredential(null,
                GSSCredential.DEFAULT_LIFETIME,
                mechOid,
                GSSCredential.ACCEPT_ONLY);

        GSSContext context = manager.createContext(myCred);

        byte[] ticket = Base64.decode("YIGCBg...==");
        context.acceptSecContext(ticket, 0, ticket.length);
        String user = context.getSrcName().toString();
        context.dispose();
    } catch (GSSException e) {
        e.printStackTrace();
    } catch (Base64DecodingException e) {
        e.printStackTrace();
    }
}

但是现在代码失败,并出现以下错误:createCredential

GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos credentails)

这是整个票证:YIGCBgYrBgEFBQKgeDB2oDAwLgYKKwYBBAGCNwICCgYJKoZIgvcSAQICBgkqhkiG9xIBAgIGCisGAQQBgjcCAh6iQgRATlRMTVNTUAABAAAAl7II4g4ADgAyAAAACgAKACgAAAAGAbEdAAAAD0xBUFRPUC0yNDVMSUZFQUNDT1VOVExMQw==


答案 1

从Java验证SPNEGO票证是一个有点复杂的过程。这是一个简短的概述,但请记住,该过程可能有很多陷阱。你真的需要了解Active Directory,Kerberos,SPNEGO和JAAS如何运作以成功诊断问题。

在开始之前,请确保您知道 Windows 域的 kerberos 领域名称。出于这个答案的目的,我将假设它是MYDOMAIN。您可以通过从 cmd 窗口运行来获取领域名称。请注意,kerberos 区分大小写,并且领域几乎总是全部大写。echo %userdnsdomain%

步骤 1 - 获取 Kerberos 密钥表

为了使 kerberos 客户端能够访问服务,它会请求表示该服务的服务主体名称 [SPN] 的票证。SPN 通常派生自计算机名称和被访问的服务类型(例如 )。为了验证特定 SPN 的 kerberos 票证,您必须具有一个密钥表文件,其中包含 Kerberos 域控制器 [KDC] 票证授予票证 [TGT] 服务和服务提供商(您)都知道的共享机密。HTTP/www.my-domain.com

就 Active Directory 而言,KDC 是域控制器,共享机密只是拥有 SPN 的帐户的纯文本密码。SPN 可能由 AD 中的计算机或用户对象拥有。

如果要定义服务,则在 AD 中设置 SPN 的最简单方法是设置基于用户的 SPN,如下所示:

  1. 在AD中创建一个密码不会过期的未priviledge服务帐户,例如使用密码SVC_HTTP_MYSERVERReallyLongRandomPass
  2. 使用 Windows 实用工具将服务 SPN 绑定到帐户。最佳做法是为主机的短名称和 FQDN 定义多个 SPN:setspn

    setspn -U -S HTTP/myserver@MYDOMAIN SVC_HTTP_MYSERVER
    setspn -U -S HTTP/myserver.my-domain.com@MYDOMAIN SVC_HTTP_MYSERVER
    
  3. 使用 Java 的实用程序为帐户生成密钥表。ktab

    ktab -k FILE:http_myserver.ktab -a HTTP/myserver@MYDOMAIN ReallyLongRandomPass
    ktab -k FILE:http_myserver.ktab -a HTTP/myserver.my-domain.com@MYDOMAIN ReallyLongRandomPass
    

如果您尝试对绑定到计算机帐户或不受您控制的用户帐户的预先存在的 SPN 进行身份验证,则上述操作将不起作用。您需要从 ActiveDirectory 本身中提取密钥表。Wireshark Kerberos页面对此有一些很好的指示。

步骤 2 - 设置 krb5.conf

在创建描述您的域的 krb5.conf 中。请确保在此处定义的领域与为 SPN 设置的领域相匹配。如果不将文件放在 JVM 目录中,可以通过在命令行上进行设置来指向它。%JAVA_HOME%/jre/lib/security-Djava.security.krb5.conf=C:\path\to\krb5.conf

例:

[libdefaults]
  default_realm = MYDOMAIN

[realms]
  MYDOMAIN = {
    kdc = dc1.my-domain.com
    default_domain = my-domain.com
  }

[domain_realm]
  .my-domain.com = MYDOMAIN
  my-domain.com = MYDOMAIN

步骤 3 - 设置 JAAS login.conf

您的 JAAS 应定义一个登录配置,将 Krb5LoginModule 设置为接受器。下面是一个示例,它假定我们上面创建的 keytab 位于 中。通过在命令行上进行设置指向 JASS 配置文件。login.confC:\http_myserver.ktab-Djava.security.auth.login.config=C:\path\to\login.conf

http_myserver_mydomain {
  com.sun.security.auth.module.Krb5LoginModule required
  principal="HTTP/myserver.my-domain.com@MYDOMAIN"
  doNotPrompt="true" 
  useKeyTab="true" 
  keyTab="C:/http_myserver.ktab"
  storeKey="true"
  isInitiator="false";
};

或者,您可以在运行时生成 JAAS 配置,如下所示:

public static Configuration getJaasKrb5TicketCfg(
    final String principal, final String realm, final File keytab) {
  return new Configuration() {
    @Override
    public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
      Map<String, String> options = new HashMap<String, String>();
      options.put("principal",    principal);
      options.put("keyTab",       keytab.getAbsolutePath());
      options.put("doNotPrompt", "true");
      options.put("useKeyTab",   "true");
      options.put("storeKey",    "true");
      options.put("isInitiator", "false");

      return new AppConfigurationEntry[] {
        new AppConfigurationEntry(
          "com.sun.security.auth.module.Krb5LoginModule",
          LoginModuleControlFlag.REQUIRED, options)
      };
    }
  };
}

您将为此配置创建一个 LoginContext,如下所示:

LoginContext ctx = new LoginContext("doesn't matter", subject, null, 
  getJaasKrbValidationCfg("HTTP/myserver.my-domain.com@MYDOMAIN", "MYDOMAIN", 
    new File("C:/path/to/my.ktab")));

第4步 - 接受机票

这有点即兴,但一般的想法是定义一个PriviledgedAction,该操作使用票证执行SPNEGO协议。请注意,此示例不检查 SPNEGO 协议是否完整。例如,如果客户端请求服务器身份验证,则需要返回在 HTTP 响应的身份验证标头中生成的令牌。acceptSecContext()

public class Krb5TicketValidateAction implements PrivilegedExceptionAction<String> {
  public Krb5TicketValidateAction(byte[] ticket, String spn) {
    this.ticket = ticket;
    this.spn = spn;
  }

  @Override
  public String run() throws Exception {
    final Oid spnegoOid = new Oid("1.3.6.1.5.5.2");

    GSSManager gssmgr = GSSManager.getInstance();

    // tell the GSSManager the Kerberos name of the service
    GSSName serviceName = gssmgr.createName(this.spn, GSSName.NT_USER_NAME);

    // get the service's credentials. note that this run() method was called by Subject.doAs(),
    // so the service's credentials (Service Principal Name and password) are already 
    // available in the Subject
    GSSCredential serviceCredentials = gssmgr.createCredential(serviceName,
      GSSCredential.INDEFINITE_LIFETIME, spnegoOid, GSSCredential.ACCEPT_ONLY);

    // create a security context for decrypting the service ticket
    GSSContext gssContext = gssmgr.createContext(serviceCredentials);

    // decrypt the service ticket
    System.out.println("Entering accpetSecContext...");
    gssContext.acceptSecContext(this.ticket, 0, this.ticket.length);

    // get the client name from the decrypted service ticket
    // note that Active Directory created the service ticket, so we can trust it
    String clientName = gssContext.getSrcName().toString();

    // clean up the context
    gssContext.dispose();

    // return the authenticated client name
    return clientName;
  }

  private final byte[] ticket;
  private final String spn;
}

然后,要对票证进行身份验证,可以执行以下操作。假定 包含来自身份验证标头的已以 64 为基数解码的票证。如果格式为 .,则应从 HTTP 请求中的标头派生。例如,如果标头是 则应该是 .ticketspnHostHTTP/<HOST>@<REALM>Hostmyserver.my-domain.comspnHTTP/myserver.my-domain.com@MYDOMAIN

public boolean isTicketValid(String spn, byte[] ticket) {
  LoginContext ctx = null;
  try {
    // this is the name from login.conf.  This could also be a parameter
    String ctxName = "http_myserver_mydomain";

    // define the principal who will validate the ticket
    Principal principal = new KerberosPrincipal(spn, KerberosPrincipal.KRB_NT_SRV_INST);
    Set<Principal> principals = new HashSet<Principal>();
    principals.add(principal);

    // define the subject to execute our secure action as
    Subject subject = new Subject(false, principals, new HashSet<Object>(), 
      new HashSet<Object>());

    // login the subject
    ctx = new LoginContext("http_myserver_mydomain", subject);
    ctx.login();

    // create a validator for the ticket and execute it
    Krb5TicketValidateAction validateAction = new Krb5TicketValidateAction(ticket, spn);
    String username = Subject.doAs(subject, validateAction);
    System.out.println("Validated service ticket for user " + username 
      + " to access service " + spn );
    return true;
  } catch(PriviledgedActionException e ) {
     System.out.println("Invalid ticket for " + spn + ": " + e);
  } catch(LoginException e) {
    System.out.println("Error creating validation LoginContext for " 
      + spn + ": " + e);
  } finally {
    try {
      if(ctx!=null) { ctx.logout(); }
    } catch(LoginException e) { /* noop */ }
  }

  return false;
}

答案 2

这不是Kerberos门票,而是SPNEGO门票。您的上下文具有错误的机制。

编辑:虽然,你现在有正确的机甲,你的客户端正在向你发送一个GNSS-API无法处理的NTLM令牌。取 Base 64 令牌,解码为原始字节并显示 ASCII 字符。如果它以 开头,它肯定不会起作用,并且您已经破坏了Kerberos设置。NTLMSSP

编辑2:这是你的票:

60 81 82 06 06 2B 06 01 05 05 02 A0 78 30 76 A0 30 30 2E 06  `..+..... x0v 00..
0A 2B 06 01 04 01 82 37 02 02 0A 06 09 2A 86 48 82 F7 12 01  .+....7.....*H÷..
02 02 06 09 2A 86 48 86 F7 12 01 02 02 06 0A 2B 06 01 04 01  ....*H÷......+....
82 37 02 02 1E A2 42 04 40 4E 54 4C 4D 53 53 50 00 01 00 00  7...¢B.@NTLMSSP....
00 97 B2 08 E2 0E 00 0E 00 32 00 00 00 0A 00 0A 00 28 00 00  .².â....2.......(..
00 06 01 B1 1D 00 00 00 0F 4C 41 50 54 4F 50 2D 32 34 35 4C  ...±.....LAPTOP-245L
49 46 45 41 43 43 4F 55 4E 54 4C 4C 43                       IFEACCOUNTLLC       

这是 SPNEGO 令牌中包装的 NTLM 令牌。这仅仅意味着 Kerberos 由于某些原因而失败,例如,

  • SPN 未注册
  • 时钟摆
  • 不允许使用 Kerberos
  • 错误的 DNS 记录

最好的选择是在客户端上使用Wireshark来查找根本原因。

请注意,Java 不支持将 NTLM 作为 SPNEGO 子机制。NTLM 仅受 SSPI 和 Heimdal 支持。


推荐