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

将所有服务插件移动到plugins/services/目录下,使目录结构更加清晰直观: • 创建plugins/services/目录统一管理服务扫描插件 • 添加init.go提供类型别名和函数导出 • 更新main.go导入路径 • 所有20个服务插件功能验证正常 新的目录结构更便于插件管理和维护。
397 lines
10 KiB
Go
397 lines
10 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
_ "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端口
|
||
}
|
||
}
|
||
|
||
// 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"},
|
||
}
|
||
}
|
||
|
||
// 逐个测试凭据
|
||
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() // 关闭测试连接
|
||
|
||
// MSSQL认证成功
|
||
common.LogSuccess(i18n.GetText("mssql_scan_success", target, cred.Username, cred.Password))
|
||
|
||
return &ScanResult{
|
||
Success: true,
|
||
Service: "mssql",
|
||
Username: cred.Username,
|
||
Password: cred.Password,
|
||
}
|
||
}
|
||
}
|
||
|
||
// 所有凭据都失败
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "mssql",
|
||
Error: fmt.Errorf("未发现弱密码"),
|
||
}
|
||
}
|
||
|
||
// Exploit 执行MSSQL利用操作 - 实现数据库查询功能
|
||
func (p *MSSQLPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||
// 建立MSSQL连接
|
||
db := p.testCredential(ctx, info, creds)
|
||
if db == nil {
|
||
return &ExploitResult{
|
||
Success: false,
|
||
Error: fmt.Errorf("MSSQL连接失败"),
|
||
}
|
||
}
|
||
defer db.Close()
|
||
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
common.LogSuccess(fmt.Sprintf("MSSQL利用开始: %s (用户: %s)", target, creds.Username))
|
||
|
||
var output strings.Builder
|
||
output.WriteString(fmt.Sprintf("=== MSSQL利用结果 - %s ===\n", target))
|
||
|
||
// 获取版本信息
|
||
if version := p.getVersion(db); version != "" {
|
||
output.WriteString(fmt.Sprintf("\n[版本信息]\n%s\n", version))
|
||
}
|
||
|
||
// 获取数据库列表
|
||
if databases := p.getDatabases(db); len(databases) > 0 {
|
||
output.WriteString(fmt.Sprintf("\n[数据库列表] (共%d个)\n", len(databases)))
|
||
for i, dbName := range databases {
|
||
if i >= 10 { // 限制显示前10个
|
||
output.WriteString("... (更多数据库)\n")
|
||
break
|
||
}
|
||
output.WriteString(fmt.Sprintf(" %s\n", dbName))
|
||
}
|
||
}
|
||
|
||
// 获取表列表(master数据库)
|
||
if tables := p.getTables(db, "master"); len(tables) > 0 {
|
||
output.WriteString(fmt.Sprintf("\n[master数据库表] (共%d个)\n", len(tables)))
|
||
for i, table := range tables {
|
||
if i >= 5 { // 限制显示前5个表
|
||
output.WriteString("... (更多表)\n")
|
||
break
|
||
}
|
||
output.WriteString(fmt.Sprintf(" %s\n", table))
|
||
}
|
||
}
|
||
|
||
// 获取用户列表
|
||
if users := p.getUsers(db); len(users) > 0 {
|
||
output.WriteString(fmt.Sprintf("\n[用户列表] (共%d个)\n", len(users)))
|
||
for i, user := range users {
|
||
if i >= 10 { // 限制显示前10个用户
|
||
output.WriteString("... (更多用户)\n")
|
||
break
|
||
}
|
||
output.WriteString(fmt.Sprintf(" %s\n", user))
|
||
}
|
||
}
|
||
|
||
// 获取权限信息
|
||
if privileges := p.getPrivileges(db, creds.Username); privileges != "" {
|
||
output.WriteString(fmt.Sprintf("\n[用户权限]\n%s\n", privileges))
|
||
}
|
||
|
||
// 检查xp_cmdshell状态
|
||
if cmdshellStatus := p.checkXpCmdshell(db); cmdshellStatus != "" {
|
||
output.WriteString(fmt.Sprintf("\n[系统命令执行]\n%s\n", cmdshellStatus))
|
||
}
|
||
|
||
common.LogSuccess(fmt.Sprintf("MSSQL利用完成: %s", target))
|
||
|
||
return &ExploitResult{
|
||
Success: true,
|
||
Output: output.String(),
|
||
}
|
||
}
|
||
|
||
// 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()
|
||
return nil
|
||
}
|
||
|
||
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)
|
||
|
||
db, err := sql.Open("mssql", connStr)
|
||
if err != nil {
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "mssql",
|
||
Error: err,
|
||
}
|
||
}
|
||
defer db.Close()
|
||
|
||
// 尝试连接(即使认证失败,也能识别服务)
|
||
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
||
defer cancel()
|
||
|
||
err = db.PingContext(pingCtx)
|
||
|
||
var banner string
|
||
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数据库服务"
|
||
} else if err == nil {
|
||
banner = "Microsoft SQL Server (连接成功)"
|
||
} else {
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "mssql",
|
||
Error: fmt.Errorf("无法识别为MSSQL服务"),
|
||
}
|
||
}
|
||
|
||
common.LogSuccess(i18n.GetText("mssql_service_identified", target, banner))
|
||
|
||
return &ScanResult{
|
||
Success: true,
|
||
Service: "mssql",
|
||
Banner: banner,
|
||
}
|
||
}
|
||
|
||
// init 自动注册插件
|
||
func init() {
|
||
RegisterPlugin("mssql", func() Plugin {
|
||
return NewMSSQLPlugin()
|
||
})
|
||
} |