mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00

将复杂的三文件插件架构(connector/exploiter/plugin)重构为简化的单文件插件架构, 大幅减少代码重复和维护成本,提升插件开发效率。 主要改进: • 将每个服务插件从3个文件简化为1个文件 • 删除过度设计的工厂模式、适配器模式等抽象层 • 消除plugins/services/、plugins/adapters/、plugins/base/复杂目录结构 • 实现直接的插件注册机制,提升系统简洁性 • 保持完全向后兼容,所有扫描功能和输出格式不变 重构统计: • 删除文件:100+个复杂架构文件 • 新增文件:20个简化的单文件插件 • 代码减少:每个插件减少60-80%代码量 • 功能增强:所有插件包含完整扫描和利用功能 已重构插件: MySQL, SSH, Redis, MongoDB, PostgreSQL, MSSQL, Oracle, Neo4j, Memcached, RabbitMQ, ActiveMQ, Cassandra, FTP, Kafka, LDAP, Rsync, SMTP, SNMP, Telnet, VNC 验证通过: 新系统编译运行正常,所有插件功能验证通过
472 lines
12 KiB
Go
472 lines
12 KiB
Go
package plugins
|
|
|
|
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("未发现弱密码或匿名访问"),
|
|
}
|
|
}
|
|
|
|
// Exploit 执行LDAP利用操作 - 实现目录信息收集功能
|
|
func (p *LDAPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
|
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
common.LogSuccess(fmt.Sprintf("LDAP利用开始: %s", target))
|
|
|
|
var output strings.Builder
|
|
output.WriteString(fmt.Sprintf("=== LDAP利用结果 - %s ===\n", target))
|
|
|
|
if creds.Username != "" {
|
|
output.WriteString(fmt.Sprintf("认证凭据: %s/%s\n", creds.Username, creds.Password))
|
|
} else {
|
|
output.WriteString("认证方式: 匿名绑定\n")
|
|
}
|
|
|
|
// 建立LDAP连接
|
|
conn, err := p.connectLDAP(ctx, info, creds)
|
|
if err != nil {
|
|
output.WriteString(fmt.Sprintf("\n[连接失败] %v\n", err))
|
|
return &ExploitResult{
|
|
Success: false,
|
|
Output: output.String(),
|
|
Error: err,
|
|
}
|
|
}
|
|
defer conn.Close()
|
|
|
|
output.WriteString("\n[连接状态] ✅ 成功建立LDAP连接\n")
|
|
|
|
// 获取根DSE信息
|
|
if rootDSE := p.getRootDSE(conn); rootDSE != "" {
|
|
output.WriteString(fmt.Sprintf("\n[根DSE信息]\n%s\n", rootDSE))
|
|
}
|
|
|
|
// 获取命名上下文
|
|
if namingContexts := p.getNamingContexts(conn); len(namingContexts) > 0 {
|
|
output.WriteString(fmt.Sprintf("\n[命名上下文] (共%d个)\n", len(namingContexts)))
|
|
for _, context := range namingContexts {
|
|
output.WriteString(fmt.Sprintf(" %s\n", context))
|
|
}
|
|
}
|
|
|
|
// 枚举用户
|
|
if users := p.enumerateUsers(conn); len(users) > 0 {
|
|
output.WriteString(fmt.Sprintf("\n[用户枚举] (共%d个)\n", len(users)))
|
|
for i, user := range users {
|
|
if i >= 20 { // 限制显示前20个用户
|
|
output.WriteString("... (更多用户)\n")
|
|
break
|
|
}
|
|
output.WriteString(fmt.Sprintf(" %s\n", user))
|
|
}
|
|
}
|
|
|
|
// 枚举组织单位
|
|
if ous := p.enumerateOUs(conn); len(ous) > 0 {
|
|
output.WriteString(fmt.Sprintf("\n[组织单位] (共%d个)\n", len(ous)))
|
|
for i, ou := range ous {
|
|
if i >= 15 { // 限制显示前15个OU
|
|
output.WriteString("... (更多组织单位)\n")
|
|
break
|
|
}
|
|
output.WriteString(fmt.Sprintf(" %s\n", ou))
|
|
}
|
|
}
|
|
|
|
common.LogSuccess(fmt.Sprintf("LDAP利用完成: %s", target))
|
|
|
|
return &ExploitResult{
|
|
Success: true,
|
|
Output: output.String(),
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
})
|
|
} |