Using GSSManager to validate a Kerberos ticket

2020-01-30 12:13发布

问题:

I have the following code:

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

Of course it fails. Here's the exception:

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

I don't know what I'm supposed to do to solve this. Honestly, I don't really understand Kerberos.

I got this ticket by sending a 401 with the appropriate header WWW-Authenticate with 'Negotiate' as the value. The browser immediately issued the same request again with an authorization header containing this ticket.

I was hoping I could validate the ticket and determine who the user is.

Do I need a keytab file? If so, what credentials would I run this under? I'm trying to use the Kerberos ticket for auth for a web-site. Would the credentials be the credentials from IIS?

What am I missing?


Update 1 From Michael-O's reply, I did a bit more googling and found this article, which led me to this article.

On table 3, I found 1.3.6.1.5.5.2 SPNEGO.

I have now added that to my credentials following the example from the first article. Here's my code:

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

But now the code is failing on createCredential with this error:

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

Here's the entire ticket: YIGCBgYrBgEFBQKgeDB2oDAwLgYKKwYBBAGCNwICCgYJKoZIgvcSAQICBgkqhkiG9xIBAgIGCisGAQQBgjcCAh6iQgRATlRMTVNTUAABAAAAl7II4g4ADgAyAAAACgAKACgAAAAGAbEdAAAAD0xBUFRPUC0yNDVMSUZFQUNDT1VOVExMQw==

回答1:

Validating an SPNEGO ticket from Java is a somewhat convoluted process. Here's a brief overview but bear in mind that the process can have tons of pitfalls. You really need to understand how Active Directory, Kerberos, SPNEGO, and JAAS all operate to successfully diagnose problems.

Before you start, make sure you know your kerberos realm name for your windows domain. For the purposes of this answer I'll assume it's MYDOMAIN. You can obtain the realm name by running echo %userdnsdomain% from a cmd window. Note that kerberos is case sensitive and the realm is almost always ALL CAPS.

Step 1 - Obtain a Kerberos Keytab

In order for a kerberos client to access a service, it requests a ticket for the Service Principal Name [SPN] that represents that service. SPNs are generally derived from the machine name and the type of service being accessed (e.g. HTTP/www.my-domain.com). In order to validate a kerberos ticket for a particular SPN, you must have a keytab file that contains a shared secret known to both the Kerberos Domain Controller [KDC] Ticket Granting Ticket [TGT] service and the service provider (you).

In terms of Active Directory, the KDC is the Domain Controller, and the shared secret is just the plain text password of the account that owns the SPN. A SPN may be owned by either a Computer or a User object within the AD.

The easiest way to setup a SPN in AD if you are defining a service is to setup a user-based SPN like so:

  1. Create an unpriviledged service account in AD whose password doesn't expire e.g. SVC_HTTP_MYSERVER with password ReallyLongRandomPass
  2. Bind the service SPN to the account using the windows setspn utility. Best practice is to define multiple SPNs for both the short name and the FQDN of the host:

    setspn -U -S HTTP/myserver@MYDOMAIN SVC_HTTP_MYSERVER
    setspn -U -S HTTP/myserver.my-domain.com@MYDOMAIN SVC_HTTP_MYSERVER
    
  3. Generate a keytab for the account using Java's ktab utility.

    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
    

If you are trying to authenticate a pre-existing SPN that is bound to a Computer account or to a User account you do not control, the above will not work. You will need to extract the keytab from ActiveDirectory itself. The Wireshark Kerberos Page has some good pointers for this.

Step 2 - Setup your krb5.conf

In %JAVA_HOME%/jre/lib/security create a krb5.conf that describes your domain. Make sure the realm you define here matches what you setup for your SPN. If you don't put the file in the JVM directory, you can point to it by setting -Djava.security.krb5.conf=C:\path\to\krb5.conf on the command line.

Example:

[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

Step 3 - Setup JAAS login.conf

Your JAAS login.conf should define a login configuration that sets up the Krb5LoginModule as a acceptor. Here's an example that assumes that the keytab we created above is in C:\http_myserver.ktab. Point to the JASS config file by setting -Djava.security.auth.login.config=C:\path\to\login.conf on the command line.

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";
};

Alternatively, you can generate a JAAS config at runtime like so:

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

You would create a LoginContext for this configuration like so:

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

Step 4 - Accepting the ticket

This is a little off-the-cuff, but the general idea is to define a PriviledgedAction that performs the SPNEGO protocol using the ticket. Note that this example does not check that SPNEGO protocol is complete. For example if the client requested server authentication, you would need to return the token generated by acceptSecContext() in the authentication header in the HTTP response.

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

Then to authenticate the ticket, you would do something like the following. Assume that ticket contains the already-base-64-decoded ticket from the authentication header. The spn should be derived from the Host header in the HTTP request if the format of HTTP/<HOST>@<REALM>. E.g. if the Host header was myserver.my-domain.com then spn should be HTTP/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:

This is not a Kerberos ticket but a SPNEGO ticket. Your context has the wrong mechanism.

Edit: Though, you now have the correct mech, you client is sending you a NTLM token which the GSS-API is not able to process. Take the Base 64 token, decode to raw bytes and display ASCII chars. If it starts with NTLMSSP, it won't work for sure and you have broken Kerberos setup.

Edit 2: This is your ticket:

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       

This is a wrapped NTLM token inside a SPNEGO token. which simply means that Kerberos has failed for some reasons, e.g.,

  • SPN not registered
  • Clockskew
  • Not allowed for Kerberos
  • Incorrect DNS records

Best option is to use Wireshark on the client to find the root cause.

Please note that Java does not support NTLM as a SPNEGO submechanism. NTLM is only supported by SSPI and Heimdal.



回答3:

If the server does not have a keytab and associated key registered the KDC, you will never be able use kerberos to validate a ticket.

Getting SPNEGO to work is tricky at best and will be next to impossible without at least a cursory understanding of how kerberos works. Try reading this dialog and see if you can get a better understanding.

http://web.mit.edu/kerberos/dialogue.html

SPNEGO requires an SPN of the form HTTP/server.example.com and you'll need to tell the GSS libraries where that keytab is when you start the server.