mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00
重构服务插件架构,移除冗余功能
- 统一所有服务插件的实现模式,移除i18n国际化依赖 - 删除硬编码的备份凭据列表,统一使用GenerateCredentials() - 移除过度工程化的Context取消检查 - 清理exploitation功能,专注于弱密码检测和服务识别 - 简化代码结构,移除冗余注释和说明文档 - 优化19个服务插件:activemq, cassandra, elasticsearch, ftp, kafka, ldap, memcached, mongodb, mssql, mysql, neo4j, oracle, postgresql, rabbitmq, rsync, smtp, snmp, telnet, vnc - 代码总量减少约40%,提升维护效率 此次重构确保插件架构的一致性和简洁性
This commit is contained in:
parent
8a79f3cf0f
commit
2e449c74ef
@ -8,34 +8,28 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// ActiveMQPlugin ActiveMQ消息队列扫描插件 - 基于STOMP协议
|
||||
type ActiveMQPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewActiveMQPlugin 创建ActiveMQ插件
|
||||
func NewActiveMQPlugin() *ActiveMQPlugin {
|
||||
return &ActiveMQPlugin{
|
||||
name: "activemq",
|
||||
ports: []int{61616, 61617, 61618, 8161}, // STOMP端口 + Web管理界面
|
||||
ports: []int{61616, 61617, 61618, 8161},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *ActiveMQPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *ActiveMQPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行ActiveMQ扫描 - STOMP协议弱密码检测
|
||||
func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -44,36 +38,18 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
|
||||
return p.identifyService(info)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("activemq")
|
||||
if len(credentials) == 0 {
|
||||
// ActiveMQ默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: ""},
|
||||
{Username: "admin", Password: "password"},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "guest", Password: "guest"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "activemq",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "activemq",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// ActiveMQ认证成功
|
||||
common.LogSuccess(i18n.GetText("activemq_scan_success", target, cred.Username, cred.Password))
|
||||
common.LogSuccess(fmt.Sprintf("ActiveMQ %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -84,7 +60,6 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "activemq",
|
||||
@ -92,39 +67,30 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
|
||||
}
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据 - STOMP协议认证
|
||||
func (p *ActiveMQPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 使用STOMP协议进行认证
|
||||
return p.authenticateSTOMP(conn, cred.Username, cred.Password)
|
||||
}
|
||||
|
||||
// authenticateSTOMP 使用STOMP协议进行身份验证
|
||||
// STOMP (Simple Text Oriented Messaging Protocol) 是ActiveMQ支持的文本协议
|
||||
func (p *ActiveMQPlugin) authenticateSTOMP(conn net.Conn, username, password string) bool {
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 构造STOMP CONNECT帧
|
||||
// STOMP是基于帧的协议,每个帧以NULL字符结尾
|
||||
stompConnect := fmt.Sprintf("CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\nlogin:%s\npasscode:%s\n\n\x00",
|
||||
username, password)
|
||||
|
||||
// 设置写超时并发送认证请求
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte(stompConnect)); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 设置读超时并读取响应
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
@ -134,26 +100,19 @@ func (p *ActiveMQPlugin) authenticateSTOMP(conn net.Conn, username, password str
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
// 检查STOMP响应
|
||||
// 成功响应应该包含"CONNECTED"帧
|
||||
// 失败响应包含"ERROR"帧
|
||||
if strings.Contains(responseStr, "CONNECTED") {
|
||||
return true
|
||||
} else if strings.Contains(responseStr, "ERROR") {
|
||||
// 错误响应,认证失败
|
||||
return false
|
||||
}
|
||||
|
||||
// 未知响应格式
|
||||
return false
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测STOMP协议
|
||||
func (p *ActiveMQPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 尝试连接
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
@ -164,7 +123,6 @@ func (p *ActiveMQPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 发送简单的STOMP CONNECT帧(无认证信息)
|
||||
stompConnect := "CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\n\n\x00"
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
@ -176,7 +134,6 @@ func (p *ActiveMQPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
}
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 512)
|
||||
n, err := conn.Read(response)
|
||||
@ -190,10 +147,9 @@ func (p *ActiveMQPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
// 检查是否为STOMP协议响应
|
||||
if strings.Contains(responseStr, "CONNECTED") || strings.Contains(responseStr, "ERROR") {
|
||||
banner := p.extractSTOMPVersion(responseStr)
|
||||
common.LogSuccess(i18n.GetText("activemq_service_identified", target, banner))
|
||||
banner := "ActiveMQ STOMP"
|
||||
common.LogSuccess(fmt.Sprintf("ActiveMQ %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -209,34 +165,7 @@ func (p *ActiveMQPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
}
|
||||
}
|
||||
|
||||
// extractSTOMPVersion 从STOMP响应中提取版本信息
|
||||
func (p *ActiveMQPlugin) extractSTOMPVersion(response string) string {
|
||||
lines := strings.Split(response, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
// 查找version头
|
||||
if strings.HasPrefix(line, "version:") {
|
||||
version := strings.TrimPrefix(line, "version:")
|
||||
return fmt.Sprintf("ActiveMQ STOMP %s", version)
|
||||
}
|
||||
// 查找server头
|
||||
if strings.HasPrefix(line, "server:") {
|
||||
server := strings.TrimPrefix(line, "server:")
|
||||
return fmt.Sprintf("ActiveMQ %s", server)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到版本信息,返回通用描述
|
||||
if strings.Contains(response, "CONNECTED") {
|
||||
return "ActiveMQ STOMP (版本未知)"
|
||||
} else if strings.Contains(response, "ERROR") {
|
||||
return "ActiveMQ STOMP (需要认证)"
|
||||
}
|
||||
|
||||
return "ActiveMQ STOMP"
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("activemq", func() Plugin {
|
||||
return NewActiveMQPlugin()
|
||||
|
@ -3,79 +3,53 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// CassandraPlugin Cassandra数据库扫描插件 - 基于gocql库
|
||||
type CassandraPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewCassandraPlugin 创建Cassandra插件
|
||||
func NewCassandraPlugin() *CassandraPlugin {
|
||||
return &CassandraPlugin{
|
||||
name: "cassandra",
|
||||
ports: []int{9042, 9160, 7000, 7001}, // CQL端口 + Thrift端口 + 集群通信端口
|
||||
ports: []int{9042, 9160, 7000, 7001},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *CassandraPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *CassandraPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行Cassandra扫描 - CQL协议弱密码检测
|
||||
func (p *CassandraPlugin) 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)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("cassandra")
|
||||
if len(credentials) == 0 {
|
||||
// Cassandra默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "cassandra", Password: "cassandra"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: ""},
|
||||
{Username: "root", Password: "root"},
|
||||
{Username: "user", Password: "user"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "cassandra",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "cassandra",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// Cassandra认证成功
|
||||
common.LogSuccess(i18n.GetText("cassandra_scan_success", target, cred.Username, cred.Password))
|
||||
common.LogSuccess(fmt.Sprintf("Cassandra %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -86,7 +60,6 @@ func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo) *Scan
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "cassandra",
|
||||
@ -94,26 +67,18 @@ func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo) *Scan
|
||||
}
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据 - 使用gocql进行CQL认证
|
||||
func (p *CassandraPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
// 解析端口
|
||||
port, err := strconv.Atoi(info.Ports)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 创建Cassandra集群配置
|
||||
cluster := gocql.NewCluster(info.Host)
|
||||
cluster.Port = port
|
||||
|
||||
// 设置连接参数
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
cluster.Timeout = timeout
|
||||
cluster.ConnectTimeout = timeout
|
||||
cluster.ProtoVersion = 4
|
||||
cluster.Consistency = gocql.One
|
||||
|
||||
// 设置认证信息
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
cluster.Authenticator = gocql.PasswordAuthenticator{
|
||||
Username: cred.Username,
|
||||
@ -121,93 +86,21 @@ func (p *CassandraPlugin) testCredential(ctx context.Context, info *common.HostI
|
||||
}
|
||||
}
|
||||
|
||||
// 支持代理连接
|
||||
if common.Socks5Proxy != "" {
|
||||
cluster.Dialer = &cassandraProxyDialer{timeout: timeout}
|
||||
}
|
||||
|
||||
// 设置重试策略
|
||||
cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: 2}
|
||||
|
||||
// 创建会话
|
||||
session, err := p.createSessionWithTimeout(ctx, cluster)
|
||||
session, err := cluster.CreateSession()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// 执行简单查询验证连接
|
||||
return p.validateConnection(ctx, session)
|
||||
var dummy interface{}
|
||||
err = session.Query("SELECT now() FROM system.local").WithContext(ctx).Scan(&dummy)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// createSessionWithTimeout 在超时控制下创建Cassandra会话
|
||||
func (p *CassandraPlugin) createSessionWithTimeout(ctx context.Context, cluster *gocql.ClusterConfig) (*gocql.Session, error) {
|
||||
// 创建会话通道以支持Context超时
|
||||
sessionChan := make(chan struct {
|
||||
session *gocql.Session
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在goroutine中创建会话
|
||||
go func() {
|
||||
session, err := cluster.CreateSession()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if session != nil {
|
||||
session.Close()
|
||||
}
|
||||
case sessionChan <- struct {
|
||||
session *gocql.Session
|
||||
err error
|
||||
}{session, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待会话创建或Context取消
|
||||
select {
|
||||
case result := <-sessionChan:
|
||||
return result.session, result.err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("创建会话超时: %v", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// validateConnection 验证Cassandra连接 - 执行系统查询
|
||||
func (p *CassandraPlugin) validateConnection(ctx context.Context, session *gocql.Session) bool {
|
||||
// 使用通道支持Context超时
|
||||
resultChan := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
// 尝试执行系统查询
|
||||
// 先尝试查询节点信息,如果失败再尝试本地信息
|
||||
var dummy interface{}
|
||||
|
||||
err := session.Query("SELECT peer FROM system.peers LIMIT 1").WithContext(ctx).Scan(&dummy)
|
||||
if err != nil {
|
||||
// 如果peer查询失败,尝试查询本地节点信息
|
||||
err = session.Query("SELECT now() FROM system.local").WithContext(ctx).Scan(&dummy)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- (err == nil):
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待查询结果或Context取消
|
||||
select {
|
||||
case success := <-resultChan:
|
||||
return success
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 尝试连接Cassandra但不认证
|
||||
func (p *CassandraPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 解析端口
|
||||
port, err := strconv.Atoi(info.Ports)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
@ -217,34 +110,23 @@ func (p *CassandraPlugin) identifyService(ctx context.Context, info *common.Host
|
||||
}
|
||||
}
|
||||
|
||||
// 创建基本集群配置(无认证)
|
||||
cluster := gocql.NewCluster(info.Host)
|
||||
cluster.Port = port
|
||||
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
cluster.Timeout = timeout
|
||||
cluster.ConnectTimeout = timeout
|
||||
cluster.ProtoVersion = 4
|
||||
|
||||
// 支持代理
|
||||
if common.Socks5Proxy != "" {
|
||||
cluster.Dialer = &cassandraProxyDialer{timeout: timeout}
|
||||
}
|
||||
|
||||
// 尝试创建会话
|
||||
session, err := p.createSessionWithTimeout(ctx, cluster)
|
||||
session, err := cluster.CreateSession()
|
||||
if err != nil {
|
||||
// 检查错误类型,判断是否为认证问题
|
||||
if p.isAuthenticationError(err) {
|
||||
// 认证错误说明服务存在但需要密码
|
||||
common.LogSuccess(i18n.GetText("cassandra_service_identified", target, "Cassandra (需要认证)"))
|
||||
if strings.Contains(strings.ToLower(err.Error()), "authentication") {
|
||||
banner := "Cassandra (需要认证)"
|
||||
common.LogSuccess(fmt.Sprintf("Cassandra %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "cassandra",
|
||||
Banner: "Cassandra (需要认证)",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "cassandra",
|
||||
@ -253,10 +135,8 @@ func (p *CassandraPlugin) identifyService(ctx context.Context, info *common.Host
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// 连接成功,获取Cassandra版本信息
|
||||
banner := p.getCassandraVersion(ctx, session)
|
||||
common.LogSuccess(i18n.GetText("cassandra_service_identified", target, banner))
|
||||
|
||||
banner := "Cassandra"
|
||||
common.LogSuccess(fmt.Sprintf("Cassandra %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "cassandra",
|
||||
@ -264,55 +144,9 @@ func (p *CassandraPlugin) identifyService(ctx context.Context, info *common.Host
|
||||
}
|
||||
}
|
||||
|
||||
// getCassandraVersion 获取Cassandra版本信息
|
||||
func (p *CassandraPlugin) getCassandraVersion(ctx context.Context, session *gocql.Session) string {
|
||||
// 尝试查询系统版本信息
|
||||
var releaseVersion string
|
||||
err := session.Query("SELECT release_version FROM system.local").WithContext(ctx).Scan(&releaseVersion)
|
||||
if err == nil && releaseVersion != "" {
|
||||
return fmt.Sprintf("Cassandra %s", releaseVersion)
|
||||
}
|
||||
|
||||
// 如果无法获取版本,返回通用描述
|
||||
return "Cassandra (版本未知)"
|
||||
}
|
||||
|
||||
// isAuthenticationError 判断是否为认证错误
|
||||
func (p *CassandraPlugin) isAuthenticationError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
// 检查常见的认证错误信息
|
||||
authErrors := []string{
|
||||
"authentication",
|
||||
"password",
|
||||
"credentials",
|
||||
"unauthorized",
|
||||
"login",
|
||||
}
|
||||
|
||||
for _, authErr := range authErrors {
|
||||
if strings.Contains(strings.ToLower(errStr), authErr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// cassandraProxyDialer 代理拨号器
|
||||
type cassandraProxyDialer struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// DialContext 实现gocql.Dialer接口
|
||||
func (d *cassandraProxyDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return common.WrapperTcpWithContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("cassandra", func() Plugin {
|
||||
return NewCassandraPlugin()
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -14,87 +13,45 @@ import (
|
||||
"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端口
|
||||
ports: []int{9200, 9300},
|
||||
}
|
||||
}
|
||||
|
||||
// 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: ""},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
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))
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("Elasticsearch %s %s:%s", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "elasticsearch",
|
||||
@ -104,35 +61,14 @@ func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo) *
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: fmt.Errorf("未发现弱密码或未授权访问"),
|
||||
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,
|
||||
@ -153,7 +89,6 @@ func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.H
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果有凭据,添加认证头
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(cred.Username + ":" + cred.Password))
|
||||
req.Header.Set("Authorization", "Basic "+auth)
|
||||
@ -165,9 +100,7 @@ func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.H
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode == 200 {
|
||||
// 读取响应内容验证是否为Elasticsearch
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false
|
||||
@ -180,215 +113,20 @@ func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.H
|
||||
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搜索引擎服务"
|
||||
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",
|
||||
@ -396,7 +134,6 @@ func (p *ElasticsearchPlugin) identifyService(ctx context.Context, info *common.
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("elasticsearch", func() Plugin {
|
||||
return NewElasticsearchPlugin()
|
||||
|
@ -3,86 +3,52 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ftplib "github.com/jlaffaye/ftp"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// FTPPlugin FTP服务扫描和利用插件 - 包含文件操作利用功能
|
||||
type FTPPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewFTPPlugin 创建FTP插件
|
||||
func NewFTPPlugin() *FTPPlugin {
|
||||
return &FTPPlugin{
|
||||
name: "ftp",
|
||||
ports: []int{21, 2121, 990}, // FTP端口 + FTPS端口
|
||||
ports: []int{21, 2121, 990},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *FTPPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *FTPPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行FTP扫描 - 弱密码检测和匿名访问检测
|
||||
func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(info)
|
||||
}
|
||||
|
||||
// 首先检查匿名访问
|
||||
if result := p.testAnonymousAccess(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("ftp_anonymous_success", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("ftp")
|
||||
if len(credentials) == 0 {
|
||||
// FTP默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "ftp", Password: "ftp"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: ""},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "test", Password: "test"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ftp",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ftp",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if conn := p.testCredential(ctx, info, cred); conn != nil {
|
||||
conn.Quit() // 关闭测试连接
|
||||
|
||||
// FTP认证成功
|
||||
common.LogSuccess(i18n.GetText("ftp_scan_success", target, cred.Username, cred.Password))
|
||||
|
||||
conn.Quit()
|
||||
common.LogSuccess(fmt.Sprintf("FTP %s %s:%s", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "ftp",
|
||||
@ -92,114 +58,38 @@ func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ftp",
|
||||
Error: fmt.Errorf("未发现弱密码或匿名访问"),
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testAnonymousAccess 测试匿名访问
|
||||
func (p *FTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
// 尝试匿名登录
|
||||
anonCreds := []Credential{
|
||||
{Username: "anonymous", Password: ""},
|
||||
{Username: "anonymous", Password: "anonymous"},
|
||||
{Username: "anonymous", Password: "guest@example.com"},
|
||||
{Username: "ftp", Password: ""},
|
||||
}
|
||||
|
||||
for _, cred := range anonCreds {
|
||||
if conn := p.testCredential(ctx, info, cred); conn != nil {
|
||||
conn.Quit()
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "ftp",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
Banner: "匿名访问",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据 - 返回FTP连接或nil
|
||||
func (p *FTPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *ftplib.ServerConn {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 使用Context控制超时的连接
|
||||
type ftpResult struct {
|
||||
conn *ftplib.ServerConn
|
||||
err error
|
||||
}
|
||||
|
||||
connChan := make(chan ftpResult, 1)
|
||||
|
||||
go func() {
|
||||
// 建立FTP连接
|
||||
conn, err := ftplib.DialTimeout(target, timeout)
|
||||
if err != nil {
|
||||
connChan <- ftpResult{nil, err}
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试登录
|
||||
err = conn.Login(cred.Username, cred.Password)
|
||||
if err != nil {
|
||||
conn.Quit()
|
||||
connChan <- ftpResult{nil, err}
|
||||
return
|
||||
}
|
||||
|
||||
connChan <- ftpResult{conn, nil}
|
||||
}()
|
||||
|
||||
// 等待连接结果或超时
|
||||
select {
|
||||
case result := <-connChan:
|
||||
if result.err != nil {
|
||||
return nil
|
||||
}
|
||||
return result.conn
|
||||
case <-ctx.Done():
|
||||
conn, err := ftplib.DialTimeout(target, timeout)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// testWritePermission 测试写权限
|
||||
func (p *FTPPlugin) testWritePermission(conn *ftplib.ServerConn, filename, content string) error {
|
||||
// 尝试创建文件并写入内容
|
||||
return conn.Stor(filename, strings.NewReader(content))
|
||||
}
|
||||
|
||||
// getSystemInfo 获取系统信息
|
||||
func (p *FTPPlugin) getSystemInfo(conn *ftplib.ServerConn) string {
|
||||
var info strings.Builder
|
||||
|
||||
// 尝试获取当前目录作为系统信息
|
||||
if pwd, err := conn.CurrentDir(); err == nil {
|
||||
info.WriteString(fmt.Sprintf("当前目录: %s\n", pwd))
|
||||
err = conn.Login(cred.Username, cred.Password)
|
||||
if err != nil {
|
||||
conn.Quit()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试列出当前目录获取更多信息
|
||||
if entries, err := conn.List("."); err == nil && len(entries) > 0 {
|
||||
info.WriteString(fmt.Sprintf("目录项数量: %d\n", len(entries)))
|
||||
}
|
||||
|
||||
return info.String()
|
||||
return conn
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测FTP服务
|
||||
|
||||
|
||||
func (p *FTPPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 尝试连接FTP服务
|
||||
conn, err := ftplib.DialTimeout(target, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
@ -210,16 +100,8 @@ func (p *FTPPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
}
|
||||
defer conn.Quit()
|
||||
|
||||
// 获取FTP服务器信息
|
||||
var banner string
|
||||
if pwd, err := conn.CurrentDir(); err == nil {
|
||||
banner = fmt.Sprintf("FTP服务 (根目录: %s)", pwd)
|
||||
} else {
|
||||
banner = "FTP服务"
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("ftp_service_identified", target, banner))
|
||||
|
||||
banner := "FTP"
|
||||
common.LogSuccess(fmt.Sprintf("FTP %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "ftp",
|
||||
@ -227,7 +109,6 @@ func (p *FTPPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("ftp", func() Plugin {
|
||||
return NewFTPPlugin()
|
||||
|
@ -7,81 +7,48 @@ import (
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// KafkaPlugin Kafka消息队列扫描和利用插件 - 包含信息收集利用功能
|
||||
type KafkaPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewKafkaPlugin 创建Kafka插件
|
||||
func NewKafkaPlugin() *KafkaPlugin {
|
||||
return &KafkaPlugin{
|
||||
name: "kafka",
|
||||
ports: []int{9092, 9093, 9094}, // Kafka broker端口
|
||||
ports: []int{9092, 9093, 9094},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *KafkaPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *KafkaPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行Kafka扫描 - 弱密码检测和未授权访问检测
|
||||
func (p *KafkaPlugin) 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("kafka_unauthorized_success", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("kafka")
|
||||
if len(credentials) == 0 {
|
||||
// Kafka默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: ""},
|
||||
{Username: "kafka", Password: "kafka"},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "test", Password: "test"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "kafka",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "kafka",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if client := p.testCredential(ctx, info, cred); client != nil {
|
||||
client.Close() // 关闭测试连接
|
||||
|
||||
// Kafka认证成功
|
||||
common.LogSuccess(i18n.GetText("kafka_scan_success", target, cred.Username, cred.Password))
|
||||
|
||||
client.Close()
|
||||
common.LogSuccess(fmt.Sprintf("Kafka %s %s:%s", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
@ -91,46 +58,24 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "kafka",
|
||||
Error: fmt.Errorf("未发现弱密码或未授权访问"),
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *KafkaPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
// 尝试无认证连接
|
||||
emptyCred := Credential{Username: "", Password: ""}
|
||||
|
||||
if client := p.testCredential(ctx, info, emptyCred); client != nil {
|
||||
client.Close()
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
Banner: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据 - 返回Kafka客户端或nil
|
||||
func (p *KafkaPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) sarama.Client {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 创建Kafka配置
|
||||
config := sarama.NewConfig()
|
||||
config.Net.DialTimeout = timeout
|
||||
config.Net.ReadTimeout = timeout
|
||||
config.Net.WriteTimeout = timeout
|
||||
config.Net.TLS.Enable = false
|
||||
config.Version = sarama.V2_0_0_0
|
||||
|
||||
// 如果提供了用户名密码,设置SASL认证
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
config.Net.SASL.Enable = true
|
||||
config.Net.SASL.Mechanism = sarama.SASLTypePlaintext
|
||||
@ -140,97 +85,18 @@ func (p *KafkaPlugin) testCredential(ctx context.Context, info *common.HostInfo,
|
||||
}
|
||||
|
||||
brokers := []string{target}
|
||||
|
||||
// 使用Context控制超时
|
||||
type kafkaResult struct {
|
||||
client sarama.Client
|
||||
err error
|
||||
}
|
||||
|
||||
clientChan := make(chan kafkaResult, 1)
|
||||
|
||||
go func() {
|
||||
// 尝试创建客户端
|
||||
client, err := sarama.NewClient(brokers, config)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if client != nil {
|
||||
client.Close()
|
||||
}
|
||||
case clientChan <- kafkaResult{client, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待客户端创建或超时
|
||||
select {
|
||||
case result := <-clientChan:
|
||||
if result.err != nil {
|
||||
return nil
|
||||
}
|
||||
return result.client
|
||||
case <-ctx.Done():
|
||||
client, err := sarama.NewClient(brokers, config)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// getConsumerGroups 获取消费者组列表
|
||||
func (p *KafkaPlugin) getConsumerGroups(client sarama.Client) ([]string, error) {
|
||||
// 创建协调器客户端获取消费者组信息
|
||||
brokers := client.Brokers()
|
||||
if len(brokers) == 0 {
|
||||
return nil, fmt.Errorf("没有可用的broker")
|
||||
}
|
||||
|
||||
broker := brokers[0] // 使用第一个broker
|
||||
|
||||
// 打开broker连接
|
||||
if err := broker.Open(client.Config()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer broker.Close()
|
||||
|
||||
// 发送ListGroups请求
|
||||
request := &sarama.ListGroupsRequest{}
|
||||
response, err := broker.ListGroups(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups := make([]string, 0, len(response.Groups))
|
||||
for groupId := range response.Groups {
|
||||
groups = append(groups, groupId)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// testProduceMessage 测试发送消息
|
||||
func (p *KafkaPlugin) testProduceMessage(client sarama.Client, topic string) error {
|
||||
config := client.Config()
|
||||
config.Producer.Return.Successes = true
|
||||
config.Producer.Timeout = 5 * time.Second
|
||||
|
||||
producer, err := sarama.NewSyncProducerFromClient(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer producer.Close()
|
||||
|
||||
// 发送测试消息
|
||||
message := &sarama.ProducerMessage{
|
||||
Topic: topic,
|
||||
Value: sarama.StringEncoder("FScan security test message"),
|
||||
}
|
||||
|
||||
_, _, err = producer.SendMessage(message)
|
||||
return err
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测Kafka服务
|
||||
func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 尝试无认证连接
|
||||
emptyCred := Credential{Username: "", Password: ""}
|
||||
client := p.testCredential(ctx, info, emptyCred)
|
||||
if client == nil {
|
||||
@ -242,18 +108,8 @@ func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 获取集群信息作为banner
|
||||
var banner string
|
||||
if err := client.RefreshMetadata(); err == nil {
|
||||
brokers := client.Brokers()
|
||||
topics, _ := client.Topics()
|
||||
banner = fmt.Sprintf("Kafka集群 (Brokers: %d, Topics: %d)", len(brokers), len(topics))
|
||||
} else {
|
||||
banner = "Kafka服务"
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("kafka_service_identified", target, banner))
|
||||
|
||||
banner := "Kafka"
|
||||
common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
@ -261,7 +117,6 @@ func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("kafka", func() Plugin {
|
||||
return NewKafkaPlugin()
|
||||
|
@ -3,88 +3,50 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
ldaplib "github.com/go-ldap/ldap/v3"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// LDAPPlugin LDAP轻量级目录访问协议扫描和利用插件 - 包含目录信息收集利用功能
|
||||
type LDAPPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewLDAPPlugin 创建LDAP插件
|
||||
func NewLDAPPlugin() *LDAPPlugin {
|
||||
return &LDAPPlugin{
|
||||
name: "ldap",
|
||||
ports: []int{389, 636, 3268, 3269}, // 389: LDAP, 636: LDAPS, 3268/3269: Global Catalog
|
||||
ports: []int{389, 636, 3268, 3269},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *LDAPPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *LDAPPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行LDAP扫描 - 匿名绑定检测和弱密码检测
|
||||
func (p *LDAPPlugin) 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.testAnonymousBind(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("ldap_anonymous_success", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("ldap")
|
||||
if len(credentials) == 0 {
|
||||
// LDAP默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "administrator", Password: "administrator"},
|
||||
{Username: "admin", Password: "password"},
|
||||
{Username: "admin", Password: "123456"},
|
||||
{Username: "ldap", Password: "ldap"},
|
||||
{Username: "manager", Password: "manager"},
|
||||
{Username: "root", Password: "root"},
|
||||
{Username: "bind", Password: "bind"},
|
||||
{Username: "guest", Password: "guest"},
|
||||
{Username: "test", Password: "test"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ldap",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ldap",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// LDAP认证成功
|
||||
common.LogSuccess(i18n.GetText("ldap_scan_success", target, cred.Username, cred.Password))
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("LDAP %s %s:%s", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "ldap",
|
||||
@ -94,36 +56,14 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ldap",
|
||||
Error: fmt.Errorf("未发现弱密码或匿名访问"),
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testAnonymousBind 测试匿名绑定
|
||||
func (p *LDAPPlugin) testAnonymousBind(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
conn, err := p.connectLDAP(ctx, info, Credential{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试匿名绑定
|
||||
if err := conn.UnauthenticatedBind(""); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "ldap",
|
||||
Banner: "匿名访问",
|
||||
}
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据
|
||||
func (p *LDAPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
conn, err := p.connectLDAP(ctx, info, cred)
|
||||
if err != nil {
|
||||
@ -131,213 +71,27 @@ func (p *LDAPPlugin) testCredential(ctx context.Context, info *common.HostInfo,
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试绑定
|
||||
bindDNs := p.generateBindDNs(cred.Username)
|
||||
|
||||
for _, bindDN := range bindDNs {
|
||||
if err := conn.Bind(bindDN, cred.Password); err == nil {
|
||||
return true
|
||||
}
|
||||
// 简单的绑定测试
|
||||
if err := conn.Bind(cred.Username, cred.Password); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// connectLDAP 连接到LDAP服务
|
||||
func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, creds Credential) (*ldaplib.Conn, error) {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 根据端口选择连接方式
|
||||
var conn *ldaplib.Conn
|
||||
var err error
|
||||
|
||||
if info.Ports == "636" { // LDAPS
|
||||
conn, err = ldaplib.DialTLS("tcp", target, nil)
|
||||
} else {
|
||||
conn, err = ldaplib.Dial("tcp", target)
|
||||
if info.Ports == "636" {
|
||||
return ldaplib.DialTLS("tcp", target, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
return ldaplib.Dial("tcp", target)
|
||||
}
|
||||
|
||||
// generateBindDNs 生成绑定DN列表
|
||||
func (p *LDAPPlugin) generateBindDNs(username string) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("cn=%s,dc=example,dc=com", username),
|
||||
fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", username),
|
||||
fmt.Sprintf("uid=%s,ou=users,dc=example,dc=com", username),
|
||||
fmt.Sprintf("uid=%s,dc=example,dc=com", username),
|
||||
fmt.Sprintf("cn=%s,cn=users,dc=example,dc=com", username),
|
||||
fmt.Sprintf("sAMAccountName=%s", username), // Active Directory
|
||||
username, // 直接使用用户名作为DN
|
||||
}
|
||||
}
|
||||
|
||||
// getRootDSE 获取根DSE信息
|
||||
func (p *LDAPPlugin) getRootDSE(conn *ldaplib.Conn) string {
|
||||
searchRequest := ldaplib.NewSearchRequest(
|
||||
"", // 根DSE
|
||||
ldaplib.ScopeBaseObject,
|
||||
ldaplib.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(objectClass=*)",
|
||||
[]string{"*", "+"}, // 请求所有属性
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil || len(sr.Entries) == 0 {
|
||||
return "无法获取根DSE信息"
|
||||
}
|
||||
|
||||
var info strings.Builder
|
||||
entry := sr.Entries[0]
|
||||
|
||||
// 显示重要属性
|
||||
importantAttrs := []string{
|
||||
"vendorName", "vendorVersion", "serverName",
|
||||
"supportedLDAPVersion", "namingContexts",
|
||||
"defaultNamingContext", "schemaNamingContext",
|
||||
"configurationNamingContext",
|
||||
}
|
||||
|
||||
for _, attr := range importantAttrs {
|
||||
if values := entry.GetAttributeValues(attr); len(values) > 0 {
|
||||
info.WriteString(fmt.Sprintf("%s: %s\n", attr, strings.Join(values, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// getNamingContexts 获取命名上下文
|
||||
func (p *LDAPPlugin) getNamingContexts(conn *ldaplib.Conn) []string {
|
||||
searchRequest := ldaplib.NewSearchRequest(
|
||||
"", // 根DSE
|
||||
ldaplib.ScopeBaseObject,
|
||||
ldaplib.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(objectClass=*)",
|
||||
[]string{"namingContexts"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil || len(sr.Entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sr.Entries[0].GetAttributeValues("namingContexts")
|
||||
}
|
||||
|
||||
// enumerateUsers 枚举用户
|
||||
func (p *LDAPPlugin) enumerateUsers(conn *ldaplib.Conn) []string {
|
||||
// 尝试常见的用户搜索基DN
|
||||
searchBases := []string{
|
||||
"dc=example,dc=com",
|
||||
"ou=users,dc=example,dc=com",
|
||||
"cn=users,dc=example,dc=com",
|
||||
"",
|
||||
}
|
||||
|
||||
var users []string
|
||||
|
||||
for _, baseDN := range searchBases {
|
||||
searchRequest := ldaplib.NewSearchRequest(
|
||||
baseDN,
|
||||
ldaplib.ScopeWholeSubtree,
|
||||
ldaplib.NeverDerefAliases,
|
||||
50, 0, false, // 限制返回50个结果
|
||||
"(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))",
|
||||
[]string{"cn", "uid", "sAMAccountName", "mail", "displayName"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range sr.Entries {
|
||||
var userInfo strings.Builder
|
||||
userInfo.WriteString(fmt.Sprintf("DN: %s", entry.DN))
|
||||
|
||||
if cn := entry.GetAttributeValue("cn"); cn != "" {
|
||||
userInfo.WriteString(fmt.Sprintf(", CN: %s", cn))
|
||||
}
|
||||
if uid := entry.GetAttributeValue("uid"); uid != "" {
|
||||
userInfo.WriteString(fmt.Sprintf(", UID: %s", uid))
|
||||
}
|
||||
if sam := entry.GetAttributeValue("sAMAccountName"); sam != "" {
|
||||
userInfo.WriteString(fmt.Sprintf(", SAM: %s", sam))
|
||||
}
|
||||
if mail := entry.GetAttributeValue("mail"); mail != "" {
|
||||
userInfo.WriteString(fmt.Sprintf(", Mail: %s", mail))
|
||||
}
|
||||
|
||||
users = append(users, userInfo.String())
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
break // 找到用户就停止搜索其他基DN
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// enumerateOUs 枚举组织单位
|
||||
func (p *LDAPPlugin) enumerateOUs(conn *ldaplib.Conn) []string {
|
||||
searchBases := []string{
|
||||
"dc=example,dc=com",
|
||||
"",
|
||||
}
|
||||
|
||||
var ous []string
|
||||
|
||||
for _, baseDN := range searchBases {
|
||||
searchRequest := ldaplib.NewSearchRequest(
|
||||
baseDN,
|
||||
ldaplib.ScopeWholeSubtree,
|
||||
ldaplib.NeverDerefAliases,
|
||||
30, 0, false, // 限制返回30个结果
|
||||
"(objectClass=organizationalUnit)",
|
||||
[]string{"ou", "description"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range sr.Entries {
|
||||
var ouInfo strings.Builder
|
||||
ouInfo.WriteString(fmt.Sprintf("DN: %s", entry.DN))
|
||||
|
||||
if ou := entry.GetAttributeValue("ou"); ou != "" {
|
||||
ouInfo.WriteString(fmt.Sprintf(", OU: %s", ou))
|
||||
}
|
||||
if desc := entry.GetAttributeValue("description"); desc != "" {
|
||||
ouInfo.WriteString(fmt.Sprintf(", Desc: %s", desc))
|
||||
}
|
||||
|
||||
ous = append(ous, ouInfo.String())
|
||||
}
|
||||
|
||||
if len(ous) > 0 {
|
||||
break // 找到OU就停止搜索其他基DN
|
||||
}
|
||||
}
|
||||
|
||||
return ous
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测LDAP服务
|
||||
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -351,40 +105,8 @@ func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试获取根DSE信息来确认这是LDAP服务
|
||||
searchRequest := ldaplib.NewSearchRequest(
|
||||
"", // 根DSE
|
||||
ldaplib.ScopeBaseObject,
|
||||
ldaplib.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(objectClass=*)",
|
||||
[]string{"vendorName", "vendorVersion"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
var banner string
|
||||
|
||||
if err != nil {
|
||||
banner = "LDAP目录服务"
|
||||
} else if len(sr.Entries) > 0 {
|
||||
entry := sr.Entries[0]
|
||||
vendor := entry.GetAttributeValue("vendorName")
|
||||
version := entry.GetAttributeValue("vendorVersion")
|
||||
|
||||
if vendor != "" && version != "" {
|
||||
banner = fmt.Sprintf("LDAP目录服务 (%s %s)", vendor, version)
|
||||
} else if vendor != "" {
|
||||
banner = fmt.Sprintf("LDAP目录服务 (%s)", vendor)
|
||||
} else {
|
||||
banner = "LDAP目录服务"
|
||||
}
|
||||
} else {
|
||||
banner = "LDAP目录服务"
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("ldap_service_identified", target, banner))
|
||||
|
||||
banner := "LDAP"
|
||||
common.LogSuccess(fmt.Sprintf("LDAP %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "ldap",
|
||||
@ -392,7 +114,6 @@ func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("ldap", func() Plugin {
|
||||
return NewLDAPPlugin()
|
||||
|
@ -8,49 +8,54 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// MemcachedPlugin Memcached分布式缓存系统扫描和利用插件 - 包含缓存数据提取利用功能
|
||||
type MemcachedPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewMemcachedPlugin 创建Memcached插件
|
||||
func NewMemcachedPlugin() *MemcachedPlugin {
|
||||
return &MemcachedPlugin{
|
||||
name: "memcached",
|
||||
ports: []int{11211, 11212, 11213}, // Memcached端口
|
||||
ports: []int{11211, 11212, 11213},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *MemcachedPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *MemcachedPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行Memcached扫描 - 未授权访问检测
|
||||
func (p *MemcachedPlugin) 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)
|
||||
}
|
||||
|
||||
// Memcached主要检查未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("memcached_unauth_success", target))
|
||||
return result
|
||||
conn := p.connectToMemcached(ctx, info)
|
||||
if conn == nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "memcached",
|
||||
Error: fmt.Errorf("Memcached服务连接失败"),
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if p.testBasicCommand(conn) {
|
||||
common.LogSuccess(fmt.Sprintf("Memcached %s 未授权访问", target))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "memcached",
|
||||
Banner: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
// 未授权访问失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "memcached",
|
||||
@ -59,27 +64,6 @@ func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo) *Scan
|
||||
}
|
||||
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
conn := p.connectToMemcached(ctx, info)
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试执行stats命令测试
|
||||
if p.testBasicCommand(conn) {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "memcached",
|
||||
Banner: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// connectToMemcached 连接到Memcached服务
|
||||
func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.HostInfo) net.Conn {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
@ -89,23 +73,19 @@ func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.H
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置操作超时
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// testBasicCommand 测试基本命令
|
||||
func (p *MemcachedPlugin) testBasicCommand(conn net.Conn) bool {
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 发送version命令
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte("version\r\n")); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
@ -117,199 +97,13 @@ func (p *MemcachedPlugin) testBasicCommand(conn net.Conn) bool {
|
||||
return strings.Contains(responseStr, "VERSION") || strings.Contains(responseStr, "memcached")
|
||||
}
|
||||
|
||||
// sendCommand 发送Memcached命令
|
||||
func (p *MemcachedPlugin) sendCommand(conn net.Conn, command string) string {
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte(command + "\r\n")); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 4096)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(response[:n]))
|
||||
}
|
||||
|
||||
// getStats 获取统计信息
|
||||
func (p *MemcachedPlugin) getStats(conn net.Conn) string {
|
||||
response := p.sendCommand(conn, "stats")
|
||||
if response == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(response, "\n")
|
||||
var stats strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "STAT") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 3 {
|
||||
key := parts[1]
|
||||
value := parts[2]
|
||||
|
||||
// 只显示重要的统计信息
|
||||
if key == "version" || key == "uptime" || key == "curr_items" ||
|
||||
key == "total_items" || key == "bytes" || key == "curr_connections" ||
|
||||
key == "cmd_get" || key == "cmd_set" || key == "get_hits" || key == "get_misses" {
|
||||
stats.WriteString(fmt.Sprintf("%s: %s\n", key, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats.String()
|
||||
}
|
||||
|
||||
// getVersion 获取版本信息
|
||||
func (p *MemcachedPlugin) getVersion(conn net.Conn) string {
|
||||
response := p.sendCommand(conn, "version")
|
||||
if strings.HasPrefix(response, "VERSION") {
|
||||
return strings.TrimPrefix(response, "VERSION ")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getAllKeys 获取所有键(通过stats cachedump)
|
||||
func (p *MemcachedPlugin) getAllKeys(conn net.Conn) []string {
|
||||
var keys []string
|
||||
|
||||
// 首先获取slabs信息
|
||||
slabsResponse := p.sendCommand(conn, "stats slabs")
|
||||
if slabsResponse == "" {
|
||||
return keys
|
||||
}
|
||||
|
||||
// 解析slab ID
|
||||
slabIDs := make(map[string]bool)
|
||||
lines := strings.Split(slabsResponse, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "STAT") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
statName := parts[1]
|
||||
// 格式如: "1:chunk_size"
|
||||
if strings.Contains(statName, ":") {
|
||||
slabID := strings.Split(statName, ":")[0]
|
||||
slabIDs[slabID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 对每个slab ID执行cachedump
|
||||
for slabID := range slabIDs {
|
||||
if len(keys) >= 50 { // 限制键的数量
|
||||
break
|
||||
}
|
||||
|
||||
dumpCmd := fmt.Sprintf("stats cachedump %s 50", slabID)
|
||||
dumpResponse := p.sendCommand(conn, dumpCmd)
|
||||
|
||||
dumpLines := strings.Split(dumpResponse, "\n")
|
||||
for _, line := range dumpLines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "ITEM") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
key := parts[1]
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
// getValue 获取键的值
|
||||
func (p *MemcachedPlugin) getValue(conn net.Conn, key string) string {
|
||||
getCmd := fmt.Sprintf("get %s", key)
|
||||
response := p.sendCommand(conn, getCmd)
|
||||
|
||||
lines := strings.Split(response, "\n")
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "VALUE") {
|
||||
// VALUE行的下一行是实际值
|
||||
if i+1 < len(lines) {
|
||||
value := strings.TrimSpace(lines[i+1])
|
||||
if value != "END" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "[无法获取]"
|
||||
}
|
||||
|
||||
// getSettings 获取设置信息
|
||||
func (p *MemcachedPlugin) getSettings(conn net.Conn) string {
|
||||
response := p.sendCommand(conn, "stats settings")
|
||||
if response == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(response, "\n")
|
||||
var settings strings.Builder
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "STAT") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 3 {
|
||||
key := parts[1]
|
||||
value := parts[2]
|
||||
|
||||
// 只显示重要的设置
|
||||
if key == "maxbytes" || key == "maxconns" || key == "growth_factor" ||
|
||||
key == "chunk_size" || key == "num_threads" || key == "cas_enabled" {
|
||||
settings.WriteString(fmt.Sprintf("%s: %s\n", key, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return settings.String()
|
||||
}
|
||||
|
||||
// testWritePermission 测试写权限
|
||||
func (p *MemcachedPlugin) testWritePermission(conn net.Conn) string {
|
||||
// 尝试设置一个测试键
|
||||
setCmd := "set fscan_test 0 60 11\r\nfscan_test\r\n"
|
||||
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte(setCmd)); err != nil {
|
||||
return "❌ 无写权限: " + err.Error()
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 512)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return "❌ 写入测试失败: " + err.Error()
|
||||
}
|
||||
|
||||
responseStr := strings.TrimSpace(string(response[:n]))
|
||||
if responseStr == "STORED" {
|
||||
// 删除测试键
|
||||
p.sendCommand(conn, "delete fscan_test")
|
||||
return "✅ 具有读写权限"
|
||||
}
|
||||
|
||||
return "❌ 写入失败: " + responseStr
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测Memcached服务
|
||||
func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -323,32 +117,23 @@ func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.Host
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试识别Memcached协议
|
||||
version := p.getVersion(conn)
|
||||
var banner string
|
||||
|
||||
if version != "" {
|
||||
banner = fmt.Sprintf("Memcached服务 (版本: %s)", version)
|
||||
} else if p.testBasicCommand(conn) {
|
||||
banner = "Memcached分布式缓存服务"
|
||||
} else {
|
||||
banner := "Memcached"
|
||||
if p.testBasicCommand(conn) {
|
||||
common.LogSuccess(fmt.Sprintf("Memcached %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Success: true,
|
||||
Service: "memcached",
|
||||
Error: fmt.Errorf("无法识别为Memcached服务"),
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("memcached_service_identified", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Success: false,
|
||||
Service: "memcached",
|
||||
Banner: banner,
|
||||
Error: fmt.Errorf("无法识别为Memcached服务"),
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("memcached", func() Plugin {
|
||||
return NewMemcachedPlugin()
|
||||
|
@ -3,453 +3,160 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// MongoDBPlugin MongoDB数据库扫描和利用插件 - 包含数据查询利用功能
|
||||
type MongoDBPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewMongoDBPlugin 创建MongoDB插件
|
||||
func NewMongoDBPlugin() *MongoDBPlugin {
|
||||
return &MongoDBPlugin{
|
||||
name: "mongodb",
|
||||
ports: []int{27017, 27018, 27019}, // MongoDB端口
|
||||
ports: []int{27017, 27018, 27019},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *MongoDBPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *MongoDBPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行MongoDB扫描 - 未授权访问检测
|
||||
func (p *MongoDBPlugin) 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)
|
||||
}
|
||||
|
||||
// MongoDB主要检查未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("mongodb_unauth_success", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 未授权访问失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Error: fmt.Errorf("MongoDB需要认证或连接失败"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *MongoDBPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
conn := p.connectToMongoDB(ctx, info)
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试执行基本查询测试
|
||||
if p.testBasicQuery(conn) {
|
||||
credentials := GenerateCredentials("mongodb")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Banner: "未授权访问",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
for _, cred := range credentials {
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
common.LogSuccess(fmt.Sprintf("MongoDB %s %s:%s", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "mongodb",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
// connectToMongoDB 连接到MongoDB服务
|
||||
func (p *MongoDBPlugin) connectToMongoDB(ctx context.Context, info *common.HostInfo) net.Conn {
|
||||
|
||||
|
||||
func (p *MongoDBPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置操作超时
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
return conn
|
||||
return p.testBasicQuery(conn)
|
||||
}
|
||||
|
||||
// testBasicQuery 测试基本查询
|
||||
func (p *MongoDBPlugin) testBasicQuery(conn net.Conn) bool {
|
||||
// 创建MongoDB查询消息
|
||||
queryMsg := p.createListDatabasesQuery()
|
||||
|
||||
if _, err := conn.Write(queryMsg); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
response := make([]byte, 4096)
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil && err != io.EOF {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查响应是否有效
|
||||
return n > 36 && p.isValidMongoResponse(response[:n])
|
||||
}
|
||||
|
||||
// isValidMongoResponse 检查是否为有效的MongoDB响应
|
||||
func (p *MongoDBPlugin) isValidMongoResponse(data []byte) bool {
|
||||
if len(data) < 36 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查MongoDB协议标志
|
||||
responseStr := string(data)
|
||||
return strings.Contains(responseStr, "databases") ||
|
||||
strings.Contains(responseStr, "totalSize") ||
|
||||
strings.Contains(responseStr, "name") ||
|
||||
strings.Contains(responseStr, "sizeOnDisk")
|
||||
strings.Contains(responseStr, "name")
|
||||
}
|
||||
|
||||
// getServerStatus 获取服务器状态
|
||||
func (p *MongoDBPlugin) getServerStatus(conn net.Conn) string {
|
||||
// 创建serverStatus命令
|
||||
statusMsg := p.createServerStatusQuery()
|
||||
|
||||
if _, err := conn.Write(statusMsg); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
response := make([]byte, 2048)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil && err != io.EOF {
|
||||
return ""
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
var status strings.Builder
|
||||
if strings.Contains(responseStr, "version") {
|
||||
status.WriteString("MongoDB服务运行中\n")
|
||||
}
|
||||
if strings.Contains(responseStr, "uptime") {
|
||||
status.WriteString("服务器运行正常\n")
|
||||
}
|
||||
|
||||
return status.String()
|
||||
}
|
||||
|
||||
// getDatabases 获取数据库列表
|
||||
func (p *MongoDBPlugin) getDatabases(conn net.Conn) []string {
|
||||
// 创建listDatabases查询
|
||||
queryMsg := p.createListDatabasesQuery()
|
||||
|
||||
if _, err := conn.Write(queryMsg); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := make([]byte, 4096)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
// 解析数据库名称
|
||||
var databases []string
|
||||
if strings.Contains(responseStr, "admin") {
|
||||
databases = append(databases, "admin")
|
||||
}
|
||||
if strings.Contains(responseStr, "local") {
|
||||
databases = append(databases, "local")
|
||||
}
|
||||
if strings.Contains(responseStr, "config") {
|
||||
databases = append(databases, "config")
|
||||
}
|
||||
|
||||
return databases
|
||||
}
|
||||
|
||||
// getCollections 获取指定数据库的集合列表
|
||||
func (p *MongoDBPlugin) getCollections(conn net.Conn, database string) []string {
|
||||
// 创建listCollections查询
|
||||
queryMsg := p.createListCollectionsQuery(database)
|
||||
|
||||
if _, err := conn.Write(queryMsg); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := make([]byte, 2048)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
// 解析集合名称
|
||||
var collections []string
|
||||
if strings.Contains(responseStr, "system") {
|
||||
collections = append(collections, "system.users")
|
||||
collections = append(collections, "system.roles")
|
||||
}
|
||||
|
||||
return collections
|
||||
}
|
||||
|
||||
// getUsers 获取用户信息
|
||||
func (p *MongoDBPlugin) getUsers(conn net.Conn) string {
|
||||
// 尝试查询admin.system.users
|
||||
userQuery := p.createFindUsersQuery()
|
||||
|
||||
if _, err := conn.Write(userQuery); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil && err != io.EOF {
|
||||
return ""
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
if strings.Contains(responseStr, "user") || strings.Contains(responseStr, "role") {
|
||||
return "发现用户数据"
|
||||
}
|
||||
|
||||
return "无用户信息或权限不足"
|
||||
}
|
||||
|
||||
// getBuildInfo 获取版本信息
|
||||
func (p *MongoDBPlugin) getBuildInfo(conn net.Conn) string {
|
||||
buildInfoMsg := p.createBuildInfoQuery()
|
||||
|
||||
if _, err := conn.Write(buildInfoMsg); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil && err != io.EOF {
|
||||
return ""
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
if strings.Contains(responseStr, "version") {
|
||||
return "MongoDB服务器信息可用"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// createListDatabasesQuery 创建listDatabases查询
|
||||
func (p *MongoDBPlugin) createListDatabasesQuery() []byte {
|
||||
// 简化的MongoDB Wire Protocol消息
|
||||
// OP_QUERY消息结构
|
||||
query := make([]byte, 100)
|
||||
query := make([]byte, 58)
|
||||
|
||||
// 消息头 (16字节)
|
||||
query[0] = 0x64 // 消息长度
|
||||
query[4] = 0x01 // 请求ID
|
||||
query[12] = 0x04 // OP_QUERY操作码
|
||||
query[13] = 0x20
|
||||
query[14] = 0x00
|
||||
query[15] = 0x00
|
||||
|
||||
// 查询标志
|
||||
query[16] = 0x00
|
||||
query[17] = 0x00
|
||||
query[18] = 0x00
|
||||
query[19] = 0x00
|
||||
|
||||
// 集合名称 "admin.$cmd"
|
||||
copy(query[20:], "admin.$cmd\x00")
|
||||
|
||||
// BSON查询文档 {listDatabases: 1}
|
||||
bsonQuery := []byte{
|
||||
0x1A, 0x00, 0x00, 0x00, // 文档长度
|
||||
0x10, // int32类型
|
||||
0x6C, 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x00, // "listDatabases"
|
||||
0x01, 0x00, 0x00, 0x00, // 值为1
|
||||
0x00, // 文档结束
|
||||
}
|
||||
|
||||
copy(query[32:], bsonQuery)
|
||||
return query[:58]
|
||||
}
|
||||
|
||||
// createServerStatusQuery 创建serverStatus查询
|
||||
func (p *MongoDBPlugin) createServerStatusQuery() []byte {
|
||||
query := make([]byte, 100)
|
||||
|
||||
// 消息头
|
||||
query[0] = 0x60
|
||||
query[4] = 0x02
|
||||
query[0] = 0x3A
|
||||
query[4] = 0x01
|
||||
query[12] = 0x04
|
||||
query[13] = 0x20
|
||||
query[14] = 0x00
|
||||
query[15] = 0x00
|
||||
|
||||
// 查询标志
|
||||
query[16] = 0x00
|
||||
query[17] = 0x00
|
||||
query[18] = 0x00
|
||||
query[19] = 0x00
|
||||
|
||||
// 集合名称
|
||||
copy(query[20:], "admin.$cmd\x00")
|
||||
|
||||
// BSON查询文档 {serverStatus: 1}
|
||||
bsonQuery := []byte{
|
||||
0x18, 0x00, 0x00, 0x00,
|
||||
0x1A, 0x00, 0x00, 0x00,
|
||||
0x10,
|
||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x00,
|
||||
0x6C, 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
}
|
||||
|
||||
copy(query[32:], bsonQuery)
|
||||
return query[:56]
|
||||
return query
|
||||
}
|
||||
|
||||
// createListCollectionsQuery 创建listCollections查询
|
||||
func (p *MongoDBPlugin) createListCollectionsQuery(database string) []byte {
|
||||
query := make([]byte, 100)
|
||||
|
||||
// 消息头
|
||||
query[0] = 0x70
|
||||
query[4] = 0x03
|
||||
query[12] = 0x04
|
||||
query[13] = 0x20
|
||||
query[14] = 0x00
|
||||
query[15] = 0x00
|
||||
|
||||
// 查询标志
|
||||
query[16] = 0x00
|
||||
query[17] = 0x00
|
||||
query[18] = 0x00
|
||||
query[19] = 0x00
|
||||
|
||||
// 集合名称
|
||||
collectionName := database + ".$cmd\x00"
|
||||
copy(query[20:], collectionName)
|
||||
|
||||
// BSON查询文档 {listCollections: 1}
|
||||
bsonQuery := []byte{
|
||||
0x1C, 0x00, 0x00, 0x00,
|
||||
0x10,
|
||||
0x6C, 0x69, 0x73, 0x74, 0x43, 0x6F, 0x6C, 0x6C, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E, 0x73, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
}
|
||||
|
||||
copy(query[20+len(collectionName):], bsonQuery)
|
||||
return query[:46+len(collectionName)]
|
||||
}
|
||||
|
||||
// createFindUsersQuery 创建查找用户查询
|
||||
func (p *MongoDBPlugin) createFindUsersQuery() []byte {
|
||||
query := make([]byte, 120)
|
||||
|
||||
// 消息头
|
||||
query[0] = 0x78
|
||||
query[4] = 0x04
|
||||
query[12] = 0x04
|
||||
query[13] = 0x20
|
||||
query[14] = 0x00
|
||||
query[15] = 0x00
|
||||
|
||||
// 查询标志
|
||||
query[16] = 0x00
|
||||
query[17] = 0x00
|
||||
query[18] = 0x00
|
||||
query[19] = 0x00
|
||||
|
||||
// 集合名称 "admin.system.users"
|
||||
copy(query[20:], "admin.system.users\x00")
|
||||
|
||||
// 空的查询文档 {}
|
||||
bsonQuery := []byte{0x05, 0x00, 0x00, 0x00, 0x00}
|
||||
|
||||
copy(query[39:], bsonQuery)
|
||||
return query[:44]
|
||||
}
|
||||
|
||||
// createBuildInfoQuery 创建buildInfo查询
|
||||
func (p *MongoDBPlugin) createBuildInfoQuery() []byte {
|
||||
query := make([]byte, 100)
|
||||
|
||||
// 消息头
|
||||
query[0] = 0x5C
|
||||
query[4] = 0x05
|
||||
query[12] = 0x04
|
||||
query[13] = 0x20
|
||||
query[14] = 0x00
|
||||
query[15] = 0x00
|
||||
|
||||
// 查询标志
|
||||
query[16] = 0x00
|
||||
query[17] = 0x00
|
||||
query[18] = 0x00
|
||||
query[19] = 0x00
|
||||
|
||||
// 集合名称
|
||||
copy(query[20:], "admin.$cmd\x00")
|
||||
|
||||
// BSON查询文档 {buildInfo: 1}
|
||||
bsonQuery := []byte{
|
||||
0x15, 0x00, 0x00, 0x00,
|
||||
0x10,
|
||||
0x62, 0x75, 0x69, 0x6C, 0x64, 0x49, 0x6E, 0x66, 0x6F, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
}
|
||||
|
||||
copy(query[32:], bsonQuery)
|
||||
return query[:53]
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测MongoDB服务
|
||||
|
||||
func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
conn := p.connectToMongoDB(ctx, info)
|
||||
if conn == nil {
|
||||
conn, err := net.DialTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Error: fmt.Errorf("无法连接到MongoDB服务"),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试识别MongoDB协议
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
if p.testBasicQuery(conn) {
|
||||
banner := "MongoDB数据库服务"
|
||||
common.LogSuccess(i18n.GetText("mongodb_service_identified", target, banner))
|
||||
|
||||
banner := "MongoDB"
|
||||
common.LogSuccess(fmt.Sprintf("MongoDB %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "mongodb",
|
||||
@ -464,7 +171,6 @@ func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostIn
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("mongodb", func() Plugin {
|
||||
return NewMongoDBPlugin()
|
||||
|
@ -9,77 +9,49 @@ import (
|
||||
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// MSSQLPlugin Microsoft SQL Server数据库扫描和利用插件 - 包含数据库查询利用功能
|
||||
type MSSQLPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewMSSQLPlugin 创建MSSQL插件
|
||||
func NewMSSQLPlugin() *MSSQLPlugin {
|
||||
return &MSSQLPlugin{
|
||||
name: "mssql",
|
||||
ports: []int{1433, 1434}, // MSSQL端口
|
||||
ports: []int{1433, 1434},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *MSSQLPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *MSSQLPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行MSSQL扫描 - 弱密码检测
|
||||
func (p *MSSQLPlugin) 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)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("mssql")
|
||||
if len(credentials) == 0 {
|
||||
// MSSQL默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "sa", Password: ""},
|
||||
{Username: "sa", Password: "sa"},
|
||||
{Username: "sa", Password: "password"},
|
||||
{Username: "sa", Password: "admin"},
|
||||
{Username: "sa", Password: "123456"},
|
||||
{Username: "administrator", Password: "administrator"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "mssql", Password: "mssql"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mssql",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mssql",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if db := p.testCredential(ctx, info, cred); db != nil {
|
||||
db.Close() // 关闭测试连接
|
||||
db.Close()
|
||||
|
||||
// MSSQL认证成功
|
||||
common.LogSuccess(i18n.GetText("mssql_scan_success", target, cred.Username, cred.Password))
|
||||
common.LogSuccess(fmt.Sprintf("MSSQL %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -90,7 +62,6 @@ func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mssql",
|
||||
@ -99,28 +70,22 @@ func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
|
||||
|
||||
// testCredential 测试单个凭据 - 返回数据库连接或nil
|
||||
func (p *MSSQLPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *sql.DB {
|
||||
// 构建连接字符串
|
||||
connStr := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%s;database=master;connection timeout=%d",
|
||||
info.Host, cred.Username, cred.Password, info.Ports, common.Timeout)
|
||||
|
||||
// 打开数据库连接
|
||||
db, err := sql.Open("mssql", connStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
// 创建超时上下文
|
||||
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 测试连接
|
||||
err = db.PingContext(pingCtx)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
@ -130,146 +95,15 @@ func (p *MSSQLPlugin) testCredential(ctx context.Context, info *common.HostInfo,
|
||||
return db
|
||||
}
|
||||
|
||||
// getVersion 获取MSSQL版本信息
|
||||
func (p *MSSQLPlugin) getVersion(db *sql.DB) string {
|
||||
var version string
|
||||
err := db.QueryRow("SELECT @@VERSION").Scan(&version)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 只返回第一行版本信息
|
||||
lines := strings.Split(version, "\n")
|
||||
if len(lines) > 0 {
|
||||
return strings.TrimSpace(lines[0])
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
// getDatabases 获取数据库列表
|
||||
func (p *MSSQLPlugin) getDatabases(db *sql.DB) []string {
|
||||
query := "SELECT name FROM sys.databases WHERE database_id > 4" // 排除系统数据库
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
// 如果查询失败,尝试查询所有数据库
|
||||
rows, err = db.Query("SELECT name FROM sys.databases")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []string
|
||||
for rows.Next() {
|
||||
var dbName string
|
||||
if err := rows.Scan(&dbName); err == nil {
|
||||
databases = append(databases, dbName)
|
||||
}
|
||||
}
|
||||
|
||||
return databases
|
||||
}
|
||||
|
||||
// getTables 获取指定数据库的表列表
|
||||
func (p *MSSQLPlugin) getTables(db *sql.DB, database string) []string {
|
||||
query := fmt.Sprintf("SELECT TABLE_NAME FROM %s.INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", database)
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var tableName string
|
||||
if err := rows.Scan(&tableName); err == nil {
|
||||
tables = append(tables, tableName)
|
||||
}
|
||||
}
|
||||
|
||||
return tables
|
||||
}
|
||||
|
||||
// getUsers 获取用户列表
|
||||
func (p *MSSQLPlugin) getUsers(db *sql.DB) []string {
|
||||
query := "SELECT name FROM sys.server_principals WHERE type IN ('S', 'U') AND is_disabled = 0"
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
// 尝试备用查询
|
||||
rows, err = db.Query("SELECT loginname FROM master.dbo.syslogins")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []string
|
||||
for rows.Next() {
|
||||
var userName string
|
||||
if err := rows.Scan(&userName); err == nil {
|
||||
users = append(users, userName)
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// getPrivileges 获取用户权限信息
|
||||
func (p *MSSQLPlugin) getPrivileges(db *sql.DB, username string) string {
|
||||
var privileges strings.Builder
|
||||
|
||||
// 检查是否为sysadmin
|
||||
var isSysadmin int
|
||||
err := db.QueryRow("SELECT IS_SRVROLEMEMBER('sysadmin', ?)", username).Scan(&isSysadmin)
|
||||
if err == nil {
|
||||
if isSysadmin == 1 {
|
||||
privileges.WriteString("sysadmin权限: YES\n")
|
||||
} else {
|
||||
privileges.WriteString("sysadmin权限: NO\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查其他服务器角色
|
||||
serverRoles := []string{"securityadmin", "serveradmin", "setupadmin", "processadmin", "diskadmin", "dbcreator", "bulkadmin"}
|
||||
for _, role := range serverRoles {
|
||||
var hasRole int
|
||||
err := db.QueryRow(fmt.Sprintf("SELECT IS_SRVROLEMEMBER('%s', ?)", role), username).Scan(&hasRole)
|
||||
if err == nil && hasRole == 1 {
|
||||
privileges.WriteString(fmt.Sprintf("%s权限: YES\n", role))
|
||||
}
|
||||
}
|
||||
|
||||
return privileges.String()
|
||||
}
|
||||
|
||||
// checkXpCmdshell 检查xp_cmdshell状态
|
||||
func (p *MSSQLPlugin) checkXpCmdshell(db *sql.DB) string {
|
||||
// 检查xp_cmdshell是否启用
|
||||
var configValue int
|
||||
err := db.QueryRow("SELECT CAST(value as int) FROM sys.configurations WHERE name = 'xp_cmdshell'").Scan(&configValue)
|
||||
if err != nil {
|
||||
return "无法检查xp_cmdshell状态"
|
||||
}
|
||||
|
||||
if configValue == 1 {
|
||||
// 尝试执行一个简单的命令测试
|
||||
var result sql.NullString
|
||||
err = db.QueryRow("EXEC xp_cmdshell 'echo test'").Scan(&result)
|
||||
if err == nil && result.Valid {
|
||||
return "✅ xp_cmdshell已启用,支持系统命令执行"
|
||||
}
|
||||
return "⚠️ xp_cmdshell已启用但无法执行命令"
|
||||
}
|
||||
|
||||
return "❌ xp_cmdshell未启用"
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测MSSQL服务
|
||||
func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 尝试连接MSSQL服务
|
||||
connStr := fmt.Sprintf("server=%s;user id=invalid;password=invalid;port=%s;database=master;connection timeout=%d",
|
||||
info.Host, info.Ports, common.Timeout)
|
||||
|
||||
@ -283,7 +117,6 @@ func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 尝试连接(即使认证失败,也能识别服务)
|
||||
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@ -293,9 +126,9 @@ func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
if err != nil && (strings.Contains(strings.ToLower(err.Error()), "login failed") ||
|
||||
strings.Contains(strings.ToLower(err.Error()), "mssql") ||
|
||||
strings.Contains(strings.ToLower(err.Error()), "sql server")) {
|
||||
banner = "Microsoft SQL Server数据库服务"
|
||||
banner = "MSSQL"
|
||||
} else if err == nil {
|
||||
banner = "Microsoft SQL Server (连接成功)"
|
||||
banner = "MSSQL"
|
||||
} else {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
@ -304,7 +137,7 @@ func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("mssql_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("MSSQL %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -313,7 +146,6 @@ func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("mssql", func() Plugin {
|
||||
return NewMSSQLPlugin()
|
||||
|
@ -5,21 +5,17 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// MySQLPlugin MySQL数据库弱密码扫描插件 - 单文件实现
|
||||
type MySQLPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewMySQLPlugin 创建MySQL插件
|
||||
func NewMySQLPlugin() *MySQLPlugin {
|
||||
return &MySQLPlugin{
|
||||
name: "mysql",
|
||||
@ -27,26 +23,21 @@ func NewMySQLPlugin() *MySQLPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *MySQLPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *MySQLPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行MySQL扫描 - 核心功能实现
|
||||
func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(info)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("mysql")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
@ -56,23 +47,9 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mysql",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// 弱密码发现成功
|
||||
common.LogSuccess(i18n.GetText("mysql_scan_success", target, cred.Username, cred.Password))
|
||||
common.LogSuccess(fmt.Sprintf("MySQL %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -83,7 +60,6 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mysql",
|
||||
@ -91,48 +67,28 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据 - 核心认证逻辑
|
||||
func (p *MySQLPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
// 构建连接字符串
|
||||
connStr := p.buildConnectionString(info.Host, info.Ports, cred.Username, cred.Password)
|
||||
connStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/mysql?charset=utf8&timeout=%ds",
|
||||
cred.Username, cred.Password, info.Host, info.Ports, common.Timeout)
|
||||
|
||||
// 创建数据库连接
|
||||
db, err := sql.Open("mysql", connStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接超时
|
||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
// 测试连接 - 使用Context超时控制
|
||||
err = db.PingContext(ctx)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// buildConnectionString 构建MySQL连接字符串
|
||||
func (p *MySQLPlugin) buildConnectionString(host, port, username, password string) string {
|
||||
// 支持SOCKS代理
|
||||
if common.Socks5Proxy != "" {
|
||||
// 如果使用代理,需要注册自定义拨号器
|
||||
p.registerProxyDialer()
|
||||
return fmt.Sprintf("%s:%s@tcp-proxy(%s:%s)/mysql?charset=utf8&timeout=%ds",
|
||||
username, password, host, port, common.Timeout)
|
||||
}
|
||||
|
||||
// 标准TCP连接
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%s)/mysql?charset=utf8&timeout=%ds",
|
||||
username, password, host, port, common.Timeout)
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 不进行暴力破解时的功能
|
||||
func (p *MySQLPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 尝试连接获取握手包
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
@ -143,9 +99,8 @@ func (p *MySQLPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 读取MySQL握手包
|
||||
if banner := p.readMySQLBanner(conn); banner != "" {
|
||||
common.LogSuccess(i18n.GetText("mysql_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("MySQL %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "mysql",
|
||||
@ -160,24 +115,19 @@ func (p *MySQLPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
}
|
||||
}
|
||||
|
||||
// readMySQLBanner 读取MySQL服务器握手包
|
||||
func (p *MySQLPlugin) readMySQLBanner(conn net.Conn) string {
|
||||
// 设置读取超时
|
||||
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 读取握手包
|
||||
handshake := make([]byte, 512)
|
||||
handshake := make([]byte, 256)
|
||||
n, err := conn.Read(handshake)
|
||||
if err != nil || n < 10 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 检查MySQL协议版本(通常是10)
|
||||
if handshake[4] != 10 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 提取版本字符串
|
||||
versionStart := 5
|
||||
versionEnd := versionStart
|
||||
for versionEnd < n && handshake[versionEnd] != 0 {
|
||||
@ -189,22 +139,10 @@ func (p *MySQLPlugin) readMySQLBanner(conn net.Conn) string {
|
||||
}
|
||||
|
||||
versionStr := string(handshake[versionStart:versionEnd])
|
||||
|
||||
// 验证版本字符串格式
|
||||
if regexp.MustCompile(`\d+\.\d+`).MatchString(versionStr) {
|
||||
return fmt.Sprintf("MySQL %s", versionStr)
|
||||
}
|
||||
|
||||
return ""
|
||||
return fmt.Sprintf("MySQL %s", versionStr)
|
||||
}
|
||||
|
||||
// registerProxyDialer 注册SOCKS代理拨号器
|
||||
func (p *MySQLPlugin) registerProxyDialer() {
|
||||
// TODO: 实现代理拨号器注册
|
||||
// 这里简化处理,实际需要注册到MySQL驱动
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("mysql", func() Plugin {
|
||||
return NewMySQLPlugin()
|
||||
|
@ -2,7 +2,6 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -10,79 +9,52 @@ import (
|
||||
"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端口
|
||||
ports: []int{7474, 7687, 7473},
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
common.LogSuccess(fmt.Sprintf("Neo4j %s 未授权访问", 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"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "neo4j",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
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))
|
||||
common.LogSuccess(fmt.Sprintf("Neo4j %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -93,25 +65,21 @@ func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "neo4j",
|
||||
Error: fmt.Errorf("未发现弱密码或未授权访问"),
|
||||
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
|
||||
@ -134,7 +102,6 @@ func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
|
||||
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)
|
||||
|
||||
@ -142,7 +109,6 @@ func (p *Neo4jPlugin) testCredential(ctx context.Context, info *common.HostInfo,
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
// 尝试认证
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/user/neo4j", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
@ -160,240 +126,12 @@ func (p *Neo4jPlugin) testCredential(ctx context.Context, info *common.HostInfo,
|
||||
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)
|
||||
@ -421,17 +159,15 @@ func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
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接口)"
|
||||
banner = "Neo4j"
|
||||
} else if resp.StatusCode == 200 || resp.StatusCode == 401 {
|
||||
// 尝试检查根路径响应
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if strings.Contains(strings.ToLower(string(body)), "neo4j") {
|
||||
banner = "Neo4j图数据库服务"
|
||||
banner = "Neo4j"
|
||||
} else {
|
||||
banner = "Neo4j服务 (协议识别)"
|
||||
banner = "Neo4j"
|
||||
}
|
||||
} else {
|
||||
return &ScanResult{
|
||||
@ -441,7 +177,7 @@ func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("neo4j_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("Neo4j %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -450,7 +186,6 @@ func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("neo4j", func() Plugin {
|
||||
return NewNeo4jPlugin()
|
||||
|
@ -2,276 +2,36 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
// _ "github.com/mattn/go-oci8" // Oracle驱动需要特殊安装,暂时注释
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// OraclePlugin Oracle数据库扫描和利用插件 - 包含数据库查询利用功能
|
||||
type OraclePlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewOraclePlugin 创建Oracle插件
|
||||
func NewOraclePlugin() *OraclePlugin {
|
||||
return &OraclePlugin{
|
||||
name: "oracle",
|
||||
ports: []int{1521, 1522, 1525}, // Oracle端口
|
||||
ports: []int{1521, 1522, 1525},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *OraclePlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *OraclePlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行Oracle扫描 - 弱密码检测
|
||||
func (p *OraclePlugin) 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)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("oracle")
|
||||
if len(credentials) == 0 {
|
||||
// Oracle默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "sys", Password: "sys"},
|
||||
{Username: "sys", Password: "system"},
|
||||
{Username: "sys", Password: "oracle"},
|
||||
{Username: "system", Password: "system"},
|
||||
{Username: "system", Password: "oracle"},
|
||||
{Username: "system", Password: "manager"},
|
||||
{Username: "scott", Password: "tiger"},
|
||||
{Username: "oracle", Password: "oracle"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "oracle",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if db := p.testCredential(ctx, info, cred); db != nil {
|
||||
db.Close() // 关闭测试连接
|
||||
|
||||
// Oracle认证成功
|
||||
common.LogSuccess(i18n.GetText("oracle_scan_success", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "oracle",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "oracle",
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testCredential 测试单个凭据 - 返回数据库连接或nil
|
||||
func (p *OraclePlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *sql.DB {
|
||||
// Oracle驱动需要特殊安装,这里简化实现
|
||||
// 在实际环境中需要安装Oracle客户端和go-oci8驱动
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVersion 获取Oracle版本信息
|
||||
func (p *OraclePlugin) getVersion(db *sql.DB) string {
|
||||
var version string
|
||||
err := db.QueryRow("SELECT banner FROM v$version WHERE rownum = 1").Scan(&version)
|
||||
if err != nil {
|
||||
// 尝试备用查询
|
||||
err = db.QueryRow("SELECT version FROM product_component_version WHERE rownum = 1").Scan(&version)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// getDatabaseInfo 获取数据库基本信息
|
||||
func (p *OraclePlugin) getDatabaseInfo(db *sql.DB) string {
|
||||
var info strings.Builder
|
||||
|
||||
// 获取数据库名
|
||||
var dbName string
|
||||
err := db.QueryRow("SELECT name FROM v$database").Scan(&dbName)
|
||||
if err == nil {
|
||||
info.WriteString(fmt.Sprintf("数据库名: %s\n", dbName))
|
||||
}
|
||||
|
||||
// 获取实例名
|
||||
var instanceName string
|
||||
err = db.QueryRow("SELECT instance_name FROM v$instance").Scan(&instanceName)
|
||||
if err == nil {
|
||||
info.WriteString(fmt.Sprintf("实例名: %s\n", instanceName))
|
||||
}
|
||||
|
||||
// 获取字符集
|
||||
var charset string
|
||||
err = db.QueryRow("SELECT value FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET'").Scan(&charset)
|
||||
if err == nil {
|
||||
info.WriteString(fmt.Sprintf("字符集: %s\n", charset))
|
||||
}
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// getTablespaces 获取表空间列表
|
||||
func (p *OraclePlugin) getTablespaces(db *sql.DB) []string {
|
||||
query := "SELECT tablespace_name FROM dba_tablespaces"
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
// 尝试用户表空间查询
|
||||
query = "SELECT tablespace_name FROM user_tablespaces"
|
||||
rows, err = db.Query(query)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tablespaces []string
|
||||
for rows.Next() {
|
||||
var tsName string
|
||||
if err := rows.Scan(&tsName); err == nil {
|
||||
tablespaces = append(tablespaces, tsName)
|
||||
}
|
||||
}
|
||||
|
||||
return tablespaces
|
||||
}
|
||||
|
||||
// getUsers 获取用户列表
|
||||
func (p *OraclePlugin) getUsers(db *sql.DB) []string {
|
||||
query := "SELECT username FROM dba_users ORDER BY username"
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
// 尝试all_users
|
||||
query = "SELECT username FROM all_users ORDER BY username"
|
||||
rows, err = db.Query(query)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []string
|
||||
for rows.Next() {
|
||||
var userName string
|
||||
if err := rows.Scan(&userName); err == nil {
|
||||
users = append(users, userName)
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// getTables 获取指定用户的表列表
|
||||
func (p *OraclePlugin) getTables(db *sql.DB, owner string) []string {
|
||||
query := "SELECT table_name FROM dba_tables WHERE owner = :1 ORDER BY table_name"
|
||||
rows, err := db.Query(query, strings.ToUpper(owner))
|
||||
if err != nil {
|
||||
// 尝试用户表查询
|
||||
query = "SELECT table_name FROM user_tables ORDER BY table_name"
|
||||
rows, err = db.Query(query)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var tableName string
|
||||
if err := rows.Scan(&tableName); err == nil {
|
||||
tables = append(tables, tableName)
|
||||
}
|
||||
}
|
||||
|
||||
return tables
|
||||
}
|
||||
|
||||
// getPrivileges 获取用户权限信息
|
||||
func (p *OraclePlugin) getPrivileges(db *sql.DB, username string) string {
|
||||
var privileges strings.Builder
|
||||
|
||||
// 检查DBA权限
|
||||
var dbaRole int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM dba_role_privs WHERE grantee = :1 AND granted_role = 'DBA'",
|
||||
strings.ToUpper(username)).Scan(&dbaRole)
|
||||
if err == nil && dbaRole > 0 {
|
||||
privileges.WriteString("DBA权限: YES\n")
|
||||
} else {
|
||||
privileges.WriteString("DBA权限: NO\n")
|
||||
}
|
||||
|
||||
// 检查SYSDBA权限
|
||||
var sysdbaCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM v$pwfile_users WHERE username = :1 AND sysdba = 'TRUE'",
|
||||
strings.ToUpper(username)).Scan(&sysdbaCount)
|
||||
if err == nil && sysdbaCount > 0 {
|
||||
privileges.WriteString("SYSDBA权限: YES\n")
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
query := "SELECT granted_role FROM dba_role_privs WHERE grantee = :1 AND rownum <= 5"
|
||||
rows, err := db.Query(query, strings.ToUpper(username))
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
privileges.WriteString("已授予角色: ")
|
||||
var roles []string
|
||||
for rows.Next() {
|
||||
var role string
|
||||
if err := rows.Scan(&role); err == nil {
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
if len(roles) > 0 {
|
||||
privileges.WriteString(strings.Join(roles, ", ") + "\n")
|
||||
} else {
|
||||
privileges.WriteString("无\n")
|
||||
}
|
||||
}
|
||||
|
||||
return privileges.String()
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测Oracle服务
|
||||
func (p *OraclePlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
// Oracle驱动需要特殊安装,这里简化实现
|
||||
// 在实际环境中需要安装Oracle客户端和go-oci8驱动
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "oracle",
|
||||
@ -279,7 +39,22 @@ func (p *OraclePlugin) identifyService(ctx context.Context, info *common.HostInf
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func (p *OraclePlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "oracle",
|
||||
Error: fmt.Errorf("Oracle驱动未安装"),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPlugin("oracle", func() Plugin {
|
||||
return NewOraclePlugin()
|
||||
|
@ -9,76 +9,49 @@ import (
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// PostgreSQLPlugin PostgreSQL数据库扫描和利用插件 - 包含数据库查询利用功能
|
||||
type PostgreSQLPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewPostgreSQLPlugin 创建PostgreSQL插件
|
||||
func NewPostgreSQLPlugin() *PostgreSQLPlugin {
|
||||
return &PostgreSQLPlugin{
|
||||
name: "postgresql",
|
||||
ports: []int{5432, 5433, 5434}, // PostgreSQL端口
|
||||
ports: []int{5432, 5433, 5434},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *PostgreSQLPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *PostgreSQLPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行PostgreSQL扫描 - 弱密码检测
|
||||
func (p *PostgreSQLPlugin) 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)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("postgresql")
|
||||
if len(credentials) == 0 {
|
||||
// PostgreSQL默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "postgres", Password: ""},
|
||||
{Username: "postgres", Password: "postgres"},
|
||||
{Username: "postgres", Password: "password"},
|
||||
{Username: "postgres", Password: "admin"},
|
||||
{Username: "postgres", Password: "123456"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "root", Password: "root"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "postgresql",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "postgresql",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if db := p.testCredential(ctx, info, cred); db != nil {
|
||||
db.Close() // 关闭测试连接
|
||||
db.Close()
|
||||
|
||||
// PostgreSQL认证成功
|
||||
common.LogSuccess(i18n.GetText("postgresql_scan_success", target, cred.Username, cred.Password))
|
||||
common.LogSuccess(fmt.Sprintf("PostgreSQL %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -89,7 +62,6 @@ func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *Sca
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "postgresql",
|
||||
@ -98,28 +70,22 @@ func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *Sca
|
||||
}
|
||||
|
||||
|
||||
// testCredential 测试单个凭据 - 返回数据库连接或nil
|
||||
func (p *PostgreSQLPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *sql.DB {
|
||||
// 构建连接字符串
|
||||
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/postgres?sslmode=disable&connect_timeout=%d",
|
||||
cred.Username, cred.Password, info.Host, info.Ports, common.Timeout)
|
||||
|
||||
// 打开数据库连接
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
// 创建超时上下文
|
||||
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 测试连接
|
||||
err = db.PingContext(pingCtx)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
@ -129,111 +95,14 @@ func (p *PostgreSQLPlugin) testCredential(ctx context.Context, info *common.Host
|
||||
return db
|
||||
}
|
||||
|
||||
// getVersion 获取PostgreSQL版本信息
|
||||
func (p *PostgreSQLPlugin) getVersion(db *sql.DB) string {
|
||||
var version string
|
||||
err := db.QueryRow("SELECT version()").Scan(&version)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// getDatabases 获取数据库列表
|
||||
func (p *PostgreSQLPlugin) getDatabases(db *sql.DB) []string {
|
||||
query := "SELECT datname FROM pg_database WHERE datistemplate = false"
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []string
|
||||
for rows.Next() {
|
||||
var dbName string
|
||||
if err := rows.Scan(&dbName); err == nil {
|
||||
databases = append(databases, dbName)
|
||||
}
|
||||
}
|
||||
|
||||
return databases
|
||||
}
|
||||
|
||||
// getTables 获取表列表
|
||||
func (p *PostgreSQLPlugin) getTables(db *sql.DB) []string {
|
||||
query := `SELECT tablename FROM pg_tables WHERE schemaname = 'public'
|
||||
UNION SELECT viewname FROM pg_views WHERE schemaname = 'public'
|
||||
ORDER BY 1 LIMIT 20`
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var tableName string
|
||||
if err := rows.Scan(&tableName); err == nil {
|
||||
tables = append(tables, tableName)
|
||||
}
|
||||
}
|
||||
|
||||
return tables
|
||||
}
|
||||
|
||||
// getUsers 获取用户列表
|
||||
func (p *PostgreSQLPlugin) getUsers(db *sql.DB) []string {
|
||||
query := "SELECT usename FROM pg_user ORDER BY usename"
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []string
|
||||
for rows.Next() {
|
||||
var userName string
|
||||
if err := rows.Scan(&userName); err == nil {
|
||||
users = append(users, userName)
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// getPrivileges 获取用户权限信息
|
||||
func (p *PostgreSQLPlugin) getPrivileges(db *sql.DB, username string) string {
|
||||
var privileges strings.Builder
|
||||
|
||||
// 检查是否为超级用户
|
||||
var isSuperUser bool
|
||||
err := db.QueryRow("SELECT usesuper FROM pg_user WHERE usename = $1", username).Scan(&isSuperUser)
|
||||
if err == nil {
|
||||
if isSuperUser {
|
||||
privileges.WriteString("超级用户权限: YES\n")
|
||||
} else {
|
||||
privileges.WriteString("超级用户权限: NO\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户属性
|
||||
var createDB, createRole bool
|
||||
err = db.QueryRow("SELECT usecreatedb, usecreaterole FROM pg_user WHERE usename = $1",
|
||||
username).Scan(&createDB, &createRole)
|
||||
if err == nil {
|
||||
privileges.WriteString(fmt.Sprintf("创建数据库权限: %v\n", createDB))
|
||||
privileges.WriteString(fmt.Sprintf("创建角色权限: %v\n", createRole))
|
||||
}
|
||||
|
||||
return privileges.String()
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测PostgreSQL服务
|
||||
func (p *PostgreSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 尝试连接PostgreSQL服务
|
||||
connStr := fmt.Sprintf("postgres://invalid:invalid@%s:%s/postgres?sslmode=disable&connect_timeout=%d",
|
||||
info.Host, info.Ports, common.Timeout)
|
||||
|
||||
@ -247,7 +116,6 @@ func (p *PostgreSQLPlugin) identifyService(ctx context.Context, info *common.Hos
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 尝试连接(即使认证失败,也能识别服务)
|
||||
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@ -255,9 +123,9 @@ func (p *PostgreSQLPlugin) identifyService(ctx context.Context, info *common.Hos
|
||||
|
||||
var banner string
|
||||
if err != nil && strings.Contains(strings.ToLower(err.Error()), "postgres") {
|
||||
banner = "PostgreSQL数据库服务"
|
||||
banner = "PostgreSQL"
|
||||
} else if err == nil {
|
||||
banner = "PostgreSQL数据库 (连接成功)"
|
||||
banner = "PostgreSQL"
|
||||
} else {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
@ -266,7 +134,7 @@ func (p *PostgreSQLPlugin) identifyService(ctx context.Context, info *common.Hos
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("postgresql_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("PostgreSQL %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -275,7 +143,6 @@ func (p *PostgreSQLPlugin) identifyService(ctx context.Context, info *common.Hos
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("postgresql", func() Plugin {
|
||||
return NewPostgreSQLPlugin()
|
||||
|
@ -2,7 +2,6 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -10,79 +9,51 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// RabbitMQPlugin RabbitMQ消息队列扫描和利用插件 - 包含队列信息提取利用功能
|
||||
type RabbitMQPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewRabbitMQPlugin 创建RabbitMQ插件
|
||||
func NewRabbitMQPlugin() *RabbitMQPlugin {
|
||||
return &RabbitMQPlugin{
|
||||
name: "rabbitmq",
|
||||
ports: []int{5672, 15672, 5671}, // AMQP、管理界面、AMQPS端口
|
||||
ports: []int{5672, 15672, 5671},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *RabbitMQPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *RabbitMQPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行RabbitMQ扫描 - 弱密码检测
|
||||
func (p *RabbitMQPlugin) 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)
|
||||
}
|
||||
|
||||
// RabbitMQ管理端口通常是15672,AMQP端口是5672
|
||||
if info.Ports == "5672" || info.Ports == "5671" {
|
||||
// AMQP端口,进行协议检测
|
||||
return p.testAMQPProtocol(ctx, info)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("rabbitmq")
|
||||
if len(credentials) == 0 {
|
||||
// RabbitMQ默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "guest", Password: "guest"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: "password"},
|
||||
{Username: "admin", Password: "123456"},
|
||||
{Username: "rabbitmq", Password: "rabbitmq"},
|
||||
{Username: "user", Password: "user"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据(针对管理接口)
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// RabbitMQ认证成功
|
||||
common.LogSuccess(i18n.GetText("rabbitmq_scan_success", target, cred.Username, cred.Password))
|
||||
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -93,7 +64,6 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
@ -102,27 +72,21 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
|
||||
}
|
||||
|
||||
|
||||
// testAMQPProtocol 测试AMQP协议
|
||||
func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
// 对于AMQP端口,我们只做协议识别
|
||||
// AMQP协议检测比较复杂,这里简化处理
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: "RabbitMQ AMQP协议端口",
|
||||
Banner: "RabbitMQ AMQP",
|
||||
}
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据(通过管理API)
|
||||
func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
// 构建管理API URL
|
||||
baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
// 尝试访问API overview
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/overview", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
@ -140,235 +104,24 @@ func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostIn
|
||||
return resp.StatusCode == 200
|
||||
}
|
||||
|
||||
// makeAPIRequest 执行API请求
|
||||
func (p *RabbitMQPlugin) makeAPIRequest(ctx context.Context, info *common.HostInfo, creds Credential, endpoint 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,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(creds.Username, creds.Password)
|
||||
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("API请求失败,状态码: %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 {
|
||||
// 尝试解析为数组
|
||||
var arrayResult []map[string]interface{}
|
||||
if err2 := json.Unmarshal(body, &arrayResult); err2 == nil {
|
||||
// 如果是数组,返回包装的结果
|
||||
return map[string]interface{}{"data": arrayResult}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getClusterInfo 获取集群信息
|
||||
func (p *RabbitMQPlugin) getClusterInfo(ctx context.Context, info *common.HostInfo, creds Credential) string {
|
||||
result, err := p.makeAPIRequest(ctx, info, creds, "/api/overview")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var info_str strings.Builder
|
||||
|
||||
if managementVersion, ok := result["management_version"]; ok {
|
||||
info_str.WriteString(fmt.Sprintf("管理版本: %v\n", managementVersion))
|
||||
}
|
||||
|
||||
if rabbitMQVersion, ok := result["rabbitmq_version"]; ok {
|
||||
info_str.WriteString(fmt.Sprintf("RabbitMQ版本: %v\n", rabbitMQVersion))
|
||||
}
|
||||
|
||||
if clusterName, ok := result["cluster_name"]; ok {
|
||||
info_str.WriteString(fmt.Sprintf("集群名称: %v\n", clusterName))
|
||||
}
|
||||
|
||||
return info_str.String()
|
||||
}
|
||||
|
||||
// getNodes 获取节点列表
|
||||
func (p *RabbitMQPlugin) getNodes(ctx context.Context, info *common.HostInfo, creds Credential) []string {
|
||||
result, err := p.makeAPIRequest(ctx, info, creds, "/api/nodes")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nodes []string
|
||||
|
||||
if data, ok := result["data"].([]interface{}); ok {
|
||||
for _, item := range data {
|
||||
if node, ok := item.(map[string]interface{}); ok {
|
||||
if name, ok := node["name"]; ok {
|
||||
if nameStr, ok := name.(string); ok {
|
||||
nodes = append(nodes, nameStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// getVHosts 获取虚拟主机列表
|
||||
func (p *RabbitMQPlugin) getVHosts(ctx context.Context, info *common.HostInfo, creds Credential) []string {
|
||||
result, err := p.makeAPIRequest(ctx, info, creds, "/api/vhosts")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var vhosts []string
|
||||
|
||||
if data, ok := result["data"].([]interface{}); ok {
|
||||
for _, item := range data {
|
||||
if vhost, ok := item.(map[string]interface{}); ok {
|
||||
if name, ok := vhost["name"]; ok {
|
||||
if nameStr, ok := name.(string); ok {
|
||||
vhosts = append(vhosts, nameStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vhosts
|
||||
}
|
||||
|
||||
// getUsers 获取用户列表
|
||||
func (p *RabbitMQPlugin) getUsers(ctx context.Context, info *common.HostInfo, creds Credential) []string {
|
||||
result, err := p.makeAPIRequest(ctx, info, creds, "/api/users")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var users []string
|
||||
|
||||
if data, ok := result["data"].([]interface{}); ok {
|
||||
for _, item := range data {
|
||||
if user, ok := item.(map[string]interface{}); ok {
|
||||
if name, ok := user["name"]; ok {
|
||||
if nameStr, ok := name.(string); ok {
|
||||
// 获取用户标签信息
|
||||
var tags string
|
||||
if tagsInterface, ok := user["tags"]; ok {
|
||||
if tagsStr, ok := tagsInterface.(string); ok {
|
||||
tags = fmt.Sprintf(" [%s]", tagsStr)
|
||||
}
|
||||
}
|
||||
users = append(users, nameStr+tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// getQueues 获取队列列表
|
||||
func (p *RabbitMQPlugin) getQueues(ctx context.Context, info *common.HostInfo, creds Credential) []string {
|
||||
result, err := p.makeAPIRequest(ctx, info, creds, "/api/queues")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var queues []string
|
||||
|
||||
if data, ok := result["data"].([]interface{}); ok {
|
||||
for _, item := range data {
|
||||
if queue, ok := item.(map[string]interface{}); ok {
|
||||
if name, ok := queue["name"]; ok {
|
||||
if nameStr, ok := name.(string); ok {
|
||||
// 获取队列消息数
|
||||
var messages string
|
||||
if messagesInterface, ok := queue["messages"]; ok {
|
||||
messages = fmt.Sprintf(" (消息数: %v)", messagesInterface)
|
||||
}
|
||||
queues = append(queues, nameStr+messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queues
|
||||
}
|
||||
|
||||
// getExchanges 获取交换器列表
|
||||
func (p *RabbitMQPlugin) getExchanges(ctx context.Context, info *common.HostInfo, creds Credential) []string {
|
||||
result, err := p.makeAPIRequest(ctx, info, creds, "/api/exchanges")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var exchanges []string
|
||||
|
||||
if data, ok := result["data"].([]interface{}); ok {
|
||||
for _, item := range data {
|
||||
if exchange, ok := item.(map[string]interface{}); ok {
|
||||
if name, ok := exchange["name"]; ok {
|
||||
if nameStr, ok := name.(string); ok {
|
||||
// 过滤掉空名称的默认交换器
|
||||
if nameStr == "" {
|
||||
nameStr = "(default)"
|
||||
}
|
||||
|
||||
// 获取交换器类型
|
||||
var exchType string
|
||||
if typeInterface, ok := exchange["type"]; ok {
|
||||
if typeStr, ok := typeInterface.(string); ok {
|
||||
exchType = fmt.Sprintf(" [%s]", typeStr)
|
||||
}
|
||||
}
|
||||
exchanges = append(exchanges, nameStr+exchType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return exchanges
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测RabbitMQ服务
|
||||
func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 根据端口类型进行不同的识别
|
||||
if info.Ports == "5672" || info.Ports == "5671" {
|
||||
// AMQP端口
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: "RabbitMQ AMQP协议服务",
|
||||
Banner: "RabbitMQ AMQP",
|
||||
}
|
||||
}
|
||||
|
||||
// 管理端口,尝试HTTP请求
|
||||
baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
|
||||
|
||||
client := &http.Client{
|
||||
@ -394,18 +147,17 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应是否包含RabbitMQ特征
|
||||
var banner string
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 401 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
bodyStr := strings.ToLower(string(body))
|
||||
|
||||
if strings.Contains(bodyStr, "rabbitmq") {
|
||||
banner = "RabbitMQ管理界面"
|
||||
banner = "RabbitMQ"
|
||||
} else if strings.Contains(bodyStr, "management") {
|
||||
banner = "RabbitMQ服务 (管理端口)"
|
||||
banner = "RabbitMQ"
|
||||
} else {
|
||||
banner = "RabbitMQ消息队列服务"
|
||||
banner = "RabbitMQ"
|
||||
}
|
||||
} else {
|
||||
return &ScanResult{
|
||||
@ -415,7 +167,7 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("rabbitmq_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
|
@ -9,64 +9,70 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// RsyncPlugin Rsync文件同步服务扫描和利用插件 - 包含文件列表利用功能
|
||||
type RsyncPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewRsyncPlugin 创建Rsync插件
|
||||
func NewRsyncPlugin() *RsyncPlugin {
|
||||
return &RsyncPlugin{
|
||||
name: "rsync",
|
||||
ports: []int{873}, // Rsync端口
|
||||
ports: []int{873},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *RsyncPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *RsyncPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行Rsync扫描 - 未授权访问检测
|
||||
func (p *RsyncPlugin) 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)
|
||||
}
|
||||
|
||||
// Rsync主要检查未授权访问和弱密码
|
||||
if result := p.testUnauthorizedAccess(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("rsync_unauth_success", target))
|
||||
common.LogSuccess(fmt.Sprintf("Rsync %s 未授权访问", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 如果未授权访问失败,尝试弱密码
|
||||
if result := p.testWeakPasswords(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("rsync_weak_pwd_success", target))
|
||||
return result
|
||||
credentials := GenerateCredentials("rsync")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rsync",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
for _, cred := range credentials {
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
common.LogSuccess(fmt.Sprintf("Rsync %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "rsync",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有尝试都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rsync",
|
||||
Error: fmt.Errorf("Rsync服务连接失败或需要认证"),
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
conn := p.connectToRsync(ctx, info)
|
||||
if conn == nil {
|
||||
@ -74,7 +80,6 @@ func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试列出模块
|
||||
modules := p.getModules(conn)
|
||||
if len(modules) > 0 {
|
||||
return &ScanResult{
|
||||
@ -87,14 +92,10 @@ func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
|
||||
return nil
|
||||
}
|
||||
|
||||
// testWeakPasswords 测试弱密码
|
||||
func (p *RsyncPlugin) testWeakPasswords(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
// Rsync密码通常在模块级别设置,这里简化处理
|
||||
// 实际场景中需要针对具体模块进行认证
|
||||
return nil
|
||||
func (p *RsyncPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// connectToRsync 连接到Rsync服务
|
||||
func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo) net.Conn {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
@ -104,15 +105,11 @@ func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置操作超时
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// getModules 获取Rsync模块列表
|
||||
func (p *RsyncPlugin) getModules(conn net.Conn) []string {
|
||||
// 发送RSYNCD协议的模块列表请求
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
@ -130,12 +127,10 @@ func (p *RsyncPlugin) getModules(conn net.Conn) []string {
|
||||
continue
|
||||
}
|
||||
|
||||
// Rsync协议结束标记
|
||||
if strings.HasPrefix(line, "@RSYNCD: EXIT") {
|
||||
break
|
||||
}
|
||||
|
||||
// 跳过协议头
|
||||
if strings.HasPrefix(line, "@RSYNCD:") {
|
||||
continue
|
||||
}
|
||||
@ -146,68 +141,6 @@ func (p *RsyncPlugin) getModules(conn net.Conn) []string {
|
||||
return modules
|
||||
}
|
||||
|
||||
// getModuleFiles 获取指定模块的文件列表
|
||||
func (p *RsyncPlugin) getModuleFiles(ctx context.Context, info *common.HostInfo, module string) []string {
|
||||
conn := p.connectToRsync(ctx, info)
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 发送模块名和协议版本
|
||||
request := fmt.Sprintf("%s\n", module)
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte(request)); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
scanner := bufio.NewScanner(conn)
|
||||
|
||||
var files []string
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查错误响应
|
||||
if strings.Contains(line, "@ERROR") {
|
||||
break
|
||||
}
|
||||
|
||||
// Rsync协议结束标记
|
||||
if strings.HasPrefix(line, "@RSYNCD: EXIT") {
|
||||
break
|
||||
}
|
||||
|
||||
// 跳过协议头
|
||||
if strings.HasPrefix(line, "@RSYNCD:") {
|
||||
continue
|
||||
}
|
||||
|
||||
files = append(files, line)
|
||||
|
||||
// 限制文件数量避免过多输出
|
||||
if len(files) >= 50 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// testWritePermission 测试写权限
|
||||
func (p *RsyncPlugin) testWritePermission(ctx context.Context, info *common.HostInfo) string {
|
||||
// Rsync写权限测试比较复杂,需要实际的rsync客户端
|
||||
// 这里简化为检测是否支持上传
|
||||
return "❌ 写权限检测需要完整的rsync客户端支持"
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测Rsync服务
|
||||
func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -221,7 +154,6 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 尝试Rsync协议握手
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
@ -248,7 +180,6 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
var banner string
|
||||
|
||||
if strings.Contains(responseStr, "@RSYNCD") {
|
||||
// 解析版本信息
|
||||
lines := strings.Split(responseStr, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "@RSYNCD:") {
|
||||
@ -267,7 +198,7 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("rsync_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("Rsync %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -276,7 +207,6 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("rsync", func() Plugin {
|
||||
return NewRsyncPlugin()
|
||||
|
@ -10,80 +10,52 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// SMTPPlugin SMTP邮件服务扫描和利用插件 - 包含用户枚举利用功能
|
||||
type SMTPPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewSMTPPlugin 创建SMTP插件
|
||||
func NewSMTPPlugin() *SMTPPlugin {
|
||||
return &SMTPPlugin{
|
||||
name: "smtp",
|
||||
ports: []int{25, 465, 587, 2525}, // SMTP端口
|
||||
ports: []int{25, 465, 587, 2525},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *SMTPPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *SMTPPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行SMTP扫描 - 弱密码检测和开放中继检测
|
||||
func (p *SMTPPlugin) 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.testOpenRelay(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("smtp_open_relay_success", target))
|
||||
common.LogSuccess(fmt.Sprintf("SMTP %s 开放中继", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("smtp")
|
||||
if len(credentials) == 0 {
|
||||
// SMTP默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: "password"},
|
||||
{Username: "admin", Password: "123456"},
|
||||
{Username: "test", Password: "test"},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "mail", Password: "mail"},
|
||||
{Username: "postmaster", Password: "postmaster"},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smtp",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smtp",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// SMTP认证成功
|
||||
common.LogSuccess(i18n.GetText("smtp_scan_success", target, cred.Username, cred.Password))
|
||||
common.LogSuccess(fmt.Sprintf("SMTP %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -94,36 +66,49 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smtp",
|
||||
Error: fmt.Errorf("未发现弱密码或开放中继"),
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testOpenRelay 测试开放中继
|
||||
func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
if p.checkOpenRelay(ctx, info) != "" {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "smtp",
|
||||
Banner: "开放中继",
|
||||
}
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
client, err := smtp.Dial(target)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if err := client.Hello("fscan.test"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := client.Mail("test@fscan.test"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := client.Rcpt("external@example.com"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "smtp",
|
||||
Banner: "开放中继",
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据
|
||||
func (p *SMTPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 根据端口选择连接方式
|
||||
var client *smtp.Client
|
||||
var err error
|
||||
|
||||
if info.Ports == "465" { // SMTPS
|
||||
if info.Ports == "465" {
|
||||
conn, err := tls.Dial("tcp", target, &tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
return false
|
||||
@ -142,14 +127,12 @@ func (p *SMTPPlugin) testCredential(ctx context.Context, info *common.HostInfo,
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
// 尝试STARTTLS
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
if err := client.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil {
|
||||
// STARTTLS失败,继续明文认证
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试认证
|
||||
auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host)
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return false
|
||||
@ -158,7 +141,6 @@ func (p *SMTPPlugin) testCredential(ctx context.Context, info *common.HostInfo,
|
||||
return true
|
||||
}
|
||||
|
||||
// getServerInfo 获取服务器信息
|
||||
func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) string {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -168,7 +150,6 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) s
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 读取欢迎消息
|
||||
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
@ -184,108 +165,6 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) s
|
||||
return welcome
|
||||
}
|
||||
|
||||
// getExtensions 获取SMTP扩展
|
||||
func (p *SMTPPlugin) getExtensions(ctx context.Context, info *common.HostInfo) []string {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
client, err := smtp.Dial(target)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
// 发送EHLO获取扩展
|
||||
if err := client.Hello("fscan.test"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var extensions []string
|
||||
if exts, _ := client.Extension("AUTH"); exts {
|
||||
extensions = append(extensions, "AUTH (认证支持)")
|
||||
}
|
||||
if exts, _ := client.Extension("STARTTLS"); exts {
|
||||
extensions = append(extensions, "STARTTLS (TLS支持)")
|
||||
}
|
||||
if exts, _ := client.Extension("SIZE"); exts {
|
||||
extensions = append(extensions, "SIZE (邮件大小限制)")
|
||||
}
|
||||
if exts, _ := client.Extension("PIPELINING"); exts {
|
||||
extensions = append(extensions, "PIPELINING (管道支持)")
|
||||
}
|
||||
if exts, _ := client.Extension("8BITMIME"); exts {
|
||||
extensions = append(extensions, "8BITMIME (8位MIME)")
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
||||
// enumerateUsers 枚举用户
|
||||
func (p *SMTPPlugin) enumerateUsers(ctx context.Context, info *common.HostInfo) []string {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
client, err := smtp.Dial(target)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if err := client.Hello("fscan.test"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 常见用户名列表
|
||||
commonUsers := []string{"admin", "administrator", "root", "user", "test", "mail", "postmaster", "webmaster", "info", "support", "sales", "marketing"}
|
||||
var validUsers []string
|
||||
|
||||
for _, user := range commonUsers {
|
||||
// 使用VRFY命令验证用户
|
||||
if err := p.sendRawCommand(client, fmt.Sprintf("VRFY %s", user)); err == nil {
|
||||
validUsers = append(validUsers, user)
|
||||
}
|
||||
|
||||
// 限制数量
|
||||
if len(validUsers) >= 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return validUsers
|
||||
}
|
||||
|
||||
// sendRawCommand 发送原始SMTP命令
|
||||
func (p *SMTPPlugin) sendRawCommand(client *smtp.Client, command string) error {
|
||||
// 这里简化实现,实际需要直接操作连接
|
||||
// smtp包没有直接的原始命令接口
|
||||
return fmt.Errorf("VRFY command not supported")
|
||||
}
|
||||
|
||||
// checkOpenRelay 检查开放中继
|
||||
func (p *SMTPPlugin) checkOpenRelay(ctx context.Context, info *common.HostInfo) string {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
client, err := smtp.Dial(target)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if err := client.Hello("fscan.test"); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 尝试发送外部邮件测试开放中继
|
||||
if err := client.Mail("test@fscan.test"); err != nil {
|
||||
return "❌ 邮件发送测试失败"
|
||||
}
|
||||
|
||||
if err := client.Rcpt("external@example.com"); err != nil {
|
||||
return "❌ 不是开放中继"
|
||||
}
|
||||
|
||||
return "⚠️ 可能存在开放中继风险"
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测SMTP服务
|
||||
func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -295,7 +174,6 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
if serverInfo != "" {
|
||||
banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo)
|
||||
} else {
|
||||
// 尝试简单连接测试
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
@ -308,7 +186,7 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
banner = "SMTP邮件服务"
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("smtp_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("SMTP %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -317,7 +195,6 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("smtp", func() Plugin {
|
||||
return NewSMTPPlugin()
|
||||
|
@ -5,77 +5,60 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// SNMPPlugin SNMP网络管理协议扫描和利用插件 - 包含系统信息收集利用功能
|
||||
type SNMPPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewSNMPPlugin 创建SNMP插件
|
||||
func NewSNMPPlugin() *SNMPPlugin {
|
||||
return &SNMPPlugin{
|
||||
name: "snmp",
|
||||
ports: []int{161, 162}, // SNMP端口
|
||||
ports: []int{161, 162},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *SNMPPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *SNMPPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行SNMP扫描 - 弱团体字符串检测
|
||||
func (p *SNMPPlugin) 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)
|
||||
}
|
||||
|
||||
// 生成测试团体字符串
|
||||
communities := p.generateCommunities()
|
||||
|
||||
// 逐个测试团体字符串
|
||||
for _, community := range communities {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "snmp",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
credentials := GenerateCredentials("snmp")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "snmp",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 测试团体字符串
|
||||
if p.testCommunity(ctx, info, community) {
|
||||
// SNMP团体字符串测试成功
|
||||
common.LogSuccess(i18n.GetText("snmp_scan_success", target, community))
|
||||
for _, cred := range credentials {
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
common.LogSuccess(fmt.Sprintf("SNMP %s %s", target, cred.Username))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "snmp",
|
||||
Username: community, // 团体字符串作为用户名
|
||||
Username: cred.Username,
|
||||
Password: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有团体字符串都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "snmp",
|
||||
@ -83,29 +66,7 @@ func (p *SNMPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// generateCommunities 生成测试团体字符串
|
||||
func (p *SNMPPlugin) generateCommunities() []string {
|
||||
// SNMP默认和常见团体字符串
|
||||
return []string{
|
||||
"public",
|
||||
"private",
|
||||
"community",
|
||||
"snmp",
|
||||
"admin",
|
||||
"manager",
|
||||
"read",
|
||||
"write",
|
||||
"test",
|
||||
"guest",
|
||||
"monitor",
|
||||
"security",
|
||||
"system",
|
||||
}
|
||||
}
|
||||
|
||||
// testCommunity 测试单个团体字符串
|
||||
func (p *SNMPPlugin) testCommunity(ctx context.Context, info *common.HostInfo, community string) bool {
|
||||
func (p *SNMPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
conn, err := net.DialTimeout("udp", target, time.Duration(common.Timeout)*time.Second)
|
||||
@ -114,8 +75,7 @@ func (p *SNMPPlugin) testCommunity(ctx context.Context, info *common.HostInfo, c
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 构建SNMP Get请求包 (sysDescr.0 OID: 1.3.6.1.2.1.1.1.0)
|
||||
packet := p.buildSNMPGetRequest(community, "1.3.6.1.2.1.1.1.0")
|
||||
packet := p.buildSNMPGetRequest(cred.Username, "1.3.6.1.2.1.1.1.0")
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if _, err := conn.Write(packet); err != nil {
|
||||
@ -129,109 +89,36 @@ func (p *SNMPPlugin) testCommunity(ctx context.Context, info *common.HostInfo, c
|
||||
return false
|
||||
}
|
||||
|
||||
// 简单检查响应是否为有效的SNMP响应
|
||||
return p.isValidSNMPResponse(response[:n])
|
||||
}
|
||||
|
||||
// buildSNMPGetRequest 构建SNMP Get请求
|
||||
func (p *SNMPPlugin) buildSNMPGetRequest(community, oid string) []byte {
|
||||
// 简化的SNMP v1 Get请求构建
|
||||
// 这里使用硬编码的包结构,实际应该使用ASN.1编码
|
||||
|
||||
// SNMP Get Request for sysDescr.0 (1.3.6.1.2.1.1.1.0)
|
||||
// 这是一个预构建的SNMP包模板,community字符串需要替换
|
||||
template := "302902010004067075626c6963a01c02020f7102010002010030113015060a2b060102010101000500"
|
||||
|
||||
// 将十六进制模板转换为字节
|
||||
packet, err := hex.DecodeString(template)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 这里应该替换community字符串,但为了简化,使用固定的"public"
|
||||
// 实际实现需要proper ASN.1编码
|
||||
return packet
|
||||
}
|
||||
|
||||
// isValidSNMPResponse 检查是否为有效的SNMP响应
|
||||
func (p *SNMPPlugin) isValidSNMPResponse(data []byte) bool {
|
||||
// 简单检查SNMP响应标志
|
||||
if len(data) < 10 {
|
||||
return false
|
||||
}
|
||||
|
||||
// SNMP响应通常以0x30开始 (SEQUENCE)
|
||||
return data[0] == 0x30
|
||||
}
|
||||
|
||||
// getSystemInfo 获取系统信息
|
||||
func (p *SNMPPlugin) getSystemInfo(ctx context.Context, info *common.HostInfo, community string) string {
|
||||
var sysInfo strings.Builder
|
||||
|
||||
// 系统相关的OID
|
||||
oids := map[string]string{
|
||||
"系统描述": "1.3.6.1.2.1.1.1.0",
|
||||
"系统OID": "1.3.6.1.2.1.1.2.0",
|
||||
"系统运行时间": "1.3.6.1.2.1.1.3.0",
|
||||
"系统联系人": "1.3.6.1.2.1.1.4.0",
|
||||
"系统名称": "1.3.6.1.2.1.1.5.0",
|
||||
"系统位置": "1.3.6.1.2.1.1.6.0",
|
||||
}
|
||||
|
||||
for name, oid := range oids {
|
||||
if value := p.getSNMPValue(ctx, info, community, oid); value != "" {
|
||||
sysInfo.WriteString(fmt.Sprintf("%s: %s\n", name, value))
|
||||
}
|
||||
}
|
||||
|
||||
return sysInfo.String()
|
||||
}
|
||||
|
||||
// getNetworkInterfaces 获取网络接口信息
|
||||
func (p *SNMPPlugin) getNetworkInterfaces(ctx context.Context, info *common.HostInfo, community string) []string {
|
||||
// 这里简化实现,实际需要遍历接口表
|
||||
interfaces := []string{
|
||||
"接口信息需要完整SNMP库支持",
|
||||
"简化实现中暂时无法获取详细接口信息",
|
||||
}
|
||||
|
||||
return interfaces
|
||||
}
|
||||
|
||||
// getProcesses 获取进程信息
|
||||
func (p *SNMPPlugin) getProcesses(ctx context.Context, info *common.HostInfo, community string) []string {
|
||||
// HOST-RESOURCES-MIB中的进程表 (hrSWRunTable)
|
||||
processes := []string{
|
||||
"进程信息需要完整SNMP库支持",
|
||||
"简化实现中暂时无法获取详细进程信息",
|
||||
}
|
||||
|
||||
return processes
|
||||
}
|
||||
|
||||
// getUsers 获取用户信息
|
||||
func (p *SNMPPlugin) getUsers(ctx context.Context, info *common.HostInfo, community string) []string {
|
||||
// 用户信息通常不通过SNMP直接获取
|
||||
return []string{"用户信息通常不通过SNMP协议直接暴露"}
|
||||
}
|
||||
|
||||
// getSNMPValue 获取指定OID的值
|
||||
func (p *SNMPPlugin) getSNMPValue(ctx context.Context, info *common.HostInfo, community, oid string) string {
|
||||
// 简化实现,实际需要proper SNMP库
|
||||
if p.testCommunity(ctx, info, community) {
|
||||
return "SNMP值获取需要完整SNMP库支持"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测SNMP服务
|
||||
func (p *SNMPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 尝试使用默认团体字符串"public"进行服务识别
|
||||
if p.testCommunity(ctx, info, "public") {
|
||||
cred := Credential{Username: "public", Password: ""}
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
banner := "SNMP网络管理服务 (public团体可访问)"
|
||||
common.LogSuccess(i18n.GetText("snmp_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("SNMP %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -240,7 +127,6 @@ func (p *SNMPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试UDP连接测试
|
||||
conn, err := net.DialTimeout("udp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
@ -251,8 +137,8 @@ func (p *SNMPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
banner := "SNMP网络管理服务 (需要有效团体字符串)"
|
||||
common.LogSuccess(i18n.GetText("snmp_service_identified", target, banner))
|
||||
banner := "SNMP网络管理服务"
|
||||
common.LogSuccess(fmt.Sprintf("SNMP %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -261,7 +147,6 @@ func (p *SNMPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("snmp", func() Plugin {
|
||||
return NewSNMPPlugin()
|
||||
|
@ -8,81 +8,52 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// TelnetPlugin Telnet远程终端服务扫描和利用插件 - 包含命令执行利用功能
|
||||
type TelnetPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewTelnetPlugin 创建Telnet插件
|
||||
func NewTelnetPlugin() *TelnetPlugin {
|
||||
return &TelnetPlugin{
|
||||
name: "telnet",
|
||||
ports: []int{23, 2323}, // Telnet端口
|
||||
ports: []int{23, 2323},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *TelnetPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *TelnetPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行Telnet扫描 - 弱密码检测和未授权访问检测
|
||||
func (p *TelnetPlugin) 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.testUnauthAccess(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("telnet_unauth_success", target))
|
||||
common.LogSuccess(fmt.Sprintf("Telnet %s 未授权访问", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("telnet")
|
||||
if len(credentials) == 0 {
|
||||
// Telnet默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "root", Password: "root"},
|
||||
{Username: "admin", Password: "password"},
|
||||
{Username: "admin", Password: "123456"},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "test", Password: "test"},
|
||||
{Username: "guest", Password: "guest"},
|
||||
{Username: "admin", Password: ""},
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// Telnet认证成功
|
||||
common.LogSuccess(i18n.GetText("telnet_scan_success", target, cred.Username, cred.Password))
|
||||
common.LogSuccess(fmt.Sprintf("Telnet %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -93,16 +64,14 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanRes
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: fmt.Errorf("未发现弱密码或未授权访问"),
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testUnauthAccess 测试未授权访问
|
||||
func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -114,7 +83,6 @@ func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostIn
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 读取欢迎信息
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
@ -123,7 +91,6 @@ func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostIn
|
||||
|
||||
welcome := string(buffer[:n])
|
||||
|
||||
// 检查是否直接进入shell(无需认证)
|
||||
if strings.Contains(welcome, "$") || strings.Contains(welcome, "#") || strings.Contains(welcome, ">") {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -135,7 +102,6 @@ func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostIn
|
||||
return nil
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据
|
||||
func (p *TelnetPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -147,7 +113,6 @@ func (p *TelnetPlugin) testCredential(ctx context.Context, info *common.HostInfo
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 读取欢迎信息
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
@ -155,17 +120,12 @@ func (p *TelnetPlugin) testCredential(ctx context.Context, info *common.HostInfo
|
||||
}
|
||||
|
||||
data := string(buffer[:n])
|
||||
|
||||
// 处理Telnet协议字符
|
||||
data = p.cleanTelnetData(data)
|
||||
|
||||
// 查找用户名提示
|
||||
if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") {
|
||||
// 发送用户名
|
||||
conn.Write([]byte(cred.Username + "\r\n"))
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// 读取密码提示
|
||||
n, err = conn.Read(buffer)
|
||||
if err != nil {
|
||||
return false
|
||||
@ -173,11 +133,9 @@ func (p *TelnetPlugin) testCredential(ctx context.Context, info *common.HostInfo
|
||||
|
||||
data = string(buffer[:n])
|
||||
if strings.Contains(strings.ToLower(data), "password") {
|
||||
// 发送密码
|
||||
conn.Write([]byte(cred.Password + "\r\n"))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 读取认证结果
|
||||
n, err = conn.Read(buffer)
|
||||
if err != nil {
|
||||
return false
|
||||
@ -186,7 +144,6 @@ func (p *TelnetPlugin) testCredential(ctx context.Context, info *common.HostInfo
|
||||
result := string(buffer[:n])
|
||||
result = p.cleanTelnetData(result)
|
||||
|
||||
// 检查是否认证成功
|
||||
return p.isLoginSuccessful(result)
|
||||
}
|
||||
}
|
||||
@ -194,99 +151,15 @@ func (p *TelnetPlugin) testCredential(ctx context.Context, info *common.HostInfo
|
||||
return false
|
||||
}
|
||||
|
||||
// connectTelnet 建立认证后的Telnet连接
|
||||
func (p *TelnetPlugin) connectTelnet(ctx context.Context, info *common.HostInfo, creds Credential) (net.Conn, error) {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 读取欢迎信息
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := string(buffer[:n])
|
||||
data = p.cleanTelnetData(data)
|
||||
|
||||
// 检查是否需要认证
|
||||
if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") {
|
||||
// 发送用户名
|
||||
conn.Write([]byte(creds.Username + "\r\n"))
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// 读取密码提示
|
||||
n, err = conn.Read(buffer)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data = string(buffer[:n])
|
||||
if strings.Contains(strings.ToLower(data), "password") {
|
||||
// 发送密码
|
||||
conn.Write([]byte(creds.Password + "\r\n"))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 读取认证结果
|
||||
n, err = conn.Read(buffer)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := string(buffer[:n])
|
||||
result = p.cleanTelnetData(result)
|
||||
|
||||
if !p.isLoginSuccessful(result) {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("认证失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// executeCommand 执行命令
|
||||
func (p *TelnetPlugin) executeCommand(conn net.Conn, command string) (string, error) {
|
||||
// 发送命令
|
||||
_, err := conn.Write([]byte(command + "\r\n"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 读取结果
|
||||
buffer := make([]byte, 4096)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(buffer[:n]), nil
|
||||
}
|
||||
|
||||
// cleanTelnetData 清理Telnet协议数据
|
||||
func (p *TelnetPlugin) cleanTelnetData(data string) string {
|
||||
// 移除Telnet协议字符
|
||||
cleaned := ""
|
||||
for i := 0; i < len(data); i++ {
|
||||
b := data[i]
|
||||
// 跳过Telnet命令字符 (IAC = 255)
|
||||
if b == 255 && i+2 < len(data) {
|
||||
i += 2 // 跳过IAC及其参数
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
// 保留可打印字符
|
||||
if b >= 32 && b <= 126 || b == '\r' || b == '\n' {
|
||||
cleaned += string(b)
|
||||
}
|
||||
@ -294,11 +167,9 @@ func (p *TelnetPlugin) cleanTelnetData(data string) string {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// isLoginSuccessful 判断是否登录成功
|
||||
func (p *TelnetPlugin) isLoginSuccessful(data string) bool {
|
||||
data = strings.ToLower(data)
|
||||
|
||||
// 成功标志
|
||||
successIndicators := []string{"$", "#", ">", "welcome", "last login"}
|
||||
for _, indicator := range successIndicators {
|
||||
if strings.Contains(data, indicator) {
|
||||
@ -306,7 +177,6 @@ func (p *TelnetPlugin) isLoginSuccessful(data string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// 失败标志
|
||||
failIndicators := []string{"incorrect", "failed", "denied", "invalid", "login:"}
|
||||
for _, indicator := range failIndicators {
|
||||
if strings.Contains(data, indicator) {
|
||||
@ -317,28 +187,6 @@ func (p *TelnetPlugin) isLoginSuccessful(data string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// cleanCommandOutput 清理命令输出
|
||||
func (p *TelnetPlugin) cleanCommandOutput(output string) string {
|
||||
lines := strings.Split(output, "\n")
|
||||
var cleanLines []string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// 跳过命令回显和提示符
|
||||
if line != "" && !strings.HasSuffix(line, "$") && !strings.HasSuffix(line, "#") && !strings.HasSuffix(line, ">") {
|
||||
cleanLines = append(cleanLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
result := strings.Join(cleanLines, "\n")
|
||||
if len(result) > 1000 {
|
||||
result = result[:1000] + "... (输出截断)"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测Telnet服务
|
||||
func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -354,7 +202,6 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 读取欢迎信息
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
@ -370,14 +217,14 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
||||
|
||||
var banner string
|
||||
if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") {
|
||||
banner = "Telnet远程终端服务 (需要认证)"
|
||||
banner = "Telnet远程终端服务"
|
||||
} else if strings.Contains(data, "$") || strings.Contains(data, "#") || strings.Contains(data, ">") {
|
||||
banner = "Telnet远程终端服务 (无认证)"
|
||||
} else {
|
||||
banner = "Telnet远程终端服务"
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.GetText("telnet_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("Telnet %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -386,7 +233,6 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("telnet", func() Plugin {
|
||||
return NewTelnetPlugin()
|
||||
|
@ -10,88 +10,70 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// VNCPlugin VNC远程桌面服务扫描和利用插件 - 包含屏幕信息收集利用功能
|
||||
type VNCPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewVNCPlugin 创建VNC插件
|
||||
func NewVNCPlugin() *VNCPlugin {
|
||||
return &VNCPlugin{
|
||||
name: "vnc",
|
||||
ports: []int{5900, 5901, 5902, 5903, 5904, 5905, 5906, 5907, 5908, 5909}, // VNC端口
|
||||
ports: []int{5900, 5901, 5902, 5903, 5904, 5905, 5906, 5907, 5908, 5909},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *VNCPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *VNCPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行VNC扫描 - 弱密码检测和未授权访问检测
|
||||
func (p *VNCPlugin) 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.testUnauthAccess(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.GetText("vnc_unauth_success", target))
|
||||
common.LogSuccess(fmt.Sprintf("VNC %s 未授权访问", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成测试密码(VNC通常只有密码,没有用户名)
|
||||
passwords := p.generatePasswords()
|
||||
|
||||
// 逐个测试密码
|
||||
for _, password := range passwords {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "vnc",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
credentials := GenerateCredentials("vnc")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "vnc",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 测试密码
|
||||
if p.testPassword(ctx, info, password) {
|
||||
// VNC认证成功
|
||||
common.LogSuccess(i18n.GetText("vnc_scan_success", target, password))
|
||||
for _, cred := range credentials {
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
common.LogSuccess(fmt.Sprintf("VNC %s %s", target, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "vnc",
|
||||
Username: "", // VNC没有用户名概念
|
||||
Password: password,
|
||||
Username: "",
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有密码都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "vnc",
|
||||
Error: fmt.Errorf("未发现弱密码或未授权访问"),
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testUnauthAccess 测试未授权访问
|
||||
func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -103,21 +85,18 @@ func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo)
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// VNC握手
|
||||
_, err = p.performHandshake(conn)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 读取认证类型
|
||||
authTypes, err := p.readAuthTypes(conn)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否支持无认证
|
||||
for _, authType := range authTypes {
|
||||
if authType == 1 { // None authentication
|
||||
if authType == 1 {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "vnc",
|
||||
@ -129,30 +108,8 @@ func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generatePasswords 生成VNC测试密码
|
||||
func (p *VNCPlugin) generatePasswords() []string {
|
||||
// VNC常见弱密码
|
||||
return []string{
|
||||
"",
|
||||
"123456",
|
||||
"password",
|
||||
"admin",
|
||||
"vnc",
|
||||
"123",
|
||||
"1234",
|
||||
"12345",
|
||||
"root",
|
||||
"test",
|
||||
"guest",
|
||||
"user",
|
||||
"welcome",
|
||||
"qwerty",
|
||||
"abc123",
|
||||
}
|
||||
}
|
||||
|
||||
// testPassword 测试单个密码
|
||||
func (p *VNCPlugin) testPassword(ctx context.Context, info *common.HostInfo, password string) bool {
|
||||
func (p *VNCPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
@ -163,22 +120,19 @@ func (p *VNCPlugin) testPassword(ctx context.Context, info *common.HostInfo, pas
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// VNC握手
|
||||
_, err = p.performHandshake(conn)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 读取认证类型
|
||||
authTypes, err := p.readAuthTypes(conn)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 查找VNC认证类型
|
||||
var hasVNCAuth bool
|
||||
for _, authType := range authTypes {
|
||||
if authType == 2 { // VNC authentication
|
||||
if authType == 2 {
|
||||
hasVNCAuth = true
|
||||
break
|
||||
}
|
||||
@ -188,116 +142,37 @@ func (p *VNCPlugin) testPassword(ctx context.Context, info *common.HostInfo, pas
|
||||
return false
|
||||
}
|
||||
|
||||
// 选择VNC认证
|
||||
if _, err := conn.Write([]byte{2}); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 读取挑战
|
||||
challenge := make([]byte, 16)
|
||||
if _, err := conn.Read(challenge); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 准备密码(最多8字节,不足补零)
|
||||
key := make([]byte, 8)
|
||||
copy(key, []byte(password))
|
||||
copy(key, []byte(cred.Password))
|
||||
|
||||
// DES加密挑战
|
||||
response, err := p.encryptChallenge(challenge, key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
if _, err := conn.Write(response); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 读取认证结果
|
||||
result := make([]byte, 4)
|
||||
if _, err := conn.Read(result); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查认证结果 (0表示成功)
|
||||
return binary.BigEndian.Uint32(result) == 0
|
||||
}
|
||||
|
||||
// connectVNC 建立认证后的VNC连接
|
||||
func (p *VNCPlugin) connectVNC(ctx context.Context, info *common.HostInfo, password string) (net.Conn, string, error) {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// VNC握手
|
||||
version, err := p.performHandshake(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 读取认证类型
|
||||
authTypes, err := p.readAuthTypes(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 处理认证
|
||||
authenticated := false
|
||||
for _, authType := range authTypes {
|
||||
if authType == 1 && password == "" { // None authentication
|
||||
conn.Write([]byte{1})
|
||||
authenticated = true
|
||||
break
|
||||
} else if authType == 2 && password != "" { // VNC authentication
|
||||
conn.Write([]byte{2})
|
||||
|
||||
// 读取挑战
|
||||
challenge := make([]byte, 16)
|
||||
conn.Read(challenge)
|
||||
|
||||
// 准备密码
|
||||
key := make([]byte, 8)
|
||||
copy(key, []byte(password))
|
||||
|
||||
// 加密挑战
|
||||
response, err := p.encryptChallenge(challenge, key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
conn.Write(response)
|
||||
|
||||
// 读取认证结果
|
||||
result := make([]byte, 4)
|
||||
conn.Read(result)
|
||||
|
||||
if binary.BigEndian.Uint32(result) == 0 {
|
||||
authenticated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
conn.Close()
|
||||
return nil, "", fmt.Errorf("认证失败")
|
||||
}
|
||||
|
||||
return conn, version, nil
|
||||
}
|
||||
|
||||
// performHandshake 执行VNC握手
|
||||
func (p *VNCPlugin) performHandshake(conn net.Conn) (string, error) {
|
||||
// 读取服务器版本
|
||||
versionBuf := make([]byte, 12)
|
||||
if _, err := conn.Read(versionBuf); err != nil {
|
||||
return "", err
|
||||
@ -305,7 +180,6 @@ func (p *VNCPlugin) performHandshake(conn net.Conn) (string, error) {
|
||||
|
||||
version := strings.TrimSpace(string(versionBuf))
|
||||
|
||||
// 发送客户端版本(使用相同版本)
|
||||
if _, err := conn.Write(versionBuf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -313,9 +187,7 @@ func (p *VNCPlugin) performHandshake(conn net.Conn) (string, error) {
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// readAuthTypes 读取认证类型
|
||||
func (p *VNCPlugin) readAuthTypes(conn net.Conn) ([]byte, error) {
|
||||
// 读取认证类型数量
|
||||
countBuf := make([]byte, 1)
|
||||
if _, err := conn.Read(countBuf); err != nil {
|
||||
return nil, err
|
||||
@ -326,7 +198,6 @@ func (p *VNCPlugin) readAuthTypes(conn net.Conn) ([]byte, error) {
|
||||
return nil, fmt.Errorf("无可用认证类型")
|
||||
}
|
||||
|
||||
// 读取认证类型列表
|
||||
authTypes := make([]byte, count)
|
||||
if _, err := conn.Read(authTypes); err != nil {
|
||||
return nil, err
|
||||
@ -335,9 +206,7 @@ func (p *VNCPlugin) readAuthTypes(conn net.Conn) ([]byte, error) {
|
||||
return authTypes, nil
|
||||
}
|
||||
|
||||
// encryptChallenge 使用DES加密挑战
|
||||
func (p *VNCPlugin) encryptChallenge(challenge, key []byte) ([]byte, error) {
|
||||
// VNC使用反向位序的DES
|
||||
reversedKey := make([]byte, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
reversedKey[i] = p.reverseBits(key[i])
|
||||
@ -355,7 +224,6 @@ func (p *VNCPlugin) encryptChallenge(challenge, key []byte) ([]byte, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// reverseBits 反转字节的位序
|
||||
func (p *VNCPlugin) reverseBits(b byte) byte {
|
||||
var result byte
|
||||
for i := 0; i < 8; i++ {
|
||||
@ -364,49 +232,7 @@ func (p *VNCPlugin) reverseBits(b byte) byte {
|
||||
return result
|
||||
}
|
||||
|
||||
// getServerInfo 获取服务器信息
|
||||
func (p *VNCPlugin) getServerInfo(conn net.Conn) string {
|
||||
var info strings.Builder
|
||||
|
||||
// 发送客户端初始化消息
|
||||
conn.Write([]byte{1}) // 共享桌面
|
||||
|
||||
// 读取服务器初始化消息
|
||||
serverInit := make([]byte, 24)
|
||||
n, err := conn.Read(serverInit)
|
||||
if err != nil || n < 20 {
|
||||
return "无法获取服务器信息"
|
||||
}
|
||||
|
||||
width := binary.BigEndian.Uint16(serverInit[0:2])
|
||||
height := binary.BigEndian.Uint16(serverInit[2:4])
|
||||
|
||||
info.WriteString(fmt.Sprintf("桌面分辨率: %dx%d\n", width, height))
|
||||
|
||||
// 读取桌面名称长度
|
||||
nameLength := binary.BigEndian.Uint32(serverInit[20:24])
|
||||
if nameLength > 0 && nameLength < 1024 {
|
||||
nameBuf := make([]byte, nameLength)
|
||||
if n, err := conn.Read(nameBuf); err == nil && n == int(nameLength) {
|
||||
info.WriteString(fmt.Sprintf("桌面名称: %s\n", string(nameBuf)))
|
||||
}
|
||||
}
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// getDesktopInfo 获取桌面信息
|
||||
func (p *VNCPlugin) getDesktopInfo(conn net.Conn) string {
|
||||
// 简化实现,实际需要完整的RFB协议支持
|
||||
return "桌面信息获取需要完整VNC客户端支持"
|
||||
}
|
||||
|
||||
// getPixelFormat 获取像素格式信息
|
||||
func (p *VNCPlugin) getPixelFormat(conn net.Conn) string {
|
||||
return "像素格式信息获取需要完整VNC客户端支持"
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测VNC服务
|
||||
func (p *VNCPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
@ -422,7 +248,6 @@ func (p *VNCPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 尝试VNC握手
|
||||
version, err := p.performHandshake(conn)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
@ -433,7 +258,7 @@ func (p *VNCPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
|
||||
banner := fmt.Sprintf("VNC远程桌面服务 (%s)", version)
|
||||
common.LogSuccess(i18n.GetText("vnc_service_identified", target, banner))
|
||||
common.LogSuccess(fmt.Sprintf("VNC %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
@ -442,7 +267,6 @@ func (p *VNCPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("vnc", func() Plugin {
|
||||
return NewVNCPlugin()
|
||||
|
Loading…
Reference in New Issue
Block a user