jfjallid / go-smb

A client library to interact with Windows RPC services such as MS-SRVS and MS-RRP.
MIT License
44 stars 9 forks source link

Any Plans for Kerberos Implementation #13

Closed redt1de closed 1 month ago

redt1de commented 8 months ago

I have been looking for a decent implementation of smb in go, and have tried several. So far I am liking your implementation best. I am hoping to create a pure go implementation of some impacket functionality and I think your encoding will play nicely with DCERPC expansion. One deal breaker as of right now is the lack of Kerberos. Currently I am using a fork of go-smb2 which implements Kerberos auth and I just thought I would share this with you as it would be nice if we could adapt this to your project.

The fork I am using is https://github.com/lorenz/go-smb2 which relies on a fork of gokrb5 with a custom GSSAPI implementation https://github.com/lorenz/gokrb5

jfjallid commented 8 months ago

The initial purpose of go-smb was to serve as a replacement for impacket for me as I don't fancy Python. However, impacket is a massive project with rather extensive implementations of the supported protocols whereas go-smb follows the approach of only implementing the support and parts of the protocols needed for the various tools I want to have in Go such as go-shareenum or go-secdump.

I do plan to add partial Kerberos support at some point for authentication (and possibly to do other fun Kerberos things) but it is not at the top of my todo list and since I'm only doing this as a hobby project it will probably not happen any time soon without outside contributions.

As far as possible I try to build go-smb without external dependencies so I will probably not integrate with gokrb5, but instead build it myself.

jfjallid commented 8 months ago

On second thought, I'll take a look at integrating with the jcmturner/gokrb5 library to see how much work it would be.

redt1de commented 8 months ago

Glad to hear that. I did some experimenting last week and was able to get a rudimentary PoC working with the Kerberos initiator from the fork of go-smb2 I mentioned, and tweaking your SessionSetup function. It will take a bit more work to make it work with the main branch of gokrb5, as this initiator depends on a fork of gokrb5 with some extra GSSAPI code, but maybe it can help point you in the right direction.

This is what I ended up with for a session setup func. Keep in mind its just a PoC to see what it would take and Ive only tested it on a handful of SMB services in the GOAD lab. Just posting it in case it helps.

func (c *Connection) SessionSetupKerberos() error {
    spnegoClient := newSpnegoClient([]Initiator{c.options.Initiator})
    log.Debugln("Sending SessionSetup1 request")
    ssreq, err := c.NewSessionSetup1Req(spnegoClient)
    if err != nil {
        log.Errorln(err)
        return err
    }
    ssres, err := NewSessionSetup1Res()
    if err != nil {
        log.Errorln(err)
        return err
    }
    ssreq.Credits = 127
    ssreq.MessageID = 1

    rr, err := c.send(ssreq)
    if err != nil {
        log.Errorln(err)
        return err
    }
    ssresbuf, err := c.recv(rr)
    if err != nil {
        log.Errorln(err)
        return err
    }

    log.Debugln("Unmarshalling SessionSetup1 response")
    if err := encoder.Unmarshal(ssresbuf, &ssres); err != nil {
        log.Errorln(err)
        return err
    }

    if ssres.Header.Status != StatusMoreProcessingRequired && ssres.Header.Status != StatusOk {
        status, found := StatusMap[ssres.Header.Status]
        if !found {
            err = fmt.Errorf("Received unknown SMB Header status for SessionSetup1 response: 0x%x\n", ssres.Header.Status)
            log.Errorln(err)
            return err
        }
        log.Debugf("NT Status Error: %v\n", status)
        return status
    }

    c.sessionID = ssres.Header.SessionID
    if c.IsSigningRequired.Load() {
        if ssres.Flags&SessionFlagIsGuest != 0 {
            err = fmt.Errorf("guest account doesn't support signing")
            log.Errorln(err)
            return err
        } else if ssres.Flags&SessionFlagIsNull != 0 {
            err = fmt.Errorf("anonymous account doesn't support signing")
            log.Errorln(err)
            return err
        }
    }

    c.sessionFlags = ssres.Flags
    if c.Session.options.DisableEncryption {
        c.sessionFlags &= ^SessionFlagEncryptData
    } else if c.supportsEncryption {
        c.sessionFlags |= SessionFlagEncryptData
    }

    switch c.dialect {
    case DialectSmb_3_1_1:
        c.Session.preauthIntegrityHashValue = c.preauthIntegrityHashValue
        switch c.preauthIntegrityHashId {
        case SHA512:
            h := sha512.New()
            h.Write(c.Session.preauthIntegrityHashValue[:])
            h.Write(rr.pkt)
            h.Sum(c.Session.preauthIntegrityHashValue[:0])

            if ssres.Header.Status == StatusMoreProcessingRequired {
                h.Reset()
                h.Write(c.Session.preauthIntegrityHashValue[:])
                h.Write(ssresbuf)
                h.Sum(c.Session.preauthIntegrityHashValue[:0])
            }
        }
    }

    if c.options.Initiator.isNullSession() {
        // Anonymous auth
        c.sessionFlags |= SessionFlagIsNull
        c.sessionFlags &= ^SessionFlagEncryptData
    }

    off := ssres.SecurityBufferOffset
    ln := ssres.SecurityBufferLength
    _, err = spnegoClient.acceptSecContext(ssresbuf[off : off+ln])
    if err != nil {
        panic(err)
    }

    // Retrieve the full username used in the authentication attempt
    // <domain\username> or just <username> if domain component is empty
    c.Session.authUsername = c.options.Initiator.getUsername()

    // Check if we authenticated as guest or with a null session. If so, disable signing and encryption
    if ((ssres.Flags & SessionFlagIsGuest) == SessionFlagIsGuest) || ((ssres.Flags & SessionFlagIsNull) == SessionFlagIsNull) {
        c.IsSigningRequired.Store(false)
        c.options.DisableEncryption = true
        c.sessionFlags = ssres.Flags              //NOTE Replace all sessionFlags here?
        c.sessionFlags &= ^SessionFlagEncryptData // Make sure encryption is disabled

        if (ssres.Flags & SessionFlagIsGuest) == SessionFlagIsGuest {
            c.sessionFlags |= SessionFlagIsGuest
        } else {
            c.sessionFlags |= SessionFlagIsNull
        }
    }

    c.IsAuthenticated = true

    // Handle signing and encryption options
    if c.sessionFlags&(SessionFlagIsGuest|SessionFlagIsNull) == 0 {
        sessionKey := spnegoClient.sessionKey()

        switch c.dialect {
        case DialectSmb_2_0_2, DialectSmb_2_1:
            dlog.Debugln("Dialect is: DialectSmb_2_0_2 or DialectSmb_2_1")
            if !c.IsSigningDisabled {
                c.Session.signer = hmac.New(sha256.New, sessionKey)
                c.Session.verifier = hmac.New(sha256.New, sessionKey)
            }
        case DialectSmb_3_1_1:
            dlog.Debugln("Dialect is: DialectSmb_3_1_1")
            // SMB 3.1.1 requires either signing or encryption of requests, so can't disable signing.
            // Signingkey is always 128bit
            signingKey := kdf(sessionKey, []byte("SMBSigningKey\x00"), c.Session.preauthIntegrityHashValue[:], 128)

            if os.Getenv("SMB_LOGKEYS") == "1" {
                sess := binary.LittleEndian.AppendUint64(nil, c.sessionID)
                dlog.Debugf("Exported session secrets: %x,%x,,\n", sess, sessionKey)
            }

            switch c.signingId {
            case AES_CMAC:
                c.Session.signer, err = cmac.New(signingKey)
                if err != nil {
                    log.Errorln(err)
                    return err
                }
                c.Session.verifier, err = cmac.New(signingKey)
                if err != nil {
                    log.Errorln(err)
                    return err
                }
            default:
                err = fmt.Errorf("Unknown signing algorithm (%d) not implemented", c.signingId)
                log.Errorln(err)
                return err
            }

            // Determine size of L variable for the KDF
            var l uint32
            switch c.cipherId {
            case AES128GCM:
                l = 128
            case AES128CCM:
                l = 128
            case AES256CCM:
                l = 256
            case AES256GCM:
                l = 256
            default:
                err = fmt.Errorf("Cipher algorithm (%d) not implemented", c.cipherId)
                log.Errorln(err)
                return err
            }

            encryptionKey := kdf(sessionKey, []byte("SMBC2SCipherKey\x00"), c.Session.preauthIntegrityHashValue[:], l)
            decryptionKey := kdf(sessionKey, []byte("SMBS2CCipherKey\x00"), c.Session.preauthIntegrityHashValue[:], l)

            switch c.cipherId {
            case AES128GCM, AES256GCM:
                dlog.Debugln("Cipher is: AES128GCM or AES256GCM")
                ciph, err := aes.NewCipher(encryptionKey)
                if err != nil {
                    log.Errorln(err)
                    return err
                }
                c.Session.encrypter, err = cipher.NewGCMWithNonceSize(ciph, 12)
                if err != nil {
                    log.Errorln(err)
                    return err
                }

                ciph, err = aes.NewCipher(decryptionKey)
                if err != nil {
                    log.Errorln(err)
                    return err
                }
                c.Session.decrypter, err = cipher.NewGCMWithNonceSize(ciph, 12)
                if err != nil {
                    log.Errorln(err)
                    return err
                }
                log.Debugln("Initialized encrypter and decrypter with GCM")
            case AES128CCM, AES256CCM:
                dlog.Debugln("Cipher is: AES128CCM or AES256CCM")
                ciph, err := aes.NewCipher(encryptionKey)
                if err != nil {
                    log.Errorln(err)
                    return err
                }
                c.Session.encrypter, err = ccm.NewCCMWithNonceAndTagSizes(ciph, 11, 16)
                ciph, err = aes.NewCipher(decryptionKey)
                if err != nil {
                    log.Errorln(err)
                    return err
                }
                c.Session.decrypter, err = ccm.NewCCMWithNonceAndTagSizes(ciph, 11, 16)
                log.Debugln("Initialized encrypter and decrypter with CCM")
            default:
                err = fmt.Errorf("Cipher algorithm (%d) not implemented", c.cipherId)
                log.Errorln(err)
                return err
            }
        }
    }

    log.Debugln("Completed NegotiateProtocol and SessionSetup")

    c.enableSession()

    return nil
}
oiweiwei commented 6 months ago

The initial purpose of go-smb was to serve as a replacement for impacket for me as I don't fancy Python. However, impacket is a massive project with rather extensive implementations of the supported protocols whereas go-smb follows the approach of only implementing the support and parts of the protocols needed for the various tools I want to have in Go such as go-shareenum or go-secdump.

Hey! You may find this project useful for writing tools as it's more about generating the golang stubs for marshaling/unmarshaling MS RPC structures / requests. (And it has sort of client implementation with kerberos/ntlm as well): https://github.com/oiweiwei/go-msrpc

jfjallid commented 3 months ago

Took me some time as I have had much else to do, but finally I've implemented support for Kerberos authentication using a modified version of the jcmturner/gokrb5 library. There is now support for authenticating with password, NT hash and AES keys (and using cached TGT or TGS on Linux only) from commit 5a367faaa30f04d76949a099064411744c2de9ab and tag v0.5.0

jfjallid commented 1 month ago

Closing this as I consider it completed.