Windows NTLM协议细节

  21 mins to read  

List [CTL]

    TL; DR

    为掌握细节并加深理解,参考MSDN实现一个NTLMSSP库。repo:https://github.com/eddieivan01/ntlmssp

    本文主要篇幅介绍NTLM认证协议的细节,对于互联网上关于NTLM常见的知识不作赘述

    文末是使用该库实现的两个Example:SSPI本地令牌协商NTLM over HTTP

    NTLM Message

    三种message中Signature, MessageType, NegotiateFlags, Payload字段是共有的

    Signature是8-byte字节数组,{ 'N', 'T', 'L', 'M', 'S', 'S', 'P', '\0' }

    MessageType是32-bit unsigned integer,在type1/type2/type3中分别为1/2/3

    NegotiateFlags是32-bit的Negotiate structure,这个结构体比较重要,记录了NTLM认证中一些协商配置项,后文会详细介绍

    Payload字段是一个变长的字节数组,message中的变长字段都是通过定长的Len/BufferOffset来存储在Payload

    除此之外还有一个8-byte的Version structure,仅作debug用,当NegotiateFlags中的NEGOTIATE_VERSION被设置时才会填充该字段

    Negotiate Message

    type NegotiateMsg struct {
        Signature      [8]byte
        MessageType    uint32
        NegotiateFlags uint32
    
        DomainNameLen          uint16
        DomainNameMaxLen       uint16
        DomainNameBufferOffset uint32
    
        WorkstationLen          uint16
        WorkstationMaxLen       uint16
        WorkstationBufferOffset uint32
    
        // Version is variable
        // Version [8]byte
        Payload []byte
    }
    

    Negotiage Message中可以设置Domain和Workstation,是否设置了这两个字段由NegotiateFlagsNTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED 指示。从这两个flag的名称也可以看出,Domain和Workstation必须是OEM编码(这里为什么单独提一下,因为后面的字段需要根据协商结果来决定是OEM还是UNICODE)

    DomainName举例如何设置变长字段:

    当Client设置DomainName时,需要设置DomainNameLenDomainName的长度,DomainNameMaxLen必须等于DomainNameLen,然后将OEM编码的DomainName写入Payload字段中,然后将DomainName距离Negotiate Message开头的偏移保存在DomainNameBufferOffset中,最后设置NegotiateFlagsNTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED

    Challenge Message

    type ChallengeMsg struct {
        Signature   [8]byte
        MessageType uint32
    
        TargetNameLen          uint16
        TargetNameMaxLen       uint16
        TargetNameBufferOffset uint32
    
        NegotiateFlags  uint32
        ServerChallenge [8]byte
        Reserved        [8]byte
    
        TargetInfoLen          uint16
        TargetInfoMaxLen       uint16
        TargetInfoBufferOffset uint32
    
        // Version is variable
        // Version [8]byte
        Payload []byte
    }
    

    TargetName

    当Client发送的Type1NegotiateFlags设置了NTLMSSP_REQUEST_TARGET时,需要设置该字段

    该字段的值是Server Name还是Domain Name由Type2NegotiateFlagsNTLMSSP_TARGET_TYPE_SERVER / NTLMSSP_TARGET_TYPE_DOMAIN决定

    该字段由协商的字符集编码(OEM or UNICODE),也就是NegotiateFlagsNTLM_NEGOTIATE_OEM / NTLMSSP_NEGOTIATE_UNICODE


    ServerChallenge很明显,Server生成的8-byte challenge

    Reserved是保留字段,全0


    TargetInfo

    如果Server设置该字段,需要同时设置Type2NegotiateFlagsNTLMSSP_NEGOTIATE_TARGET_INFO

    该变长字段是一个AV_PAIR structure,其中包括:

    MsvAvEOL
    * MsvAvNbComputerName
    * MsvAvNbDomainName
    * MsvAvDnsComputerName
    * MsvAvDnsDomainName
    * MsvAvDnsTreeName
    MsvAvFlags
    MsvAvTimestamp
    MsvAvSingleHost
    * MsvAvTargetName
    MsvAvChannelBindings
    

    带星号的textual value必须是UNICODE编码

    AV_PAIR还会在NTLMv2 Response中的NTLMv2_CLIENT_CHALLENGE中出现

    Authenticate Message

    type AuthenticateMsg struct {
        Signature   [8]byte
        MessageType uint32
    
        LmChallengeResponseLen          uint16
        LmChallengeResponseMaxLen       uint16
        LmChallengeResponseBufferOffset uint32
    
        NtChallengeResponseLen          uint16
        NtChallengeResponseMaxLen       uint16
        NtChallengeResponseBufferOffset uint32
    
        DomainNameLen          uint16
        DomainNameMaxLen       uint16
        DomainNameBufferOffset uint32
    
        UserNameLen          uint16
        UserNameMaxLen       uint16
        UserNameBufferOffset uint32
    
        WorkstationLen          uint16
        WorkstationMaxLen       uint16
        WorkstationBufferOffset uint32
    
        EncryptedRandomSessionKeyLen          uint16
        EncryptedRandomSessionKeyMaxLen       uint16
        EncryptedRandomSessionKeyBufferOffset uint32
    
        NegotiateFlags uint32
        // Version is variable
        // Version        [8]byte
    
        // The MIC field is omitted in Windows NT, Windows 2000, Windows XP, and Windows Server 2003.
        // MIC     [16]byte
        Payload []byte
    }
    

    LmChallengeResponse / NtChallengeResponse在后面计算响应时介绍

    DomainName / UserName / Workstaion是认证的用户名/域名/工作组名,必须是协商的编码

    EncryptedRandomSessionKey是Client生成的一个Session Key,用于安全会话通信

    还有一个MIC(message integrity code)字段,这个字段和VERSION一样,都有可能不填充,但并没有flag来指示是否填充,微软只丢了一句话:The MIC field is omitted in Windows NT, Windows 2000, Windows XP, and Windows Server 2003.。所以在解析时只能根据Offset探测是否有MIC字段

    ( 其实在AV_PAIRMsvAvFlags字段有这样一个标注:0x00000002: Indicates that the client is providing message integrity in the MIC field (section 2.2.1.3) in the AUTHENTICATE_MESSAGE.,然而Type3中并没有AV_PAIR

    Negotiate Flags

    const (
        NEGOTIATE_56BIT_ENCRYPTION           = 0x80000000
        NEGOTIATE_EXPLICIT_KEY_EXCHANGE      = 0x40000000
        NEGOTIATE_128BIT_SESSION_KEY         = 0x20000000
        NEGOTIATE_R1_UNUSED                  = 0x10000000
        NEGOTIATE_R2_UNUSED                  = 0x8000000
        NEGOTIATE_R3_UNUSED                  = 0x4000000
        NEGOTIATE_VERSION                    = 0x2000000
        NEGOTIATE_R4_UNUSED                  = 0x1000000
        NEGOTIATE_TARGET_INFO                = 0x800000
        NEGOTIATE_REQUEST_NON_NT_SESSION_KEY = 0x400000
        NEGOTIATE_R5_UNUSED                  = 0x200000
        NEGOTIATE_IDENTITY_LEVEL_TOKEN       = 0x100000
        NEGOTIATE_EXTENDED_SESSION_SECURITY  = 0x80000
        NEGOTIATE_R6_UNUSED                  = 0x40000
        NEGOTIATE_TARGET_TYPE_SERVER         = 0x20000
        NEGOTIATE_TARGET_TYPE_DOMAIN         = 0x10000
        NEGOTIATE_ALWAYS_SIGN                = 0x8000
        NEGOTIATE_R7_UNUSED                  = 0x4000
        NEGOTIATE_OEM_WORKSTATION_SUPPLIED   = 0x2000
        NEGOTIATE_OEM_DOMAIN_SUPPLIED        = 0x1000
        NEGOTIATE_ANONYMOUS                  = 0x800
        NEGOTIATE_R8_UNUSED                  = 0x400
        NEGOTIATE_NTLM                       = 0x200
        NEGOTIATE_R9_UNUSED                  = 0x100
        NEGOTIATE_LM_SESSION_KEY             = 0x80
        NEGOTIATE_DATAGRAM_CONNECTIONLESS    = 0x40
        NEGOTIATE_SEAL                       = 0x20
        NEGOTIATE_SIGN                       = 0x10
        NEGOTIATE_R10_UNUSED                 = 0x8
        NEGOTIATE_REQUEST_TARGET_NAME        = 0x4
        NEGOTIATE_OEM_CHARSET                = 0x2
        NEGOTIATE_UNICODE_CHARSET            = 0x1
    )
    

    一些常见的flag:

    NEGOTIATE_56BIT_ENCRYPTION / NEGOTIATE_128BIT_SESSION_KEY:56-bit or 128-bit session key

    NEGOTIATE_VERSION:是否填充VERSION字段

    NEGOTIATE_TARGET_INFO:在Type2中设置,是否有TargetInfo字段

    NEGOTIATE_EXTENDED_SESSION_SECURITY:扩展会话安全,后文计算响应中会用到

    NEGOTIATE_TARGET_TYPE_SERVER / NEGOTIATE_TARGET_TYPE_DOMAINType2中设置,TargetName值的类型

    NEGOTIATE_OEM_WORKSTATION_SUPPLIED / NEGOTIATE_OEM_DOMAIN_SUPPLIEDType1中是否有DOMAIN / WORKSTATION字段

    NEGOTIATE_ANONYMOUS:匿名认证

    NEGOTIATE_DATAGRAM_CONNECTIONLESS:使用connectionless(UDP)

    NEGOTIATE_REQUEST_TARGET_NAMEType1中设置,请求TargetName

    NEGOTIATE_OEM_CHARSET / NEGOTIATE_UNICODE_CHARSET:编码协商

    Response Compute

    Note The LM and NTLM authentication versions are not negotiated by the protocol. It MUST be configured on both the client and the server prior to authentication.

    所以NegotiateFlags中的NEGOTIATE_REQUEST_NON_NT_SESSION_KEY / NEGOTIATE_NTLM / NEGOTIATE_LM_SESSION_KEY 和认证过程都没什么关系(它们和后续的会话安全有关),唯一和认证过程响应计算有关的是NEGOTIATE_EXTENDED_SESSION_SECURITY

    Server在验证时需要通过Response长度(是否大于24)和NEGOTIATE_EXTENDED_SESSION_SECURITY来决定使用哪种计算方法

    认证有以下三种,后两种都有Client生成的nonce,缓解了彩虹表攻击:

    具体计算方法不赘述了,直接看代码吧

    NTLMv1 Authentication

    分别计算LmChallengeResponseNtChallengeResponse然后设置到Type3(实际情况里可能只发送其中一种)

    这两个Response的计算过程基本相同(除了一个需要NT hash,一个需要LM hash)

    func ComputeLMv1Response(challenge []byte, lmhash []byte) []byte {
        lmhash = append(lmhash, []byte{0, 0, 0, 0, 0}...)
    
        output := append(desEnc(padding(lmhash[:7]), challenge), desEnc(padding(lmhash[7:14]), challenge)...)
        output = append(output, desEnc(padding(lmhash[14:]), challenge)...)
        return output
    }
    
    func ComputeNTLMv1Response(challenge []byte, nthash []byte) []byte {
        return ComputeLMv1Response(challenge, nthash)
    }
    

    NTLMv1 With Client Challenge

    If NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY set, requests usage of the NTLM v2 session security. NTLM v2 session security is a misnomer because it is not NTLM v2. It is NTLM v1 using the extended session security that is also in NTLM v2.

    当设置了NEGOTIATE_EXTENDED_SESSION_SECURITY,需要先生成8-byte的nonce,然后填充16个'\0',设置在Type3LmChallengeResponse字段

    接着计算NTLMv2SessionResponse并设置在Type3NtChallengeResponse

    func ComputeNTLMv2SessionResponse(challenge []byte, clientNonce []byte, nthash []byte) []byte {
        if clientNonce == nil {
            clientNonce = make([]byte, 8)
            rand.Read(clientNonce)
        }
    
        sessionHash := md5Hash(append(challenge, clientNonce...))[:8]
        nthash = append(nthash, []byte{0, 0, 0, 0, 0}...)
        output := append(desEnc(padding(nthash[:7]), sessionHash), desEnc(padding(nthash[7:14]), sessionHash)...)
        return append(output, desEnc(padding(nthash[14:21]), sessionHash)...)
    }
    
    func (am *AuthenticateMsg) SetNTLMResponse(version int, challenge []byte, pwd []byte) {
        if version == 1 && am.NegotiateFlags&NEGOTIATE_EXTENDED_SESSION_SECURITY != 0 {
            nonce := [24]byte{}
            rand.Read(nonce[:8])
            am.LmChallengeResponseLen = 24
            am.LmChallengeResponseMaxLen = 24
            am.LmChallengeResponseBufferOffset = am.ptr
            am.Payload = append(am.Payload, nonce[:]...)
            am.ptr += uint32(am.LmChallengeResponseLen)
    
            ntsresp := ComputeNTLMv2SessionResponse(challenge, nonce[:8], NtHash([]byte(pwd)))
            am.NtChallengeResponseLen = uint16(len(ntsresp))
            am.NtChallengeResponseMaxLen = am.NtChallengeResponseLen
            am.NtChallengeResponseBufferOffset = am.ptr
            am.Payload = append(am.Payload, ntsresp...)
            am.ptr += uint32(am.NtChallengeResponseLen)
    
        } else {
            am.setLmResponse(version, challenge, pwd)
            am.setNtResponse(version, challenge, pwd)
        }
    }
    

    NTLMv2 Authentication

    LMv2Response的计算,值得注意的是,LMv2 Response的计算用的是NT Hash

    func ComputeLMv2Response(challenge []byte, usernameWithDomainOrServer []byte, nthash []byte, clientNonce []byte) []byte {
        if clientNonce == nil {
            clientNonce = make([]byte, 8)
            rand.Read(clientNonce)
        }
        hsh := hmacMd5(nthash, bytes.ToUpper(usernameWithDomainOrServer))
        return append(hmacMd5(hsh, append(challenge, clientNonce...)), clientNonce...)
    }
    

    NTLMv2Response的结构:

    type NTLMv2Response struct {
        Response        [16]byte
        ClientChallenge NTLMv2ClientChallenge
    }
    
    type NTLMv2ClientChallenge struct {
        RespType            byte
        HiRespType          byte
        Reserved1           uint16
        Reserved2           uint32
        TimeStamp           uint64
        ChallengeFromClient [8]byte
        Reserved3           uint32
        AVPair              map[string]interface{}
    }
    

    NTLMv2Response.Response也就是常说的NTProofStr,后面附加的blob是NTLMv2ClientChallenge结构

    计算过程相比于LMv2Response,只是把一个8-byte的nonce替换成了NTLMv2ClientChallenge结构,所以一样可以复用LMv2的计算函数

    func ComputeNTLMv2Response(challenge []byte, usernameWithDomainOrServer []byte, nthash []byte, clientChallenge []byte) []byte {
        if clientChallenge == nil {
            nonce := [8]byte{}
            rand.Read(nonce[:])
            cc := NTLMv2ClientChallenge{
                RespType:            1,
                HiRespType:          1,
                Reserved1:           0,
                Reserved2:           0,
                TimeStamp:           (uint64(time.Now().UnixNano()) / 100) + 116444736000000000,
                ChallengeFromClient: nonce,
                Reserved3:           0,
                AVPair:              nil,
            }
            clientChallenge = cc.Marshal()
        }
        return ComputeLMv2Response(challenge, usernameWithDomainOrServer, nthash, clientChallenge)
    }
    

    一些细节

    字节序

    因为message中有integer类型的字段,所以序列化时涉及到字节序的问题。测试后得出,不论上层协议是什么(SMB, HTTP, RPC…),NTLMSSP都是小端序,也就意味着在主流架构上直接强转integer指针就行了:

    bs = append(bs, (*(*[4]byte)(unsafe.Pointer(&nm.NegotiateFlags)))[:]...)
    

    Negotiate Flags的协商

    协商意味着Client发送的Type1NegotiateFlags和Server回复的Type2NegotiateFlags中相应的flag相同代表协商成功

    例如Type1设置了NEGOTIATE_EXTENDED_SESSION_SECURITYType2也设置了NEGOTIATE_EXTENDED_SESSION_SECURITY,代表EXTENDED_SESSION_SECURITY协商成功。接着在Type3中无论设置NEGOTIATE_EXTENDED_SESSION_SECURITY与否,都会使用EXTENDED_SESSION_SECURITY响应计算方式

    Example

    使用该库写了两个Example,分别是NTLM本地令牌协商和NTLM over HTTP

    完整代码见https://github.com/eddieivan01/ntlmssp/tree/master/example

    Local NTLM Negotiate

    通过Go加载security.dll,然后调用SSPI的AcceptSecurityContext进行令牌协商

    func localNegotiate() {
        lpPackageName := []byte("Negotiate")
        hCredential := credHandle{}
        time := timeStamp{}
    
        ret, _, err := acquireCredentialsHandleA.Call(
            0,
            (uintptr)(unsafe.Pointer(&lpPackageName[0])),
            SECPKG_CRED_INBOUND,
            0, 0, 0, 0,
            (uintptr)(unsafe.Pointer(&hCredential)),
            (uintptr)(unsafe.Pointer(&time)),
        )
        if ret != SEC_E_OK {
            fmt.Println(err)
            return
        }
    
        hContext := ctxtHandle{}
        secBufClient := secBuffer{}
        secBufDescClient := secBufferDesc{}
        initTokenContextBuffer(&secBufDescClient, &secBufClient)
    
        type1 := ntlmssp.NewNegotiateMsg(nil)
        type1.NegotiateFlags |= ntlmssp.NEGOTIATE_128BIT_SESSION_KEY |
            ntlmssp.NEGOTIATE_56BIT_ENCRYPTION |
            ntlmssp.NEGOTIATE_UNICODE_CHARSET |
            ntlmssp.NEGOTIATE_EXTENDED_SESSION_SECURITY
    
        bs := type1.Marshal('<')
        secBufClient.cbBuffer = uint32(len(bs))
        secBufClient.pvBuffer = (uintptr)(unsafe.Pointer(&bs[0]))
    
        secBufServer := secBuffer{}
        secBufDescServer := secBufferDesc{}
        initTokenContextBuffer(&secBufDescServer, &secBufServer)
    
        var fContextAttr uint32
        var tsExpiry timeStamp
    
        ret, _, err = acceptSecurityContext.Call(
            (uintptr)(unsafe.Pointer(&hCredential)),
            0,
            (uintptr)(unsafe.Pointer(&secBufDescClient)),
            ASC_REQ_ALLOCATE_MEMORY|ASC_REQ_CONNECTION,
            SECURITY_NATIVE_DREP,
            (uintptr)(unsafe.Pointer(&hContext)),
            (uintptr)(unsafe.Pointer(&secBufDescServer)),
            (uintptr)(unsafe.Pointer(&fContextAttr)),
            (uintptr)(unsafe.Pointer(&tsExpiry)),
        )
        if ret != SEC_I_CONTINUE_NEEDED {
            fmt.Println(err)
            return
        }
        type2 := ntlmssp.NewChallengeMsg(loadByteArray(secBufServer.pvBuffer, secBufServer.cbBuffer))
        type2.Display()
    
        type3 := ntlmssp.NewAuthenticateMsg(nil)
        type3.NegotiateFlags = type2.NegotiateFlags
        // type3.NegotiateFlags &^= ntlmssp.NEGOTIATE_EXTENDED_SESSION_SECURITY
        type3.SetUserName(username)
        type3.SetWorkstation(servername)
        type3.SetNTLMResponse(1, type2.ServerChallenge[:], password)
        type3.Display()
        bs = type3.Marshal('<')
    
        initTokenContextBuffer(&secBufDescClient, &secBufClient)
        secBufClient.pvBuffer = (uintptr)(unsafe.Pointer(&bs[0]))
        secBufClient.cbBuffer = uint32(len(bs))
        initTokenContextBuffer(&secBufDescServer, &secBufServer)
    
        ret, _, err = acceptSecurityContext.Call(
            (uintptr)(unsafe.Pointer(&hCredential)),
            (uintptr)(unsafe.Pointer(&hContext)),
            (uintptr)(unsafe.Pointer(&secBufDescClient)),
            ASC_REQ_ALLOCATE_MEMORY|ASC_REQ_CONNECTION,
            SECURITY_NATIVE_DREP,
            (uintptr)(unsafe.Pointer(&hContext)),
            (uintptr)(unsafe.Pointer(&secBufDescServer)),
            (uintptr)(unsafe.Pointer(&fContextAttr)),
            (uintptr)(unsafe.Pointer(&tsExpiry)),
        )
    
        if ret == SEC_E_INVALID_TOKEN {
            fmt.Println("NTLM auth error,", err)
        }
    
        if ret == SEC_E_LOGON_DENIED {
            fmt.Println("Username or password wrong,", err)
            return
        }
    
        if ret == SEC_E_OK {
            fmt.Println("Auth ok,", err)
        }
    }
    

    这里只能使用NTLMv1和NTLMv1 With Client Challenge认证,使用NTLMv2会返回SEC_E_INVALID_TOKEN

    我认为应该不是响应计算有误,因为密码错误会返回SEC_E_LOGON_DENIED(猜测不支持NTLMv2,但没有找到相关文档的说明)

    NTLM over HTTP

    在WireShark中可以看到协议解析无误

    Server端:仅仅是一个Demo,实际情况下需要保存协商结果,做一些更复杂的校验

    func handler(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        if auth == "" {
            w.Header().Set("WWW-Authenticate", "NTLM")
            w.WriteHeader(401)
            return
        }
    
        bs, err := base64.StdEncoding.DecodeString(auth[5:])
        if err != nil {
            w.Header().Set("WWW-Authenticate", "NTLM")
            w.WriteHeader(401)
            w.Write([]byte("Malformed base64"))
            return
        }
    
        switch bs[8] {
        case 1:
            type1 := ntlmssp.NewNegotiateMsg(bs)
            type2 := ntlmssp.NewChallengeMsg(nil)
    
            type2.NegotiateFlags = type1.NegotiateFlags
            type2.NegotiateFlags &^= ntlmssp.NEGOTIATE_VERSION
            type2.NegotiateFlags |= ntlmssp.NEGOTIATE_EXTENDED_SESSION_SECURITY | ntlmssp.NEGOTIATE_TARGET_TYPE_DOMAIN
            type2.SetServerChallenge(challenge)
            type2.SetTargetName([]byte("XYZ.LAB"))
    
            type2.SetTargetInfo(map[string]interface{}{
                "MsvAvNbComputerName":  "WIN-123456",
                "MsvAvNbDomainName":    "XYZ.LAB",
                "MsvAvDnsComputerName": "DC$",
                "MsvAvDnsDomainName":   "XYZ.LAB",
            })
    
            w.Header().Set("WWW-Authenticate", "NTLM "+base64.StdEncoding.EncodeToString(type2.Marshal('<')))
            w.WriteHeader(401)
        case 3:
            type3 := ntlmssp.NewAuthenticateMsg(bs)
            ok := false
            if type3.NtChallengeResponseLen <= 24 {
                // NTLMv2 session
                if type3.NegotiateFlags&ntlmssp.NEGOTIATE_EXTENDED_SESSION_SECURITY != 0 {
                    ntsResp := ntlmssp.ComputeNTLMv2SessionResponse(
                        challenge,
                        type3.LmChallengeResponse()[:8],
                        ntlmssp.NtHash(pwd))
                    if bytes.Equal(ntsResp, type3.NtChallengeResponseBytes()) {
                        ok = true
                    }
                } else {
                    // NTLM
                    ntResp := ntlmssp.ComputeNTLMv1Response(challenge, ntlmssp.NtHash(pwd))
                    if bytes.Equal(ntResp, type3.NtChallengeResponseBytes()) {
                        ok = true
                    }
                }
            } else {
                // NTLMv2
                userNameWithDomainOrServer := type3.UserNameBytes()
                if type3.DomainNameLen != 0 {
                    userNameWithDomainOrServer = append(userNameWithDomainOrServer, type3.DomainNameBytes()...)
                } else if type3.WorkstationLen != 0 {
                    userNameWithDomainOrServer = append(userNameWithDomainOrServer, type3.WorkstationBytes()...)
                }
    
                ntResp := ntlmssp.ComputeNTLMv2Response(
                    challenge,
                    userNameWithDomainOrServer,
                    ntlmssp.NtHash(pwd),
                    type3.NtChallengeResponseBytes()[16:],
                )
                if bytes.Equal(ntResp, type3.NtChallengeResponseBytes()) {
                    ok = true
                }
            }
    
            if ok {
                w.Write([]byte("OK"))
            } else {
                w.Write([]byte("Auth fail"))
            }
        default:
            w.Header().Set("WWW-Authenticate", "NTLM")
            w.WriteHeader(401)
            w.Write([]byte("Malformed NTLMSSP"))
            return
        }
    }
    

    Client端

    func client() {
        resp, err := nic.Post(url, nil)
        if err != nil || resp.StatusCode != 401 || resp.Header.Get("WWW-Authenticate") != "NTLM" {
            fmt.Println("type1 error")
            return
        }
    
        type1 := ntlmssp.NewNegotiateMsg(nil)
        type1.NegotiateFlags |= ntlmssp.NEGOTIATE_OEM_DOMAIN_SUPPLIED |
            ntlmssp.NEGOTIATE_OEM_WORKSTATION_SUPPLIED |
            ntlmssp.NEGOTIATE_128BIT_SESSION_KEY |
            ntlmssp.NEGOTIATE_56BIT_ENCRYPTION |
            ntlmssp.NEGOTIATE_UNICODE_CHARSET |
            ntlmssp.NEGOTIATE_REQUEST_TARGET_NAME
        type1.SetDomainName([]byte("CC.LAB"))
        type1.SetWorkstation([]byte("WIN-123456"))
        type1.Display()
    
        resp, err = nic.Post(url, nic.H{
            Headers: nic.KV{
                "Authorization": "NTLM " + base64.StdEncoding.EncodeToString(type1.Marshal('<')),
            },
        })
        if err != nil {
            fmt.Println(err)
            return
        }
    
        // trip "NTLM "
        bs, err := base64.StdEncoding.DecodeString(resp.Header.Get("WWW-Authenticate")[5:])
        if err != nil {
            fmt.Println("type2 error")
            return
        }
        type2 := ntlmssp.NewChallengeMsg(bs)
        type2.Display()
    
        type3 := ntlmssp.NewAuthenticateMsg(nil)
        type3.NegotiateFlags = type2.NegotiateFlags
        // type3.NegotiateFlags &^= ntlmssp.NEGOTIATE_EXTENDED_SESSION_SECURITY
    
        type3.SetUserName([]byte("admin"))
        type3.SetDomainName([]byte("LAB"))
        type3.SetNTLMResponse(2, type2.ServerChallenge[:], pwd)
        type3.Display()
    
        resp, err = nic.Post(url, nic.H{
            Headers: nic.KV{
                "Authorization": "NTLM " + base64.StdEncoding.EncodeToString(type3.Marshal('<')),
            },
        })
    
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Println(resp.Text)
    }