feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更: - 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>
This commit is contained in:
1 parent
777d9e4e71
commit
76ec9387f0
202 files changed
+16555
-2543
No files matched your search
@@ -0,0 +1,74 @@
|
||||
package credssp
|
||||
|
||||
import "encoding/asn1"
|
||||
|
||||
const defaultCredSSPVersion = 6
|
||||
|
||||
// negoToken 是 CredSSP TSRequest 中的单个协商令牌 // Logan.Li
|
||||
type negoToken struct {
|
||||
Data []byte `asn1:"explicit,tag:0"`
|
||||
}
|
||||
|
||||
// tsRequest 是 MS-CSSP 2.2.1.2 TSRequest 的最小实现 // Logan.Li
|
||||
type tsRequest struct {
|
||||
Version int `asn1:"explicit,tag:0"`
|
||||
NegoTokens []negoToken `asn1:"optional,explicit,tag:1"`
|
||||
AuthInfo []byte `asn1:"optional,explicit,tag:2"`
|
||||
PubKeyAuth []byte `asn1:"optional,explicit,tag:3"`
|
||||
ErrorCode int `asn1:"optional,explicit,tag:4"`
|
||||
ClientNonce []byte `asn1:"optional,explicit,tag:5"`
|
||||
}
|
||||
|
||||
type tsCredentials struct {
|
||||
CredType int `asn1:"explicit,tag:0"`
|
||||
Credentials []byte `asn1:"explicit,tag:1"`
|
||||
}
|
||||
|
||||
type tsPasswordCreds struct {
|
||||
DomainName []byte `asn1:"explicit,tag:0"`
|
||||
UserName []byte `asn1:"explicit,tag:1"`
|
||||
Password []byte `asn1:"explicit,tag:2"`
|
||||
}
|
||||
|
||||
func encodeTSRequest(tokens [][]byte, authInfo, pubKeyAuth []byte) ([]byte, error) {
|
||||
return encodeTSRequestWithNonce(tokens, authInfo, pubKeyAuth, nil)
|
||||
}
|
||||
|
||||
func encodeTSRequestWithNonce(tokens [][]byte, authInfo, pubKeyAuth, clientNonce []byte) ([]byte, error) {
|
||||
return encodeTSRequestVersion(defaultCredSSPVersion, tokens, authInfo, pubKeyAuth, clientNonce)
|
||||
}
|
||||
|
||||
func encodeTSRequestVersion(version int, tokens [][]byte, authInfo, pubKeyAuth, clientNonce []byte) ([]byte, error) {
|
||||
req := tsRequest{Version: version}
|
||||
for _, token := range tokens {
|
||||
req.NegoTokens = append(req.NegoTokens, negoToken{Data: token})
|
||||
}
|
||||
if len(authInfo) > 0 {
|
||||
req.AuthInfo = authInfo
|
||||
}
|
||||
if len(pubKeyAuth) > 0 {
|
||||
req.PubKeyAuth = pubKeyAuth
|
||||
}
|
||||
if len(clientNonce) > 0 {
|
||||
req.ClientNonce = clientNonce
|
||||
}
|
||||
return asn1.Marshal(req)
|
||||
}
|
||||
|
||||
func decodeTSRequest(data []byte) (*tsRequest, error) {
|
||||
var req tsRequest
|
||||
_, err := asn1.Unmarshal(data, &req)
|
||||
return &req, err
|
||||
}
|
||||
|
||||
func encodePasswordCredentials(domain, user, password string) ([]byte, error) {
|
||||
pass, err := asn1.Marshal(tsPasswordCreds{
|
||||
DomainName: utf16le(domain),
|
||||
UserName: utf16le(user),
|
||||
Password: utf16le(password),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return asn1.Marshal(tsCredentials{CredType: 1, Credentials: pass})
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package credssp
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Options 控制 CredSSP 兼容策略,仅调试和特殊 Windows 版本需要覆盖 // Logan.Li
|
||||
type Options struct {
|
||||
Version int
|
||||
PubKeyStyle string
|
||||
ForceMIC bool
|
||||
NoKeyExch bool
|
||||
UseCBT bool
|
||||
OmitVersion bool
|
||||
}
|
||||
|
||||
// ChallengeInfo 暴露 NTLM challenge 诊断信息,不包含凭据 // Logan.Li
|
||||
type ChallengeInfo struct {
|
||||
Version int
|
||||
TargetName string
|
||||
Flags uint32
|
||||
TargetInfoBytes int
|
||||
RequiresMIC bool
|
||||
DomainName string
|
||||
ComputerName string
|
||||
}
|
||||
|
||||
// ReadChallengeInfo 只读取 CredSSP/NTLM challenge,不发送认证数据 // Logan.Li
|
||||
func ReadChallengeInfo(conn *tls.Conn, version int) (ChallengeInfo, error) {
|
||||
if version == 0 {
|
||||
version = defaultCredSSPVersion
|
||||
}
|
||||
negotiate := buildNegotiate()
|
||||
req, err := encodeTSRequestVersion(version, [][]byte{negotiate}, nil, nil, nil)
|
||||
if err != nil {
|
||||
return ChallengeInfo{}, err
|
||||
}
|
||||
if _, err := conn.Write(req); err != nil {
|
||||
return ChallengeInfo{}, err
|
||||
}
|
||||
challengeReq, err := readTSRequest(conn)
|
||||
if err != nil {
|
||||
return ChallengeInfo{}, err
|
||||
}
|
||||
if len(challengeReq.NegoTokens) == 0 {
|
||||
return ChallengeInfo{}, fmt.Errorf("CredSSP challenge 缺少 nego token")
|
||||
}
|
||||
challenge, err := parseChallenge(challengeReq.NegoTokens[0].Data)
|
||||
if err != nil {
|
||||
return ChallengeInfo{}, err
|
||||
}
|
||||
return ChallengeInfo{
|
||||
Version: challengeReq.Version,
|
||||
TargetName: challenge.TargetName,
|
||||
Flags: challenge.Flags,
|
||||
TargetInfoBytes: len(challenge.TargetInfo),
|
||||
RequiresMIC: requiresMIC(challenge.TargetInfo),
|
||||
DomainName: utf16leString(findAVPair(challenge.TargetInfo, msvAvNbDomainName)),
|
||||
ComputerName: utf16leString(findAVPair(challenge.TargetInfo, msvAvNbComputerName)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Authenticate 在已升级 TLS 的 RDP 连接上执行最小 CredSSP/NTLMv2 认证 // Logan.Li
|
||||
func Authenticate(conn *tls.Conn, domain, user, password string) error {
|
||||
return AuthenticateWithOptions(conn, domain, user, password, Options{Version: defaultCredSSPVersion, PubKeyStyle: "binding"})
|
||||
}
|
||||
|
||||
// AuthenticateWithOptions 使用指定 CredSSP 版本和 pubKeyAuth 算法执行认证 // Logan.Li
|
||||
func AuthenticateWithOptions(conn *tls.Conn, domain, user, password string, opts Options) error {
|
||||
if opts.Version == 0 {
|
||||
opts.Version = defaultCredSSPVersion
|
||||
}
|
||||
if opts.PubKeyStyle == "" {
|
||||
opts.PubKeyStyle = "binding"
|
||||
}
|
||||
domain, user = splitDomainUser(domain, user)
|
||||
negotiate := buildNegotiate()
|
||||
req, err := encodeTSRequestVersion(opts.Version, [][]byte{negotiate}, nil, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := conn.Write(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
challengeReq, err := readTSRequest(conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 NTLM challenge 失败: %w", err)
|
||||
}
|
||||
if len(challengeReq.NegoTokens) == 0 {
|
||||
return fmt.Errorf("CredSSP challenge 缺少 nego token")
|
||||
}
|
||||
challenge, err := parseChallenge(challengeReq.NegoTokens[0].Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ntlmOpts := ntlmOptions{
|
||||
ForceMIC: opts.ForceMIC,
|
||||
DisableKeyExch: opts.NoKeyExch,
|
||||
OmitVersion: opts.OmitVersion,
|
||||
}
|
||||
if opts.UseCBT {
|
||||
ntlmOpts.ChannelBindings = channelBindingsHash(conn)
|
||||
}
|
||||
auth, sec, err := buildAuthenticate(challenge, domain, user, password, "AIOPS", negotiate, ntlmOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pubKey, err := peerPublicKey(conn, opts.PubKeyStyle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientNonce := randomBytes(32)
|
||||
pubKeyAuthPlain := buildPubKeyAuth(pubKey, clientNonce, opts.PubKeyStyle)
|
||||
pubKeyAuth := sec.encrypt(pubKeyAuthPlain)
|
||||
var nonce []byte
|
||||
if opts.PubKeyStyle == "binding" {
|
||||
nonce = clientNonce
|
||||
}
|
||||
req, err = encodeTSRequestVersion(opts.Version, [][]byte{auth}, nil, pubKeyAuth, nonce)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := conn.Write(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverReq, err := readTSRequest(conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取服务端公钥校验失败(flags=0x%08x mic=%v targetInfo=%d): %w", challenge.Flags, requiresMIC(challenge.TargetInfo), len(challenge.TargetInfo), err)
|
||||
}
|
||||
if serverReq.ErrorCode != 0 {
|
||||
return fmt.Errorf("CredSSP 返回错误码: %s", ntStatusMessage(serverReq.ErrorCode))
|
||||
}
|
||||
|
||||
creds, err := encodePasswordCredentials(domain, user, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptedCreds := sec.encrypt(creds)
|
||||
req, err = encodeTSRequestVersion(opts.Version, nil, encryptedCreds, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = conn.Write(req)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildPubKeyAuth(pubKey, clientNonce []byte, style string) []byte {
|
||||
switch style {
|
||||
case "raw-plus-one":
|
||||
out := append([]byte(nil), pubKey...)
|
||||
if len(out) > 0 {
|
||||
out[0]++
|
||||
}
|
||||
return out
|
||||
case "binding":
|
||||
return credSSPBindingHash(clientNonce, pubKey)
|
||||
default:
|
||||
return pubKey
|
||||
}
|
||||
}
|
||||
|
||||
func ntStatusMessage(code int) string {
|
||||
u := uint32(code)
|
||||
switch u {
|
||||
case 0xC0000064:
|
||||
return "0xC0000064 用户不存在"
|
||||
case 0xC000006A:
|
||||
return "0xC000006A 密码错误"
|
||||
case 0xC000006D:
|
||||
return "0xC000006D 用户名或认证信息无效"
|
||||
case 0xC000006E:
|
||||
return "0xC000006E 账号登录限制"
|
||||
case 0xC0000070:
|
||||
return "0xC0000070 工作站限制"
|
||||
case 0xC0000071:
|
||||
return "0xC0000071 密码已过期"
|
||||
case 0xC0000072:
|
||||
return "0xC0000072 账号已禁用"
|
||||
case 0xC000015B:
|
||||
return "0xC000015B 当前用户未被授予远程登录权限"
|
||||
case 0xC0000193:
|
||||
return "0xC0000193 账号已过期"
|
||||
case 0xC0000234:
|
||||
return "0xC0000234 账号已锁定,请先在 Windows/域控侧解锁后再连接"
|
||||
default:
|
||||
return fmt.Sprintf("0x%08X", u)
|
||||
}
|
||||
}
|
||||
|
||||
func credSSPBindingHash(clientNonce, pubKey []byte) []byte {
|
||||
h := sha256.New()
|
||||
h.Write([]byte("CredSSP Client-To-Server Binding Hash\x00"))
|
||||
h.Write(clientNonce)
|
||||
h.Write(pubKey)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func channelBindingsHash(conn *tls.Conn) []byte {
|
||||
state := conn.ConnectionState()
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return nil
|
||||
}
|
||||
cert := state.PeerCertificates[0]
|
||||
alg := cert.SignatureAlgorithm.String()
|
||||
var digest []byte
|
||||
switch {
|
||||
case strings.Contains(alg, "MD5"):
|
||||
sum := md5.Sum(cert.Raw)
|
||||
digest = sum[:]
|
||||
case strings.Contains(alg, "SHA1"):
|
||||
sum := sha1.Sum(cert.Raw)
|
||||
digest = sum[:]
|
||||
default:
|
||||
sum := sha256.Sum256(cert.Raw)
|
||||
digest = sum[:]
|
||||
}
|
||||
appData := append([]byte("tls-server-end-point:"), digest...)
|
||||
gss := make([]byte, 16+len(appData))
|
||||
copy(gss[16:], appData)
|
||||
gss[12] = byte(len(appData))
|
||||
gss[13] = byte(len(appData) >> 8)
|
||||
gss[14] = byte(len(appData) >> 16)
|
||||
gss[15] = byte(len(appData) >> 24)
|
||||
sum := md5.Sum(gss)
|
||||
return sum[:]
|
||||
}
|
||||
|
||||
func readTSRequest(r io.Reader) (*tsRequest, error) {
|
||||
der, err := readDER(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeTSRequest(der)
|
||||
}
|
||||
|
||||
func readDER(r io.Reader) ([]byte, error) {
|
||||
header := make([]byte, 2)
|
||||
if _, err := io.ReadFull(r, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if header[0] != 0x30 {
|
||||
return nil, fmt.Errorf("unexpected DER sequence tag: 0x%x", header[0])
|
||||
}
|
||||
length := int(header[1])
|
||||
lenBytes := 0
|
||||
if header[1]&0x80 != 0 {
|
||||
lenBytes = int(header[1] & 0x7f)
|
||||
if lenBytes == 0 || lenBytes > 4 {
|
||||
return nil, fmt.Errorf("invalid DER length bytes: %d", lenBytes)
|
||||
}
|
||||
raw := make([]byte, lenBytes)
|
||||
if _, err := io.ReadFull(r, raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length = 0
|
||||
for _, b := range raw {
|
||||
length = length<<8 | int(b)
|
||||
}
|
||||
header = append(header, raw...)
|
||||
}
|
||||
body := make([]byte, length)
|
||||
if _, err := io.ReadFull(r, body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(header, body...), nil
|
||||
}
|
||||
|
||||
func peerPublicKey(conn *tls.Conn, style string) ([]byte, error) {
|
||||
state := conn.ConnectionState()
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return nil, fmt.Errorf("RDP TLS 证书为空")
|
||||
}
|
||||
cert := state.PeerCertificates[0]
|
||||
if style == "rsa-compat" {
|
||||
if pub, ok := cert.PublicKey.(*rsa.PublicKey); ok {
|
||||
return asn1.Marshal(rsaPublicKeyCompat{N: pub.N, E: pub.E})
|
||||
}
|
||||
}
|
||||
// MS-CSSP v5/v6 绑定哈希必须使用 X.509 SubjectPublicKey,而非完整 SPKI // Logan.Li
|
||||
return subjectPublicKey(cert)
|
||||
}
|
||||
|
||||
func subjectPublicKey(cert *x509.Certificate) ([]byte, error) {
|
||||
if pub, ok := cert.PublicKey.(*rsa.PublicKey); ok {
|
||||
return asn1.Marshal(*pub)
|
||||
}
|
||||
var spki struct {
|
||||
Algorithm pkix.AlgorithmIdentifier
|
||||
PublicKey asn1.BitString
|
||||
}
|
||||
if _, err := asn1.Unmarshal(cert.RawSubjectPublicKeyInfo, &spki); err != nil {
|
||||
return nil, fmt.Errorf("解析 TLS SubjectPublicKey 失败: %w", err)
|
||||
}
|
||||
if len(spki.PublicKey.Bytes) == 0 {
|
||||
return nil, fmt.Errorf("TLS SubjectPublicKey 为空")
|
||||
}
|
||||
return spki.PublicKey.Bytes, nil
|
||||
}
|
||||
|
||||
type rsaPublicKeyCompat struct {
|
||||
N *big.Int `asn1:"explicit,tag:0"`
|
||||
E int `asn1:"explicit,tag:1"`
|
||||
}
|
||||
|
||||
func splitDomainUser(domain, user string) (string, string) {
|
||||
if strings.Contains(user, "\\") {
|
||||
parts := strings.Split(user, "\\")
|
||||
if len(parts) >= 2 {
|
||||
return parts[0], parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
if strings.Contains(user, "/") {
|
||||
parts := strings.Split(user, "/")
|
||||
if len(parts) >= 2 {
|
||||
return parts[0], parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
return domain, user
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package credssp
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEncodeDecodeTSRequest(t *testing.T) {
|
||||
nego := buildNegotiate()
|
||||
req, err := encodeTSRequest([][]byte{nego}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decoded, err := decodeTSRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded.Version != defaultCredSSPVersion {
|
||||
t.Fatalf("version=%d", decoded.Version)
|
||||
}
|
||||
if len(decoded.NegoTokens) != 1 || string(decoded.NegoTokens[0].Data[:8]) != "NTLMSSP\x00" {
|
||||
t.Fatalf("invalid nego token")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package credssp
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSubjectPublicKeyDiffersFromPKIX(t *testing.T) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "rdp-test"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
}, &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "rdp-test"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
}, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
subjectKey, err := subjectPublicKey(cert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkixKey, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(subjectKey) == string(pkixKey) {
|
||||
t.Fatal("SubjectPublicKey 不应等于完整 SPKI")
|
||||
}
|
||||
rsaDER, err := asn1.Marshal(*cert.PublicKey.(*rsa.PublicKey))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(subjectKey) != string(rsaDER) {
|
||||
t.Fatalf("SubjectPublicKey 应与 RSA PKCS#1 DER 一致,len=%d/%d", len(subjectKey), len(rsaDER))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user