Definition

This article discusses the different ways to obtain a Kerberos TGT in Python and compares them.

Preparation

For testing, we will deploy MIT Kerberos in Docker (you can deploy FreeIPA, but it will be redundant)

The simplest Dockerfile:

FROM debian

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y krb5-kdc krb5-admin-server

RUN kdb5_util create -s -P admin123 -r EXAMPLE.ORG

COPY krb5.conf /etc/krb5.conf
COPY kadm5.acl /etc/krb5kdc/kadm5.acl

RUN service krb5-kdc start || true
RUN service krb5-admin-server start || true

RUN kadmin.local -q "add_principal -pw admin123 admin/admin@EXAMPLE.ORG"
RUN kadmin.local -q "add_principal -pw admin123 admin@EXAMPLE.ORG"

EXPOSE 88

ENTRYPOINT service krb5-kdc start && service krb5-admin-server start && /bin/bash

Before building, let’s create files from COPY instructions:

kadm5.acl:

# This file Is the access control list for krb5 administration.
# When this file is edited run service krb5-admin-server restart to activate
# One common way to set up Kerberos administration is to allow any principal 
# ending in /admin  is given full administrative rights.
# To enable this, uncomment the following line:
*/admin *

krb5.conf

[libdefaults]
  default_realm = EXAMPLE.ORG
  dns_lookup_realm = false
  dns_lookup_kdc = false
  dns_canonicalize_hostname = false

[realms]
  EXAMPLE.ORG = {
    kdc = kdc.example.org
    admin_server = kdc.example.org
  }

[domain_realm]
  .example.org = EXAMPLE.ORG
  example.org = EXAMPLE.ORG

The dns_* options are needed to avoid wasting time on DNS when getting a ticket

Building the image:

docker build . -t my_kerberos

Run in terminal:

docker run -h kdc.example.org -it --rm my_kerberos bash

After starting the container, be sure to add an entry to the /etc/hosts file with the container hostname:

# You can find the IP using the commands below, or find it yourself and add it to the /etc/hosts file
id=$(docker container ls --all --filter=ancestor="my_kerberos" --format "{{.ID}}")
ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $id)

echo "$ip kdc.example.org" >> /etc/hosts

Let’s try to get a ticket from another terminal:

echo admin123 | KRB5_CONFIG=krb5.conf kinit admin

We check that the ticket has been received:

klist

Practice

After setting up the test environment, you can start checking

Method #1: Subprocess

The simplest method, the only advantage over gssapi is the built-in support for multithreading (in gevent for sure, I don’t know about other libraries)

Let’s write a Python script:

#!/usr/bin/python3

import time
import subprocess
import os


os.environ["KRB5_CONFIG"] = "./krb5.conf"

start = time.perf_counter()

for _ in range(1):
    subprocess.run(['kinit', "admin"], input=b'admin123', stdout=subprocess.DEVNULL)

end = (time.perf_counter() - start) * 1000
print("The time of execution of above program is :", end, "ms")

Result of execution for for _ in range(1): and for _ in range(100): Python Subporcess TGT

We get the speed of receiving a ticket on average 8-9 ms (~10 ms for 1 receipt, 8-9 ms for 100 receipts)

Способ №2: python-gssapi

The recommended method uses a call to functions from the C language, which makes the code for obtaining a ticket thread-blocking (in gevent you can bypass it using pool.spawn())

To fully imitate the behavior of kinit, we will add saving the ticket to a file, but this step can be skipped, since the ticket can be used from a variable

Writing a Python script:

import time
import gssapi
import os


os.environ["KRB5_CONFIG"] = "./krb5.conf"

start = time.perf_counter()

name = gssapi.Name("admin@EXAMPLE.ORG", gssapi.NameType.kerberos_principal)

for _ in range(1):
    creds = gssapi.raw.acquire_cred_with_password(name, b"admin123")
    res = gssapi.Credentials(creds.creds)
    res.store(store={"ccache": "FILE:/tmp/test_krb5cc"}, overwrite=True)

end = (time.perf_counter() - start) * 1000
print("The time of execution of above program is :", end, "ms")

Result of execution for for _ in range(1): and for _ in range(100): Python gssapi TGT

We get the speed of receiving a ticket on average 5-7 ms (~7 ms for 1 receipt, 5-6 ms for 100 receipts)

Method #3: minikerberos

A little-known library written in pure Python, but has a large set of functions and classes for working with Kerberos, including for pentesting

Has a built-in ability to work with asyncio, but can work with others, since it is written in pure Python

Cons: obviously lower speed, as well as inconvenient kerberos_url format (for example, you can’t use admin/admin as a user because of formatting)

Writing a Python script:

#!/usr/bin/python3

import os
import time
from minikerberos.common.factory import KerberosClientFactory


os.environ["KRB5_CONFIG"] = "./krb5.conf"

start = time.perf_counter()

kerberos_url = "kerberos+password://EXAMPLE.ORG\\admin:admin123@172.20.0.2"

for _ in range(1):
    cu = KerberosClientFactory.from_url(kerberos_url)
    client = cu.get_client_blocking()
    client.get_TGT(with_pac=None)
    client.ccache.to_file('/tmp/test_krb5cc')

end = (time.perf_counter() - start) * 1000
print("The time of execution of above program is :", end, "ms")

Result of execution for for _ in range(1): and for _ in range(100): Python minikerberos TGT

We get the speed of receiving a ticket on average 27-33 ms (~30-33 ms for 1 receipt, 27-28 ms for 100 receipts)

Other libraries

The well-known impacket library has functions for working with Kerberos, but I couldn’t get them to work

For working with Kerberos in HTTP, there is a requests_gssapi library