package services import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/common/i18n" ) // Neo4jPlugin Neo4j图数据库扫描和利用插件 - 包含图数据查询利用功能 type Neo4jPlugin struct { name string ports []int } // NewNeo4jPlugin 创建Neo4j插件 func NewNeo4jPlugin() *Neo4jPlugin { return &Neo4jPlugin{ name: "neo4j", ports: []int{7474, 7687, 7473}, // Neo4j HTTP、Bolt、HTTPS端口 } } // GetName 实现Plugin接口 func (p *Neo4jPlugin) GetName() string { return p.name } // GetPorts 实现Plugin接口 func (p *Neo4jPlugin) GetPorts() []int { return p.ports } // Scan 执行Neo4j扫描 - 未授权访问和弱密码检测 func (p *Neo4jPlugin) 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.testUnauthorizedAccess(ctx, info); result != nil && result.Success { common.LogSuccess(i18n.GetText("neo4j_unauth_success", target)) return result } // 生成测试凭据 credentials := GenerateCredentials("neo4j") if len(credentials) == 0 { // Neo4j默认凭据 credentials = []Credential{ {Username: "neo4j", Password: "neo4j"}, {Username: "neo4j", Password: "admin"}, {Username: "neo4j", Password: "password"}, {Username: "neo4j", Password: "123456"}, {Username: "admin", Password: "admin"}, {Username: "admin", Password: "neo4j"}, } } // 逐个测试凭据 for _, cred := range credentials { // 检查Context是否被取消 select { case <-ctx.Done(): return &ScanResult{ Success: false, Service: "neo4j", Error: ctx.Err(), } default: } // 测试凭据 if p.testCredential(ctx, info, cred) { // Neo4j认证成功 common.LogSuccess(i18n.GetText("neo4j_scan_success", target, cred.Username, cred.Password)) return &ScanResult{ Success: true, Service: "neo4j", Username: cred.Username, Password: cred.Password, } } } // 所有凭据都失败 return &ScanResult{ Success: false, Service: "neo4j", Error: fmt.Errorf("未发现弱密码或未授权访问"), } } // testUnauthorizedAccess 测试未授权访问 func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult { // 尝试无认证访问 baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, } // 检查是否可以直接访问数据库 req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/db/data/", nil) if err != nil { return nil } resp, err := client.Do(req) if err != nil { return nil } defer resp.Body.Close() if resp.StatusCode == 200 { return &ScanResult{ Success: true, Service: "neo4j", Banner: "未授权访问", } } return nil } // testCredential 测试单个凭据 func (p *Neo4jPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool { baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, } // 尝试认证 req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/user/neo4j", nil) if err != nil { return false } req.SetBasicAuth(cred.Username, cred.Password) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return false } defer resp.Body.Close() return resp.StatusCode == 200 } // executeQuery 执行Cypher查询 func (p *Neo4jPlugin) executeQuery(ctx context.Context, info *common.HostInfo, creds Credential, query string) (map[string]interface{}, error) { baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, } // 构建查询请求 queryData := map[string]interface{}{ "statements": []map[string]interface{}{ { "statement": query, }, }, } jsonData, err := json.Marshal(queryData) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, "POST", baseURL+"/db/data/transaction/commit", strings.NewReader(string(jsonData))) if err != nil { return nil, err } req.SetBasicAuth(creds.Username, creds.Password) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("查询失败,状态码: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var result map[string]interface{} err = json.Unmarshal(body, &result) if err != nil { return nil, err } return result, nil } // getServerInfo 获取服务器信息 func (p *Neo4jPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, creds Credential) string { baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, } req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/db/data/", nil) if err != nil { return "" } req.SetBasicAuth(creds.Username, creds.Password) req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return "" } defer resp.Body.Close() if resp.StatusCode != 200 { return "" } body, err := io.ReadAll(resp.Body) if err != nil { return "" } var serverInfo map[string]interface{} err = json.Unmarshal(body, &serverInfo) if err != nil { return "" } var info_str strings.Builder if version, ok := serverInfo["neo4j_version"]; ok { info_str.WriteString(fmt.Sprintf("Neo4j版本: %v\n", version)) } if extensions, ok := serverInfo["extensions"]; ok { info_str.WriteString(fmt.Sprintf("扩展信息: %v\n", extensions)) } return info_str.String() } // getDatabaseStats 获取数据库统计信息 func (p *Neo4jPlugin) getDatabaseStats(ctx context.Context, info *common.HostInfo, creds Credential) string { // 执行统计查询 result, err := p.executeQuery(ctx, info, creds, "MATCH (n) RETURN count(n) as node_count") if err != nil { return "" } var stats strings.Builder // 解析结果 if results, ok := result["results"].([]interface{}); ok && len(results) > 0 { if firstResult, ok := results[0].(map[string]interface{}); ok { if data, ok := firstResult["data"].([]interface{}); ok && len(data) > 0 { if row, ok := data[0].(map[string]interface{}); ok { if rowData, ok := row["row"].([]interface{}); ok && len(rowData) > 0 { stats.WriteString(fmt.Sprintf("节点总数: %v\n", rowData[0])) } } } } } // 获取关系统计 relResult, err := p.executeQuery(ctx, info, creds, "MATCH ()-[r]->() RETURN count(r) as rel_count") if err == nil { if results, ok := relResult["results"].([]interface{}); ok && len(results) > 0 { if firstResult, ok := results[0].(map[string]interface{}); ok { if data, ok := firstResult["data"].([]interface{}); ok && len(data) > 0 { if row, ok := data[0].(map[string]interface{}); ok { if rowData, ok := row["row"].([]interface{}); ok && len(rowData) > 0 { stats.WriteString(fmt.Sprintf("关系总数: %v\n", rowData[0])) } } } } } } return stats.String() } // getNodeLabels 获取节点标签列表 func (p *Neo4jPlugin) getNodeLabels(ctx context.Context, info *common.HostInfo, creds Credential) []string { result, err := p.executeQuery(ctx, info, creds, "CALL db.labels()") if err != nil { return nil } var labels []string // 解析结果 if results, ok := result["results"].([]interface{}); ok && len(results) > 0 { if firstResult, ok := results[0].(map[string]interface{}); ok { if data, ok := firstResult["data"].([]interface{}); ok { for _, item := range data { if row, ok := item.(map[string]interface{}); ok { if rowData, ok := row["row"].([]interface{}); ok && len(rowData) > 0 { if label, ok := rowData[0].(string); ok { labels = append(labels, label) } } } } } } } return labels } // getRelationshipTypes 获取关系类型列表 func (p *Neo4jPlugin) getRelationshipTypes(ctx context.Context, info *common.HostInfo, creds Credential) []string { result, err := p.executeQuery(ctx, info, creds, "CALL db.relationshipTypes()") if err != nil { return nil } var relationships []string // 解析结果 if results, ok := result["results"].([]interface{}); ok && len(results) > 0 { if firstResult, ok := results[0].(map[string]interface{}); ok { if data, ok := firstResult["data"].([]interface{}); ok { for _, item := range data { if row, ok := item.(map[string]interface{}); ok { if rowData, ok := row["row"].([]interface{}); ok && len(rowData) > 0 { if rel, ok := rowData[0].(string); ok { relationships = append(relationships, rel) } } } } } } } return relationships } // getProcedures 获取存储过程列表 func (p *Neo4jPlugin) getProcedures(ctx context.Context, info *common.HostInfo, creds Credential) []string { result, err := p.executeQuery(ctx, info, creds, "CALL dbms.procedures() YIELD name RETURN name LIMIT 10") if err != nil { return nil } var procedures []string // 解析结果 if results, ok := result["results"].([]interface{}); ok && len(results) > 0 { if firstResult, ok := results[0].(map[string]interface{}); ok { if data, ok := firstResult["data"].([]interface{}); ok { for _, item := range data { if row, ok := item.(map[string]interface{}); ok { if rowData, ok := row["row"].([]interface{}); ok && len(rowData) > 0 { if proc, ok := rowData[0].(string); ok { procedures = append(procedures, proc) } } } } } } } return procedures } // identifyService 服务识别 - 检测Neo4j服务 func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) client := &http.Client{ Timeout: time.Duration(common.Timeout) * time.Second, } req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil) if err != nil { return &ScanResult{ Success: false, Service: "neo4j", Error: err, } } resp, err := client.Do(req) if err != nil { return &ScanResult{ Success: false, Service: "neo4j", Error: err, } } defer resp.Body.Close() // 检查响应头或内容是否包含Neo4j特征 var banner string if resp.Header.Get("Server") != "" && strings.Contains(strings.ToLower(resp.Header.Get("Server")), "neo4j") { banner = "Neo4j图数据库 (HTTP接口)" } else if resp.StatusCode == 200 || resp.StatusCode == 401 { // 尝试检查根路径响应 body, _ := io.ReadAll(resp.Body) if strings.Contains(strings.ToLower(string(body)), "neo4j") { banner = "Neo4j图数据库服务" } else { banner = "Neo4j服务 (协议识别)" } } else { return &ScanResult{ Success: false, Service: "neo4j", Error: fmt.Errorf("无法识别为Neo4j服务"), } } common.LogSuccess(i18n.GetText("neo4j_service_identified", target, banner)) return &ScanResult{ Success: true, Service: "neo4j", Banner: banner, } } // init 自动注册插件 func init() { RegisterPlugin("neo4j", func() Plugin { return NewNeo4jPlugin() }) }