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个 - 架构复杂度: 大幅简化
458 lines
11 KiB
Go
458 lines
11 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("未发现弱密码或未授权访问"),
|
|
}
|
|
}
|
|
|
|
|
|
// 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()
|
|
})
|
|
} |