package services import ( "context" "crypto/tls" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/shadow1ng/fscan/common" ) // ElasticsearchPlugin Elasticsearch搜索引擎扫描插件 - 弱密码检测和未授权访问检测 type ElasticsearchPlugin struct { name string ports []int } // NewElasticsearchPlugin 创建Elasticsearch插件 func NewElasticsearchPlugin() *ElasticsearchPlugin { return &ElasticsearchPlugin{ name: "elasticsearch", ports: []int{9200, 9300}, // Elasticsearch端口 } } // GetName 实现Plugin接口 func (p *ElasticsearchPlugin) GetName() string { return p.name } // GetPorts 实现Plugin接口 func (p *ElasticsearchPlugin) GetPorts() []int { return p.ports } // Scan 执行Elasticsearch扫描 - 弱密码检测和未授权访问检测 func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) // 检查端口 if info.Ports != "9200" && info.Ports != "9300" { return &ScanResult{ Success: false, Service: "elasticsearch", Error: fmt.Errorf("Elasticsearch插件仅支持9200和9300端口"), } } // 如果禁用暴力破解,只做服务识别 if common.DisableBrute { return p.identifyService(ctx, info) } // 首先测试未授权访问 if result := p.testUnauthAccess(ctx, info); result != nil && result.Success { common.LogSuccess(fmt.Sprintf("Elasticsearch %s 未授权访问", target)) return result } // 生成测试凭据 credentials := GenerateCredentials("elasticsearch") if len(credentials) == 0 { // Elasticsearch默认凭据 credentials = []Credential{ {Username: "", Password: ""}, {Username: "elastic", Password: ""}, {Username: "elastic", Password: "elastic"}, {Username: "elastic", Password: "password"}, {Username: "elastic", Password: "123456"}, {Username: "admin", Password: "admin"}, {Username: "admin", Password: ""}, } } // 逐个测试凭据 for _, cred := range credentials { // 检查Context是否被取消 select { case <-ctx.Done(): return &ScanResult{ Success: false, Service: "elasticsearch", Error: ctx.Err(), } default: } // 测试凭据 if p.testCredential(ctx, info, cred) { // Elasticsearch认证成功 common.LogSuccess(fmt.Sprintf("Elasticsearch %s 弱密码 %s:%s", target, cred.Username, cred.Password)) return &ScanResult{ Success: true, Service: "elasticsearch", Username: cred.Username, Password: cred.Password, } } } // 所有凭据都失败 return &ScanResult{ Success: false, Service: "elasticsearch", Error: fmt.Errorf("未发现弱密码或未授权访问"), } } // ElasticsearchClusterInfo 集群信息结构 type ElasticsearchClusterInfo struct { ClusterName string Version string NodeName string } // testUnauthAccess 测试未授权访问 func (p *ElasticsearchPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult { if p.testCredential(ctx, info, Credential{Username: "", Password: ""}) { return &ScanResult{ Success: true, Service: "elasticsearch", Banner: "未授权访问", } } return nil } // testCredential 测试单个凭据 func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool { client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } // 构建URL protocol := "http" if info.Ports == "9443" { protocol = "https" } url := fmt.Sprintf("%s://%s:%s/", protocol, info.Host, info.Ports) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return false } // 如果有凭据,添加认证头 if cred.Username != "" || cred.Password != "" { auth := base64.StdEncoding.EncodeToString([]byte(cred.Username + ":" + cred.Password)) req.Header.Set("Authorization", "Basic "+auth) } resp, err := client.Do(req) if err != nil { return false } defer resp.Body.Close() // 检查响应状态码 if resp.StatusCode == 200 { // 读取响应内容验证是否为Elasticsearch body, err := io.ReadAll(resp.Body) if err != nil { return false } bodyStr := string(body) return strings.Contains(bodyStr, "elasticsearch") || strings.Contains(bodyStr, "cluster_name") } return false } // getClusterInfo 获取集群信息 func (p *ElasticsearchPlugin) getClusterInfo(ctx context.Context, info *common.HostInfo, creds Credential) *ElasticsearchClusterInfo { client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } protocol := "http" if info.Ports == "9443" { protocol = "https" } url := fmt.Sprintf("%s://%s:%s/", protocol, info.Host, info.Ports) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil } if creds.Username != "" || creds.Password != "" { auth := base64.StdEncoding.EncodeToString([]byte(creds.Username + ":" + creds.Password)) req.Header.Set("Authorization", "Basic "+auth) } resp, err := client.Do(req) if err != nil { return nil } defer resp.Body.Close() if resp.StatusCode != 200 { return nil } body, err := io.ReadAll(resp.Body) if err != nil { return nil } // 解析JSON响应 var clusterData map[string]interface{} if err := json.Unmarshal(body, &clusterData); err != nil { return nil } clusterInfo := &ElasticsearchClusterInfo{} if clusterName, ok := clusterData["cluster_name"].(string); ok { clusterInfo.ClusterName = clusterName } if nodeName, ok := clusterData["name"].(string); ok { clusterInfo.NodeName = nodeName } if version, ok := clusterData["version"].(map[string]interface{}); ok { if number, ok := version["number"].(string); ok { clusterInfo.Version = number } } return clusterInfo } // getIndices 获取索引列表 func (p *ElasticsearchPlugin) getIndices(ctx context.Context, info *common.HostInfo, creds Credential) []string { client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } protocol := "http" if info.Ports == "9443" { protocol = "https" } url := fmt.Sprintf("%s://%s:%s/_cat/indices?format=json", protocol, info.Host, info.Ports) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil } if creds.Username != "" || creds.Password != "" { auth := base64.StdEncoding.EncodeToString([]byte(creds.Username + ":" + creds.Password)) req.Header.Set("Authorization", "Basic "+auth) } resp, err := client.Do(req) if err != nil { return nil } defer resp.Body.Close() if resp.StatusCode != 200 { return nil } body, err := io.ReadAll(resp.Body) if err != nil { return nil } // 解析索引信息 var indices []map[string]interface{} if err := json.Unmarshal(body, &indices); err != nil { return nil } var indexNames []string for _, index := range indices { if indexName, ok := index["index"].(string); ok { indexNames = append(indexNames, indexName) } } return indexNames } // checkSensitiveData 检查敏感数据 func (p *ElasticsearchPlugin) checkSensitiveData(ctx context.Context, info *common.HostInfo, creds Credential) map[string]int { sensitiveData := make(map[string]int) client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } protocol := "http" if info.Ports == "9443" { protocol = "https" } // 获取所有索引 indices := p.getIndices(ctx, info, creds) if len(indices) == 0 { return sensitiveData } // 检查常见的敏感索引 sensitivePatterns := []string{"user", "account", "password", "credential", "login", "auth", "admin", "config"} for _, index := range indices { indexLower := strings.ToLower(index) for _, pattern := range sensitivePatterns { if strings.Contains(indexLower, pattern) { // 获取该索引的文档数量 url := fmt.Sprintf("%s://%s:%s/%s/_count", protocol, info.Host, info.Ports, index) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { continue } if creds.Username != "" || creds.Password != "" { auth := base64.StdEncoding.EncodeToString([]byte(creds.Username + ":" + creds.Password)) req.Header.Set("Authorization", "Basic "+auth) } resp, err := client.Do(req) if err != nil { continue } if resp.StatusCode == 200 { body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { continue } var countData map[string]interface{} if err := json.Unmarshal(body, &countData); err != nil { continue } if count, ok := countData["count"].(float64); ok { sensitiveData[fmt.Sprintf("%s (索引: %s)", pattern, index)] = int(count) } } else { resp.Body.Close() } break } } } return sensitiveData } // identifyService 服务识别 - 检测Elasticsearch服务 func (p *ElasticsearchPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { if p.testCredential(ctx, info, Credential{Username: "", Password: ""}) { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) banner := "Elasticsearch搜索引擎服务" common.LogSuccess(fmt.Sprintf("Elasticsearch %s %s", target, banner)) return &ScanResult{ Success: true, Service: "elasticsearch", Banner: banner, } } return &ScanResult{ Success: false, Service: "elasticsearch", Error: fmt.Errorf("无法识别为Elasticsearch服务"), } } // init 自动注册插件 func init() { RegisterPlugin("elasticsearch", func() Plugin { return NewElasticsearchPlugin() }) }