fscan/plugins/services/elasticsearch.go
ZacharyZcR 6cf5719e8a refactor: 彻底清理插件系统,消除虚假利用功能
- 删除整个legacy插件系统(7794行代码)
- 完成所有插件向单文件架构迁移
- 移除19个插件的虚假Exploit功能,只保留真实利用:
  * Redis: 文件写入、SSH密钥注入、计划任务
  * SSH: 命令执行
  * MS17010: EternalBlue漏洞利用
- 统一插件接口,简化架构复杂度
- 清理临时文件和备份文件

重构效果:
- 代码行数: -7794行
- 插件文件数: 从3文件架构→单文件架构
- 真实利用插件: 从22个→3个
- 架构复杂度: 大幅简化
2025-08-26 11:43:48 +08:00

404 lines
9.8 KiB
Go

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()
})
}