fscan/plugins/services/neo4j.go
ZacharyZcR e082e2bb59 refactor: 重组插件目录结构,提升管理直观性
将所有服务插件移动到plugins/services/目录下,使目录结构更加清晰直观:
• 创建plugins/services/目录统一管理服务扫描插件
• 添加init.go提供类型别名和函数导出
• 更新main.go导入路径
• 所有20个服务插件功能验证正常

新的目录结构更便于插件管理和维护。
2025-08-26 00:02:13 +08:00

519 lines
13 KiB
Go

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("未发现弱密码或未授权访问"),
}
}
// Exploit 执行Neo4j利用操作 - 实现图数据查询功能
func (p *Neo4jPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
common.LogSuccess(fmt.Sprintf("Neo4j利用开始: %s (用户: %s)", target, creds.Username))
var output strings.Builder
output.WriteString(fmt.Sprintf("=== Neo4j利用结果 - %s ===\n", target))
// 获取服务器信息
if serverInfo := p.getServerInfo(ctx, info, creds); serverInfo != "" {
output.WriteString(fmt.Sprintf("\n[服务器信息]\n%s\n", serverInfo))
}
// 获取数据库统计信息
if dbStats := p.getDatabaseStats(ctx, info, creds); dbStats != "" {
output.WriteString(fmt.Sprintf("\n[数据库统计]\n%s\n", dbStats))
}
// 获取节点标签
if labels := p.getNodeLabels(ctx, info, creds); len(labels) > 0 {
output.WriteString(fmt.Sprintf("\n[节点标签] (共%d个)\n", len(labels)))
for i, label := range labels {
if i >= 10 { // 限制显示前10个
output.WriteString("... (更多标签)\n")
break
}
output.WriteString(fmt.Sprintf(" %s\n", label))
}
}
// 获取关系类型
if relationships := p.getRelationshipTypes(ctx, info, creds); len(relationships) > 0 {
output.WriteString(fmt.Sprintf("\n[关系类型] (共%d个)\n", len(relationships)))
for i, rel := range relationships {
if i >= 10 { // 限制显示前10个
output.WriteString("... (更多关系)\n")
break
}
output.WriteString(fmt.Sprintf(" %s\n", rel))
}
}
// 获取存储过程
if procedures := p.getProcedures(ctx, info, creds); len(procedures) > 0 {
output.WriteString(fmt.Sprintf("\n[存储过程] (共%d个)\n", len(procedures)))
for i, proc := range procedures {
if i >= 5 { // 限制显示前5个
output.WriteString("... (更多存储过程)\n")
break
}
output.WriteString(fmt.Sprintf(" %s\n", proc))
}
}
common.LogSuccess(fmt.Sprintf("Neo4j利用完成: %s", target))
return &ExploitResult{
Success: true,
Output: output.String(),
}
}
// 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()
})
}