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个 - 架构复杂度: 大幅简化
404 lines
9.8 KiB
Go
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()
|
|
})
|
|
} |