主要变更: - RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、 backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API - Windows SMB 文件传输:backend/pkg/winsmb - 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情 - Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs - 服务器导入助手:server_import_helpers、ImportCredentialSettings - 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块) - 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-* - .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库) Co-Authored-By: Claude <noreply@anthropic.com>
387 lines
11 KiB
Go
387 lines
11 KiB
Go
package credssp
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"crypto/rc4"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/md4"
|
|
)
|
|
|
|
const (
|
|
ntlmNegotiateUnicode uint32 = 0x00000001
|
|
ntlmRequestTarget uint32 = 0x00000004
|
|
ntlmNegotiateSign uint32 = 0x00000010
|
|
ntlmNegotiateSeal uint32 = 0x00000020
|
|
ntlmNegotiateNTLM uint32 = 0x00000200
|
|
ntlmNegotiateAlwaysSign uint32 = 0x00008000
|
|
ntlmNegotiateExtendedSessionSecurity uint32 = 0x00080000
|
|
ntlmNegotiateTargetInfo uint32 = 0x00800000
|
|
ntlmNegotiateVersion uint32 = 0x02000000
|
|
ntlmNegotiate128 uint32 = 0x20000000
|
|
ntlmNegotiateKeyExch uint32 = 0x40000000
|
|
ntlmNegotiate56 uint32 = 0x80000000
|
|
|
|
msvAvEOL uint16 = 0x0000
|
|
msvAvNbComputerName uint16 = 0x0001
|
|
msvAvNbDomainName uint16 = 0x0002
|
|
msvAvFlags uint16 = 0x0006
|
|
msvAvTimestamp uint16 = 0x0007
|
|
msvChannelBindings uint16 = 0x000A
|
|
)
|
|
|
|
const ntlmClientFlags = ntlmNegotiateUnicode |
|
|
ntlmRequestTarget |
|
|
ntlmNegotiateSign |
|
|
ntlmNegotiateSeal |
|
|
ntlmNegotiateNTLM |
|
|
ntlmNegotiateAlwaysSign |
|
|
ntlmNegotiateExtendedSessionSecurity |
|
|
ntlmNegotiateTargetInfo |
|
|
ntlmNegotiateVersion |
|
|
ntlmNegotiate128 |
|
|
ntlmNegotiateKeyExch
|
|
|
|
type ntlmChallenge struct {
|
|
Raw []byte
|
|
Flags uint32
|
|
TargetName string
|
|
ServerChallenge []byte
|
|
TargetInfo []byte
|
|
Timestamp []byte
|
|
}
|
|
|
|
type ntlmOptions struct {
|
|
ForceMIC bool
|
|
DisableKeyExch bool
|
|
ChannelBindings []byte
|
|
OmitVersion bool
|
|
}
|
|
|
|
type ntlmSecurity struct {
|
|
clientSigningKey []byte
|
|
serverSigningKey []byte
|
|
clientSealing *rc4.Cipher
|
|
serverSealing *rc4.Cipher
|
|
seq uint32
|
|
}
|
|
|
|
func buildNegotiate() []byte {
|
|
buf := bytes.NewBuffer(nil)
|
|
buf.Write([]byte("NTLMSSP\x00"))
|
|
writeU32(buf, 1)
|
|
writeU32(buf, ntlmClientFlags)
|
|
writeSecBuffer(buf, 0, 0)
|
|
writeSecBuffer(buf, 0, 0)
|
|
buf.Write(ntlmVersion())
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func parseChallenge(data []byte) (*ntlmChallenge, error) {
|
|
if len(data) < 48 || string(data[:8]) != "NTLMSSP\x00" || binary.LittleEndian.Uint32(data[8:12]) != 2 {
|
|
return nil, fmt.Errorf("invalid NTLM challenge")
|
|
}
|
|
targetNameLen := int(binary.LittleEndian.Uint16(data[12:14]))
|
|
targetNameOffset := int(binary.LittleEndian.Uint32(data[16:20]))
|
|
targetInfoLen := int(binary.LittleEndian.Uint16(data[40:42]))
|
|
targetInfoOffset := int(binary.LittleEndian.Uint32(data[44:48]))
|
|
if targetInfoOffset < 0 || targetInfoOffset+targetInfoLen > len(data) {
|
|
return nil, fmt.Errorf("invalid NTLM target info")
|
|
}
|
|
targetName := ""
|
|
if targetNameOffset >= 0 && targetNameOffset+targetNameLen <= len(data) {
|
|
targetName = utf16leString(data[targetNameOffset : targetNameOffset+targetNameLen])
|
|
}
|
|
targetInfo := data[targetInfoOffset : targetInfoOffset+targetInfoLen]
|
|
return &ntlmChallenge{
|
|
Raw: append([]byte(nil), data...),
|
|
Flags: binary.LittleEndian.Uint32(data[20:24]),
|
|
TargetName: targetName,
|
|
ServerChallenge: append([]byte(nil), data[24:32]...),
|
|
TargetInfo: append([]byte(nil), targetInfo...),
|
|
Timestamp: findAVPair(targetInfo, msvAvTimestamp),
|
|
}, nil
|
|
}
|
|
|
|
func buildAuthenticate(ch *ntlmChallenge, domain, user, password, workstation string, negotiate []byte, opts ntlmOptions) ([]byte, *ntlmSecurity, error) {
|
|
if domain == "" {
|
|
domain = ch.TargetName
|
|
}
|
|
if domain == "" {
|
|
domain = utf16leString(findAVPair(ch.TargetInfo, msvAvNbDomainName))
|
|
}
|
|
respKey := ntowfv2(password, user, domain)
|
|
clientChallenge := randomBytes(8)
|
|
timestamp := ch.Timestamp
|
|
if len(timestamp) == 0 {
|
|
timestamp = windowsFileTime(time.Now())
|
|
}
|
|
|
|
blob := bytes.NewBuffer(nil)
|
|
blob.Write([]byte{0x01, 0x01, 0, 0, 0, 0, 0, 0})
|
|
blob.Write(timestamp)
|
|
blob.Write(clientChallenge)
|
|
blob.Write([]byte{0, 0, 0, 0})
|
|
targetInfo := ch.TargetInfo
|
|
if len(opts.ChannelBindings) == 16 {
|
|
targetInfo = withChannelBindings(ch.TargetInfo, opts.ChannelBindings)
|
|
}
|
|
blob.Write(targetInfo)
|
|
blob.Write([]byte{0, 0, 0, 0})
|
|
|
|
ntProof := hmacMD5(respKey, append(append([]byte(nil), ch.ServerChallenge...), blob.Bytes()...))
|
|
ntResp := append(ntProof, blob.Bytes()...)
|
|
lmResp := append(hmacMD5(respKey, append(append([]byte(nil), ch.ServerChallenge...), clientChallenge...)), clientChallenge...)
|
|
sessionBaseKey := hmacMD5(respKey, ntProof)
|
|
exportedSessionKey := randomBytes(16)
|
|
encryptedSessionKey := rc4k(sessionBaseKey, exportedSessionKey)
|
|
|
|
domainBytes := utf16le(domain)
|
|
userBytes := utf16le(user)
|
|
workstationBytes := utf16le(workstation)
|
|
flags := ch.Flags & ntlmClientFlags
|
|
flags |= ntlmNegotiateUnicode | ntlmNegotiateExtendedSessionSecurity | ntlmNegotiateNTLM
|
|
if opts.DisableKeyExch {
|
|
flags &^= ntlmNegotiateKeyExch
|
|
exportedSessionKey = sessionBaseKey
|
|
encryptedSessionKey = nil
|
|
}
|
|
|
|
includeMIC := opts.ForceMIC || requiresMIC(ch.TargetInfo)
|
|
baseLen := 72
|
|
if opts.OmitVersion {
|
|
baseLen = 64
|
|
}
|
|
if includeMIC {
|
|
baseLen += 16
|
|
}
|
|
payload := bytes.NewBuffer(nil)
|
|
offset := uint32(baseLen)
|
|
lmOffset := offset
|
|
payload.Write(lmResp)
|
|
offset += uint32(len(lmResp))
|
|
ntOffset := offset
|
|
payload.Write(ntResp)
|
|
offset += uint32(len(ntResp))
|
|
domainOffset := offset
|
|
payload.Write(domainBytes)
|
|
offset += uint32(len(domainBytes))
|
|
userOffset := offset
|
|
payload.Write(userBytes)
|
|
offset += uint32(len(userBytes))
|
|
workstationOffset := offset
|
|
payload.Write(workstationBytes)
|
|
offset += uint32(len(workstationBytes))
|
|
sessionOffset := offset
|
|
if len(encryptedSessionKey) > 0 {
|
|
payload.Write(encryptedSessionKey)
|
|
}
|
|
|
|
msg := bytes.NewBuffer(nil)
|
|
msg.Write([]byte("NTLMSSP\x00"))
|
|
writeU32(msg, 3)
|
|
writeSecBuffer(msg, uint16(len(lmResp)), lmOffset)
|
|
writeSecBuffer(msg, uint16(len(ntResp)), ntOffset)
|
|
writeSecBuffer(msg, uint16(len(domainBytes)), domainOffset)
|
|
writeSecBuffer(msg, uint16(len(userBytes)), userOffset)
|
|
writeSecBuffer(msg, uint16(len(workstationBytes)), workstationOffset)
|
|
writeSecBuffer(msg, uint16(len(encryptedSessionKey)), sessionOffset)
|
|
writeU32(msg, flags)
|
|
if !opts.OmitVersion {
|
|
msg.Write(ntlmVersion())
|
|
}
|
|
micPos := -1
|
|
if includeMIC {
|
|
micPos = msg.Len()
|
|
msg.Write(make([]byte, 16))
|
|
}
|
|
msg.Write(payload.Bytes())
|
|
|
|
if includeMIC {
|
|
micInput := bytes.NewBuffer(nil)
|
|
micInput.Write(negotiate)
|
|
micInput.Write(ch.Raw)
|
|
micInput.Write(msg.Bytes())
|
|
mic := hmacMD5(exportedSessionKey, micInput.Bytes())
|
|
copy(msg.Bytes()[micPos:micPos+16], mic)
|
|
}
|
|
|
|
clientSeal, _ := rc4.NewCipher(md5sum(append(exportedSessionKey, []byte("session key to client-to-server sealing key magic constant\x00")...)))
|
|
serverSeal, _ := rc4.NewCipher(md5sum(append(exportedSessionKey, []byte("session key to server-to-client sealing key magic constant\x00")...)))
|
|
sec := &ntlmSecurity{
|
|
clientSigningKey: md5sum(append(exportedSessionKey, []byte("session key to client-to-server signing key magic constant\x00")...)),
|
|
serverSigningKey: md5sum(append(exportedSessionKey, []byte("session key to server-to-client signing key magic constant\x00")...)),
|
|
clientSealing: clientSeal,
|
|
serverSealing: serverSeal,
|
|
}
|
|
return msg.Bytes(), sec, nil
|
|
}
|
|
|
|
func (s *ntlmSecurity) encrypt(data []byte) []byte {
|
|
sealed := make([]byte, len(data))
|
|
s.clientSealing.XORKeyStream(sealed, data)
|
|
macInput := bytes.NewBuffer(nil)
|
|
writeU32(macInput, s.seq)
|
|
macInput.Write(data)
|
|
checksum := hmacMD5(s.clientSigningKey, macInput.Bytes())[:8]
|
|
encryptedChecksum := make([]byte, 8)
|
|
s.clientSealing.XORKeyStream(encryptedChecksum, checksum)
|
|
out := bytes.NewBuffer(nil)
|
|
writeU32(out, 1)
|
|
out.Write(encryptedChecksum)
|
|
writeU32(out, s.seq)
|
|
out.Write(sealed)
|
|
s.seq++
|
|
return out.Bytes()
|
|
}
|
|
|
|
func ntowfv2(password, user, domain string) []byte {
|
|
h := md4.New()
|
|
h.Write(utf16le(password))
|
|
return hmacMD5(h.Sum(nil), utf16le(strings.ToUpper(user)+domain))
|
|
}
|
|
|
|
func hmacMD5(key, data []byte) []byte {
|
|
h := hmac.New(md5.New, key)
|
|
h.Write(data)
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
func md5sum(data []byte) []byte {
|
|
sum := md5.Sum(data)
|
|
return sum[:]
|
|
}
|
|
|
|
func rc4k(key, data []byte) []byte {
|
|
c, _ := rc4.NewCipher(key)
|
|
out := make([]byte, len(data))
|
|
c.XORKeyStream(out, data)
|
|
return out
|
|
}
|
|
|
|
func randomBytes(n int) []byte {
|
|
buf := make([]byte, n)
|
|
_, _ = rand.Read(buf)
|
|
return buf
|
|
}
|
|
|
|
func windowsFileTime(t time.Time) []byte {
|
|
v := uint64(t.UnixNano()/100) + 116444736000000000
|
|
out := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(out, v)
|
|
return out
|
|
}
|
|
|
|
func findAVPair(data []byte, id uint16) []byte {
|
|
for len(data) >= 4 {
|
|
avID := binary.LittleEndian.Uint16(data[0:2])
|
|
avLen := int(binary.LittleEndian.Uint16(data[2:4]))
|
|
data = data[4:]
|
|
if avLen > len(data) {
|
|
return nil
|
|
}
|
|
if avID == msvAvEOL {
|
|
return nil
|
|
}
|
|
if avID == id {
|
|
return append([]byte(nil), data[:avLen]...)
|
|
}
|
|
data = data[avLen:]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func requiresMIC(targetInfo []byte) bool {
|
|
flags := findAVPair(targetInfo, msvAvFlags)
|
|
if len(flags) < 4 {
|
|
return false
|
|
}
|
|
return binary.LittleEndian.Uint32(flags)&0x00000002 != 0
|
|
}
|
|
|
|
func withChannelBindings(targetInfo, hash []byte) []byte {
|
|
out := bytes.NewBuffer(nil)
|
|
inserted := false
|
|
for len(targetInfo) >= 4 {
|
|
avID := binary.LittleEndian.Uint16(targetInfo[0:2])
|
|
avLen := int(binary.LittleEndian.Uint16(targetInfo[2:4]))
|
|
if 4+avLen > len(targetInfo) {
|
|
return targetInfo
|
|
}
|
|
if avID == msvAvEOL {
|
|
if !inserted {
|
|
writeU16(out, msvChannelBindings)
|
|
writeU16(out, 16)
|
|
out.Write(hash[:16])
|
|
}
|
|
out.Write(targetInfo[:4])
|
|
return out.Bytes()
|
|
}
|
|
if avID == msvChannelBindings {
|
|
writeU16(out, msvChannelBindings)
|
|
writeU16(out, 16)
|
|
out.Write(hash[:16])
|
|
inserted = true
|
|
} else {
|
|
out.Write(targetInfo[:4+avLen])
|
|
}
|
|
targetInfo = targetInfo[4+avLen:]
|
|
}
|
|
if !inserted {
|
|
writeU16(out, msvChannelBindings)
|
|
writeU16(out, 16)
|
|
out.Write(hash[:16])
|
|
}
|
|
out.Write([]byte{0, 0, 0, 0})
|
|
return out.Bytes()
|
|
}
|
|
|
|
func writeSecBuffer(buf *bytes.Buffer, length uint16, offset uint32) {
|
|
writeU16(buf, length)
|
|
writeU16(buf, length)
|
|
writeU32(buf, offset)
|
|
}
|
|
|
|
func writeU16(buf *bytes.Buffer, v uint16) {
|
|
_ = binary.Write(buf, binary.LittleEndian, v)
|
|
}
|
|
|
|
func writeU32(buf *bytes.Buffer, v uint32) {
|
|
_ = binary.Write(buf, binary.LittleEndian, v)
|
|
}
|
|
|
|
func ntlmVersion() []byte {
|
|
out := []byte{10, 0, 0, 0, 0, 0, 0, 15}
|
|
binary.LittleEndian.PutUint16(out[2:4], 19045)
|
|
return out
|
|
}
|
|
|
|
func utf16le(s string) []byte {
|
|
out := bytes.NewBuffer(nil)
|
|
for _, r := range s {
|
|
if r > 0xffff {
|
|
r = '?'
|
|
}
|
|
writeU16(out, uint16(r))
|
|
}
|
|
return out.Bytes()
|
|
}
|
|
|
|
func utf16leString(data []byte) string {
|
|
runes := make([]rune, 0, len(data)/2)
|
|
for i := 0; i+1 < len(data); i += 2 {
|
|
v := binary.LittleEndian.Uint16(data[i : i+2])
|
|
if v == 0 {
|
|
continue
|
|
}
|
|
runes = append(runes, rune(v))
|
|
}
|
|
return string(runes)
|
|
}
|