internal/honeypot/rdp/credssp.go

package rdp

import (
	"encoding/asn1"
	"fmt"
)

// TSRequest is the top-level structure for CredSSP messages
//
//	TSRequest ::= SEQUENCE {
//	    version [0] INTEGER,
//	    negoTokens [1] SEQUENCE OF NegoData OPTIONAL,
//	    authInfo [2] OCTET STRING OPTIONAL,
//	    pubKeyAuth [3] OCTET STRING OPTIONAL
//	}
type tsRequest struct {
	Version    int        `asn1:"tag:0,explicit"`
	NegoTokens []negoData `asn1:"tag:1,explicit,optional"`
	AuthInfo   []byte     `asn1:"tag:2,explicit,optional"`
	PubKeyAuth []byte     `asn1:"tag:3,explicit,optional"`
}

//	NegoData ::= SEQUENCE {
//	    negoToken [0] OCTET STRING
//	}
type negoData struct {
	NegoToken []byte `asn1:"tag:0,explicit"`
}

// ParseTSRequest parses a CredSSP TSRequest from bytes
func parseTSRequest(data []byte) (*tsRequest, error) {
	var ts tsRequest
	_, err := asn1.Unmarshal(data, &ts)
	if err != nil {
		return nil, err
	}
	return &ts, nil
}

// BuildTSResponse builds a CredSSP TSRequest message (used for responses)
func buildTSResponse(token []byte) ([]byte, error) {
	ts := tsRequest{
		Version: 6, // Updated to version 6 for modern clients
		NegoTokens: []negoData{
			{NegoToken: token},
		},
	}
	return asn1.Marshal(ts)
}

// NTLM signatures and message types
const (
	NTLMSSP_SIGNATURE = "NTLMSSP\x00"
	NTLM_TYPE1        = 1 // Negotiate
	NTLM_TYPE2        = 2 // Challenge
	NTLM_TYPE3        = 3 // Authenticate
)

// Minimal NTLM helper to extract username/hash from Type 3 message
func parseNTLMType3(data []byte) (username, domain, workstation, ntlmHash string) {
	if len(data) < 64 || string(data[:8]) != NTLMSSP_SIGNATURE {
		return
	}

	// Helper to read NTLM fields (offset and length)
	readField := func(offset int) []byte {
		if len(data) < offset+8 {
			return nil
		}
		length := int(data[offset]) | int(data[offset+1])<<8
		start := int(data[offset+4]) | int(data[offset+5])<<8 | int(data[offset+6])<<16 | int(data[offset+7])<<24
		if start+length > len(data) {
			return nil
		}
		return data[start : start+length]
	}

	// NTLM Type 3 offsets:
	// Workstation: 44
	// Domain: 28
	// User: 36
	// NTLM Response: 20

	domainBytes := readField(28)
	userBytes := readField(36)
	workstationBytes := readField(44)
	ntlmResponse := readField(20)

	// UTF-16 to ASCII (crude but usually enough for honeypot purposes)
	utf16ToAscii := func(b []byte) string {
		if len(b) == 0 {
			return ""
		}
		res := make([]byte, 0, len(b)/2)
		for i := 0; i < len(b); i += 2 {
			res = append(res, b[i])
		}
		return string(res)
	}

	username = utf16ToAscii(userBytes)
	domain = utf16ToAscii(domainBytes)
	workstation = utf16ToAscii(workstationBytes)

	// NTLM response is usually hex encoded for logging
	// In NTLMv2, we only want the 16-byte proof string
	if len(ntlmResponse) > 16 {
		ntlmResponse = ntlmResponse[:16]
	}
	ntlmHash = fmt.Sprintf("%x", ntlmResponse)

	return
}