From 6fb6f85ee778b0bece91b95b103f4381c13c65b4 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sat, 17 Jan 2026 14:20:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(ldap):=20=E6=B7=BB=E5=8A=A0NTLM=20Hash?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E6=94=AF=E6=8C=81=20(#433)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/i18n/locales/en.yaml | 2 + common/i18n/locales/zh.yaml | 2 + plugins/services/ldap.go | 83 +++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/common/i18n/locales/en.yaml b/common/i18n/locales/en.yaml index 92f5d88..ef9d726 100644 --- a/common/i18n/locales/en.yaml +++ b/common/i18n/locales/en.yaml @@ -396,6 +396,8 @@ start_scan: # Format: {service}_{type} - type: credential/unauth/service/vuln ldap_credential: other: "LDAP {{.Arg1}} {{.Arg2}}:{{.Arg3}}" +ldap_hash_credential: + other: "LDAP {{.Arg1}} {{.Arg2}}\\{{.Arg3}} [Hash:{{.Arg4}}]" ldap_service: other: "LDAP {{.Arg1}} {{.Arg2}}" kafka_credential: diff --git a/common/i18n/locales/zh.yaml b/common/i18n/locales/zh.yaml index 7f80582..a61abb9 100644 --- a/common/i18n/locales/zh.yaml +++ b/common/i18n/locales/zh.yaml @@ -396,6 +396,8 @@ start_scan: # 格式: {service}_{type} - type: credential/unauth/service/vuln ldap_credential: other: "LDAP {{.Arg1}} {{.Arg2}}:{{.Arg3}}" +ldap_hash_credential: + other: "LDAP {{.Arg1}} {{.Arg2}}\\{{.Arg3}} [Hash:{{.Arg4}}]" ldap_service: other: "LDAP {{.Arg1}} {{.Arg2}}" kafka_credential: diff --git a/plugins/services/ldap.go b/plugins/services/ldap.go index 0ea8ba5..c5c650c 100644 --- a/plugins/services/ldap.go +++ b/plugins/services/ldap.go @@ -30,6 +30,14 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co target := info.Target() + // Hash 认证优先:检查是否配置了 Hash 和 Domain + if len(config.Credentials.HashValues) > 0 && config.Credentials.Domain != "" { + result := p.tryHashAuth(ctx, info, config, state) + if result != nil && result.Success { + return result + } + } + credentials := GenerateCredentials("ldap", config) if len(credentials) == 0 { return &ScanResult{ @@ -108,6 +116,81 @@ func (w *ldapConnWrapper) Close() error { return w.Conn.Close() } +// tryHashAuth 尝试 NTLM Hash 认证 +func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { + target := info.Target() + domain := config.Credentials.Domain + users := config.Credentials.Userdict["ldap"] + + // 如果没有用户名,使用默认用户名 + if len(users) == 0 { + users = []string{"administrator", "admin"} + } + + for _, user := range users { + for _, hash := range config.Credentials.HashValues { + select { + case <-ctx.Done(): + return &ScanResult{ + Success: false, + Service: "ldap", + Error: ctx.Err(), + } + default: + } + + result := p.doNTLMHashAuth(ctx, info, domain, user, hash, config, state) + if result.Success { + // 截断 hash 用于显示 + displayHash := hash + if len(hash) > 16 { + displayHash = hash[:16] + "..." + } + common.LogVuln(i18n.Tr("ldap_hash_credential", target, domain, user, displayHash)) + return &ScanResult{ + Type: plugins.ResultTypeVuln, + Success: true, + Service: "ldap", + Username: user, + Password: hash, // 使用 Password 字段存储 hash + } + } + } + } + + return nil +} + +// doNTLMHashAuth 执行单次 NTLM Hash 认证 +func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, domain, username, hash string, config *common.Config, state *common.State) *AuthResult { + conn, err := p.connectLDAP(ctx, info, config) + if err != nil { + state.IncrementTCPFailedPacketCount() + return &AuthResult{ + Success: false, + ErrorType: classifyLDAPErrorType(err), + Error: err, + } + } + state.IncrementTCPSuccessPacketCount() + + if err := conn.NTLMBindWithHash(domain, username, hash); err == nil { + return &AuthResult{ + Success: true, + Conn: &ldapConnWrapper{conn}, + ErrorType: ErrorTypeUnknown, + Error: nil, + } + } + + _ = conn.Close() + return &AuthResult{ + Success: false, + ErrorType: ErrorTypeAuth, + Error: fmt.Errorf("NTLM hash authentication failed"), + } +} + // connectLDAP 连接LDAP服务器 func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, config *common.Config) (*ldaplib.Conn, error) { target := info.Target()