Search code examples
pythonencryptionhttp-postkerberos

Encrypt message body using kerberos


How, we would encrypt me message body of the request using kerberos? I have kerberos ticket active.

My main goal is to send first request as authentication part and it is ok, i get 200 from the WEC. This is the code i am using:

import requests
import kerberos


def get_kerberos_token(service):
    __, krb_context = kerberos.authGSSClientInit(service)

    kerberos.authGSSClientStep(krb_context, "")
    negotiate_details = kerberos.authGSSClientResponse(krb_context)

    return negotiate_details


soap_message = "soap xml"
auth_url = "http://localhost:8081/wsman"

auth_headers = {
    'Host': 'localhost:8081',
    'Content-Length': '0',
    'Authorization': f'Kerberos {get_kerberos_token(service="http@localhost")}',
    'Content-Type': 'application/soap+xml;charset=UTF-8',
    'Accept-Encoding': 'gzip'
}

session = requests.Session()
auth_response = session.post(auth_url, headers=auth_headers)

print(f"Auth Response status code: {auth_response.status_code}")
print(f"Auth Response content: {auth_response.text}")

boundary = "Encrypted Boundary"
payload = f"--{boundary}\r\nContent-Type: application/HTTP-Kerberos-session-encrypted\r\nOriginalContent: type=application/soap+xml;charset=UTF-8;Length={len(soap_message)}\r\n--{boundary}\r\nContent-Type: application/octet-stream\r\n{soap_message.encode('utf-8')}--{boundary}\r\n"

# Define headers for the second request
soap_headers = {
    'Host': 'localhost:8081',
    'User-Agent': 'Go-http-client/1.1',
    'Content-Type': f'multipart/encrypted;protocol="application/HTTP-Kerberos-session-encrypted";boundary="{boundary}"',
    'Accept-Encoding': 'gzip'
}
# Send the second request with the multipart/encrypted body
soap_url = "http://localhost:8081/wsman/subscriptions/24f5eb95-d9b1-1005-8062-697970657274/0"
soap_response = session.post(soap_url, headers=soap_headers,
                             data=payload)

print(f"SOAP Response status code: {soap_response.status_code}")
print(f"SOAP Response content: {soap_response.text}")

This is what i get from the WEC if i run it:

Auth Response status code: 200
Auth Response content: 
SOAP Response status code: 400
SOAP Response content: failed to unwrap kerberos packet

From, what i see, the request body in the second request, needs to be encrypted with kerberos which i actually don't know how to do it. Is there a way to do it?

As a reference, this is what a WEC is expecting as well -> https://github.com/cea-sec/openwec/blob/main/doc/protocol.md#the-client-sends-a-enumerate-request


Solution

  • Don't build this yourself unless you have to – Python already has at least two implementations of WinRM/WSMAN available, which will handle Kerberos and everything else for you:

    • the winrm.Protocol() or winrm.Transport() classes of pywinrm;
    • the pypsrp.wsman.WSMan() class of pypsrp.

    Although the documentation of one is aimed towards running commands through WinRS and the other towards PS-Remoting, neither is inherently limited to that – both necessarily have a generic WinRM/WSMAN client hidden inside, e.g. pywinrm would give you winrm.Transport() that can send/receive arbitrary payloads. Both support Kerberos authentication and encryption (as well as CredSSP if you need that).

    client = winrm.Protocol(endpoint="...",
                            auth_method="kerberos")
    
    resp = client.send_message(request)
    
    client = pypsrp.wsman.WSMan(server="foo.example.com",
                                auth="kerberos")
    
    # Low level:
    resp = client.transport.send(request_xml)
    
    # High level:
    #resp = client.invoke(WSManAction.CREATE, ...)
    #resp = client.create(...)
    

    If you do want to build the WSMAN client yourself:

    Encryption would need to be done using the same krb_context as was used to create the authentication token. The context object will have an encrypt() or wrap() function for that purpose. Specifically, pykerberos has .authGSSWinRMEncryptMessage() that will use the correct parameters for WinRM, and .authGSSClientWrap() for all other purposes.

    Earlier versions of pywinrm used pykerberos via requests-kerberos, whose HTTPKerberosAuth() object used to have .wrap_winrm() and .unwrap_winrm() methods that did something like this (sadly the requests-kerberos helper methods have been removed since):

    # Code copied from requests_kerberos/kerberos_.py
    
    enc_message, signature = kerberos.authGSSWinRMEncryptMessage(krb_context,
                                                                 raw_message)
    
    # Code copied from pywinrm/winrm/encryption.py
    
    #session.auth = requests_kerberos.HTTPKerberosAuth(...)
    #enc_message, signature = session.auth.wrap_winrm(hostname, raw_message)
    
    payload = struct.pack("<i", len(signature)) + signature + enc_message
    

    That being said, I recommend upgrading from the mostly-abandoned pykerberos to pyspnego (or to the Windows-specific sspilib or Unix-specific python-gssapi for really manual work). All three have more pleasant APIs than pykerberos (which is a 1:1 clone of an Objective-C API). For example, this is how you'd use pyspnego to do the same thing – it was created for pypsrp, so it also happens to have WinRM-specific helpers:

    # Code copied from pypsrp/pypsrp/negotiate.py
    
    krb_context = spnego.client(hostname="localhost",
                                # The 'HTTP' service name should always be
                                # uppercase, even if AD lets you do otherwise
                                service="HTTP",
                                protocol="negotiate")
    
    auth_token = krb_context.step()
    
    # Code copied from pypsrp/pypsrp/encryption.py
    
    header, enc_message, pad_length = krb_context.wrap_winrm(raw_message)
    
    payload = struct.pack("<i", len(signature)) + signature + enc_message
    length = len(payload) + pad_length