mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00

- 删除整个legacy插件系统(7794行代码) - 完成所有插件向单文件架构迁移 - 移除19个插件的虚假Exploit功能,只保留真实利用: * Redis: 文件写入、SSH密钥注入、计划任务 * SSH: 命令执行 * MS17010: EternalBlue漏洞利用 - 统一插件接口,简化架构复杂度 - 清理临时文件和备份文件 重构效果: - 代码行数: -7794行 - 插件文件数: 从3文件架构→单文件架构 - 真实利用插件: 从22个→3个 - 架构复杂度: 大幅简化
400 lines
9.5 KiB
Go
400 lines
9.5 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
ldaplib "github.com/go-ldap/ldap/v3"
|
|
"github.com/shadow1ng/fscan/common"
|
|
"github.com/shadow1ng/fscan/common/i18n"
|
|
)
|
|
|
|
// LDAPPlugin LDAP轻量级目录访问协议扫描和利用插件 - 包含目录信息收集利用功能
|
|
type LDAPPlugin struct {
|
|
name string
|
|
ports []int
|
|
}
|
|
|
|
// NewLDAPPlugin 创建LDAP插件
|
|
func NewLDAPPlugin() *LDAPPlugin {
|
|
return &LDAPPlugin{
|
|
name: "ldap",
|
|
ports: []int{389, 636, 3268, 3269}, // 389: LDAP, 636: LDAPS, 3268/3269: Global Catalog
|
|
}
|
|
}
|
|
|
|
// GetName 实现Plugin接口
|
|
func (p *LDAPPlugin) GetName() string {
|
|
return p.name
|
|
}
|
|
|
|
// GetPorts 实现Plugin接口
|
|
func (p *LDAPPlugin) GetPorts() []int {
|
|
return p.ports
|
|
}
|
|
|
|
// Scan 执行LDAP扫描 - 匿名绑定检测和弱密码检测
|
|
func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
|
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
|
|
// 如果禁用暴力破解,只做服务识别
|
|
if common.DisableBrute {
|
|
return p.identifyService(ctx, info)
|
|
}
|
|
|
|
// 首先测试匿名绑定
|
|
if result := p.testAnonymousBind(ctx, info); result != nil && result.Success {
|
|
common.LogSuccess(i18n.GetText("ldap_anonymous_success", target))
|
|
return result
|
|
}
|
|
|
|
// 生成测试凭据
|
|
credentials := GenerateCredentials("ldap")
|
|
if len(credentials) == 0 {
|
|
// LDAP默认凭据
|
|
credentials = []Credential{
|
|
{Username: "admin", Password: "admin"},
|
|
{Username: "administrator", Password: "administrator"},
|
|
{Username: "admin", Password: "password"},
|
|
{Username: "admin", Password: "123456"},
|
|
{Username: "ldap", Password: "ldap"},
|
|
{Username: "manager", Password: "manager"},
|
|
{Username: "root", Password: "root"},
|
|
{Username: "bind", Password: "bind"},
|
|
{Username: "guest", Password: "guest"},
|
|
{Username: "test", Password: "test"},
|
|
}
|
|
}
|
|
|
|
// 逐个测试凭据
|
|
for _, cred := range credentials {
|
|
// 检查Context是否被取消
|
|
select {
|
|
case <-ctx.Done():
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "ldap",
|
|
Error: ctx.Err(),
|
|
}
|
|
default:
|
|
}
|
|
|
|
// 测试凭据
|
|
if p.testCredential(ctx, info, cred) {
|
|
// LDAP认证成功
|
|
common.LogSuccess(i18n.GetText("ldap_scan_success", target, cred.Username, cred.Password))
|
|
|
|
return &ScanResult{
|
|
Success: true,
|
|
Service: "ldap",
|
|
Username: cred.Username,
|
|
Password: cred.Password,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 所有凭据都失败
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "ldap",
|
|
Error: fmt.Errorf("未发现弱密码或匿名访问"),
|
|
}
|
|
}
|
|
|
|
|
|
// testAnonymousBind 测试匿名绑定
|
|
func (p *LDAPPlugin) testAnonymousBind(ctx context.Context, info *common.HostInfo) *ScanResult {
|
|
conn, err := p.connectLDAP(ctx, info, Credential{})
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer conn.Close()
|
|
|
|
// 尝试匿名绑定
|
|
if err := conn.UnauthenticatedBind(""); err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &ScanResult{
|
|
Success: true,
|
|
Service: "ldap",
|
|
Banner: "匿名访问",
|
|
}
|
|
}
|
|
|
|
// testCredential 测试单个凭据
|
|
func (p *LDAPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
|
conn, err := p.connectLDAP(ctx, info, cred)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer conn.Close()
|
|
|
|
// 尝试绑定
|
|
bindDNs := p.generateBindDNs(cred.Username)
|
|
|
|
for _, bindDN := range bindDNs {
|
|
if err := conn.Bind(bindDN, cred.Password); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// connectLDAP 连接到LDAP服务
|
|
func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, creds Credential) (*ldaplib.Conn, error) {
|
|
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
|
|
// 根据端口选择连接方式
|
|
var conn *ldaplib.Conn
|
|
var err error
|
|
|
|
if info.Ports == "636" { // LDAPS
|
|
conn, err = ldaplib.DialTLS("tcp", target, nil)
|
|
} else {
|
|
conn, err = ldaplib.Dial("tcp", target)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// generateBindDNs 生成绑定DN列表
|
|
func (p *LDAPPlugin) generateBindDNs(username string) []string {
|
|
return []string{
|
|
fmt.Sprintf("cn=%s,dc=example,dc=com", username),
|
|
fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", username),
|
|
fmt.Sprintf("uid=%s,ou=users,dc=example,dc=com", username),
|
|
fmt.Sprintf("uid=%s,dc=example,dc=com", username),
|
|
fmt.Sprintf("cn=%s,cn=users,dc=example,dc=com", username),
|
|
fmt.Sprintf("sAMAccountName=%s", username), // Active Directory
|
|
username, // 直接使用用户名作为DN
|
|
}
|
|
}
|
|
|
|
// getRootDSE 获取根DSE信息
|
|
func (p *LDAPPlugin) getRootDSE(conn *ldaplib.Conn) string {
|
|
searchRequest := ldaplib.NewSearchRequest(
|
|
"", // 根DSE
|
|
ldaplib.ScopeBaseObject,
|
|
ldaplib.NeverDerefAliases,
|
|
0, 0, false,
|
|
"(objectClass=*)",
|
|
[]string{"*", "+"}, // 请求所有属性
|
|
nil,
|
|
)
|
|
|
|
sr, err := conn.Search(searchRequest)
|
|
if err != nil || len(sr.Entries) == 0 {
|
|
return "无法获取根DSE信息"
|
|
}
|
|
|
|
var info strings.Builder
|
|
entry := sr.Entries[0]
|
|
|
|
// 显示重要属性
|
|
importantAttrs := []string{
|
|
"vendorName", "vendorVersion", "serverName",
|
|
"supportedLDAPVersion", "namingContexts",
|
|
"defaultNamingContext", "schemaNamingContext",
|
|
"configurationNamingContext",
|
|
}
|
|
|
|
for _, attr := range importantAttrs {
|
|
if values := entry.GetAttributeValues(attr); len(values) > 0 {
|
|
info.WriteString(fmt.Sprintf("%s: %s\n", attr, strings.Join(values, ", ")))
|
|
}
|
|
}
|
|
|
|
return info.String()
|
|
}
|
|
|
|
// getNamingContexts 获取命名上下文
|
|
func (p *LDAPPlugin) getNamingContexts(conn *ldaplib.Conn) []string {
|
|
searchRequest := ldaplib.NewSearchRequest(
|
|
"", // 根DSE
|
|
ldaplib.ScopeBaseObject,
|
|
ldaplib.NeverDerefAliases,
|
|
0, 0, false,
|
|
"(objectClass=*)",
|
|
[]string{"namingContexts"},
|
|
nil,
|
|
)
|
|
|
|
sr, err := conn.Search(searchRequest)
|
|
if err != nil || len(sr.Entries) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return sr.Entries[0].GetAttributeValues("namingContexts")
|
|
}
|
|
|
|
// enumerateUsers 枚举用户
|
|
func (p *LDAPPlugin) enumerateUsers(conn *ldaplib.Conn) []string {
|
|
// 尝试常见的用户搜索基DN
|
|
searchBases := []string{
|
|
"dc=example,dc=com",
|
|
"ou=users,dc=example,dc=com",
|
|
"cn=users,dc=example,dc=com",
|
|
"",
|
|
}
|
|
|
|
var users []string
|
|
|
|
for _, baseDN := range searchBases {
|
|
searchRequest := ldaplib.NewSearchRequest(
|
|
baseDN,
|
|
ldaplib.ScopeWholeSubtree,
|
|
ldaplib.NeverDerefAliases,
|
|
50, 0, false, // 限制返回50个结果
|
|
"(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))",
|
|
[]string{"cn", "uid", "sAMAccountName", "mail", "displayName"},
|
|
nil,
|
|
)
|
|
|
|
sr, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, entry := range sr.Entries {
|
|
var userInfo strings.Builder
|
|
userInfo.WriteString(fmt.Sprintf("DN: %s", entry.DN))
|
|
|
|
if cn := entry.GetAttributeValue("cn"); cn != "" {
|
|
userInfo.WriteString(fmt.Sprintf(", CN: %s", cn))
|
|
}
|
|
if uid := entry.GetAttributeValue("uid"); uid != "" {
|
|
userInfo.WriteString(fmt.Sprintf(", UID: %s", uid))
|
|
}
|
|
if sam := entry.GetAttributeValue("sAMAccountName"); sam != "" {
|
|
userInfo.WriteString(fmt.Sprintf(", SAM: %s", sam))
|
|
}
|
|
if mail := entry.GetAttributeValue("mail"); mail != "" {
|
|
userInfo.WriteString(fmt.Sprintf(", Mail: %s", mail))
|
|
}
|
|
|
|
users = append(users, userInfo.String())
|
|
}
|
|
|
|
if len(users) > 0 {
|
|
break // 找到用户就停止搜索其他基DN
|
|
}
|
|
}
|
|
|
|
return users
|
|
}
|
|
|
|
// enumerateOUs 枚举组织单位
|
|
func (p *LDAPPlugin) enumerateOUs(conn *ldaplib.Conn) []string {
|
|
searchBases := []string{
|
|
"dc=example,dc=com",
|
|
"",
|
|
}
|
|
|
|
var ous []string
|
|
|
|
for _, baseDN := range searchBases {
|
|
searchRequest := ldaplib.NewSearchRequest(
|
|
baseDN,
|
|
ldaplib.ScopeWholeSubtree,
|
|
ldaplib.NeverDerefAliases,
|
|
30, 0, false, // 限制返回30个结果
|
|
"(objectClass=organizationalUnit)",
|
|
[]string{"ou", "description"},
|
|
nil,
|
|
)
|
|
|
|
sr, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, entry := range sr.Entries {
|
|
var ouInfo strings.Builder
|
|
ouInfo.WriteString(fmt.Sprintf("DN: %s", entry.DN))
|
|
|
|
if ou := entry.GetAttributeValue("ou"); ou != "" {
|
|
ouInfo.WriteString(fmt.Sprintf(", OU: %s", ou))
|
|
}
|
|
if desc := entry.GetAttributeValue("description"); desc != "" {
|
|
ouInfo.WriteString(fmt.Sprintf(", Desc: %s", desc))
|
|
}
|
|
|
|
ous = append(ous, ouInfo.String())
|
|
}
|
|
|
|
if len(ous) > 0 {
|
|
break // 找到OU就停止搜索其他基DN
|
|
}
|
|
}
|
|
|
|
return ous
|
|
}
|
|
|
|
// identifyService 服务识别 - 检测LDAP服务
|
|
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
|
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
|
|
conn, err := p.connectLDAP(ctx, info, Credential{})
|
|
if err != nil {
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "ldap",
|
|
Error: err,
|
|
}
|
|
}
|
|
defer conn.Close()
|
|
|
|
// 尝试获取根DSE信息来确认这是LDAP服务
|
|
searchRequest := ldaplib.NewSearchRequest(
|
|
"", // 根DSE
|
|
ldaplib.ScopeBaseObject,
|
|
ldaplib.NeverDerefAliases,
|
|
0, 0, false,
|
|
"(objectClass=*)",
|
|
[]string{"vendorName", "vendorVersion"},
|
|
nil,
|
|
)
|
|
|
|
sr, err := conn.Search(searchRequest)
|
|
var banner string
|
|
|
|
if err != nil {
|
|
banner = "LDAP目录服务"
|
|
} else if len(sr.Entries) > 0 {
|
|
entry := sr.Entries[0]
|
|
vendor := entry.GetAttributeValue("vendorName")
|
|
version := entry.GetAttributeValue("vendorVersion")
|
|
|
|
if vendor != "" && version != "" {
|
|
banner = fmt.Sprintf("LDAP目录服务 (%s %s)", vendor, version)
|
|
} else if vendor != "" {
|
|
banner = fmt.Sprintf("LDAP目录服务 (%s)", vendor)
|
|
} else {
|
|
banner = "LDAP目录服务"
|
|
}
|
|
} else {
|
|
banner = "LDAP目录服务"
|
|
}
|
|
|
|
common.LogSuccess(i18n.GetText("ldap_service_identified", target, banner))
|
|
|
|
return &ScanResult{
|
|
Success: true,
|
|
Service: "ldap",
|
|
Banner: banner,
|
|
}
|
|
}
|
|
|
|
// init 自动注册插件
|
|
func init() {
|
|
RegisterPlugin("ldap", func() Plugin {
|
|
return NewLDAPPlugin()
|
|
})
|
|
} |