From 7b8d2b4add94964a87b481c10fcb65d6c4206a7e Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Fri, 8 Aug 2025 12:54:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0LDAP=E8=BD=BB?= =?UTF-8?q?=E9=87=8F=E7=BA=A7=E7=9B=AE=E5=BD=95=E8=AE=BF=E9=97=AE=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E4=B8=93=E4=B8=9A=E6=89=AB=E6=8F=8F=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Common/i18n/messages/plugins.go | 22 +++ Core/Registry.go | 1 + Plugins/services/ldap/connector.go | 124 ++++++++++++++++ Plugins/services/ldap/exploiter.go | 36 +++++ Plugins/services/ldap/plugin.go | 222 +++++++++++++++++++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 Plugins/services/ldap/connector.go create mode 100644 Plugins/services/ldap/exploiter.go create mode 100644 Plugins/services/ldap/plugin.go diff --git a/Common/i18n/messages/plugins.go b/Common/i18n/messages/plugins.go index d476bf9..deb8ff9 100644 --- a/Common/i18n/messages/plugins.go +++ b/Common/i18n/messages/plugins.go @@ -628,4 +628,26 @@ var PluginMessages = map[string]map[string]string{ LangZH: "文件上传测试", LangEN: "File Upload Test", }, + + // ========================= LDAP插件消息 ========================= + "ldap_weak_pwd_success": { + LangZH: "LDAP弱密码: %s [%s:%s]", + LangEN: "LDAP weak password: %s [%s:%s]", + }, + "ldap_anonymous_access": { + LangZH: "LDAP服务 %s 匿名访问成功", + LangEN: "LDAP service %s anonymous access successful", + }, + "ldap_service_identified": { + LangZH: "LDAP服务识别成功: %s - %s", + LangEN: "LDAP service identified: %s - %s", + }, + "ldap_connection_failed": { + LangZH: "LDAP连接失败: %v", + LangEN: "LDAP connection failed: %v", + }, + "ldap_auth_failed": { + LangZH: "LDAP认证失败: %v", + LangEN: "LDAP authentication failed: %v", + }, } \ No newline at end of file diff --git a/Core/Registry.go b/Core/Registry.go index 02f1521..57b346d 100644 --- a/Core/Registry.go +++ b/Core/Registry.go @@ -11,6 +11,7 @@ import ( _ "github.com/shadow1ng/fscan/plugins/services/ftp" _ "github.com/shadow1ng/fscan/plugins/services/imap" _ "github.com/shadow1ng/fscan/plugins/services/kafka" + _ "github.com/shadow1ng/fscan/plugins/services/ldap" _ "github.com/shadow1ng/fscan/plugins/services/mysql" _ "github.com/shadow1ng/fscan/plugins/services/redis" _ "github.com/shadow1ng/fscan/plugins/services/ssh" diff --git a/Plugins/services/ldap/connector.go b/Plugins/services/ldap/connector.go new file mode 100644 index 0000000..e50031a --- /dev/null +++ b/Plugins/services/ldap/connector.go @@ -0,0 +1,124 @@ +package ldap + +import ( + "context" + "fmt" + "strings" + + ldaplib "github.com/go-ldap/ldap/v3" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// LDAPConnection LDAP连接包装器 +type LDAPConnection struct { + client *ldaplib.Conn + target string +} + +// LDAPConnector LDAP连接器实现 +type LDAPConnector struct { + host string + port string +} + +// NewLDAPConnector 创建LDAP连接器 +func NewLDAPConnector() *LDAPConnector { + return &LDAPConnector{} +} + +// Connect 连接到LDAP服务 +func (c *LDAPConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) { + c.host = info.Host + c.port = info.Ports + + target := fmt.Sprintf("%s:%s", c.host, c.port) + + // 使用Context控制的TCP连接 + conn, err := common.WrapperTcpWithContext(ctx, "tcp", target) + if err != nil { + return nil, fmt.Errorf("LDAP连接失败: %v", err) + } + + // 创建LDAP连接 + ldapConn := ldaplib.NewConn(conn, false) + go ldapConn.Start() // 在goroutine中启动连接处理 + + return &LDAPConnection{ + client: ldapConn, + target: target, + }, nil +} + +// Authenticate 认证 +func (c *LDAPConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error { + ldapConn, ok := conn.(*LDAPConnection) + if !ok { + return fmt.Errorf("无效的连接类型") + } + + // 在goroutine中进行认证操作,支持Context取消 + resultChan := make(chan error, 1) + + go func() { + var err error + + if cred.Username == "" && cred.Password == "" { + // 匿名绑定 + err = ldapConn.client.UnauthenticatedBind("") + } else { + // 构建绑定DN - 支持多种常见格式,优先管理员DN + bindDNs := []string{ + fmt.Sprintf("cn=%s,dc=example,dc=com", cred.Username), // 管理员绑定格式 + fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", cred.Username), // 用户绑定格式 + fmt.Sprintf("uid=%s,ou=users,dc=example,dc=com", cred.Username), + fmt.Sprintf("uid=%s,dc=example,dc=com", cred.Username), + cred.Username, // 直接使用用户名作为DN + } + + // 尝试不同的绑定DN格式 + var bindErr error + for _, bindDN := range bindDNs { + bindErr = ldapConn.client.Bind(bindDN, cred.Password) + if bindErr == nil { + break + } + } + err = bindErr + } + + // 绑定成功即表示认证成功,不需要额外搜索验证 + // 因为某些LDAP配置下普通用户没有搜索权限 + + select { + case resultChan <- err: + case <-ctx.Done(): + } + }() + + // 等待认证结果或Context取消 + select { + case err := <-resultChan: + if err != nil { + // 检查是否是认证失败 + if strings.Contains(strings.ToLower(err.Error()), "bind") || + strings.Contains(strings.ToLower(err.Error()), "authentication") || + strings.Contains(strings.ToLower(err.Error()), "invalid credentials") || + strings.Contains(strings.ToLower(err.Error()), "49") { // LDAP错误码49表示认证失败 + return fmt.Errorf("LDAP认证失败") + } + return fmt.Errorf("LDAP操作失败: %v", err) + } + return nil + case <-ctx.Done(): + return fmt.Errorf("LDAP认证超时: %v", ctx.Err()) + } +} + +// Close 关闭连接 +func (c *LDAPConnector) Close(conn interface{}) error { + if ldapConn, ok := conn.(*LDAPConnection); ok && ldapConn.client != nil { + ldapConn.client.Close() + } + return nil +} \ No newline at end of file diff --git a/Plugins/services/ldap/exploiter.go b/Plugins/services/ldap/exploiter.go new file mode 100644 index 0000000..6a568b9 --- /dev/null +++ b/Plugins/services/ldap/exploiter.go @@ -0,0 +1,36 @@ +package ldap + +import ( + "context" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// LDAPExploiter LDAP利用器实现 - 最小化版本,不提供利用功能 +type LDAPExploiter struct { + *base.BaseExploiter +} + +// NewLDAPExploiter 创建LDAP利用器 +func NewLDAPExploiter() *LDAPExploiter { + exploiter := &LDAPExploiter{ + BaseExploiter: base.NewBaseExploiter("ldap"), + } + + // LDAP插件不提供利用功能 + exploiter.setupExploitMethods() + + return exploiter +} + +// setupExploitMethods 设置利用方法 +func (e *LDAPExploiter) setupExploitMethods() { + // LDAP插件不提供利用功能,仅进行弱密码扫描和匿名访问检测 +} + +// Exploit 利用接口实现 - 空实现 +func (e *LDAPExploiter) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + // LDAP插件不提供利用功能 + return nil, nil +} \ No newline at end of file diff --git a/Plugins/services/ldap/plugin.go b/Plugins/services/ldap/plugin.go new file mode 100644 index 0000000..5846f7a --- /dev/null +++ b/Plugins/services/ldap/plugin.go @@ -0,0 +1,222 @@ +package ldap + +import ( + "context" + "fmt" + "net" + "strings" + "time" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// LDAPPlugin LDAP插件实现 +type LDAPPlugin struct { + *base.ServicePlugin + exploiter *LDAPExploiter +} + +// NewLDAPPlugin 创建LDAP插件 +func NewLDAPPlugin() *LDAPPlugin { + // 插件元数据 + metadata := &base.PluginMetadata{ + Name: "ldap", + Version: "2.0.0", + Author: "fscan-team", + Description: "LDAP轻量级目录访问协议扫描和利用插件", + Category: "service", + Ports: []int{389, 636, 3268, 3269}, // 389: LDAP, 636: LDAPS, 3268/3269: Global Catalog + Protocols: []string{"tcp"}, + Tags: []string{"ldap", "directory", "bruteforce", "anonymous"}, + } + + // 创建连接器和服务插件 + connector := NewLDAPConnector() + servicePlugin := base.NewServicePlugin(metadata, connector) + + // 创建LDAP插件 + plugin := &LDAPPlugin{ + ServicePlugin: servicePlugin, + exploiter: NewLDAPExploiter(), + } + + // 设置能力 + plugin.SetCapabilities([]base.Capability{ + base.CapWeakPassword, + base.CapUnauthorized, + base.CapDataExtraction, + }) + + return plugin +} + +// Scan 重写扫描方法,先检测匿名访问 +func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + // 如果禁用了暴力破解,只进行服务识别 + if common.DisableBrute { + return p.performServiceIdentification(ctx, info) + } + + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 先尝试匿名访问 + anonymousCred := &base.Credential{Username: "", Password: ""} + anonymousResult, err := p.ScanCredential(ctx, info, anonymousCred) + if err == nil && anonymousResult.Success { + // 匿名访问成功 + common.LogSuccess(i18n.GetText("ldap_anonymous_access", target)) + + return &base.ScanResult{ + Success: true, + Service: "LDAP", + Credentials: []*base.Credential{anonymousCred}, + Extra: map[string]interface{}{ + "service": "LDAP", + "port": info.Ports, + "unauthorized": true, + "access_type": "anonymous", + }, + }, nil + } + + // 执行基础的密码扫描 + result, err := p.ServicePlugin.Scan(ctx, info) + if err != nil || !result.Success { + return result, err + } + + // 记录成功的弱密码发现 + cred := result.Credentials[0] + common.LogSuccess(i18n.GetText("ldap_weak_pwd_success", target, cred.Username, cred.Password)) + + return result, nil +} + +// generateCredentials 重写凭据生成方法 +func (p *LDAPPlugin) generateCredentials() []*base.Credential { + // 获取LDAP专用的用户名字典 + usernames := common.Userdict["ldap"] + if len(usernames) == 0 { + // 默认LDAP用户名 + usernames = []string{"admin", "administrator", "ldap", "root", "manager", "directory", "test", "user"} + } + + return base.GenerateCredentials(usernames, common.Passwords) +} + +// Exploit 使用exploiter执行利用 +func (p *LDAPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return p.exploiter.Exploit(ctx, info, creds) +} + +// GetExploitMethods 获取利用方法 +func (p *LDAPPlugin) GetExploitMethods() []base.ExploitMethod { + return p.exploiter.GetExploitMethods() +} + +// IsExploitSupported 检查利用支持 +func (p *LDAPPlugin) IsExploitSupported(method base.ExploitType) bool { + return p.exploiter.IsExploitSupported(method) +} + +// performServiceIdentification 执行LDAP服务识别(-nobr模式) +func (p *LDAPPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 尝试连接LDAP服务获取基本信息 + ldapInfo, isLDAP := p.identifyLDAPService(ctx, info) + if isLDAP { + // 记录服务识别成功 + common.LogSuccess(i18n.GetText("ldap_service_identified", target, ldapInfo)) + + return &base.ScanResult{ + Success: true, + Service: "LDAP", + Banner: ldapInfo, + Extra: map[string]interface{}{ + "service": "LDAP", + "port": info.Ports, + "info": ldapInfo, + }, + }, nil + } + + // 如果无法识别为LDAP,返回失败 + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("无法识别为LDAP服务"), + }, nil +} + +// identifyLDAPService 通过连接识别LDAP服务 +func (p *LDAPPlugin) identifyLDAPService(ctx context.Context, info *common.HostInfo) (string, bool) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 尝试建立TCP连接 + conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) + if err != nil { + return "", false + } + defer conn.Close() + + // 尝试创建LDAP连接并获取基本信息 + connector := NewLDAPConnector() + + // 使用context包装TCP连接 + ldapConn, err := connector.Connect(ctx, info) + if err != nil { + return "", false + } + defer connector.Close(ldapConn) + + // 尝试匿名绑定以确认LDAP服务 + anonymousCred := &base.Credential{Username: "", Password: ""} + err = connector.Authenticate(ctx, ldapConn, anonymousCred) + + if err != nil { + // 检查错误是否表明这是LDAP服务但需要认证 + errStr := strings.ToLower(err.Error()) + if strings.Contains(errStr, "ldap") || + strings.Contains(errStr, "bind") || + strings.Contains(errStr, "authentication") || + strings.Contains(errStr, "49") { // LDAP认证失败错误码 + return fmt.Sprintf("LDAP服务 (需要认证): %v", err), true + } + return "", false + } + + // 匿名绑定成功,这是LDAP服务 + return "LDAP服务 (支持匿名访问)", true +} + +// ============================================================================= +// 插件注册 +// ============================================================================= + +// RegisterLDAPPlugin 注册LDAP插件 +func RegisterLDAPPlugin() { + factory := base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "ldap", + Version: "2.0.0", + Author: "fscan-team", + Description: "LDAP轻量级目录访问协议扫描和利用插件", + Category: "service", + Ports: []int{389, 636, 3268, 3269}, // 389: LDAP, 636: LDAPS, 3268/3269: Global Catalog + Protocols: []string{"tcp"}, + Tags: []string{"ldap", "directory", "bruteforce", "anonymous"}, + }, + func() base.Plugin { + return NewLDAPPlugin() + }, + ) + + base.GlobalPluginRegistry.Register("ldap", factory) +} + +// 自动注册 +func init() { + RegisterLDAPPlugin() +} \ No newline at end of file