mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00

将所有服务插件移动到plugins/services/目录下,使目录结构更加清晰直观: • 创建plugins/services/目录统一管理服务扫描插件 • 添加init.go提供类型别名和函数导出 • 更新main.go导入路径 • 所有20个服务插件功能验证正常 新的目录结构更便于插件管理和维护。
519 lines
13 KiB
Go
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()
|
|
})
|
|
} |