Initial commit of implementation
This commit is contained in:
@@ -1,2 +1,9 @@
|
|||||||
# go-ntlmssp
|
# go-ntlmssp
|
||||||
NTLM/Negotiate authentication over HTTP
|
Golang package that provides NTLM/Negotiate authentication over HTTP
|
||||||
|
|
||||||
|
Protocol details from https://msdn.microsoft.com/en-us/library/cc236621.aspx
|
||||||
|
Implementation hints from http://davenport.sourceforge.net/ntlm.html
|
||||||
|
|
||||||
|
This package only implements authentication, no key exchange or encryption. It
|
||||||
|
only supports Unicode (UTF16LE) encoding of protocol strings, no OEM encoding.
|
||||||
|
This package implements NTLMv2.
|
128
authenticate_message.go
Normal file
128
authenticate_message.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authenicateMessage struct {
|
||||||
|
LmChallengeResponse []byte
|
||||||
|
NtChallengeResponse []byte
|
||||||
|
|
||||||
|
TargetName string
|
||||||
|
UserName string
|
||||||
|
|
||||||
|
// only set if negotiateFlag_NTLMSSP_NEGOTIATE_KEY_EXCH
|
||||||
|
EncryptedRandomSessionKey []byte
|
||||||
|
|
||||||
|
NegotiateFlags negotiateFlags
|
||||||
|
|
||||||
|
MIC []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticateMessageFields struct {
|
||||||
|
messageHeader
|
||||||
|
LmChallengeResponse varField
|
||||||
|
NtChallengeResponse varField
|
||||||
|
TargetName varField
|
||||||
|
UserName varField
|
||||||
|
Workstation varField
|
||||||
|
_ [8]byte
|
||||||
|
NegotiateFlags negotiateFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m authenicateMessage) MarshalBinary() ([]byte, error) {
|
||||||
|
if !m.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEUNICODE) {
|
||||||
|
return nil, errors.New("Only unicode is supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
target, user := toUnicode(m.TargetName), toUnicode(m.UserName)
|
||||||
|
workstation := toUnicode("go-ntlmssp")
|
||||||
|
|
||||||
|
ptr := binary.Size(&authenticateMessageFields{})
|
||||||
|
f := authenticateMessageFields{
|
||||||
|
messageHeader: newMessageHeader(3),
|
||||||
|
NegotiateFlags: m.NegotiateFlags,
|
||||||
|
LmChallengeResponse: newVarField(&ptr, len(m.LmChallengeResponse)),
|
||||||
|
NtChallengeResponse: newVarField(&ptr, len(m.NtChallengeResponse)),
|
||||||
|
TargetName: newVarField(&ptr, len(target)),
|
||||||
|
UserName: newVarField(&ptr, len(user)),
|
||||||
|
Workstation: newVarField(&ptr, len(workstation)),
|
||||||
|
}
|
||||||
|
|
||||||
|
f.NegotiateFlags.Unset(negotiateFlagNTLMSSPNEGOTIATEVERSION)
|
||||||
|
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
if err := binary.Write(&b, binary.LittleEndian, &f); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Write(&b, binary.LittleEndian, &m.LmChallengeResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Write(&b, binary.LittleEndian, &m.NtChallengeResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Write(&b, binary.LittleEndian, &target); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Write(&b, binary.LittleEndian, &user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Write(&b, binary.LittleEndian, &workstation); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message
|
||||||
|
//that was received from the server
|
||||||
|
func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byte, error) {
|
||||||
|
if user == "" && password == "" {
|
||||||
|
return nil, errors.New("Anonymous authentication not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cm challengeMessage
|
||||||
|
if err := cm.UnmarshalBinary(challengeMessageData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATELMKEY) {
|
||||||
|
return nil, errors.New("Only NTLM v2 is supported, but server requested v1 (NTLMSSP_NEGOTIATE_LM_KEY)")
|
||||||
|
}
|
||||||
|
if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEKEYEXCH) {
|
||||||
|
return nil, errors.New("Key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)")
|
||||||
|
}
|
||||||
|
|
||||||
|
am := authenicateMessage{
|
||||||
|
UserName: user,
|
||||||
|
TargetName: cm.TargetName,
|
||||||
|
NegotiateFlags: cm.NegotiateFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := cm.TargetInfo[avIDMsvAvTimestamp]
|
||||||
|
if timestamp == nil { // no time sent, take current time
|
||||||
|
ft := uint64(time.Now().UnixNano()) / 100
|
||||||
|
ft += 116444736000000000 // add time between unix & windows offset
|
||||||
|
timestamp = make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint64(timestamp, ft)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientChallenge := make([]byte, 8)
|
||||||
|
rand.Reader.Read(clientChallenge)
|
||||||
|
|
||||||
|
ntlmV2Hash := getNtlmV2Hash(password, user, cm.TargetName)
|
||||||
|
|
||||||
|
am.NtChallengeResponse = computeNtlmV2Response(ntlmV2Hash,
|
||||||
|
cm.ServerChallenge[:], clientChallenge, timestamp, cm.TargetInfoRaw)
|
||||||
|
|
||||||
|
if cm.TargetInfoRaw == nil {
|
||||||
|
am.LmChallengeResponse = computeLmV2Response(ntlmV2Hash,
|
||||||
|
cm.ServerChallenge[:], clientChallenge)
|
||||||
|
}
|
||||||
|
|
||||||
|
return am.MarshalBinary()
|
||||||
|
}
|
33
authheader.go
Normal file
33
authheader.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authheader string
|
||||||
|
|
||||||
|
func (h authheader) IsBasic() bool {
|
||||||
|
return strings.HasPrefix(string(h), "Basic ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h authheader) IsNegotiate() bool {
|
||||||
|
return strings.HasPrefix(string(h), "Negotiate")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h authheader) GetData() ([]byte, error) {
|
||||||
|
p := strings.Split(string(h), " ")
|
||||||
|
if len(p) < 2 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.DecodeString(string(p[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h authheader) GetBasicCreds() (username, password string, err error) {
|
||||||
|
d, err := h.GetData()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(string(d), ":", 2)
|
||||||
|
return parts[0], parts[1], nil
|
||||||
|
}
|
17
avids.go
Normal file
17
avids.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
type avID uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
avIDMsvAvEOL avID = iota
|
||||||
|
avIDMsvAvNbComputerName
|
||||||
|
avIDMsvAvNbDomainName
|
||||||
|
avIDMsvAvDNSComputerName
|
||||||
|
avIDMsvAvDNSDomainName
|
||||||
|
avIDMsvAvDNSTreeName
|
||||||
|
avIDMsvAvFlags
|
||||||
|
avIDMsvAvTimestamp
|
||||||
|
avIDMsvAvSingleHost
|
||||||
|
avIDMsvAvTargetName
|
||||||
|
avIDMsvChannelBindings
|
||||||
|
)
|
82
challenge_message.go
Normal file
82
challenge_message.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type challengeMessageFields struct {
|
||||||
|
messageHeader
|
||||||
|
TargetName varField
|
||||||
|
NegotiateFlags negotiateFlags
|
||||||
|
ServerChallenge [8]byte
|
||||||
|
_ [8]byte
|
||||||
|
TargetInfo varField
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m challengeMessageFields) IsValid() bool {
|
||||||
|
return m.messageHeader.IsValid() && m.MessageType == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
type challengeMessage struct {
|
||||||
|
challengeMessageFields
|
||||||
|
TargetName string
|
||||||
|
TargetInfo map[avID][]byte
|
||||||
|
TargetInfoRaw []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *challengeMessage) UnmarshalBinary(data []byte) error {
|
||||||
|
r := bytes.NewReader(data)
|
||||||
|
err := binary.Read(r, binary.LittleEndian, &m.challengeMessageFields)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !m.challengeMessageFields.IsValid() {
|
||||||
|
return fmt.Errorf("Message is not a valid challenge message: %+v", m.challengeMessageFields.messageHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.challengeMessageFields.TargetName.Len > 0 {
|
||||||
|
m.TargetName, err = m.challengeMessageFields.TargetName.ReadStringFrom(data, m.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEUNICODE))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.challengeMessageFields.TargetInfo.Len > 0 {
|
||||||
|
d, err := m.challengeMessageFields.TargetInfo.ReadFrom(data)
|
||||||
|
m.TargetInfoRaw = d
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.TargetInfo = make(map[avID][]byte)
|
||||||
|
r := bytes.NewReader(d)
|
||||||
|
for {
|
||||||
|
var id avID
|
||||||
|
var l uint16
|
||||||
|
err = binary.Read(r, binary.LittleEndian, &id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if id == avIDMsvAvEOL {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
err = binary.Read(r, binary.LittleEndian, &l)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value := make([]byte, l)
|
||||||
|
n, err := r.Read(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n != int(l) {
|
||||||
|
return fmt.Errorf("Expected to read %d bytes, got only %d", l, n)
|
||||||
|
}
|
||||||
|
m.TargetInfo[id] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
21
messageheader.go
Normal file
21
messageheader.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var signature = [8]byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0}
|
||||||
|
|
||||||
|
type messageHeader struct {
|
||||||
|
Signature [8]byte
|
||||||
|
MessageType uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h messageHeader) IsValid() bool {
|
||||||
|
return bytes.Equal(h.Signature[:], signature[:]) &&
|
||||||
|
h.MessageType > 0 && h.MessageType < 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMessageHeader(messageType uint32) messageHeader {
|
||||||
|
return messageHeader{signature, messageType}
|
||||||
|
}
|
44
negotiate_flags.go
Normal file
44
negotiate_flags.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
type negotiateFlags uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
/*A*/ negotiateFlagNTLMSSPNEGOTIATEUNICODE negotiateFlags = 1 << 0
|
||||||
|
/*B*/ negotiateFlagNTLMNEGOTIATEOEM = 1 << 1
|
||||||
|
/*C*/ negotiateFlagNTLMSSPREQUESTTARGET = 1 << 2
|
||||||
|
|
||||||
|
/*D*/ negotiateFlagNTLMSSPNEGOTIATESIGN = 1 << 4
|
||||||
|
/*E*/ negotiateFlagNTLMSSPNEGOTIATESEAL = 1 << 5
|
||||||
|
/*F*/ negotiateFlagNTLMSSPNEGOTIATEDATAGRAM = 1 << 6
|
||||||
|
/*G*/ negotiateFlagNTLMSSPNEGOTIATELMKEY = 1 << 7
|
||||||
|
|
||||||
|
/*H*/ negotiateFlagNTLMSSPNEGOTIATENTLM = 1 << 9
|
||||||
|
|
||||||
|
/*J*/ negotiateFlagANONYMOUS = 1 << 11
|
||||||
|
/*K*/ negotiateFlagNTLMSSPNEGOTIATEOEMDOMAINSUPPLIED = 1 << 12
|
||||||
|
/*L*/ negotiateFlagNTLMSSPNEGOTIATEOEMWORKSTATIONSUPPLIED = 1 << 13
|
||||||
|
|
||||||
|
/*M*/ negotiateFlagNTLMSSPNEGOTIATEALWAYSSIGN = 1 << 15
|
||||||
|
/*N*/ negotiateFlagNTLMSSPTARGETTYPEDOMAIN = 1 << 16
|
||||||
|
/*O*/ negotiateFlagNTLMSSPTARGETTYPESERVER = 1 << 17
|
||||||
|
|
||||||
|
/*P*/ negotiateFlagNTLMSSPNEGOTIATEEXTENDEDSESSIONSECURITY = 1 << 19
|
||||||
|
/*Q*/ negotiateFlagNTLMSSPNEGOTIATEIDENTIFY = 1 << 20
|
||||||
|
|
||||||
|
/*R*/ negotiateFlagNTLMSSPREQUESTNONNTSESSIONKEY = 1 << 22
|
||||||
|
/*S*/ negotiateFlagNTLMSSPNEGOTIATETARGETINFO = 1 << 23
|
||||||
|
|
||||||
|
/*T*/ negotiateFlagNTLMSSPNEGOTIATEVERSION = 1 << 25
|
||||||
|
|
||||||
|
/*U*/ negotiateFlagNTLMSSPNEGOTIATE128 = 1 << 29
|
||||||
|
/*V*/ negotiateFlagNTLMSSPNEGOTIATEKEYEXCH = 1 << 30
|
||||||
|
/*W*/ negotiateFlagNTLMSSPNEGOTIATE56 = 1 << 31
|
||||||
|
)
|
||||||
|
|
||||||
|
func (field negotiateFlags) Has(flags negotiateFlags) bool {
|
||||||
|
return field&flags == flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *negotiateFlags) Unset(flags negotiateFlags) {
|
||||||
|
*field = *field ^ (*field & flags)
|
||||||
|
}
|
31
negotiate_message.go
Normal file
31
negotiate_message.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
type negotiateMessageFields struct {
|
||||||
|
messageHeader
|
||||||
|
NegotiateFlags negotiateFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewNegotiateMessage creates a new NEGOTIATE message with the
|
||||||
|
//flags that this package supports.
|
||||||
|
func NewNegotiateMessage() []byte {
|
||||||
|
m := negotiateMessageFields{
|
||||||
|
messageHeader: newMessageHeader(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.NegotiateFlags = negotiateFlagNTLMSSPREQUESTTARGET |
|
||||||
|
negotiateFlagNTLMSSPNEGOTIATENTLM |
|
||||||
|
negotiateFlagNTLMSSPNEGOTIATEALWAYSSIGN |
|
||||||
|
negotiateFlagNTLMSSPNEGOTIATEUNICODE
|
||||||
|
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
err := binary.Write(&b, binary.LittleEndian, &m)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
101
negotiator.go
Normal file
101
negotiator.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Negotiator is a http.Roundtripper decorator that automatically
|
||||||
|
//converts basic authentication to NTLM/Negotiate authentication when appropriate.
|
||||||
|
type Negotiator struct{ http.RoundTripper }
|
||||||
|
|
||||||
|
//RoundTrip sends the request to the server, handling any authentication
|
||||||
|
//re-sends as needed.
|
||||||
|
func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) {
|
||||||
|
body := bytes.Buffer{}
|
||||||
|
_, err = body.ReadFrom(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Body.Close()
|
||||||
|
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
|
||||||
|
|
||||||
|
reqauth := authheader(req.Header.Get("Authorization"))
|
||||||
|
if reqauth.IsBasic() {
|
||||||
|
// first try anonymous, in case the server still finds us
|
||||||
|
// authenticated from previous traffic
|
||||||
|
req.Header.Del("Authorization")
|
||||||
|
|
||||||
|
res, err = l.RoundTripper.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode != 401 {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resauth := authheader(res.Header.Get("Www-Authenticate"))
|
||||||
|
if !resauth.IsNegotiate() {
|
||||||
|
// Unauthorized, Negotiate not requested, let's try with basic auth
|
||||||
|
res.Body.Close()
|
||||||
|
req.Header.Set("Authorization", string(reqauth))
|
||||||
|
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
|
||||||
|
|
||||||
|
res, err = l.RoundTripper.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == 401 {
|
||||||
|
resauth := authheader(res.Header.Get("Www-Authenticate"))
|
||||||
|
if reqauth.IsBasic() && resauth.IsNegotiate() {
|
||||||
|
// 401 with request:Basic and response:Negotiate
|
||||||
|
res.Body.Close()
|
||||||
|
|
||||||
|
// recycle credentials
|
||||||
|
u, p, err := reqauth.GetBasicCreds()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// send negotiate
|
||||||
|
negotiateMessage := NewNegotiateMessage()
|
||||||
|
req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(negotiateMessage))
|
||||||
|
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
|
||||||
|
|
||||||
|
res, err = l.RoundTripper.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive challenge?
|
||||||
|
resauth = authheader(res.Header.Get("Www-Authenticate"))
|
||||||
|
challengeMessage, err := resauth.GetData()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !resauth.IsNegotiate() || len(challengeMessage) == 0 {
|
||||||
|
// Negotiation failed, let client deal with response
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
|
||||||
|
// send authenticate
|
||||||
|
authenticateMessage, err := ProcessChallenge(challengeMessage, u, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(authenticateMessage))
|
||||||
|
req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
|
||||||
|
|
||||||
|
res, err = l.RoundTripper.RoundTrip(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
51
nlmp.go
Normal file
51
nlmp.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Package ntlmssp provides NTLM/Negotiate authentication over HTTP
|
||||||
|
//
|
||||||
|
// Protocol details from https://msdn.microsoft.com/en-us/library/cc236621.aspx,
|
||||||
|
// implementation hints from http://davenport.sourceforge.net/ntlm.html .
|
||||||
|
// This package only implements authentication, no key exchange or encryption. It
|
||||||
|
// only supports Unicode (UTF16LE) encoding of protocol strings, no OEM encoding.
|
||||||
|
// This package implements NTLMv2.
|
||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/md5"
|
||||||
|
"golang.org/x/crypto/md4"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getNtlmV2Hash(password, username, target string) []byte {
|
||||||
|
return hmacMd5(getNtlmHash(password), toUnicode(strings.ToUpper(username)+target))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNtlmHash(password string) []byte {
|
||||||
|
hash := md4.New()
|
||||||
|
hash.Write(toUnicode(password))
|
||||||
|
return hash.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeNtlmV2Response(ntlmV2Hash, serverChallenge, clientChallenge,
|
||||||
|
timestamp, targetInfo []byte) []byte {
|
||||||
|
|
||||||
|
temp := []byte{1, 1, 0, 0, 0, 0, 0, 0}
|
||||||
|
temp = append(temp, timestamp...)
|
||||||
|
temp = append(temp, clientChallenge...)
|
||||||
|
temp = append(temp, 0, 0, 0, 0)
|
||||||
|
temp = append(temp, targetInfo...)
|
||||||
|
temp = append(temp, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
NTProofStr := hmacMd5(ntlmV2Hash, serverChallenge, temp)
|
||||||
|
return append(NTProofStr, temp...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeLmV2Response(ntlmV2Hash, serverChallenge, clientChallenge []byte) []byte {
|
||||||
|
return append(hmacMd5(ntlmV2Hash, serverChallenge, clientChallenge), clientChallenge...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hmacMd5(key []byte, data ...[]byte) []byte {
|
||||||
|
mac := hmac.New(md5.New, key)
|
||||||
|
for _, d := range data {
|
||||||
|
mac.Write(d)
|
||||||
|
}
|
||||||
|
return mac.Sum(nil)
|
||||||
|
}
|
71
nlmp_test.go
Normal file
71
nlmp_test.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// test cases from http://davenport.sourceforge.net/ntlm.html
|
||||||
|
|
||||||
|
var username = "user"
|
||||||
|
var password = "SecREt01"
|
||||||
|
var target = "DOMAIN"
|
||||||
|
var challenge = []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}
|
||||||
|
|
||||||
|
func TestCalculateNTLMv2Response(t *testing.T) {
|
||||||
|
NTLMv2Hash := getNtlmV2Hash(password, username, target)
|
||||||
|
ClientChallenge := []byte{0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44}
|
||||||
|
Time := []byte{0x00, 0x90, 0xd3, 0x36, 0xb7, 0x34, 0xc3, 0x01}
|
||||||
|
targetInfo := []byte{0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, 0x04, 0x00, 0x14, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x03, 0x00, 0x22, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||||
|
|
||||||
|
v := computeNtlmV2Response(NTLMv2Hash, challenge, ClientChallenge, Time, targetInfo)
|
||||||
|
|
||||||
|
if expected := []byte{
|
||||||
|
0xcb, 0xab, 0xbc, 0xa7, 0x13, 0xeb, 0x79, 0x5d, 0x04, 0xc9, 0x7a, 0xbc, 0x01, 0xee, 0x49, 0x83,
|
||||||
|
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xd3, 0x36, 0xb7, 0x34, 0xc3, 0x01,
|
||||||
|
0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x0c, 0x00,
|
||||||
|
0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00,
|
||||||
|
0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, 0x04, 0x00, 0x14, 0x00,
|
||||||
|
0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00,
|
||||||
|
0x6f, 0x00, 0x6d, 0x00, 0x03, 0x00, 0x22, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00,
|
||||||
|
0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00,
|
||||||
|
0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
}; !bytes.Equal(v, expected) {
|
||||||
|
t.Fatalf("expected %x, got %x", expected, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateLMv2Response(t *testing.T) {
|
||||||
|
NTLMv2Hash := getNtlmV2Hash(password, username, target)
|
||||||
|
ClientChallenge := []byte{0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44}
|
||||||
|
|
||||||
|
v := computeLmV2Response(NTLMv2Hash, challenge, ClientChallenge)
|
||||||
|
|
||||||
|
if expected := []byte{
|
||||||
|
0xd6, 0xe6, 0x15, 0x2e, 0xa2, 0x5d, 0x03, 0xb7, 0xc6, 0xba, 0x66, 0x29, 0xc2, 0xd6, 0xaa, 0xf0, 0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44,
|
||||||
|
}; !bytes.Equal(v, expected) {
|
||||||
|
t.Fatalf("expected %x, got %x", expected, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToUnicode(t *testing.T) {
|
||||||
|
v := toUnicode(password)
|
||||||
|
if expected := []byte{0x53, 0x00, 0x65, 0x00, 0x63, 0x00, 0x52, 0x00, 0x45, 0x00, 0x74, 0x00, 0x30, 0x00, 0x31, 0x00}; !bytes.Equal(v, expected) {
|
||||||
|
t.Fatalf("expected %v, got %v", expected, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNTLMhash(t *testing.T) {
|
||||||
|
v := getNtlmHash(password)
|
||||||
|
if expected := []byte{0xcd, 0x06, 0xca, 0x7c, 0x7e, 0x10, 0xc9, 0x9b, 0x1d, 0x33, 0xb7, 0x48, 0x5a, 0x2e, 0xd8, 0x08}; !bytes.Equal(v, expected) {
|
||||||
|
t.Fatalf("expected %v, got %v", expected, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNTLMv2Hash(t *testing.T) {
|
||||||
|
v := getNtlmV2Hash(password, username, target)
|
||||||
|
if expected := []byte{0x04, 0xb8, 0xe0, 0xba, 0x74, 0x28, 0x9c, 0xc5, 0x40, 0x82, 0x6b, 0xab, 0x1d, 0xee, 0x63, 0xae}; !bytes.Equal(v, expected) {
|
||||||
|
t.Fatalf("expected %v, got %v", expected, v)
|
||||||
|
}
|
||||||
|
}
|
29
unicode.go
Normal file
29
unicode.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"unicode/utf16"
|
||||||
|
)
|
||||||
|
|
||||||
|
// helper func's for dealing with Windows Unicode (UTF16LE)
|
||||||
|
|
||||||
|
func fromUnicode(d []byte) (string, error) {
|
||||||
|
if len(d)%2 > 0 {
|
||||||
|
return "", errors.New("Unicode (UTF 16 LE) specified, but uneven data length")
|
||||||
|
}
|
||||||
|
s := make([]uint16, len(d)/2)
|
||||||
|
err := binary.Read(bytes.NewReader(d), binary.LittleEndian, &s)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(utf16.Decode(s)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUnicode(s string) []byte {
|
||||||
|
uints := utf16.Encode([]rune(s))
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
binary.Write(&b, binary.LittleEndian, &uints)
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
40
varfield.go
Normal file
40
varfield.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package ntlmssp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type varField struct {
|
||||||
|
Len uint16
|
||||||
|
MaxLen uint16
|
||||||
|
BufferOffset uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f varField) ReadFrom(buffer []byte) ([]byte, error) {
|
||||||
|
if len(buffer) < int(f.BufferOffset+uint32(f.Len)) {
|
||||||
|
return nil, errors.New("Error reading data, varField extends beyond buffer")
|
||||||
|
}
|
||||||
|
return buffer[f.BufferOffset : f.BufferOffset+uint32(f.Len)], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f varField) ReadStringFrom(buffer []byte, unicode bool) (string, error) {
|
||||||
|
d, err := f.ReadFrom(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if unicode { // UTF-16LE encoding scheme
|
||||||
|
return fromUnicode(d)
|
||||||
|
}
|
||||||
|
// OEM encoding, close enough to ASCII, since no code page is specified
|
||||||
|
return string(d), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVarField(ptr *int, fieldsize int) varField {
|
||||||
|
f := varField{
|
||||||
|
Len: uint16(fieldsize),
|
||||||
|
MaxLen: uint16(fieldsize),
|
||||||
|
BufferOffset: uint32(*ptr),
|
||||||
|
}
|
||||||
|
*ptr += fieldsize
|
||||||
|
return f
|
||||||
|
}
|
Reference in New Issue
Block a user