mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00
refactor: 完成插件架构重构,移除冗余文件并统一legacy插件引用
- 移动adapters目录到plugins下统一管理 - 删除已迁移的老版本插件文件,避免代码重复 - 更新所有legacy适配器引用,统一使用legacy目录下的源文件 - 新增FindNet插件的legacy适配器,支持135端口RPC扫描 - 修复包导入路径,确保适配器正常工作 - 清理插件目录结构,分离新架构插件和legacy插件
This commit is contained in:
parent
f06bd3c1ac
commit
99b0481616
@ -28,6 +28,16 @@ import (
|
|||||||
_ "github.com/shadow1ng/fscan/plugins/services/snmp"
|
_ "github.com/shadow1ng/fscan/plugins/services/snmp"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/services/ssh"
|
_ "github.com/shadow1ng/fscan/plugins/services/ssh"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/services/telnet"
|
_ "github.com/shadow1ng/fscan/plugins/services/telnet"
|
||||||
|
|
||||||
|
// 导入Legacy插件适配器
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/legacy/netbios"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/legacy/ms17010"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/legacy/smb"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/legacy/smb2"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/legacy/smbghost"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/legacy/rdp"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/legacy/elasticsearch"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/legacy/findnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
@ -1,318 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ActiveMQCredential 表示一个ActiveMQ凭据
|
|
||||||
type ActiveMQCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveMQScanResult 表示扫描结果
|
|
||||||
type ActiveMQScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential ActiveMQCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
func ActiveMQScan(info *common.HostInfo) (tmperr error) {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 先尝试默认账户
|
|
||||||
common.LogDebug("尝试默认账户 admin:admin")
|
|
||||||
|
|
||||||
defaultCredential := ActiveMQCredential{Username: "admin", Password: "admin"}
|
|
||||||
defaultResult := tryActiveCredential(ctx, info, defaultCredential, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if defaultResult.Success {
|
|
||||||
saveActiveMQSuccess(info, target, defaultResult.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成所有凭据组合
|
|
||||||
credentials := generateActiveMQCredentials(common.Userdict["activemq"], common.Passwords)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["activemq"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentActiveMQScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveActiveMQSuccess(info, target, result.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("ActiveMQ扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了默认凭据
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateActiveMQCredentials 生成ActiveMQ的用户名密码组合
|
|
||||||
func generateActiveMQCredentials(users, passwords []string) []ActiveMQCredential {
|
|
||||||
var credentials []ActiveMQCredential
|
|
||||||
for _, user := range users {
|
|
||||||
for _, pass := range passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, ActiveMQCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return credentials
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentActiveMQScan 并发扫描ActiveMQ服务
|
|
||||||
func concurrentActiveMQScan(ctx context.Context, info *common.HostInfo, credentials []ActiveMQCredential, timeoutSeconds int64, maxRetries int) *ActiveMQScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *ActiveMQScanResult, 1)
|
|
||||||
workChan := make(chan ActiveMQCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryActiveCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("ActiveMQ并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryActiveCredential 尝试单个ActiveMQ凭据
|
|
||||||
func tryActiveCredential(ctx context.Context, info *common.HostInfo, credential ActiveMQCredential, timeoutSeconds int64, maxRetries int) *ActiveMQScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &ActiveMQScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建单个连接超时的上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, err := ActiveMQConn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &ActiveMQScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ActiveMQScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveMQConn 尝试ActiveMQ连接
|
|
||||||
func ActiveMQConn(ctx context.Context, info *common.HostInfo, user string, pass string) (bool, error) {
|
|
||||||
addr := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
|
||||||
|
|
||||||
// 使用上下文创建带超时的连接
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, time.Duration(common.Timeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中处理认证
|
|
||||||
go func() {
|
|
||||||
// STOMP协议的CONNECT命令
|
|
||||||
stompConnect := fmt.Sprintf("CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\nlogin:%s\npasscode:%s\n\n\x00", user, pass)
|
|
||||||
|
|
||||||
// 发送认证请求
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
|
||||||
if _, err := conn.Write([]byte(stompConnect)); err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{false, err}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
|
||||||
respBuf := make([]byte, 1024)
|
|
||||||
n, err := conn.Read(respBuf)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{false, err}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查认证结果
|
|
||||||
response := string(respBuf[:n])
|
|
||||||
|
|
||||||
var success bool
|
|
||||||
var resultErr error
|
|
||||||
|
|
||||||
if strings.Contains(response, "CONNECTED") {
|
|
||||||
success = true
|
|
||||||
resultErr = nil
|
|
||||||
} else if strings.Contains(response, "Authentication failed") || strings.Contains(response, "ERROR") {
|
|
||||||
success = false
|
|
||||||
resultErr = fmt.Errorf("认证失败")
|
|
||||||
} else {
|
|
||||||
success = false
|
|
||||||
resultErr = fmt.Errorf("未知响应: %s", response)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{success, resultErr}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待认证结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result.success, result.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveActiveMQSuccess 记录并保存ActiveMQ成功结果
|
|
||||||
func saveActiveMQSuccess(info *common.HostInfo, target string, credential ActiveMQCredential) {
|
|
||||||
successMsg := fmt.Sprintf("ActiveMQ服务 %s 成功爆破 用户名: %v 密码: %v",
|
|
||||||
target, credential.Username, credential.Password)
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
result := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "activemq",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(result)
|
|
||||||
}
|
|
@ -1,365 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gocql/gocql"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CassandraProxyDialer 实现gocql.Dialer接口,支持代理连接
|
|
||||||
type CassandraProxyDialer struct {
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *CassandraProxyDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
host, port, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return common.WrapperTcpWithContext(ctx, network, fmt.Sprintf("%s:%s", host, port))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CassandraCredential 表示一个Cassandra凭据
|
|
||||||
type CassandraCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CassandraScanResult 表示扫描结果
|
|
||||||
type CassandraScanResult struct {
|
|
||||||
Success bool
|
|
||||||
IsAnonymous bool
|
|
||||||
Error error
|
|
||||||
Credential CassandraCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
func CassandraScan(info *common.HostInfo) (tmperr error) {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 先尝试无认证访问
|
|
||||||
common.LogDebug("尝试无认证访问...")
|
|
||||||
|
|
||||||
anonymousCredential := CassandraCredential{Username: "", Password: ""}
|
|
||||||
anonymousResult := tryCassandraCredential(ctx, info, anonymousCredential, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if anonymousResult.Success {
|
|
||||||
saveCassandraSuccess(info, target, anonymousResult.Credential, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成所有凭据组合
|
|
||||||
credentials := generateCassandraCredentials(common.Userdict["cassandra"], common.Passwords)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["cassandra"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentCassandraScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveCassandraSuccess(info, target, result.Credential, false)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Cassandra扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateCassandraCredentials 生成Cassandra的用户名密码组合
|
|
||||||
func generateCassandraCredentials(users, passwords []string) []CassandraCredential {
|
|
||||||
var credentials []CassandraCredential
|
|
||||||
for _, user := range users {
|
|
||||||
for _, pass := range passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, CassandraCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return credentials
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentCassandraScan 并发扫描Cassandra服务
|
|
||||||
func concurrentCassandraScan(ctx context.Context, info *common.HostInfo, credentials []CassandraCredential, timeoutSeconds int64, maxRetries int) *CassandraScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *CassandraScanResult, 1)
|
|
||||||
workChan := make(chan CassandraCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryCassandraCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Cassandra并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryCassandraCredential 尝试单个Cassandra凭据
|
|
||||||
func tryCassandraCredential(ctx context.Context, info *common.HostInfo, credential CassandraCredential, timeoutSeconds int64, maxRetries int) *CassandraScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &CassandraScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建单个连接超时的上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, err := CassandraConn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &CassandraScanResult{
|
|
||||||
Success: true,
|
|
||||||
IsAnonymous: credential.Username == "" && credential.Password == "",
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CassandraScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CassandraConn 尝试Cassandra连接,支持上下文超时
|
|
||||||
func CassandraConn(ctx context.Context, info *common.HostInfo, user string, pass string) (bool, error) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
|
|
||||||
cluster := gocql.NewCluster(host)
|
|
||||||
cluster.Port, _ = strconv.Atoi(port)
|
|
||||||
cluster.Timeout = timeout
|
|
||||||
cluster.ConnectTimeout = timeout
|
|
||||||
cluster.ProtoVersion = 4
|
|
||||||
cluster.Consistency = gocql.One
|
|
||||||
|
|
||||||
// 如果配置了代理,设置自定义Dialer
|
|
||||||
if common.Socks5Proxy != "" {
|
|
||||||
cluster.Dialer = &CassandraProxyDialer{
|
|
||||||
timeout: timeout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if user != "" || pass != "" {
|
|
||||||
cluster.Authenticator = gocql.PasswordAuthenticator{
|
|
||||||
Username: user,
|
|
||||||
Password: pass,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: 3}
|
|
||||||
|
|
||||||
// 创建会话通道
|
|
||||||
sessionChan := make(chan struct {
|
|
||||||
session *gocql.Session
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在后台创建会话,以便可以通过上下文取消
|
|
||||||
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}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待会话创建或上下文取消
|
|
||||||
var session *gocql.Session
|
|
||||||
var err error
|
|
||||||
select {
|
|
||||||
case result := <-sessionChan:
|
|
||||||
session, err = result.session, result.err
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
// 尝试执行查询,测试连接是否成功
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
var version string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// 尝试两种查询,确保至少一种成功
|
|
||||||
err = session.Query("SELECT peer FROM system.peers").WithContext(ctx).Scan(&version)
|
|
||||||
if err != nil {
|
|
||||||
err = session.Query("SELECT now() FROM system.local").WithContext(ctx).Scan(&version)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{err == nil, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待查询结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result.success, result.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveCassandraSuccess 记录并保存Cassandra成功结果
|
|
||||||
func saveCassandraSuccess(info *common.HostInfo, target string, credential CassandraCredential, isAnonymous bool) {
|
|
||||||
var successMsg string
|
|
||||||
var details map[string]interface{}
|
|
||||||
|
|
||||||
if isAnonymous {
|
|
||||||
successMsg = fmt.Sprintf("Cassandra服务 %s 无认证访问成功", target)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "cassandra",
|
|
||||||
"auth_type": "anonymous",
|
|
||||||
"type": "unauthorized-access",
|
|
||||||
"description": "数据库允许无认证访问",
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("Cassandra服务 %s 爆破成功 用户名: %v 密码: %v",
|
|
||||||
target, credential.Username, credential.Password)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "cassandra",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
result := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(result)
|
|
||||||
}
|
|
@ -1,307 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ElasticCredential 表示Elasticsearch的凭据
|
|
||||||
type ElasticCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ElasticScanResult 表示扫描结果
|
|
||||||
type ElasticScanResult struct {
|
|
||||||
Success bool
|
|
||||||
IsUnauth bool
|
|
||||||
Error error
|
|
||||||
Credential ElasticCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
func ElasticScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 首先测试无认证访问
|
|
||||||
common.LogDebug("尝试无认证访问...")
|
|
||||||
unauthResult := tryElasticCredential(ctx, info, ElasticCredential{"", ""}, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if unauthResult.Success {
|
|
||||||
// 无需认证情况
|
|
||||||
saveElasticResult(info, target, unauthResult.Credential, true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []ElasticCredential
|
|
||||||
for _, user := range common.Userdict["elastic"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, ElasticCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["elastic"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 并发扫描
|
|
||||||
result := concurrentElasticScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveElasticResult(info, target, result.Credential, false)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Elasticsearch扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1是因为尝试了无认证
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentElasticScan 并发扫描Elasticsearch服务
|
|
||||||
func concurrentElasticScan(ctx context.Context, info *common.HostInfo, credentials []ElasticCredential, timeoutSeconds int64, maxRetries int) *ElasticScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *ElasticScanResult, 1)
|
|
||||||
workChan := make(chan ElasticCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryElasticCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Elasticsearch并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryElasticCredential 尝试单个Elasticsearch凭据
|
|
||||||
func tryElasticCredential(ctx context.Context, info *common.HostInfo, credential ElasticCredential, timeoutSeconds int64, maxRetries int) *ElasticScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &ElasticScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
success, err := ElasticConn(ctx, info, credential.Username, credential.Password, timeoutSeconds)
|
|
||||||
if success {
|
|
||||||
isUnauth := credential.Username == "" && credential.Password == ""
|
|
||||||
return &ElasticScanResult{
|
|
||||||
Success: true,
|
|
||||||
IsUnauth: isUnauth,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ElasticScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ElasticConn 尝试Elasticsearch连接
|
|
||||||
func ElasticConn(ctx context.Context, info *common.HostInfo, user string, pass string, timeoutSeconds int64) (bool, error) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
|
||||||
|
|
||||||
// 创建带有超时的HTTP客户端
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: timeout,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
baseURL := fmt.Sprintf("http://%s:%s", host, port)
|
|
||||||
|
|
||||||
// 使用上下文创建请求
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/_cat/indices", nil)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if user != "" || pass != "" {
|
|
||||||
auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
|
|
||||||
req.Header.Add("Authorization", "Basic "+auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中执行HTTP请求
|
|
||||||
go func() {
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{false, err}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{resp.StatusCode == 200, nil}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result.success, result.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveElasticResult 保存Elasticsearch扫描结果
|
|
||||||
func saveElasticResult(info *common.HostInfo, target string, credential ElasticCredential, isUnauth bool) {
|
|
||||||
var successMsg string
|
|
||||||
var details map[string]interface{}
|
|
||||||
|
|
||||||
if isUnauth {
|
|
||||||
successMsg = fmt.Sprintf("Elasticsearch服务 %s 无需认证", target)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "elasticsearch",
|
|
||||||
"type": "unauthorized-access",
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("Elasticsearch服务 %s 爆破成功 用户名: %v 密码: %v",
|
|
||||||
target, credential.Username, credential.Password)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "elasticsearch",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
result := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(result)
|
|
||||||
}
|
|
346
Plugins/FTP.go
346
Plugins/FTP.go
@ -1,346 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/jlaffaye/ftp"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FtpCredential 表示一个FTP凭据
|
|
||||||
type FtpCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// FtpScanResult 表示FTP扫描结果
|
|
||||||
type FtpScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential FtpCredential
|
|
||||||
Directories []string
|
|
||||||
IsAnonymous bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func FtpScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 首先尝试匿名登录
|
|
||||||
common.LogDebug("尝试匿名登录...")
|
|
||||||
anonymousResult := tryFtpCredential(ctx, info, FtpCredential{"anonymous", ""}, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if anonymousResult.Success {
|
|
||||||
// 匿名登录成功
|
|
||||||
saveFtpResult(info, target, anonymousResult)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []FtpCredential
|
|
||||||
for _, user := range common.Userdict["ftp"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, FtpCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["ftp"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentFtpScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 保存成功结果
|
|
||||||
saveFtpResult(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("FTP扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名登录
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentFtpScan 并发扫描FTP服务
|
|
||||||
func concurrentFtpScan(ctx context.Context, info *common.HostInfo, credentials []FtpCredential, timeoutSeconds int64, maxRetries int) *FtpScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *FtpScanResult, 1)
|
|
||||||
workChan := make(chan FtpCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryFtpCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("FTP并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryFtpCredential 尝试单个FTP凭据
|
|
||||||
func tryFtpCredential(ctx context.Context, info *common.HostInfo, credential FtpCredential, timeoutSeconds int64, maxRetries int) *FtpScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &FtpScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
directories []string
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中尝试连接
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
go func() {
|
|
||||||
defer cancel()
|
|
||||||
success, dirs, err := FtpConn(info, credential.Username, credential.Password)
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
directories []string
|
|
||||||
err error
|
|
||||||
}{success, dirs, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或超时
|
|
||||||
var success bool
|
|
||||||
var dirs []string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
success = result.success
|
|
||||||
dirs = result.directories
|
|
||||||
err = result.err
|
|
||||||
case <-connCtx.Done():
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
// 全局超时
|
|
||||||
return &FtpScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 单个连接超时
|
|
||||||
err = fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
|
|
||||||
if success {
|
|
||||||
isAnonymous := credential.Username == "anonymous" && credential.Password == ""
|
|
||||||
return &FtpScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
Directories: dirs,
|
|
||||||
IsAnonymous: isAnonymous,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 登录错误不需要重试
|
|
||||||
if strings.Contains(err.Error(), "Login incorrect") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接数过多需要等待
|
|
||||||
if strings.Contains(err.Error(), "too many connections") {
|
|
||||||
common.LogDebug("连接数过多,等待5秒...")
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &FtpScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FtpConn 建立FTP连接并尝试登录
|
|
||||||
func FtpConn(info *common.HostInfo, user string, pass string) (success bool, directories []string, err error) {
|
|
||||||
Host, Port := info.Host, info.Ports
|
|
||||||
|
|
||||||
// 建立FTP连接
|
|
||||||
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(common.Timeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if conn != nil {
|
|
||||||
conn.Quit()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 尝试登录
|
|
||||||
if err = conn.Login(user, pass); err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取目录信息
|
|
||||||
dirs, err := conn.List("")
|
|
||||||
if err == nil && len(dirs) > 0 {
|
|
||||||
directories = make([]string, 0, min(6, len(dirs)))
|
|
||||||
for i := 0; i < len(dirs) && i < 6; i++ {
|
|
||||||
name := dirs[i].Name
|
|
||||||
if len(name) > 50 {
|
|
||||||
name = name[:50]
|
|
||||||
}
|
|
||||||
directories = append(directories, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, directories, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveFtpResult 保存FTP扫描结果
|
|
||||||
func saveFtpResult(info *common.HostInfo, target string, result *FtpScanResult) {
|
|
||||||
var successMsg string
|
|
||||||
var details map[string]interface{}
|
|
||||||
|
|
||||||
if result.IsAnonymous {
|
|
||||||
successMsg = fmt.Sprintf("FTP服务 %s 匿名登录成功!", target)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "ftp",
|
|
||||||
"username": "anonymous",
|
|
||||||
"password": "",
|
|
||||||
"type": "anonymous-login",
|
|
||||||
"directories": result.Directories,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("FTP服务 %s 成功爆破 用户名: %v 密码: %v",
|
|
||||||
target, result.Credential.Username, result.Credential.Password)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "ftp",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
"directories": result.Directories,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
// min 返回两个整数中的较小值
|
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
327
Plugins/IMAP.go
327
Plugins/IMAP.go
@ -1,327 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IMAPCredential 表示一个IMAP凭据
|
|
||||||
type IMAPCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMAPScanResult 表示IMAP扫描结果
|
|
||||||
type IMAPScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential IMAPCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMAPScan 主扫描函数
|
|
||||||
func IMAPScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []IMAPCredential
|
|
||||||
for _, user := range common.Userdict["imap"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, IMAPCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["imap"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 并发扫描
|
|
||||||
result := concurrentIMAPScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveIMAPResult(info, target, result.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("IMAP扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentIMAPScan 并发扫描IMAP服务
|
|
||||||
func concurrentIMAPScan(ctx context.Context, info *common.HostInfo, credentials []IMAPCredential, timeoutSeconds int64, maxRetries int) *IMAPScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *IMAPScanResult, 1)
|
|
||||||
workChan := make(chan IMAPCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryIMAPCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("IMAP并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryIMAPCredential 尝试单个IMAP凭据
|
|
||||||
func tryIMAPCredential(ctx context.Context, info *common.HostInfo, credential IMAPCredential, timeoutSeconds int64, maxRetries int) *IMAPScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &IMAPScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建单个连接超时的上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, err := IMAPConn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &IMAPScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &IMAPScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMAPConn 连接测试函数
|
|
||||||
func IMAPConn(ctx context.Context, info *common.HostInfo, user string, pass string) (bool, error) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
addr := fmt.Sprintf("%s:%s", host, port)
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中尝试连接
|
|
||||||
go func() {
|
|
||||||
// 先尝试普通连接
|
|
||||||
conn, err := common.WrapperTcpWithContext(ctx, "tcp", addr)
|
|
||||||
if err == nil {
|
|
||||||
flag, authErr := tryIMAPAuth(conn, user, pass, timeout)
|
|
||||||
conn.Close()
|
|
||||||
if authErr == nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{flag, nil}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果普通连接失败或认证失败,尝试TLS连接
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用支持代理的TLS连接
|
|
||||||
tlsConn, tlsErr := common.WrapperTlsWithContext(ctx, "tcp", addr, tlsConfig)
|
|
||||||
if tlsErr != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{false, fmt.Errorf("TLS连接失败: %v", tlsErr)}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer tlsConn.Close()
|
|
||||||
|
|
||||||
flag, authErr := tryIMAPAuth(tlsConn, user, pass, timeout)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{flag, authErr}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result.success, result.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryIMAPAuth 尝试IMAP认证
|
|
||||||
func tryIMAPAuth(conn net.Conn, user string, pass string, timeout time.Duration) (bool, error) {
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
|
|
||||||
reader := bufio.NewReader(conn)
|
|
||||||
_, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("读取欢迎消息失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
loginCmd := fmt.Sprintf("a001 LOGIN \"%s\" \"%s\"\r\n", user, pass)
|
|
||||||
_, err = conn.Write([]byte(loginCmd))
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("发送登录命令失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
response, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
return false, fmt.Errorf("认证失败")
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("读取响应失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(response, "a001 OK") {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(response, "a001 NO") || strings.Contains(response, "a001 BAD") {
|
|
||||||
return false, fmt.Errorf("认证失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveIMAPResult 保存IMAP扫描结果
|
|
||||||
func saveIMAPResult(info *common.HostInfo, target string, credential IMAPCredential) {
|
|
||||||
successMsg := fmt.Sprintf("IMAP服务 %s 爆破成功 用户名: %v 密码: %v",
|
|
||||||
target, credential.Username, credential.Password)
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "imap",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
328
Plugins/Kafka.go
328
Plugins/Kafka.go
@ -1,328 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/IBM/sarama"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KafkaCredential 表示Kafka凭据
|
|
||||||
type KafkaCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// KafkaScanResult 表示扫描结果
|
|
||||||
type KafkaScanResult struct {
|
|
||||||
Success bool
|
|
||||||
IsUnauth bool
|
|
||||||
Error error
|
|
||||||
Credential KafkaCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
func KafkaScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 先尝试无认证访问
|
|
||||||
common.LogDebug("尝试无认证访问...")
|
|
||||||
unauthResult := tryKafkaCredential(ctx, info, KafkaCredential{"", ""}, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if unauthResult.Success {
|
|
||||||
// 无认证访问成功
|
|
||||||
common.LogSuccess(fmt.Sprintf("Kafka服务 %s 无需认证即可访问", target))
|
|
||||||
|
|
||||||
// 保存无认证访问结果
|
|
||||||
result := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "kafka",
|
|
||||||
"type": "unauthorized-access",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []KafkaCredential
|
|
||||||
for _, user := range common.Userdict["kafka"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, KafkaCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["kafka"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentKafkaScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 保存爆破成功结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "kafka",
|
|
||||||
"type": "weak-password",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
common.LogSuccess(fmt.Sprintf("Kafka服务 %s 爆破成功 用户名: %s 密码: %s",
|
|
||||||
target, result.Credential.Username, result.Credential.Password))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Kafka扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了无认证
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentKafkaScan 并发扫描Kafka服务
|
|
||||||
func concurrentKafkaScan(ctx context.Context, info *common.HostInfo, credentials []KafkaCredential, timeoutSeconds int64, maxRetries int) *KafkaScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *KafkaScanResult, 1)
|
|
||||||
workChan := make(chan KafkaCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryKafkaCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Kafka并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryKafkaCredential 尝试单个Kafka凭据
|
|
||||||
func tryKafkaCredential(ctx context.Context, info *common.HostInfo, credential KafkaCredential, timeoutSeconds int64, maxRetries int) *KafkaScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &KafkaScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建单个连接超时的上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
|
|
||||||
// 在协程中执行Kafka连接
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
success, err := KafkaConn(info, credential.Username, credential.Password)
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
// 连接超时或被取消
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{success, err}:
|
|
||||||
// 发送结果
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或超时
|
|
||||||
var success bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
success = result.success
|
|
||||||
err = result.err
|
|
||||||
case <-connCtx.Done():
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
// 全局超时
|
|
||||||
cancel()
|
|
||||||
return &KafkaScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 单个连接超时
|
|
||||||
err = fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() // 清理单个连接上下文
|
|
||||||
|
|
||||||
if success {
|
|
||||||
isUnauth := credential.Username == "" && credential.Password == ""
|
|
||||||
return &KafkaScanResult{
|
|
||||||
Success: true,
|
|
||||||
IsUnauth: isUnauth,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 记录错误
|
|
||||||
common.LogError(fmt.Sprintf("Kafka尝试失败 用户名: %s 密码: %s 错误: %v",
|
|
||||||
credential.Username, credential.Password, err))
|
|
||||||
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &KafkaScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KafkaConn 尝试 Kafka 连接
|
|
||||||
func KafkaConn(info *common.HostInfo, user string, pass string) (bool, error) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
|
|
||||||
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 user != "" || pass != "" {
|
|
||||||
config.Net.SASL.Enable = true
|
|
||||||
config.Net.SASL.Mechanism = sarama.SASLTypePlaintext
|
|
||||||
config.Net.SASL.User = user
|
|
||||||
config.Net.SASL.Password = pass
|
|
||||||
config.Net.SASL.Handshake = true
|
|
||||||
}
|
|
||||||
|
|
||||||
brokers := []string{fmt.Sprintf("%s:%s", host, port)}
|
|
||||||
|
|
||||||
// 尝试作为消费者连接测试
|
|
||||||
consumer, err := sarama.NewConsumer(brokers, config)
|
|
||||||
if err == nil {
|
|
||||||
defer consumer.Close()
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果消费者连接失败,尝试作为客户端连接
|
|
||||||
client, err := sarama.NewClient(brokers, config)
|
|
||||||
if err == nil {
|
|
||||||
defer client.Close()
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查错误类型
|
|
||||||
if strings.Contains(err.Error(), "SASL") ||
|
|
||||||
strings.Contains(err.Error(), "authentication") ||
|
|
||||||
strings.Contains(err.Error(), "credentials") {
|
|
||||||
return false, fmt.Errorf("认证失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
308
Plugins/LDAP.go
308
Plugins/LDAP.go
@ -1,308 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LDAPCredential 表示一个LDAP凭据
|
|
||||||
type LDAPCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// LDAPScanResult 表示LDAP扫描结果
|
|
||||||
type LDAPScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential LDAPCredential
|
|
||||||
IsAnonymous bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func LDAPScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 首先尝试匿名访问
|
|
||||||
common.LogDebug("尝试匿名访问...")
|
|
||||||
anonymousResult := tryLDAPCredential(ctx, info, LDAPCredential{"", ""}, common.Timeout, 1)
|
|
||||||
|
|
||||||
if anonymousResult.Success {
|
|
||||||
// 匿名访问成功
|
|
||||||
saveLDAPResult(info, target, anonymousResult)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []LDAPCredential
|
|
||||||
for _, user := range common.Userdict["ldap"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, LDAPCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["ldap"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentLDAPScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveLDAPResult(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("LDAP扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentLDAPScan 并发扫描LDAP服务
|
|
||||||
func concurrentLDAPScan(ctx context.Context, info *common.HostInfo, credentials []LDAPCredential, timeoutSeconds int64, maxRetries int) *LDAPScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *LDAPScanResult, 1)
|
|
||||||
workChan := make(chan LDAPCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryLDAPCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("LDAP并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryLDAPCredential 尝试单个LDAP凭据
|
|
||||||
func tryLDAPCredential(ctx context.Context, info *common.HostInfo, credential LDAPCredential, timeoutSeconds int64, maxRetries int) *LDAPScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &LDAPScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, err := LDAPConn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
isAnonymous := credential.Username == "" && credential.Password == ""
|
|
||||||
return &LDAPScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
IsAnonymous: isAnonymous,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &LDAPScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LDAPConn 尝试LDAP连接
|
|
||||||
func LDAPConn(ctx context.Context, info *common.HostInfo, user string, pass string) (bool, error) {
|
|
||||||
address := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
||||||
|
|
||||||
// 使用上下文控制的拨号过程
|
|
||||||
conn, err := common.WrapperTcpWithContext(ctx, "tcp", address)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用已连接的TCP连接创建LDAP连接
|
|
||||||
l := ldap.NewConn(conn, false)
|
|
||||||
defer l.Close()
|
|
||||||
|
|
||||||
// 在单独的协程中启动LDAP连接
|
|
||||||
go l.Start()
|
|
||||||
|
|
||||||
// 创建一个完成通道
|
|
||||||
done := make(chan error, 1)
|
|
||||||
|
|
||||||
// 在协程中进行绑定和搜索操作,确保可以被上下文取消
|
|
||||||
go func() {
|
|
||||||
// 尝试绑定
|
|
||||||
var err error
|
|
||||||
if user != "" {
|
|
||||||
// 使用更通用的绑定DN模式
|
|
||||||
bindDN := fmt.Sprintf("cn=%s,dc=example,dc=com", user)
|
|
||||||
err = l.Bind(bindDN, pass)
|
|
||||||
} else {
|
|
||||||
// 匿名绑定
|
|
||||||
err = l.UnauthenticatedBind("")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
done <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试简单搜索以验证权限
|
|
||||||
searchRequest := ldap.NewSearchRequest(
|
|
||||||
"dc=example,dc=com",
|
|
||||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
|
||||||
"(objectClass=*)",
|
|
||||||
[]string{"dn"},
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err = l.Search(searchRequest)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待操作完成或上下文取消
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveLDAPResult 保存LDAP扫描结果
|
|
||||||
func saveLDAPResult(info *common.HostInfo, target string, result *LDAPScanResult) {
|
|
||||||
var successMsg string
|
|
||||||
var details map[string]interface{}
|
|
||||||
|
|
||||||
if result.IsAnonymous {
|
|
||||||
successMsg = fmt.Sprintf("LDAP服务 %s 匿名访问成功", target)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "ldap",
|
|
||||||
"type": "anonymous-access",
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("LDAP服务 %s 爆破成功 用户名: %v 密码: %v",
|
|
||||||
target, result.Credential.Username, result.Credential.Password)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "ldap",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,289 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// SMB协议加密的请求数据
|
|
||||||
negotiateProtocolRequest_enc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
|
|
||||||
sessionSetupRequest_enc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
|
|
||||||
treeConnectRequest_enc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
|
||||||
transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
|
||||||
trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
|
|
||||||
|
|
||||||
// SMB协议解密后的请求数据
|
|
||||||
negotiateProtocolRequest []byte
|
|
||||||
sessionSetupRequest []byte
|
|
||||||
treeConnectRequest []byte
|
|
||||||
transNamedPipeRequest []byte
|
|
||||||
trans2SessionSetupRequest []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// 解密协议请求
|
|
||||||
decrypted, err := AesDecrypt(negotiateProtocolRequest_enc, key)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("协议请求解密错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("协议请求解码错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解密会话请求
|
|
||||||
decrypted, err = AesDecrypt(sessionSetupRequest_enc, key)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("会话请求解密错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
sessionSetupRequest, err = hex.DecodeString(decrypted)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("会话请求解码错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解密连接请求
|
|
||||||
decrypted, err = AesDecrypt(treeConnectRequest_enc, key)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("连接请求解密错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
treeConnectRequest, err = hex.DecodeString(decrypted)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("连接请求解码错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解密管道请求
|
|
||||||
decrypted, err = AesDecrypt(transNamedPipeRequest_enc, key)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("管道请求解密错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
transNamedPipeRequest, err = hex.DecodeString(decrypted)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("管道请求解码错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解密会话设置请求
|
|
||||||
decrypted, err = AesDecrypt(trans2SessionSetupRequest_enc, key)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("会话设置解密错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("会话设置解码错误: %v", err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MS17010 扫描入口函数
|
|
||||||
func MS17010(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := MS17010Scan(info)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("%s:%s - %v", info.Host, info.Ports, err))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func MS17010Scan(info *common.HostInfo) error {
|
|
||||||
ip := info.Host
|
|
||||||
|
|
||||||
// 连接目标
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(common.Timeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("连接错误: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)); err != nil {
|
|
||||||
return fmt.Errorf("设置超时错误: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SMB协议协商
|
|
||||||
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
|
||||||
return fmt.Errorf("发送协议请求错误: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reply := make([]byte, 1024)
|
|
||||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取协议响应错误: %v", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("协议响应不完整")
|
|
||||||
}
|
|
||||||
|
|
||||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
|
||||||
return fmt.Errorf("协议协商被拒绝")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 建立会话
|
|
||||||
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
|
||||||
return fmt.Errorf("发送会话请求错误: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := conn.Read(reply)
|
|
||||||
if err != nil || n < 36 {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取会话响应错误: %v", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("会话响应不完整")
|
|
||||||
}
|
|
||||||
|
|
||||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
|
||||||
return fmt.Errorf("会话建立失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取系统信息
|
|
||||||
var os string
|
|
||||||
sessionSetupResponse := reply[36:n]
|
|
||||||
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
|
||||||
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
|
||||||
if n != int(byteCount)+45 {
|
|
||||||
common.LogError(fmt.Sprintf("无效会话响应 %s:445", ip))
|
|
||||||
} else {
|
|
||||||
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
|
||||||
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
|
|
||||||
os = string(sessionSetupResponse[10:i])
|
|
||||||
os = strings.Replace(os, string([]byte{0x00}), "", -1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 树连接请求
|
|
||||||
userID := reply[32:34]
|
|
||||||
treeConnectRequest[32] = userID[0]
|
|
||||||
treeConnectRequest[33] = userID[1]
|
|
||||||
|
|
||||||
if _, err = conn.Write(treeConnectRequest); err != nil {
|
|
||||||
return fmt.Errorf("发送树连接请求错误: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取树连接响应错误: %v", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("树连接响应不完整")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 命名管道请求
|
|
||||||
treeID := reply[28:30]
|
|
||||||
transNamedPipeRequest[28] = treeID[0]
|
|
||||||
transNamedPipeRequest[29] = treeID[1]
|
|
||||||
transNamedPipeRequest[32] = userID[0]
|
|
||||||
transNamedPipeRequest[33] = userID[1]
|
|
||||||
|
|
||||||
if _, err = conn.Write(transNamedPipeRequest); err != nil {
|
|
||||||
return fmt.Errorf("发送管道请求错误: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取管道响应错误: %v", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("管道响应不完整")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 漏洞检测部分添加 Output
|
|
||||||
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
|
|
||||||
// 构造基本详情
|
|
||||||
details := map[string]interface{}{
|
|
||||||
"port": "445",
|
|
||||||
"vulnerability": "MS17-010",
|
|
||||||
}
|
|
||||||
if os != "" {
|
|
||||||
details["os"] = os
|
|
||||||
common.LogSuccess(fmt.Sprintf("发现漏洞 %s [%s] MS17-010", ip, os))
|
|
||||||
} else {
|
|
||||||
common.LogSuccess(fmt.Sprintf("发现漏洞 %s MS17-010", ip))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存 MS17-010 漏洞结果
|
|
||||||
result := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: ip,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(result)
|
|
||||||
|
|
||||||
// DOUBLEPULSAR 后门检测
|
|
||||||
trans2SessionSetupRequest[28] = treeID[0]
|
|
||||||
trans2SessionSetupRequest[29] = treeID[1]
|
|
||||||
trans2SessionSetupRequest[32] = userID[0]
|
|
||||||
trans2SessionSetupRequest[33] = userID[1]
|
|
||||||
|
|
||||||
if _, err = conn.Write(trans2SessionSetupRequest); err != nil {
|
|
||||||
return fmt.Errorf("发送后门检测请求错误: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取后门检测响应错误: %v", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("后门检测响应不完整")
|
|
||||||
}
|
|
||||||
|
|
||||||
if reply[34] == 0x51 {
|
|
||||||
common.LogSuccess(fmt.Sprintf("发现后门 %s DOUBLEPULSAR", ip))
|
|
||||||
|
|
||||||
// 保存 DOUBLEPULSAR 后门结果
|
|
||||||
backdoorResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: ip,
|
|
||||||
Status: "backdoor",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": "445",
|
|
||||||
"type": "DOUBLEPULSAR",
|
|
||||||
"os": os,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(backdoorResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shellcode 利用部分保持不变
|
|
||||||
if common.Shellcode != "" {
|
|
||||||
defer MS17010EXP(info)
|
|
||||||
}
|
|
||||||
} else if os != "" {
|
|
||||||
common.LogBase(fmt.Sprintf("系统信息 %s [%s]", ip, os))
|
|
||||||
|
|
||||||
// 保存系统信息
|
|
||||||
sysResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeService,
|
|
||||||
Target: ip,
|
|
||||||
Status: "identified",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": "445",
|
|
||||||
"service": "smb",
|
|
||||||
"os": os,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(sysResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
333
Plugins/MSSQL.go
333
Plugins/MSSQL.go
@ -1,333 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
mssql "github.com/denisenkom/go-mssqldb"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MSSQLProxyDialer 自定义dialer结构体
|
|
||||||
type MSSQLProxyDialer struct {
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialContext 实现mssql.Dialer接口,支持socks代理
|
|
||||||
func (d *MSSQLProxyDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return common.WrapperTcpWithContext(ctx, network, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MssqlCredential 表示一个MSSQL凭据
|
|
||||||
type MssqlCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MssqlScanResult 表示MSSQL扫描结果
|
|
||||||
type MssqlScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential MssqlCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
// MssqlScan 执行MSSQL服务扫描
|
|
||||||
func MssqlScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []MssqlCredential
|
|
||||||
for _, user := range common.Userdict["mssql"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, MssqlCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["mssql"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentMssqlScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveMssqlResult(info, target, result.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("MSSQL扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentMssqlScan 并发扫描MSSQL服务
|
|
||||||
func concurrentMssqlScan(ctx context.Context, info *common.HostInfo, credentials []MssqlCredential, timeoutSeconds int64, maxRetries int) *MssqlScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *MssqlScanResult, 1)
|
|
||||||
workChan := make(chan MssqlCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryMssqlCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("MSSQL并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryMssqlCredential 尝试单个MSSQL凭据
|
|
||||||
func tryMssqlCredential(ctx context.Context, info *common.HostInfo, credential MssqlCredential, timeoutSeconds int64, maxRetries int) *MssqlScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &MssqlScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时的上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, err := MssqlConn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &MssqlScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MssqlScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MssqlConn 尝试MSSQL连接
|
|
||||||
func MssqlConn(ctx context.Context, info *common.HostInfo, user string, pass string) (bool, error) {
|
|
||||||
host, port, username, password := info.Host, info.Ports, user, pass
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
|
|
||||||
// 构造连接字符串
|
|
||||||
connStr := fmt.Sprintf(
|
|
||||||
"server=%s;user id=%s;password=%s;port=%v;encrypt=disable;",
|
|
||||||
host, username, password, port,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 检查是否需要使用socks代理
|
|
||||||
if common.Socks5Proxy != "" {
|
|
||||||
// 使用自定义dialer创建连接器
|
|
||||||
connector, err := mssql.NewConnector(connStr)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置自定义dialer
|
|
||||||
connector.Dialer = &MSSQLProxyDialer{
|
|
||||||
timeout: time.Duration(common.Timeout) * time.Millisecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用连接器创建数据库连接
|
|
||||||
db := sql.OpenDB(connector)
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// 设置连接参数
|
|
||||||
db.SetConnMaxLifetime(timeout)
|
|
||||||
db.SetConnMaxIdleTime(timeout)
|
|
||||||
db.SetMaxIdleConns(0)
|
|
||||||
db.SetMaxOpenConns(1)
|
|
||||||
|
|
||||||
// 通过上下文执行ping操作,以支持超时控制
|
|
||||||
pingCtx, pingCancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer pingCancel()
|
|
||||||
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
errChan <- db.PingContext(pingCtx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待ping结果或者超时
|
|
||||||
select {
|
|
||||||
case err := <-errChan:
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
// 全局超时或取消
|
|
||||||
return false, ctx.Err()
|
|
||||||
case <-pingCtx.Done():
|
|
||||||
if pingCtx.Err() == context.DeadlineExceeded {
|
|
||||||
// 单个连接超时
|
|
||||||
return false, fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
return false, pingCtx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用标准连接方式
|
|
||||||
db, err := sql.Open("mssql", connStr)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// 设置连接参数
|
|
||||||
db.SetConnMaxLifetime(timeout)
|
|
||||||
db.SetConnMaxIdleTime(timeout)
|
|
||||||
db.SetMaxIdleConns(0)
|
|
||||||
db.SetMaxOpenConns(1)
|
|
||||||
|
|
||||||
// 通过上下文执行ping操作,以支持超时控制
|
|
||||||
pingCtx, pingCancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer pingCancel()
|
|
||||||
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
errChan <- db.PingContext(pingCtx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待ping结果或者超时
|
|
||||||
select {
|
|
||||||
case err := <-errChan:
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
// 全局超时或取消
|
|
||||||
return false, ctx.Err()
|
|
||||||
case <-pingCtx.Done():
|
|
||||||
if pingCtx.Err() == context.DeadlineExceeded {
|
|
||||||
// 单个连接超时
|
|
||||||
return false, fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
return false, pingCtx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveMssqlResult 保存MSSQL扫描结果
|
|
||||||
func saveMssqlResult(info *common.HostInfo, target string, credential MssqlCredential) {
|
|
||||||
successMsg := fmt.Sprintf("MSSQL %s %v %v", target, credential.Username, credential.Password)
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "mssql",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MemcachedScanResult 表示Memcached扫描结果
|
|
||||||
type MemcachedScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Stats string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemcachedScan 检测Memcached未授权访问
|
|
||||||
func MemcachedScan(info *common.HostInfo) error {
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 Memcached %s", realhost))
|
|
||||||
|
|
||||||
// 尝试连接并检查未授权访问
|
|
||||||
result := tryMemcachedConnection(ctx, info, common.Timeout)
|
|
||||||
|
|
||||||
if result.Success {
|
|
||||||
// 保存成功结果
|
|
||||||
scanResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "memcached",
|
|
||||||
"type": "unauthorized-access",
|
|
||||||
"description": "Memcached unauthorized access",
|
|
||||||
"stats": result.Stats,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(scanResult)
|
|
||||||
common.LogSuccess(fmt.Sprintf("Memcached %s 未授权访问", realhost))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
common.LogDebug("Memcached扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("Memcached扫描完成: %s", realhost))
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryMemcachedConnection 尝试连接Memcached并检查未授权访问
|
|
||||||
func tryMemcachedConnection(ctx context.Context, info *common.HostInfo, timeoutSeconds int64) *MemcachedScanResult {
|
|
||||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
|
||||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan *MemcachedScanResult, 1)
|
|
||||||
|
|
||||||
// 创建连接上下文,带超时
|
|
||||||
connCtx, connCancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer connCancel()
|
|
||||||
|
|
||||||
// 在协程中尝试连接
|
|
||||||
go func() {
|
|
||||||
// 构建结果结构
|
|
||||||
result := &MemcachedScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: nil,
|
|
||||||
Stats: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 建立TCP连接
|
|
||||||
client, err := common.WrapperTcpWithTimeout("tcp", realhost, timeout)
|
|
||||||
if err != nil {
|
|
||||||
result.Error = err
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- result:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
// 设置操作截止时间
|
|
||||||
if err := client.SetDeadline(time.Now().Add(timeout)); err != nil {
|
|
||||||
result.Error = err
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- result:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送stats命令
|
|
||||||
if _, err := client.Write([]byte("stats\n")); err != nil {
|
|
||||||
result.Error = err
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- result:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
rev := make([]byte, 1024)
|
|
||||||
n, err := client.Read(rev)
|
|
||||||
if err != nil {
|
|
||||||
result.Error = err
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- result:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查响应是否包含统计信息
|
|
||||||
response := string(rev[:n])
|
|
||||||
if strings.Contains(response, "STAT") {
|
|
||||||
result.Success = true
|
|
||||||
result.Stats = response
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送结果
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- result:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result
|
|
||||||
case <-connCtx.Done():
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
// 全局上下文取消
|
|
||||||
return &MemcachedScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 连接超时
|
|
||||||
return &MemcachedScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("连接超时"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,274 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ModbusScanResult 表示 Modbus 扫描结果
|
|
||||||
type ModbusScanResult struct {
|
|
||||||
Success bool
|
|
||||||
DeviceInfo string
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModbusScan 执行 Modbus 服务扫描
|
|
||||||
func ModbusScan(info *common.HostInfo) error {
|
|
||||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始 Modbus 扫描: %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 执行扫描
|
|
||||||
result := tryModbusScan(ctx, info, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if result.Success {
|
|
||||||
// 保存扫描结果
|
|
||||||
saveModbusResult(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Modbus 扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
if result.Error != nil {
|
|
||||||
common.LogDebug(fmt.Sprintf("Modbus 扫描失败: %v", result.Error))
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
common.LogDebug("Modbus 扫描完成,未发现服务")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryModbusScan 尝试单个 Modbus 扫描
|
|
||||||
func tryModbusScan(ctx context.Context, info *common.HostInfo, timeoutSeconds int64, maxRetries int) *ModbusScanResult {
|
|
||||||
var lastErr error
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
target := fmt.Sprintf("%s:%s", host, port)
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &ModbusScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试 Modbus 扫描: %s", retry+1, target))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建单个连接超时的上下文
|
|
||||||
connCtx, connCancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan *ModbusScanResult, 1)
|
|
||||||
|
|
||||||
// 在协程中执行扫描
|
|
||||||
go func() {
|
|
||||||
// 尝试建立连接
|
|
||||||
conn, err := common.WrapperTcpWithContext(connCtx, "tcp", target)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- &ModbusScanResult{Success: false, Error: err}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 构造 Modbus TCP 请求包 - 读取设备ID
|
|
||||||
request := buildModbusRequest()
|
|
||||||
|
|
||||||
// 设置读写超时
|
|
||||||
conn.SetDeadline(time.Now().Add(time.Duration(timeoutSeconds) * time.Second))
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
_, err = conn.Write(request)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- &ModbusScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("发送Modbus请求失败: %v", err),
|
|
||||||
}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
response := make([]byte, 256)
|
|
||||||
n, err := conn.Read(response)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- &ModbusScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("读取Modbus响应失败: %v", err),
|
|
||||||
}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证响应
|
|
||||||
if isValidModbusResponse(response[:n]) {
|
|
||||||
// 获取设备信息
|
|
||||||
deviceInfo := parseModbusResponse(response[:n])
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- &ModbusScanResult{
|
|
||||||
Success: true,
|
|
||||||
DeviceInfo: deviceInfo,
|
|
||||||
}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- &ModbusScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("非Modbus服务或访问被拒绝"),
|
|
||||||
}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待扫描结果或超时
|
|
||||||
var result *ModbusScanResult
|
|
||||||
select {
|
|
||||||
case res := <-resultChan:
|
|
||||||
result = res
|
|
||||||
case <-connCtx.Done():
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
connCancel()
|
|
||||||
return &ModbusScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = &ModbusScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("连接超时"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connCancel()
|
|
||||||
|
|
||||||
if result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = result.Error
|
|
||||||
if result.Error != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(result.Error); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ModbusScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildModbusRequest 构建Modbus TCP请求包
|
|
||||||
func buildModbusRequest() []byte {
|
|
||||||
request := make([]byte, 12)
|
|
||||||
|
|
||||||
// Modbus TCP头部
|
|
||||||
binary.BigEndian.PutUint16(request[0:], 0x0001) // 事务标识符
|
|
||||||
binary.BigEndian.PutUint16(request[2:], 0x0000) // 协议标识符
|
|
||||||
binary.BigEndian.PutUint16(request[4:], 0x0006) // 长度
|
|
||||||
request[6] = 0x01 // 单元标识符
|
|
||||||
|
|
||||||
// Modbus 请求
|
|
||||||
request[7] = 0x01 // 功能码: Read Coils
|
|
||||||
binary.BigEndian.PutUint16(request[8:], 0x0000) // 起始地址
|
|
||||||
binary.BigEndian.PutUint16(request[10:], 0x0001) // 读取数量
|
|
||||||
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidModbusResponse 验证Modbus响应是否有效
|
|
||||||
func isValidModbusResponse(response []byte) bool {
|
|
||||||
if len(response) < 9 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查协议标识符
|
|
||||||
protocolID := binary.BigEndian.Uint16(response[2:])
|
|
||||||
if protocolID != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查功能码
|
|
||||||
funcCode := response[7]
|
|
||||||
if funcCode == 0x81 { // 错误响应
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseModbusResponse 解析Modbus响应获取设备信息
|
|
||||||
func parseModbusResponse(response []byte) string {
|
|
||||||
if len(response) < 9 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取更多设备信息
|
|
||||||
unitID := response[6]
|
|
||||||
funcCode := response[7]
|
|
||||||
|
|
||||||
// 简单的设备信息提取,实际应用中可以提取更多信息
|
|
||||||
info := fmt.Sprintf("Unit ID: %d, Function: 0x%02X", unitID, funcCode)
|
|
||||||
|
|
||||||
// 如果是读取线圈响应,尝试解析线圈状态
|
|
||||||
if funcCode == 0x01 && len(response) >= 10 {
|
|
||||||
byteCount := response[8]
|
|
||||||
if byteCount > 0 && len(response) >= 9+int(byteCount) {
|
|
||||||
coilValue := response[9] & 0x01 // 获取第一个线圈状态
|
|
||||||
info += fmt.Sprintf(", Coil Status: %d", coilValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveModbusResult 保存Modbus扫描结果
|
|
||||||
func saveModbusResult(info *common.HostInfo, target string, result *ModbusScanResult) {
|
|
||||||
// 保存扫描结果
|
|
||||||
scanResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "modbus",
|
|
||||||
"type": "unauthorized-access",
|
|
||||||
"device_info": result.DeviceInfo,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(scanResult)
|
|
||||||
|
|
||||||
// 控制台输出
|
|
||||||
common.LogSuccess(fmt.Sprintf("Modbus服务 %s 无认证访问", target))
|
|
||||||
if result.DeviceInfo != "" {
|
|
||||||
common.LogSuccess(fmt.Sprintf("设备信息: %s", result.DeviceInfo))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,191 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MongodbScan 执行MongoDB未授权扫描
|
|
||||||
func MongodbScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始MongoDB扫描: %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
isUnauth bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中执行扫描
|
|
||||||
go func() {
|
|
||||||
isUnauth, err := MongodbUnauth(ctx, info)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
isUnauth bool
|
|
||||||
err error
|
|
||||||
}{isUnauth, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或超时
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
if result.err != nil {
|
|
||||||
errlog := fmt.Sprintf("MongoDB %v %v", target, result.err)
|
|
||||||
common.LogError(errlog)
|
|
||||||
return result.err
|
|
||||||
} else if result.isUnauth {
|
|
||||||
// 记录控制台输出
|
|
||||||
common.LogSuccess(fmt.Sprintf("MongoDB %v 未授权访问", target))
|
|
||||||
|
|
||||||
// 保存未授权访问结果
|
|
||||||
scanResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "mongodb",
|
|
||||||
"type": "unauthorized-access",
|
|
||||||
"protocol": "mongodb",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(scanResult)
|
|
||||||
} else {
|
|
||||||
common.LogDebug(fmt.Sprintf("MongoDB %v 需要认证", target))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogError(fmt.Sprintf("MongoDB扫描超时: %s", target))
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MongodbUnauth 检测MongoDB未授权访问
|
|
||||||
func MongodbUnauth(ctx context.Context, info *common.HostInfo) (bool, error) {
|
|
||||||
msgPacket := createOpMsgPacket()
|
|
||||||
queryPacket := createOpQueryPacket()
|
|
||||||
|
|
||||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("检测MongoDB未授权访问: %s", realhost))
|
|
||||||
|
|
||||||
// 尝试OP_MSG查询
|
|
||||||
common.LogDebug("尝试使用OP_MSG协议")
|
|
||||||
reply, err := checkMongoAuth(ctx, realhost, msgPacket)
|
|
||||||
if err != nil {
|
|
||||||
common.LogDebug(fmt.Sprintf("OP_MSG查询失败: %v, 尝试使用OP_QUERY协议", err))
|
|
||||||
// 失败则尝试OP_QUERY查询
|
|
||||||
reply, err = checkMongoAuth(ctx, realhost, queryPacket)
|
|
||||||
if err != nil {
|
|
||||||
common.LogDebug(fmt.Sprintf("OP_QUERY查询也失败: %v", err))
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查响应结果
|
|
||||||
common.LogDebug(fmt.Sprintf("收到响应,长度: %d", len(reply)))
|
|
||||||
if strings.Contains(reply, "totalLinesWritten") {
|
|
||||||
common.LogDebug("响应中包含totalLinesWritten,确认未授权访问")
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug("响应未包含预期内容,可能需要认证")
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkMongoAuth 检查MongoDB认证状态
|
|
||||||
func checkMongoAuth(ctx context.Context, address string, packet []byte) (string, error) {
|
|
||||||
common.LogDebug(fmt.Sprintf("建立MongoDB连接: %s", address))
|
|
||||||
|
|
||||||
// 使用带超时的连接
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", address, time.Duration(common.Timeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("连接失败: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 检查上下文是否已取消
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return "", ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置读写超时
|
|
||||||
if err := conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)); err != nil {
|
|
||||||
return "", fmt.Errorf("设置超时失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送查询包
|
|
||||||
common.LogDebug("发送查询包")
|
|
||||||
if _, err := conn.Write(packet); err != nil {
|
|
||||||
return "", fmt.Errorf("发送查询失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 再次检查上下文是否已取消
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return "", ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
common.LogDebug("读取响应")
|
|
||||||
reply := make([]byte, 2048)
|
|
||||||
count, err := conn.Read(reply)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return "", fmt.Errorf("读取响应失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
return "", fmt.Errorf("收到空响应")
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("成功接收响应,字节数: %d", count))
|
|
||||||
return string(reply[:count]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createOpMsgPacket 创建OP_MSG查询包
|
|
||||||
func createOpMsgPacket() []byte {
|
|
||||||
return []byte{
|
|
||||||
0x69, 0x00, 0x00, 0x00, // messageLength
|
|
||||||
0x39, 0x00, 0x00, 0x00, // requestID
|
|
||||||
0x00, 0x00, 0x00, 0x00, // responseTo
|
|
||||||
0xdd, 0x07, 0x00, 0x00, // opCode OP_MSG
|
|
||||||
0x00, 0x00, 0x00, 0x00, // flagBits
|
|
||||||
// sections db.adminCommand({getLog: "startupWarnings"})
|
|
||||||
0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64, 0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69, 0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69, 0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84, 0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// createOpQueryPacket 创建OP_QUERY查询包
|
|
||||||
func createOpQueryPacket() []byte {
|
|
||||||
return []byte{
|
|
||||||
0x48, 0x00, 0x00, 0x00, // messageLength
|
|
||||||
0x02, 0x00, 0x00, 0x00, // requestID
|
|
||||||
0x00, 0x00, 0x00, 0x00, // responseTo
|
|
||||||
0xd4, 0x07, 0x00, 0x00, // opCode OP_QUERY
|
|
||||||
0x00, 0x00, 0x00, 0x00, // flags
|
|
||||||
0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x24, 0x63, 0x6d, 0x64, 0x00, // fullCollectionName admin.$cmd
|
|
||||||
0x00, 0x00, 0x00, 0x00, // numberToSkip
|
|
||||||
0x01, 0x00, 0x00, 0x00, // numberToReturn
|
|
||||||
// query db.adminCommand({getLog: "startupWarnings"})
|
|
||||||
0x21, 0x00, 0x00, 0x00, 0x2, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MysqlScan 执行MySQL服务扫描
|
|
||||||
// 现在完全使用新的插件架构
|
|
||||||
func MysqlScan(info *common.HostInfo) error {
|
|
||||||
// 使用新的插件架构
|
|
||||||
if adapter.TryNewArchitecture("mysql", info) {
|
|
||||||
return nil // 新架构处理成功
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果新架构不支持,记录错误(理论上不应该发生)
|
|
||||||
common.LogError("MySQL插件新架构不可用,请检查插件注册")
|
|
||||||
return nil
|
|
||||||
}
|
|
361
Plugins/Neo4j.go
361
Plugins/Neo4j.go
@ -1,361 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Neo4jCredential 表示一个Neo4j凭据
|
|
||||||
type Neo4jCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neo4jScanResult 表示Neo4j扫描结果
|
|
||||||
type Neo4jScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential Neo4jCredential
|
|
||||||
IsUnauth bool
|
|
||||||
IsDefaultCreds bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func Neo4jScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 初始检查列表 - 无认证和默认凭证
|
|
||||||
initialCredentials := []Neo4jCredential{
|
|
||||||
{"", ""}, // 无认证
|
|
||||||
{"neo4j", "neo4j"}, // 默认凭证
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先检查无认证和默认凭证
|
|
||||||
common.LogDebug("尝试默认凭证...")
|
|
||||||
for _, credential := range initialCredentials {
|
|
||||||
common.LogDebug(fmt.Sprintf("尝试: %s:%s", credential.Username, credential.Password))
|
|
||||||
|
|
||||||
result := tryNeo4jCredential(ctx, info, credential, common.Timeout, 1)
|
|
||||||
if result.Success {
|
|
||||||
// 标记结果类型
|
|
||||||
if credential.Username == "" && credential.Password == "" {
|
|
||||||
result.IsUnauth = true
|
|
||||||
} else {
|
|
||||||
result.IsDefaultCreds = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
saveNeo4jResult(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []Neo4jCredential
|
|
||||||
for _, user := range common.Userdict["neo4j"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, Neo4jCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["neo4j"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentNeo4jScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveNeo4jResult(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Neo4j扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+len(initialCredentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentNeo4jScan 并发扫描Neo4j服务
|
|
||||||
func concurrentNeo4jScan(ctx context.Context, info *common.HostInfo, credentials []Neo4jCredential, timeoutSeconds int64, maxRetries int) *Neo4jScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *Neo4jScanResult, 1)
|
|
||||||
workChan := make(chan Neo4jCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryNeo4jCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Neo4j并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryNeo4jCredential 尝试单个Neo4j凭据
|
|
||||||
func tryNeo4jCredential(ctx context.Context, info *common.HostInfo, credential Neo4jCredential, timeoutSeconds int64, maxRetries int) *Neo4jScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &Neo4jScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中尝试连接
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
go func() {
|
|
||||||
defer cancel()
|
|
||||||
success, err := Neo4jConn(info, credential.Username, credential.Password)
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{success, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或超时
|
|
||||||
var success bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
success = result.success
|
|
||||||
err = result.err
|
|
||||||
case <-connCtx.Done():
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
// 全局超时
|
|
||||||
return &Neo4jScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 单个连接超时
|
|
||||||
err = fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &Neo4jScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Neo4jScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neo4jConn 尝试Neo4j连接
|
|
||||||
func Neo4jConn(info *common.HostInfo, user string, pass string) (bool, error) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
|
|
||||||
// 构造Neo4j URL
|
|
||||||
uri := fmt.Sprintf("bolt://%s:%s", host, port)
|
|
||||||
|
|
||||||
// 配置驱动选项
|
|
||||||
config := func(c *neo4j.Config) {
|
|
||||||
c.SocketConnectTimeout = timeout
|
|
||||||
c.ConnectionAcquisitionTimeout = timeout
|
|
||||||
|
|
||||||
// 注意:Neo4j驱动可能不支持代理配置
|
|
||||||
// 如果需要代理支持,可能需要使用更底层的连接方式
|
|
||||||
}
|
|
||||||
|
|
||||||
var driver neo4j.Driver
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// 尝试建立连接
|
|
||||||
if user != "" || pass != "" {
|
|
||||||
// 有认证信息时使用认证
|
|
||||||
driver, err = neo4j.NewDriver(uri, neo4j.BasicAuth(user, pass, ""), config)
|
|
||||||
} else {
|
|
||||||
// 无认证时使用NoAuth
|
|
||||||
driver, err = neo4j.NewDriver(uri, neo4j.NoAuth(), config)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer driver.Close()
|
|
||||||
|
|
||||||
// 测试连接有效性
|
|
||||||
err = driver.VerifyConnectivity()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试执行简单查询以确认权限
|
|
||||||
session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead})
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
_, err = session.Run("MATCH (n) RETURN count(n) LIMIT 1", nil)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveNeo4jResult 保存Neo4j扫描结果
|
|
||||||
func saveNeo4jResult(info *common.HostInfo, target string, result *Neo4jScanResult) {
|
|
||||||
var successMsg string
|
|
||||||
var details map[string]interface{}
|
|
||||||
|
|
||||||
if result.IsUnauth {
|
|
||||||
// 无认证访问
|
|
||||||
successMsg = fmt.Sprintf("Neo4j服务 %s 无需认证即可访问", target)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "neo4j",
|
|
||||||
"type": "unauthorized-access",
|
|
||||||
}
|
|
||||||
} else if result.IsDefaultCreds {
|
|
||||||
// 默认凭证
|
|
||||||
successMsg = fmt.Sprintf("Neo4j服务 %s 默认凭证可用 用户名: %s 密码: %s",
|
|
||||||
target, result.Credential.Username, result.Credential.Password)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "neo4j",
|
|
||||||
"type": "default-credentials",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 弱密码
|
|
||||||
successMsg = fmt.Sprintf("Neo4j服务 %s 爆破成功 用户名: %s 密码: %s",
|
|
||||||
target, result.Credential.Username, result.Credential.Password)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "neo4j",
|
|
||||||
"type": "weak-password",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
@ -1,400 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errNetBIOS = errors.New("netbios error")
|
|
||||||
|
|
||||||
func NetBIOS(info *common.HostInfo) error {
|
|
||||||
netbios, _ := NetBIOS1(info)
|
|
||||||
output := netbios.String()
|
|
||||||
if len(output) > 0 {
|
|
||||||
result := fmt.Sprintf("NetBios %-15s %s", info.Host, output)
|
|
||||||
common.LogSuccess(result)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
details := map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加有效的 NetBIOS 信息
|
|
||||||
if netbios.ComputerName != "" {
|
|
||||||
details["computer_name"] = netbios.ComputerName
|
|
||||||
}
|
|
||||||
if netbios.DomainName != "" {
|
|
||||||
details["domain_name"] = netbios.DomainName
|
|
||||||
}
|
|
||||||
if netbios.NetDomainName != "" {
|
|
||||||
details["netbios_domain"] = netbios.NetDomainName
|
|
||||||
}
|
|
||||||
if netbios.NetComputerName != "" {
|
|
||||||
details["netbios_computer"] = netbios.NetComputerName
|
|
||||||
}
|
|
||||||
if netbios.WorkstationService != "" {
|
|
||||||
details["workstation_service"] = netbios.WorkstationService
|
|
||||||
}
|
|
||||||
if netbios.ServerService != "" {
|
|
||||||
details["server_service"] = netbios.ServerService
|
|
||||||
}
|
|
||||||
if netbios.DomainControllers != "" {
|
|
||||||
details["domain_controllers"] = netbios.DomainControllers
|
|
||||||
}
|
|
||||||
if netbios.OsVersion != "" {
|
|
||||||
details["os_version"] = netbios.OsVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetBIOS信息已通过上面的LogSuccess记录,不需要额外保存结果
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errNetBIOS
|
|
||||||
}
|
|
||||||
|
|
||||||
func NetBIOS1(info *common.HostInfo) (netbios NetBiosInfo, err error) {
|
|
||||||
netbios, err = GetNbnsname(info)
|
|
||||||
var payload0 []byte
|
|
||||||
if netbios.ServerService != "" || netbios.WorkstationService != "" {
|
|
||||||
ss := netbios.ServerService
|
|
||||||
if ss == "" {
|
|
||||||
ss = netbios.WorkstationService
|
|
||||||
}
|
|
||||||
name := netbiosEncode(ss)
|
|
||||||
payload0 = append(payload0, []byte("\x81\x00\x00D ")...)
|
|
||||||
payload0 = append(payload0, name...)
|
|
||||||
payload0 = append(payload0, []byte("\x00 EOENEBFACACACACACACACACACACACACA\x00")...)
|
|
||||||
}
|
|
||||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
|
||||||
var conn net.Conn
|
|
||||||
conn, err = common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Ports == "139" && len(payload0) > 0 {
|
|
||||||
_, err1 := conn.Write(payload0)
|
|
||||||
if err1 != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err1 = ReadBytes(conn)
|
|
||||||
if err1 != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.Write(NegotiateSMBv1Data1)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = ReadBytes(conn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.Write(NegotiateSMBv1Data2)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var ret []byte
|
|
||||||
ret, err = ReadBytes(conn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
netbios2, err := ParseNTLM(ret)
|
|
||||||
JoinNetBios(&netbios, &netbios2)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetNbnsname(info *common.HostInfo) (netbios NetBiosInfo, err error) {
|
|
||||||
senddata1 := []byte{102, 102, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 32, 67, 75, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 0, 0, 33, 0, 1}
|
|
||||||
//senddata1 := []byte("ff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01")
|
|
||||||
realhost := fmt.Sprintf("%s:137", info.Host)
|
|
||||||
conn, err := net.DialTimeout("udp", realhost, time.Duration(common.Timeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = conn.Write(senddata1)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
text, _ := ReadBytes(conn)
|
|
||||||
netbios, err = ParseNetBios(text)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytetoint(text byte) (int, error) {
|
|
||||||
num1 := fmt.Sprintf("%v", text)
|
|
||||||
num, err := strconv.Atoi(num1)
|
|
||||||
return num, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func netbiosEncode(name string) (output []byte) {
|
|
||||||
var names []int
|
|
||||||
src := fmt.Sprintf("%-16s", name)
|
|
||||||
for _, a := range src {
|
|
||||||
char_ord := int(a)
|
|
||||||
high_4_bits := char_ord >> 4
|
|
||||||
low_4_bits := char_ord & 0x0f
|
|
||||||
names = append(names, high_4_bits, low_4_bits)
|
|
||||||
}
|
|
||||||
for _, one := range names {
|
|
||||||
out := (one + 0x41)
|
|
||||||
output = append(output, byte(out))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
UNIQUE_NAMES = map[string]string{
|
|
||||||
"\x00": "WorkstationService",
|
|
||||||
"\x03": "Messenger Service",
|
|
||||||
"\x06": "RAS Server Service",
|
|
||||||
"\x1F": "NetDDE Service",
|
|
||||||
"\x20": "ServerService",
|
|
||||||
"\x21": "RAS Client Service",
|
|
||||||
"\xBE": "Network Monitor Agent",
|
|
||||||
"\xBF": "Network Monitor Application",
|
|
||||||
"\x1D": "Master Browser",
|
|
||||||
"\x1B": "Domain Master Browser",
|
|
||||||
}
|
|
||||||
|
|
||||||
GROUP_NAMES = map[string]string{
|
|
||||||
"\x00": "DomainName",
|
|
||||||
"\x1C": "DomainControllers",
|
|
||||||
"\x1E": "Browser Service Elections",
|
|
||||||
}
|
|
||||||
|
|
||||||
NetBIOS_ITEM_TYPE = map[string]string{
|
|
||||||
"\x01\x00": "NetBiosComputerName",
|
|
||||||
"\x02\x00": "NetBiosDomainName",
|
|
||||||
"\x03\x00": "ComputerName",
|
|
||||||
"\x04\x00": "DomainName",
|
|
||||||
"\x05\x00": "DNS tree name",
|
|
||||||
"\x07\x00": "Time stamp",
|
|
||||||
}
|
|
||||||
NegotiateSMBv1Data1 = []byte{
|
|
||||||
0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F,
|
|
||||||
0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02,
|
|
||||||
0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F,
|
|
||||||
0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70,
|
|
||||||
0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30,
|
|
||||||
0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54,
|
|
||||||
0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00,
|
|
||||||
}
|
|
||||||
NegotiateSMBv1Data2 = []byte{
|
|
||||||
0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
|
||||||
0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60,
|
|
||||||
0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30,
|
|
||||||
0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04,
|
|
||||||
0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08,
|
|
||||||
0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00,
|
|
||||||
0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00,
|
|
||||||
0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00,
|
|
||||||
0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
|
||||||
0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
|
|
||||||
0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00,
|
|
||||||
0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
|
||||||
0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00,
|
|
||||||
0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type NetBiosInfo struct {
|
|
||||||
GroupName string
|
|
||||||
WorkstationService string `yaml:"WorkstationService"`
|
|
||||||
ServerService string `yaml:"ServerService"`
|
|
||||||
DomainName string `yaml:"DomainName"`
|
|
||||||
DomainControllers string `yaml:"DomainControllers"`
|
|
||||||
ComputerName string `yaml:"ComputerName"`
|
|
||||||
OsVersion string `yaml:"OsVersion"`
|
|
||||||
NetDomainName string `yaml:"NetBiosDomainName"`
|
|
||||||
NetComputerName string `yaml:"NetBiosComputerName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info *NetBiosInfo) String() (output string) {
|
|
||||||
var text string
|
|
||||||
//ComputerName 信息比较全
|
|
||||||
if info.ComputerName != "" {
|
|
||||||
if !strings.Contains(info.ComputerName, ".") && info.GroupName != "" {
|
|
||||||
text = fmt.Sprintf("%s\\%s", info.GroupName, info.ComputerName)
|
|
||||||
} else {
|
|
||||||
text = info.ComputerName
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//组信息
|
|
||||||
if info.DomainName != "" {
|
|
||||||
text += info.DomainName
|
|
||||||
text += "\\"
|
|
||||||
} else if info.NetDomainName != "" {
|
|
||||||
text += info.NetDomainName
|
|
||||||
text += "\\"
|
|
||||||
}
|
|
||||||
//机器名
|
|
||||||
if info.ServerService != "" {
|
|
||||||
text += info.ServerService
|
|
||||||
} else if info.WorkstationService != "" {
|
|
||||||
text += info.WorkstationService
|
|
||||||
} else if info.NetComputerName != "" {
|
|
||||||
text += info.NetComputerName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if text == "" {
|
|
||||||
} else if info.DomainControllers != "" {
|
|
||||||
output = fmt.Sprintf("DC:%-24s", text)
|
|
||||||
} else {
|
|
||||||
output = fmt.Sprintf("%-30s", text)
|
|
||||||
}
|
|
||||||
if info.OsVersion != "" {
|
|
||||||
output += " " + info.OsVersion
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseNetBios(input []byte) (netbios NetBiosInfo, err error) {
|
|
||||||
if len(input) < 57 {
|
|
||||||
err = errNetBIOS
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data := input[57:]
|
|
||||||
var num int
|
|
||||||
num, err = bytetoint(input[56:57][0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var msg string
|
|
||||||
for i := 0; i < num; i++ {
|
|
||||||
if len(data) < 18*i+16 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
name := string(data[18*i : 18*i+15])
|
|
||||||
flag_bit := data[18*i+15 : 18*i+16]
|
|
||||||
if GROUP_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
|
||||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
|
||||||
} else if UNIQUE_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
|
||||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
|
||||||
} else if string(flag_bit) == "\x00" || len(data) >= 18*i+18 {
|
|
||||||
name_flags := data[18*i+16 : 18*i+18][0]
|
|
||||||
if name_flags >= 128 {
|
|
||||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
|
||||||
} else {
|
|
||||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
msg += fmt.Sprintf("%s \n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(msg) == 0 {
|
|
||||||
err = errNetBIOS
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
|
||||||
if netbios.DomainName != "" {
|
|
||||||
netbios.GroupName = netbios.DomainName
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseNTLM(ret []byte) (netbios NetBiosInfo, err error) {
|
|
||||||
if len(ret) < 47 {
|
|
||||||
err = errNetBIOS
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var num1, num2 int
|
|
||||||
num1, err = bytetoint(ret[43:44][0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
num2, err = bytetoint(ret[44:45][0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
length := num1 + num2*256
|
|
||||||
if len(ret) < 48+length {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
os_version := ret[47+length:]
|
|
||||||
tmp1 := bytes.ReplaceAll(os_version, []byte{0x00, 0x00}, []byte{124})
|
|
||||||
tmp1 = bytes.ReplaceAll(tmp1, []byte{0x00}, []byte{})
|
|
||||||
ostext := string(tmp1[:len(tmp1)-1])
|
|
||||||
ss := strings.Split(ostext, "|")
|
|
||||||
netbios.OsVersion = ss[0]
|
|
||||||
start := bytes.Index(ret, []byte("NTLMSSP"))
|
|
||||||
if len(ret) < start+45 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
num1, err = bytetoint(ret[start+40 : start+41][0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
num2, err = bytetoint(ret[start+41 : start+42][0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
length = num1 + num2*256
|
|
||||||
_, err = bytetoint(ret[start+44 : start+45][0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offset, err := bytetoint(ret[start+44 : start+45][0])
|
|
||||||
if err != nil || len(ret) < start+offset+length {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var msg string
|
|
||||||
index := start + offset
|
|
||||||
for index < start+offset+length {
|
|
||||||
item_type := ret[index : index+2]
|
|
||||||
num1, err = bytetoint(ret[index+2 : index+3][0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
num2, err = bytetoint(ret[index+3 : index+4][0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
item_length := num1 + num2*256
|
|
||||||
item_content := bytes.ReplaceAll(ret[index+4:index+4+item_length], []byte{0x00}, []byte{})
|
|
||||||
index += 4 + item_length
|
|
||||||
if string(item_type) == "\x07\x00" {
|
|
||||||
//Time stamp, 不需要输出
|
|
||||||
} else if NetBIOS_ITEM_TYPE[string(item_type)] != "" {
|
|
||||||
msg += fmt.Sprintf("%s: %s\n", NetBIOS_ITEM_TYPE[string(item_type)], string(item_content))
|
|
||||||
} else if string(item_type) == "\x00\x00" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func JoinNetBios(netbios1, netbios2 *NetBiosInfo) *NetBiosInfo {
|
|
||||||
netbios1.ComputerName = netbios2.ComputerName
|
|
||||||
netbios1.NetDomainName = netbios2.NetDomainName
|
|
||||||
netbios1.NetComputerName = netbios2.NetComputerName
|
|
||||||
if netbios2.DomainName != "" {
|
|
||||||
netbios1.DomainName = netbios2.DomainName
|
|
||||||
}
|
|
||||||
netbios1.OsVersion = netbios2.OsVersion
|
|
||||||
return netbios1
|
|
||||||
}
|
|
@ -1,436 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
_ "github.com/sijms/go-ora/v2"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OracleCredential 表示一个Oracle凭据
|
|
||||||
type OracleCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// OracleScanResult 表示Oracle扫描结果
|
|
||||||
type OracleScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential OracleCredential
|
|
||||||
ServiceName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 常见Oracle服务名列表
|
|
||||||
var commonServiceNames = []string{"XE", "ORCL", "ORCLPDB1", "XEPDB1", "PDBORCL"}
|
|
||||||
|
|
||||||
func OracleScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建常见高危凭据列表(优先测试)
|
|
||||||
highRiskCredentials := []OracleCredential{
|
|
||||||
{Username: "SYS", Password: "123456"},
|
|
||||||
{Username: "SYSTEM", Password: "123456"},
|
|
||||||
{Username: "SYS", Password: "oracle"},
|
|
||||||
{Username: "SYSTEM", Password: "oracle"},
|
|
||||||
{Username: "SYS", Password: "password"},
|
|
||||||
{Username: "SYSTEM", Password: "password"},
|
|
||||||
{Username: "SYS", Password: "sys123"},
|
|
||||||
{Username: "SYS", Password: "change_on_install"},
|
|
||||||
{Username: "SYSTEM", Password: "manager"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先尝试常见高危凭据
|
|
||||||
common.LogDebug("尝试常见高危凭据...")
|
|
||||||
for _, cred := range highRiskCredentials {
|
|
||||||
result := tryAllServiceNames(ctx, info, cred, common.Timeout, 1)
|
|
||||||
if result != nil && result.Success {
|
|
||||||
saveOracleResult(info, target, result.Credential, result.ServiceName)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建完整凭据列表
|
|
||||||
var credentials []OracleCredential
|
|
||||||
for _, user := range common.Userdict["oracle"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
// 转换用户名为大写,提高匹配率
|
|
||||||
credentials = append(credentials, OracleCredential{
|
|
||||||
Username: strings.ToUpper(user),
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["oracle"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentOracleScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveOracleResult(info, target, result.Credential, result.ServiceName)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Oracle扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+len(highRiskCredentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryAllServiceNames 尝试所有常见服务名
|
|
||||||
func tryAllServiceNames(ctx context.Context, info *common.HostInfo, credential OracleCredential, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
|
||||||
for _, serviceName := range commonServiceNames {
|
|
||||||
result := tryOracleCredential(ctx, info, credential, serviceName, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
result.ServiceName = serviceName
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对SYS用户尝试SYSDBA模式
|
|
||||||
if strings.ToUpper(credential.Username) == "SYS" {
|
|
||||||
result = tryOracleSysCredential(ctx, info, credential, serviceName, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
result.ServiceName = serviceName
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentOracleScan 并发扫描Oracle服务
|
|
||||||
func concurrentOracleScan(ctx context.Context, info *common.HostInfo, credentials []OracleCredential, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *OracleScanResult, 1)
|
|
||||||
workChan := make(chan OracleCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// 尝试所有常见服务名
|
|
||||||
result := tryAllServiceNames(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result != nil && result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Oracle并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryOracleCredential 尝试单个Oracle凭据
|
|
||||||
func tryOracleCredential(ctx context.Context, info *common.HostInfo, credential OracleCredential, serviceName string, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &OracleScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s@%s", retry+1, credential.Username, credential.Password, serviceName))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
|
|
||||||
// 在协程中执行数据库连接
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
success, err := OracleConn(connCtx, info, credential.Username, credential.Password, serviceName, false)
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
// 已超时或取消,不发送结果
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{success, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或连接超时
|
|
||||||
var success bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
success = result.success
|
|
||||||
err = result.err
|
|
||||||
case <-connCtx.Done():
|
|
||||||
err = connCtx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消连接超时上下文
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &OracleScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
ServiceName: serviceName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 如果是认证错误,不需要重试
|
|
||||||
if strings.Contains(err.Error(), "ORA-01017") {
|
|
||||||
break // 认证失败
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &OracleScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryOracleSysCredential 尝试SYS用户SYSDBA模式连接
|
|
||||||
func tryOracleSysCredential(ctx context.Context, info *common.HostInfo, credential OracleCredential, serviceName string, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &OracleScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试SYS用户SYSDBA模式: %s:%s@%s", retry+1, credential.Username, credential.Password, serviceName))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
|
|
||||||
// 在协程中执行数据库连接
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
success, err := OracleConn(connCtx, info, credential.Username, credential.Password, serviceName, true)
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
// 已超时或取消,不发送结果
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{success, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或连接超时
|
|
||||||
var success bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
success = result.success
|
|
||||||
err = result.err
|
|
||||||
case <-connCtx.Done():
|
|
||||||
err = connCtx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消连接超时上下文
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &OracleScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
ServiceName: serviceName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 如果是认证错误,不需要重试
|
|
||||||
if strings.Contains(err.Error(), "ORA-01017") {
|
|
||||||
break // 认证失败
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &OracleScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OracleConn 尝试Oracle连接
|
|
||||||
func OracleConn(ctx context.Context, info *common.HostInfo, user string, pass string, serviceName string, asSysdba bool) (bool, error) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
|
|
||||||
// 构造连接字符串,添加更多参数
|
|
||||||
connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/%s?connect_timeout=%d",
|
|
||||||
user, pass, host, port, serviceName, common.Timeout)
|
|
||||||
|
|
||||||
// 对SYS用户使用SYSDBA权限
|
|
||||||
if asSysdba {
|
|
||||||
connStr += "&sysdba=1"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 建立数据库连接
|
|
||||||
db, err := sql.Open("oracle", connStr)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// 设置连接参数
|
|
||||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
|
||||||
db.SetConnMaxIdleTime(time.Duration(common.Timeout) * time.Second)
|
|
||||||
db.SetMaxIdleConns(0)
|
|
||||||
db.SetMaxOpenConns(1)
|
|
||||||
|
|
||||||
// 使用上下文测试连接
|
|
||||||
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 测试连接
|
|
||||||
err = db.PingContext(pingCtx)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不需要额外的查询验证,连接成功即可
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveOracleResult 保存Oracle扫描结果
|
|
||||||
func saveOracleResult(info *common.HostInfo, target string, credential OracleCredential, serviceName string) {
|
|
||||||
var successMsg string
|
|
||||||
if strings.ToUpper(credential.Username) == "SYS" {
|
|
||||||
successMsg = fmt.Sprintf("Oracle %s 成功爆破 用户名: %v 密码: %v 服务名: %s (可能需要SYSDBA权限)",
|
|
||||||
target, credential.Username, credential.Password, serviceName)
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("Oracle %s 成功爆破 用户名: %v 密码: %v 服务名: %s",
|
|
||||||
target, credential.Username, credential.Password, serviceName)
|
|
||||||
}
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "oracle",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"service_name": serviceName,
|
|
||||||
"type": "weak-password",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
414
Plugins/POP3.go
414
Plugins/POP3.go
@ -1,414 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// POP3Credential 表示一个POP3凭据
|
|
||||||
type POP3Credential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// POP3ScanResult 表示POP3扫描结果
|
|
||||||
type POP3ScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential POP3Credential
|
|
||||||
IsTLS bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func POP3Scan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []POP3Credential
|
|
||||||
for _, user := range common.Userdict["pop3"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, POP3Credential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["pop3"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描,但需要限制速率
|
|
||||||
result := concurrentPOP3Scan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
savePOP3Result(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("POP3扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentPOP3Scan 并发扫描POP3服务(包含速率限制)
|
|
||||||
func concurrentPOP3Scan(ctx context.Context, info *common.HostInfo, credentials []POP3Credential, timeoutSeconds int64, maxRetries int) *POP3ScanResult {
|
|
||||||
// 不使用ModuleThreadNum控制并发数,必须单线程
|
|
||||||
maxConcurrent := 1
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 1 // POP3默认并发更低
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *POP3ScanResult, 1)
|
|
||||||
|
|
||||||
// 创建限速通道,控制请求频率
|
|
||||||
// 每次发送前需要从中获取令牌,确保请求间隔
|
|
||||||
rateLimiter := make(chan struct{}, maxConcurrent)
|
|
||||||
|
|
||||||
// 初始填充令牌
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
rateLimiter <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用动态的请求间隔
|
|
||||||
requestInterval := 1500 * time.Millisecond // 默认间隔1.5秒
|
|
||||||
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 创建任务队列
|
|
||||||
taskQueue := make(chan POP3Credential, len(credentials))
|
|
||||||
for _, cred := range credentials {
|
|
||||||
taskQueue <- cred
|
|
||||||
}
|
|
||||||
close(taskQueue)
|
|
||||||
|
|
||||||
// 记录已处理的凭据数
|
|
||||||
var processedCount int32
|
|
||||||
processedCountMutex := &sync.Mutex{}
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(workerID int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for credential := range taskQueue {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
case <-rateLimiter:
|
|
||||||
// 获取令牌,可以发送请求
|
|
||||||
processedCountMutex.Lock()
|
|
||||||
processedCount++
|
|
||||||
currentCount := processedCount
|
|
||||||
processedCountMutex.Unlock()
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 工作线程 %d 尝试: %s:%s",
|
|
||||||
currentCount, len(credentials), workerID, credential.Username, credential.Password))
|
|
||||||
|
|
||||||
result := tryPOP3Credential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
|
|
||||||
// 尝试完成后添加延迟,然后归还令牌
|
|
||||||
time.Sleep(requestInterval)
|
|
||||||
|
|
||||||
// 未被取消的情况下归还令牌
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
// 如果已经取消,不再归还令牌
|
|
||||||
default:
|
|
||||||
rateLimiter <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("POP3并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryPOP3Credential 尝试单个POP3凭据
|
|
||||||
func tryPOP3Credential(ctx context.Context, info *common.HostInfo, credential POP3Credential, timeoutSeconds int64, maxRetries int) *POP3ScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &POP3ScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
// 重试间隔时间增加,避免触发服务器限制
|
|
||||||
retryDelay := time.Duration(retry*2000) * time.Millisecond
|
|
||||||
time.Sleep(retryDelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, isTLS, err := POP3Conn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &POP3ScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
IsTLS: isTLS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 处理特定错误情况
|
|
||||||
if strings.Contains(strings.ToLower(err.Error()), "too many connections") ||
|
|
||||||
strings.Contains(strings.ToLower(err.Error()), "connection refused") ||
|
|
||||||
strings.Contains(strings.ToLower(err.Error()), "timeout") {
|
|
||||||
// 服务器可能限制连接,增加等待时间
|
|
||||||
waitTime := time.Duration((retry+1)*3000) * time.Millisecond
|
|
||||||
common.LogDebug(fmt.Sprintf("服务器可能限制连接,等待 %v 后重试", waitTime))
|
|
||||||
time.Sleep(waitTime)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &POP3ScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POP3Conn 尝试POP3连接
|
|
||||||
func POP3Conn(ctx context.Context, info *common.HostInfo, user string, pass string) (success bool, isTLS bool, err error) {
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
addr := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
isTLS bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中尝试连接,支持取消
|
|
||||||
go func() {
|
|
||||||
// 首先尝试普通连接
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
|
||||||
if err == nil {
|
|
||||||
flag, authErr := tryPOP3Auth(conn, user, pass, timeout)
|
|
||||||
conn.Close()
|
|
||||||
if authErr == nil && flag {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
isTLS bool
|
|
||||||
err error
|
|
||||||
}{flag, false, nil}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果普通连接失败,尝试TLS连接
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
// 对于TLS连接,暂时使用标准dialer
|
|
||||||
// TODO: 实现通过socks代理的TLS连接
|
|
||||||
tempDialer := &net.Dialer{Timeout: timeout}
|
|
||||||
tlsConn, tlsErr := tls.DialWithDialer(tempDialer, "tcp", addr, tlsConfig)
|
|
||||||
if tlsErr != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
isTLS bool
|
|
||||||
err error
|
|
||||||
}{false, false, fmt.Errorf("连接失败: %v", tlsErr)}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer tlsConn.Close()
|
|
||||||
|
|
||||||
flag, authErr := tryPOP3Auth(tlsConn, user, pass, timeout)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
isTLS bool
|
|
||||||
err error
|
|
||||||
}{flag, true, authErr}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result.success, result.isTLS, result.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, false, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryPOP3Auth 尝试POP3认证
|
|
||||||
func tryPOP3Auth(conn net.Conn, user string, pass string, timeout time.Duration) (bool, error) {
|
|
||||||
reader := bufio.NewReader(conn)
|
|
||||||
|
|
||||||
// 设置较长的超时时间以适应一些较慢的服务器
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
|
|
||||||
// 读取欢迎信息
|
|
||||||
response, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("读取欢迎消息失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有错误信息
|
|
||||||
if strings.Contains(strings.ToLower(response), "error") ||
|
|
||||||
strings.Contains(strings.ToLower(response), "too many") {
|
|
||||||
return false, fmt.Errorf("服务器拒绝连接: %s", strings.TrimSpace(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送用户名前等待一小段时间
|
|
||||||
time.Sleep(300 * time.Millisecond)
|
|
||||||
|
|
||||||
// 发送用户名
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
_, err = conn.Write([]byte(fmt.Sprintf("USER %s\r\n", user)))
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("发送用户名失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取用户名响应
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
response, err = reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("读取用户名响应失败: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(response, "+OK") {
|
|
||||||
return false, fmt.Errorf("用户名无效: %s", strings.TrimSpace(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送密码前等待一小段时间
|
|
||||||
time.Sleep(300 * time.Millisecond)
|
|
||||||
|
|
||||||
// 发送密码
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
_, err = conn.Write([]byte(fmt.Sprintf("PASS %s\r\n", pass)))
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("发送密码失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取密码响应
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
response, err = reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("读取密码响应失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(response, "+OK") {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, fmt.Errorf("认证失败: %s", strings.TrimSpace(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
// savePOP3Result 保存POP3扫描结果
|
|
||||||
func savePOP3Result(info *common.HostInfo, target string, result *POP3ScanResult) {
|
|
||||||
tlsStatus := ""
|
|
||||||
if result.IsTLS {
|
|
||||||
tlsStatus = " (TLS)"
|
|
||||||
}
|
|
||||||
|
|
||||||
successMsg := fmt.Sprintf("POP3服务 %s 用户名: %v 密码: %v%s",
|
|
||||||
target, result.Credential.Username, result.Credential.Password, tlsStatus)
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "pop3",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
"tls": result.IsTLS,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
@ -1,320 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"database/sql/driver"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lib/pq"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PostgresProxyDialer 自定义dialer结构体
|
|
||||||
type PostgresProxyDialer struct {
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial 实现pq.Dialer接口,支持socks代理
|
|
||||||
func (d *PostgresProxyDialer) Dial(network, address string) (net.Conn, error) {
|
|
||||||
return common.WrapperTcpWithTimeout(network, address, d.timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialTimeout 实现具有超时的连接
|
|
||||||
func (d *PostgresProxyDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
|
||||||
return common.WrapperTcpWithTimeout(network, address, timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostgresCredential 表示一个PostgreSQL凭据
|
|
||||||
type PostgresCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostgresScanResult 表示PostgreSQL扫描结果
|
|
||||||
type PostgresScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential PostgresCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostgresScan 执行PostgreSQL服务扫描
|
|
||||||
func PostgresScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []PostgresCredential
|
|
||||||
for _, user := range common.Userdict["postgresql"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, PostgresCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["postgresql"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentPostgresScan(ctx, info, credentials, common.Timeout+10, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
savePostgresResult(info, target, result.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("PostgreSQL扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentPostgresScan 并发扫描PostgreSQL服务
|
|
||||||
func concurrentPostgresScan(ctx context.Context, info *common.HostInfo, credentials []PostgresCredential, timeoutSeconds int64, maxRetries int) *PostgresScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *PostgresScanResult, 1)
|
|
||||||
workChan := make(chan PostgresCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryPostgresCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("PostgreSQL并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryPostgresCredential 尝试单个PostgreSQL凭据
|
|
||||||
func tryPostgresCredential(ctx context.Context, info *common.HostInfo, credential PostgresCredential, timeoutSeconds int64, maxRetries int) *PostgresScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &PostgresScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建单个连接超时的上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, err := PostgresConn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &PostgresScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PostgresScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostgresConn 尝试PostgreSQL连接
|
|
||||||
func PostgresConn(ctx context.Context, info *common.HostInfo, user string, pass string) (bool, error) {
|
|
||||||
// 构造连接字符串
|
|
||||||
connStr := fmt.Sprintf(
|
|
||||||
"postgres://%v:%v@%v:%v/postgres?sslmode=disable&connect_timeout=%d",
|
|
||||||
user, pass, info.Host, info.Ports, common.Timeout/1000, // 转换为秒
|
|
||||||
)
|
|
||||||
|
|
||||||
// 检查是否需要使用socks代理
|
|
||||||
if common.Socks5Proxy != "" {
|
|
||||||
// 使用自定义dialer通过socks代理连接
|
|
||||||
dialer := &PostgresProxyDialer{
|
|
||||||
timeout: time.Duration(common.Timeout) * time.Millisecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用pq.DialOpen通过自定义dialer建立连接
|
|
||||||
conn, err := pq.DialOpen(dialer, connStr)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 转换为sql.DB进行测试
|
|
||||||
db := sql.OpenDB(&postgresConnector{conn: conn})
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// 使用上下文测试连接
|
|
||||||
err = db.PingContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简单查询测试权限
|
|
||||||
var version string
|
|
||||||
err = db.QueryRowContext(ctx, "SELECT version()").Scan(&version)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用标准连接方式
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// 设置连接参数
|
|
||||||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Millisecond)
|
|
||||||
db.SetMaxOpenConns(1)
|
|
||||||
db.SetMaxIdleConns(0)
|
|
||||||
|
|
||||||
// 使用上下文测试连接
|
|
||||||
err = db.PingContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简单查询测试权限
|
|
||||||
var version string
|
|
||||||
err = db.QueryRowContext(ctx, "SELECT version()").Scan(&version)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// postgresConnector 封装driver.Conn为sql.driver.Connector
|
|
||||||
type postgresConnector struct {
|
|
||||||
conn driver.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *postgresConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
|
||||||
return c.conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *postgresConnector) Driver() driver.Driver {
|
|
||||||
return &pq.Driver{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// savePostgresResult 保存PostgreSQL扫描结果
|
|
||||||
func savePostgresResult(info *common.HostInfo, target string, credential PostgresCredential) {
|
|
||||||
successMsg := fmt.Sprintf("PostgreSQL服务 %s 成功爆破 用户名: %v 密码: %v",
|
|
||||||
target, credential.Username, credential.Password)
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "postgresql",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
401
Plugins/RDP.go
401
Plugins/RDP.go
@ -1,401 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"github.com/tomatome/grdp/core"
|
|
||||||
"github.com/tomatome/grdp/glog"
|
|
||||||
"github.com/tomatome/grdp/protocol/nla"
|
|
||||||
"github.com/tomatome/grdp/protocol/pdu"
|
|
||||||
"github.com/tomatome/grdp/protocol/rfb"
|
|
||||||
"github.com/tomatome/grdp/protocol/sec"
|
|
||||||
"github.com/tomatome/grdp/protocol/t125"
|
|
||||||
"github.com/tomatome/grdp/protocol/tpkt"
|
|
||||||
"github.com/tomatome/grdp/protocol/x224"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RDPCredential 表示一个RDP凭据
|
|
||||||
type RDPCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
// RDPScanResult 表示RDP扫描结果
|
|
||||||
type RDPScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential RDPCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
// RdpScan 执行RDP服务扫描
|
|
||||||
func RdpScan(info *common.HostInfo) error {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
common.LogError(fmt.Sprintf("RDP扫描panic: %v", r))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
port, _ := strconv.Atoi(info.Ports)
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, port)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []RDPCredential
|
|
||||||
for _, user := range common.Userdict["rdp"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, RDPCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
Domain: common.Domain,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["rdp"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentRdpScan(ctx, info, credentials, port, common.Timeout)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveRdpResult(info, target, port, result.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("RDP扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentRdpScan 并发扫描RDP服务
|
|
||||||
func concurrentRdpScan(ctx context.Context, info *common.HostInfo, credentials []RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *RDPScanResult, 1)
|
|
||||||
workChan := make(chan RDPCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryRdpCredential(scanCtx, info.Host, credential, port, timeoutSeconds)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("RDP并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryRdpCredential 尝试单个RDP凭据
|
|
||||||
func tryRdpCredential(ctx context.Context, host string, credential RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan *RDPScanResult, 1)
|
|
||||||
|
|
||||||
// 在协程中进行连接尝试
|
|
||||||
go func() {
|
|
||||||
success, err := RdpConn(host, credential.Domain, credential.Username, credential.Password, port, timeoutSeconds)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
// 上下文已取消,不返回结果
|
|
||||||
case resultChan <- &RDPScanResult{
|
|
||||||
Success: success,
|
|
||||||
Error: err,
|
|
||||||
Credential: credential,
|
|
||||||
}:
|
|
||||||
// 成功发送结果
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &RDPScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
|
|
||||||
// 单个连接超时
|
|
||||||
return &RDPScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("连接超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RdpConn 尝试RDP连接
|
|
||||||
func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
glog.Error("RDP连接panic:", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%s:%d", ip, port)
|
|
||||||
|
|
||||||
// 创建RDP客户端
|
|
||||||
client := NewClient(target, glog.NONE)
|
|
||||||
if err := client.Login(domain, user, password, timeout); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveRdpResult 保存RDP扫描结果
|
|
||||||
func saveRdpResult(info *common.HostInfo, target string, port int, credential RDPCredential) {
|
|
||||||
var successMsg string
|
|
||||||
|
|
||||||
if credential.Domain != "" {
|
|
||||||
successMsg = fmt.Sprintf("RDP %v Domain: %v\\%v Password: %v",
|
|
||||||
target, credential.Domain, credential.Username, credential.Password)
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("RDP %v Username: %v Password: %v",
|
|
||||||
target, credential.Username, credential.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
details := map[string]interface{}{
|
|
||||||
"port": port,
|
|
||||||
"service": "rdp",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
}
|
|
||||||
|
|
||||||
if credential.Domain != "" {
|
|
||||||
details["domain"] = credential.Domain
|
|
||||||
}
|
|
||||||
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client RDP客户端结构
|
|
||||||
type Client struct {
|
|
||||||
Host string // 服务地址(ip:port)
|
|
||||||
tpkt *tpkt.TPKT // TPKT协议层
|
|
||||||
x224 *x224.X224 // X224协议层
|
|
||||||
mcs *t125.MCSClient // MCS协议层
|
|
||||||
sec *sec.Client // 安全层
|
|
||||||
pdu *pdu.Client // PDU协议层
|
|
||||||
vnc *rfb.RFB // VNC协议(可选)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient 创建新的RDP客户端
|
|
||||||
func NewClient(host string, logLevel glog.LEVEL) *Client {
|
|
||||||
// 配置日志
|
|
||||||
glog.SetLevel(logLevel)
|
|
||||||
logger := log.New(os.Stdout, "", 0)
|
|
||||||
glog.SetLogger(logger)
|
|
||||||
|
|
||||||
return &Client{
|
|
||||||
Host: host,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login 执行RDP登录
|
|
||||||
func (g *Client) Login(domain, user, pwd string, timeout int64) error {
|
|
||||||
// 建立TCP连接
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("[连接错误] %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
glog.Info(conn.LocalAddr().String())
|
|
||||||
|
|
||||||
// 初始化协议栈
|
|
||||||
g.initProtocolStack(conn, domain, user, pwd)
|
|
||||||
|
|
||||||
// 建立X224连接
|
|
||||||
if err = g.x224.Connect(); err != nil {
|
|
||||||
return fmt.Errorf("[X224连接错误] %v", err)
|
|
||||||
}
|
|
||||||
glog.Info("等待连接建立...")
|
|
||||||
|
|
||||||
// 等待连接完成
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
breakFlag := false
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
// 设置事件处理器
|
|
||||||
g.setupEventHandlers(wg, &breakFlag, &err)
|
|
||||||
|
|
||||||
// 添加额外的超时保护
|
|
||||||
connectionDone := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(connectionDone)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-connectionDone:
|
|
||||||
// 连接过程正常完成
|
|
||||||
return err
|
|
||||||
case <-time.After(time.Duration(timeout) * time.Second):
|
|
||||||
// 超时
|
|
||||||
if !breakFlag {
|
|
||||||
breakFlag = true
|
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
return fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initProtocolStack 初始化RDP协议栈
|
|
||||||
func (g *Client) initProtocolStack(conn net.Conn, domain, user, pwd string) {
|
|
||||||
// 创建协议层实例
|
|
||||||
g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd))
|
|
||||||
g.x224 = x224.New(g.tpkt)
|
|
||||||
g.mcs = t125.NewMCSClient(g.x224)
|
|
||||||
g.sec = sec.NewClient(g.mcs)
|
|
||||||
g.pdu = pdu.NewClient(g.sec)
|
|
||||||
|
|
||||||
// 设置认证信息
|
|
||||||
g.sec.SetUser(user)
|
|
||||||
g.sec.SetPwd(pwd)
|
|
||||||
g.sec.SetDomain(domain)
|
|
||||||
|
|
||||||
// 配置协议层关联
|
|
||||||
g.tpkt.SetFastPathListener(g.sec)
|
|
||||||
g.sec.SetFastPathListener(g.pdu)
|
|
||||||
g.pdu.SetFastPathSender(g.tpkt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupEventHandlers 设置PDU事件处理器
|
|
||||||
func (g *Client) setupEventHandlers(wg *sync.WaitGroup, breakFlag *bool, err *error) {
|
|
||||||
// 错误处理
|
|
||||||
g.pdu.On("error", func(e error) {
|
|
||||||
*err = e
|
|
||||||
glog.Error("错误:", e)
|
|
||||||
g.pdu.Emit("done")
|
|
||||||
})
|
|
||||||
|
|
||||||
// 连接关闭
|
|
||||||
g.pdu.On("close", func() {
|
|
||||||
*err = errors.New("连接关闭")
|
|
||||||
glog.Info("连接已关闭")
|
|
||||||
g.pdu.Emit("done")
|
|
||||||
})
|
|
||||||
|
|
||||||
// 连接成功
|
|
||||||
g.pdu.On("success", func() {
|
|
||||||
*err = nil
|
|
||||||
glog.Info("连接成功")
|
|
||||||
g.pdu.Emit("done")
|
|
||||||
})
|
|
||||||
|
|
||||||
// 连接就绪
|
|
||||||
g.pdu.On("ready", func() {
|
|
||||||
glog.Info("连接就绪")
|
|
||||||
g.pdu.Emit("done")
|
|
||||||
})
|
|
||||||
|
|
||||||
// 屏幕更新
|
|
||||||
g.pdu.On("update", func(rectangles []pdu.BitmapData) {
|
|
||||||
glog.Info("屏幕更新:", rectangles)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 完成处理
|
|
||||||
g.pdu.On("done", func() {
|
|
||||||
if !*breakFlag {
|
|
||||||
*breakFlag = true
|
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,309 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
amqp "github.com/rabbitmq/amqp091-go"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RabbitMQCredential 表示一个RabbitMQ凭据
|
|
||||||
type RabbitMQCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// RabbitMQScanResult 表示扫描结果
|
|
||||||
type RabbitMQScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential RabbitMQCredential
|
|
||||||
ErrorMsg string // 保存详细的错误信息
|
|
||||||
}
|
|
||||||
|
|
||||||
// RabbitMQScan 执行 RabbitMQ 服务扫描
|
|
||||||
func RabbitMQScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 先测试默认账号 guest/guest
|
|
||||||
common.LogDebug("尝试默认账号 guest/guest")
|
|
||||||
defaultCredential := RabbitMQCredential{Username: "guest", Password: "guest"}
|
|
||||||
defaultResult := tryRabbitMQCredential(ctx, info, defaultCredential, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if defaultResult.Success {
|
|
||||||
saveRabbitMQResult(info, target, defaultResult.Credential)
|
|
||||||
return nil
|
|
||||||
} else if defaultResult.Error != nil {
|
|
||||||
// 打印默认账号的详细错误信息
|
|
||||||
common.LogDebug(fmt.Sprintf("默认账号 guest/guest 失败,详细错误: %s", defaultResult.ErrorMsg))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建其他凭据列表
|
|
||||||
var credentials []RabbitMQCredential
|
|
||||||
for _, user := range common.Userdict["rabbitmq"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, RabbitMQCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["rabbitmq"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentRabbitMQScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveRabbitMQResult(info, target, result.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("RabbitMQ扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了默认账号
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentRabbitMQScan 并发扫描RabbitMQ服务
|
|
||||||
func concurrentRabbitMQScan(ctx context.Context, info *common.HostInfo, credentials []RabbitMQCredential, timeoutSeconds int64, maxRetries int) *RabbitMQScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *RabbitMQScanResult, 1)
|
|
||||||
workChan := make(chan RabbitMQCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryRabbitMQCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("RabbitMQ并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryRabbitMQCredential 尝试单个RabbitMQ凭据
|
|
||||||
func tryRabbitMQCredential(ctx context.Context, info *common.HostInfo, credential RabbitMQCredential, timeoutSeconds int64, maxRetries int) *RabbitMQScanResult {
|
|
||||||
var lastErr error
|
|
||||||
var errorMsg string
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &RabbitMQScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
ErrorMsg: "全局超时",
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, err, detailErr := RabbitMQConn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &RabbitMQScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
errorMsg = detailErr
|
|
||||||
|
|
||||||
// 打印详细的错误信息,包括所有原始错误信息
|
|
||||||
common.LogDebug(fmt.Sprintf("凭据 %s:%s 失败,错误详情: %s",
|
|
||||||
credential.Username, credential.Password, errorMsg))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// 可以根据错误信息类型来决定是否需要重试
|
|
||||||
// 例如,如果错误是认证错误,则无需重试
|
|
||||||
if strings.Contains(errorMsg, "ACCESS_REFUSED") {
|
|
||||||
common.LogDebug("认证错误,无需重试")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &RabbitMQScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
ErrorMsg: errorMsg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RabbitMQConn 尝试 RabbitMQ 连接
|
|
||||||
func RabbitMQConn(ctx context.Context, info *common.HostInfo, user string, pass string) (bool, error, string) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
|
|
||||||
// 构造 AMQP URL
|
|
||||||
amqpURL := fmt.Sprintf("amqp://%s:%s@%s:%s/", user, pass, host, port)
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
detailErr string
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中尝试连接
|
|
||||||
go func() {
|
|
||||||
// 配置连接
|
|
||||||
config := amqp.Config{
|
|
||||||
Dial: func(network, addr string) (net.Conn, error) {
|
|
||||||
dialer := &net.Dialer{Timeout: time.Duration(common.Timeout) * time.Second}
|
|
||||||
return dialer.DialContext(ctx, network, addr)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试连接
|
|
||||||
conn, err := amqp.DialConfig(amqpURL, config)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
detailErr := err.Error()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
detailErr string
|
|
||||||
}{false, err, detailErr}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
detailErr string
|
|
||||||
}{true, nil, ""}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result.success, result.err, result.detailErr
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err(), ctx.Err().Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveRabbitMQResult 保存RabbitMQ扫描结果
|
|
||||||
func saveRabbitMQResult(info *common.HostInfo, target string, credential RabbitMQCredential) {
|
|
||||||
successMsg := fmt.Sprintf("RabbitMQ服务 %s 连接成功 用户名: %v 密码: %v",
|
|
||||||
target, credential.Username, credential.Password)
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "rabbitmq",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RedisScan 执行Redis服务扫描
|
|
||||||
// 现在完全使用新的插件架构
|
|
||||||
func RedisScan(info *common.HostInfo) error {
|
|
||||||
// 使用新的插件架构
|
|
||||||
if adapter.TryNewArchitecture("redis", info) {
|
|
||||||
return nil // 新架构处理成功
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果新架构不支持,记录错误(理论上不应该发生)
|
|
||||||
common.LogError("Redis插件新架构不可用,请检查插件注册")
|
|
||||||
return nil
|
|
||||||
}
|
|
477
Plugins/Rsync.go
477
Plugins/Rsync.go
@ -1,477 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RsyncCredential 表示一个Rsync凭据
|
|
||||||
type RsyncCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// RsyncScanResult 表示Rsync扫描结果
|
|
||||||
type RsyncScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential RsyncCredential
|
|
||||||
IsAnonymous bool
|
|
||||||
ModuleName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func RsyncScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 首先尝试匿名访问
|
|
||||||
common.LogDebug("尝试匿名访问...")
|
|
||||||
anonymousResult := tryRsyncCredential(ctx, info, RsyncCredential{"", ""}, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if anonymousResult.Success {
|
|
||||||
// 匿名访问成功
|
|
||||||
saveRsyncResult(info, target, anonymousResult)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []RsyncCredential
|
|
||||||
for _, user := range common.Userdict["rsync"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, RsyncCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["rsync"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentRsyncScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 保存成功结果
|
|
||||||
saveRsyncResult(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Rsync扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentRsyncScan 并发扫描Rsync服务
|
|
||||||
func concurrentRsyncScan(ctx context.Context, info *common.HostInfo, credentials []RsyncCredential, timeoutSeconds int64, maxRetries int) *RsyncScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *RsyncScanResult, 1)
|
|
||||||
workChan := make(chan RsyncCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryRsyncCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Rsync并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryRsyncCredential 尝试单个Rsync凭据
|
|
||||||
func tryRsyncCredential(ctx context.Context, info *common.HostInfo, credential RsyncCredential, timeoutSeconds int64, maxRetries int) *RsyncScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &RsyncScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, moduleName, err := RsyncConn(connCtx, info, credential.Username, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
isAnonymous := credential.Username == "" && credential.Password == ""
|
|
||||||
return &RsyncScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
IsAnonymous: isAnonymous,
|
|
||||||
ModuleName: moduleName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &RsyncScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RsyncConn 尝试Rsync连接
|
|
||||||
func RsyncConn(ctx context.Context, info *common.HostInfo, user string, pass string) (bool, string, error) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
|
|
||||||
// 建立连接
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%s", host, port), timeout)
|
|
||||||
if err != nil {
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 创建结果通道用于超时控制
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
moduleName string
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中处理连接,以支持上下文取消
|
|
||||||
go func() {
|
|
||||||
buffer := make([]byte, 1024)
|
|
||||||
|
|
||||||
// 1. 读取服务器初始greeting
|
|
||||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
|
||||||
n, err := conn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
moduleName string
|
|
||||||
err error
|
|
||||||
}{false, "", err}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
greeting := string(buffer[:n])
|
|
||||||
if !strings.HasPrefix(greeting, "@RSYNCD:") {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
moduleName string
|
|
||||||
err error
|
|
||||||
}{false, "", fmt.Errorf("不是Rsync服务")}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取服务器版本号
|
|
||||||
version := strings.TrimSpace(strings.TrimPrefix(greeting, "@RSYNCD:"))
|
|
||||||
|
|
||||||
// 2. 回应相同的版本号
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
|
||||||
_, err = conn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version)))
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
moduleName string
|
|
||||||
err error
|
|
||||||
}{false, "", err}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 选择模块 - 先列出可用模块
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
|
||||||
_, err = conn.Write([]byte("#list\n"))
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
moduleName string
|
|
||||||
err error
|
|
||||||
}{false, "", err}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 读取模块列表
|
|
||||||
var moduleList strings.Builder
|
|
||||||
for {
|
|
||||||
// 检查上下文是否取消
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
|
||||||
n, err = conn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
chunk := string(buffer[:n])
|
|
||||||
moduleList.WriteString(chunk)
|
|
||||||
if strings.Contains(chunk, "@RSYNCD: EXIT") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modules := strings.Split(moduleList.String(), "\n")
|
|
||||||
for _, module := range modules {
|
|
||||||
if strings.HasPrefix(module, "@RSYNCD") || module == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取模块名
|
|
||||||
moduleName := strings.Fields(module)[0]
|
|
||||||
|
|
||||||
// 检查上下文是否取消
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 为每个模块创建新连接尝试认证
|
|
||||||
authConn, err := common.WrapperTcpWithTimeout(host, port, timeout)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer authConn.Close() // 重复初始握手
|
|
||||||
authConn.SetReadDeadline(time.Now().Add(timeout))
|
|
||||||
_, err = authConn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
authConn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
authConn.SetWriteDeadline(time.Now().Add(timeout))
|
|
||||||
_, err = authConn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version)))
|
|
||||||
if err != nil {
|
|
||||||
authConn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 选择模块
|
|
||||||
authConn.SetWriteDeadline(time.Now().Add(timeout))
|
|
||||||
_, err = authConn.Write([]byte(moduleName + "\n"))
|
|
||||||
if err != nil {
|
|
||||||
authConn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 等待认证挑战
|
|
||||||
authConn.SetReadDeadline(time.Now().Add(timeout))
|
|
||||||
n, err = authConn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
authConn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
authResponse := string(buffer[:n])
|
|
||||||
if strings.Contains(authResponse, "@RSYNCD: OK") {
|
|
||||||
// 模块不需要认证
|
|
||||||
if user == "" && pass == "" {
|
|
||||||
authConn.Close()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
moduleName string
|
|
||||||
err error
|
|
||||||
}{true, moduleName, nil}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if strings.Contains(authResponse, "@RSYNCD: AUTHREQD") {
|
|
||||||
if user != "" && pass != "" {
|
|
||||||
// 8. 发送认证信息
|
|
||||||
authString := fmt.Sprintf("%s %s\n", user, pass)
|
|
||||||
authConn.SetWriteDeadline(time.Now().Add(timeout))
|
|
||||||
_, err = authConn.Write([]byte(authString))
|
|
||||||
if err != nil {
|
|
||||||
authConn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. 读取认证结果
|
|
||||||
authConn.SetReadDeadline(time.Now().Add(timeout))
|
|
||||||
n, err = authConn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
authConn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(string(buffer[:n]), "@ERROR") {
|
|
||||||
authConn.Close()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
moduleName string
|
|
||||||
err error
|
|
||||||
}{true, moduleName, nil}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
authConn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果执行到这里,没有找到成功的认证
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
moduleName string
|
|
||||||
err error
|
|
||||||
}{false, "", fmt.Errorf("认证失败或无可用模块")}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return result.success, result.moduleName, result.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, "", ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveRsyncResult 保存Rsync扫描结果
|
|
||||||
func saveRsyncResult(info *common.HostInfo, target string, result *RsyncScanResult) {
|
|
||||||
var successMsg string
|
|
||||||
var details map[string]interface{}
|
|
||||||
|
|
||||||
if result.IsAnonymous {
|
|
||||||
successMsg = fmt.Sprintf("Rsync服务 %s 匿名访问成功 模块: %s", target, result.ModuleName)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "rsync",
|
|
||||||
"type": "anonymous-access",
|
|
||||||
"module": result.ModuleName,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("Rsync服务 %s 爆破成功 用户名: %v 密码: %v 模块: %s",
|
|
||||||
target, result.Credential.Username, result.Credential.Password, result.ModuleName)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "rsync",
|
|
||||||
"type": "weak-password",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
"module": result.ModuleName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
299
Plugins/SMB.go
299
Plugins/SMB.go
@ -1,299 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"github.com/stacktitan/smb/smb"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SmbCredential 表示一个SMB凭据
|
|
||||||
type SmbCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmbScanResult 表示SMB扫描结果
|
|
||||||
type SmbScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential SmbCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
func SmbScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []SmbCredential
|
|
||||||
for _, user := range common.Userdict["smb"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, SmbCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["smb"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentSmbScan(ctx, info, credentials, common.Timeout)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveSmbResult(info, target, result.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("SMB扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentSmbScan 并发扫描SMB服务
|
|
||||||
func concurrentSmbScan(ctx context.Context, info *common.HostInfo, credentials []SmbCredential, timeoutSeconds int64) *SmbScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *SmbScanResult, 1)
|
|
||||||
workChan := make(chan SmbCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 记录用户锁定状态,避免继续尝试已锁定的用户
|
|
||||||
lockedUsers := make(map[string]bool)
|
|
||||||
var lockedMutex sync.Mutex
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// 检查用户是否已锁定
|
|
||||||
lockedMutex.Lock()
|
|
||||||
locked := lockedUsers[credential.Username]
|
|
||||||
lockedMutex.Unlock()
|
|
||||||
if locked {
|
|
||||||
common.LogDebug(fmt.Sprintf("跳过已锁定用户: %s", credential.Username))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result := trySmbCredential(scanCtx, info, credential, timeoutSeconds)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查账号锁定错误
|
|
||||||
if result.Error != nil && strings.Contains(result.Error.Error(), "账号锁定") {
|
|
||||||
lockedMutex.Lock()
|
|
||||||
lockedUsers[credential.Username] = true
|
|
||||||
lockedMutex.Unlock()
|
|
||||||
common.LogError(fmt.Sprintf("用户 %s 已被锁定", credential.Username))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// 检查用户是否已锁定
|
|
||||||
lockedMutex.Lock()
|
|
||||||
locked := lockedUsers[cred.Username]
|
|
||||||
lockedMutex.Unlock()
|
|
||||||
if locked {
|
|
||||||
continue // 跳过已锁定用户
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("SMB并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// trySmbCredential 尝试单个SMB凭据
|
|
||||||
func trySmbCredential(ctx context.Context, info *common.HostInfo, credential SmbCredential, timeoutSeconds int64) *SmbScanResult {
|
|
||||||
// 创建单个连接超时上下文的结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中尝试连接
|
|
||||||
go func() {
|
|
||||||
signal := make(chan struct{}, 1)
|
|
||||||
success, err := SmblConn(info, credential.Username, credential.Password, signal)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{success, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或超时
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
return &SmbScanResult{
|
|
||||||
Success: result.success,
|
|
||||||
Error: result.err,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &SmbScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
|
|
||||||
return &SmbScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("连接超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveSmbResult 保存SMB扫描结果
|
|
||||||
func saveSmbResult(info *common.HostInfo, target string, credential SmbCredential) {
|
|
||||||
// 构建结果消息
|
|
||||||
var successMsg string
|
|
||||||
details := map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "smb",
|
|
||||||
"username": credential.Username,
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
}
|
|
||||||
|
|
||||||
if common.Domain != "" {
|
|
||||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s\\%s:%s", target, common.Domain, credential.Username, credential.Password)
|
|
||||||
details["domain"] = common.Domain
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s:%s", target, credential.Username, credential.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录成功日志
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
result := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmblConn 尝试建立SMB连接并认证
|
|
||||||
func SmblConn(info *common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) {
|
|
||||||
options := smb.Options{
|
|
||||||
Host: info.Host,
|
|
||||||
Port: 445,
|
|
||||||
User: user,
|
|
||||||
Password: pass,
|
|
||||||
Domain: common.Domain,
|
|
||||||
Workstation: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
session, err := smb.NewSession(options, false)
|
|
||||||
if err == nil {
|
|
||||||
defer session.Close()
|
|
||||||
if session.IsAuthenticated {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("认证失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理错误信息中的换行符和多余空格
|
|
||||||
errMsg := strings.TrimSpace(strings.ReplaceAll(err.Error(), "\n", " "))
|
|
||||||
if strings.Contains(errMsg, "NT Status Error") {
|
|
||||||
switch {
|
|
||||||
case strings.Contains(errMsg, "STATUS_LOGON_FAILURE"):
|
|
||||||
err = fmt.Errorf("密码错误")
|
|
||||||
case strings.Contains(errMsg, "STATUS_ACCOUNT_LOCKED_OUT"):
|
|
||||||
err = fmt.Errorf("账号锁定")
|
|
||||||
case strings.Contains(errMsg, "STATUS_ACCESS_DENIED"):
|
|
||||||
err = fmt.Errorf("拒绝访问")
|
|
||||||
case strings.Contains(errMsg, "STATUS_ACCOUNT_DISABLED"):
|
|
||||||
err = fmt.Errorf("账号禁用")
|
|
||||||
case strings.Contains(errMsg, "STATUS_PASSWORD_EXPIRED"):
|
|
||||||
err = fmt.Errorf("密码过期")
|
|
||||||
case strings.Contains(errMsg, "STATUS_USER_SESSION_DELETED"):
|
|
||||||
return false, fmt.Errorf("会话断开")
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("认证失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signal <- struct{}{}
|
|
||||||
return false, err
|
|
||||||
}
|
|
492
Plugins/SMB2.go
492
Plugins/SMB2.go
@ -1,492 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
|
|
||||||
"github.com/hirochachacha/go-smb2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Smb2Credential 表示一个SMB2凭据
|
|
||||||
type Smb2Credential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Hash []byte
|
|
||||||
IsHash bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smb2ScanResult 表示SMB2扫描结果
|
|
||||||
type Smb2ScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential Smb2Credential
|
|
||||||
Shares []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmbScan2 执行SMB2服务的认证扫描,支持密码和哈希两种认证方式
|
|
||||||
func SmbScan2(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始SMB2扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 根据是否提供哈希选择认证模式
|
|
||||||
if len(common.HashBytes) > 0 {
|
|
||||||
return smbHashScan(ctx, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
return smbPasswordScan(ctx, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
// smbPasswordScan 使用密码进行SMB2认证扫描
|
|
||||||
func smbPasswordScan(ctx context.Context, info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []Smb2Credential
|
|
||||||
for _, user := range common.Userdict["smb"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.ReplaceAll(pass, "{user}", user)
|
|
||||||
credentials = append(credentials, Smb2Credential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
Hash: []byte{},
|
|
||||||
IsHash: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始SMB2密码认证扫描 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["smb"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
return concurrentSmb2Scan(ctx, info, credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// smbHashScan 使用哈希进行SMB2认证扫描
|
|
||||||
func smbHashScan(ctx context.Context, info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []Smb2Credential
|
|
||||||
for _, user := range common.Userdict["smb"] {
|
|
||||||
for _, hash := range common.HashBytes {
|
|
||||||
credentials = append(credentials, Smb2Credential{
|
|
||||||
Username: user,
|
|
||||||
Password: "",
|
|
||||||
Hash: hash,
|
|
||||||
IsHash: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始SMB2哈希认证扫描 (总用户数: %d, 总哈希数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["smb"]), len(common.HashBytes), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
return concurrentSmb2Scan(ctx, info, credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentSmb2Scan 并发扫描SMB2服务
|
|
||||||
func concurrentSmb2Scan(ctx context.Context, info *common.HostInfo, credentials []Smb2Credential) error {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *Smb2ScanResult, 1)
|
|
||||||
workChan := make(chan Smb2Credential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 记录共享信息是否已打印和锁定的用户
|
|
||||||
var (
|
|
||||||
sharesPrinted bool
|
|
||||||
lockedUsers = make(map[string]bool)
|
|
||||||
mutex sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// 检查用户是否已锁定
|
|
||||||
mutex.Lock()
|
|
||||||
locked := lockedUsers[credential.Username]
|
|
||||||
currentSharesPrinted := sharesPrinted
|
|
||||||
mutex.Unlock()
|
|
||||||
|
|
||||||
if locked {
|
|
||||||
common.LogDebug(fmt.Sprintf("跳过已锁定用户: %s", credential.Username))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试凭据
|
|
||||||
result := trySmb2Credential(scanCtx, info, credential, currentSharesPrinted)
|
|
||||||
|
|
||||||
// 更新共享信息打印状态
|
|
||||||
if result.Shares != nil && len(result.Shares) > 0 && !currentSharesPrinted {
|
|
||||||
mutex.Lock()
|
|
||||||
sharesPrinted = true
|
|
||||||
mutex.Unlock()
|
|
||||||
|
|
||||||
// 打印共享信息
|
|
||||||
logShareInfo(info, credential.Username, credential.Password, credential.Hash, result.Shares)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查认证成功
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查账户锁定
|
|
||||||
if result.Error != nil {
|
|
||||||
errMsg := result.Error.Error()
|
|
||||||
if strings.Contains(errMsg, "account has been automatically locked") ||
|
|
||||||
strings.Contains(errMsg, "account has been locked") ||
|
|
||||||
strings.Contains(errMsg, "user account has been automatically locked") {
|
|
||||||
|
|
||||||
mutex.Lock()
|
|
||||||
lockedUsers[credential.Username] = true
|
|
||||||
mutex.Unlock()
|
|
||||||
|
|
||||||
common.LogError(fmt.Sprintf("用户 %s 已被锁定", credential.Username))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// 检查用户是否已锁定
|
|
||||||
mutex.Lock()
|
|
||||||
locked := lockedUsers[cred.Username]
|
|
||||||
mutex.Unlock()
|
|
||||||
|
|
||||||
if locked {
|
|
||||||
continue // 跳过已锁定用户
|
|
||||||
}
|
|
||||||
|
|
||||||
if cred.IsHash {
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s Hash:%s",
|
|
||||||
i+1, len(credentials), cred.Username, common.HashValue))
|
|
||||||
} else {
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s",
|
|
||||||
i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
}
|
|
||||||
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
// 记录成功结果
|
|
||||||
logSuccessfulAuth(info, result.Credential.Username,
|
|
||||||
result.Credential.Password, result.Credential.Hash)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("SMB2扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// trySmb2Credential 尝试单个SMB2凭据
|
|
||||||
func trySmb2Credential(ctx context.Context, info *common.HostInfo, credential Smb2Credential, hasprint bool) *Smb2ScanResult {
|
|
||||||
// 创建单个连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 在协程中尝试连接
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
shares []string
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
success, err, shares := Smb2Con(connCtx, info, credential.Username,
|
|
||||||
credential.Password, credential.Hash, hasprint)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
shares []string
|
|
||||||
err error
|
|
||||||
}{success, shares, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或超时
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
if result.success {
|
|
||||||
return &Smb2ScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
Shares: result.shares,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 失败时记录错误
|
|
||||||
if result.err != nil {
|
|
||||||
logFailedAuth(info, credential.Username, credential.Password, credential.Hash, result.err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Smb2ScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: result.err,
|
|
||||||
Credential: credential,
|
|
||||||
Shares: result.shares,
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-connCtx.Done():
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
// 全局超时
|
|
||||||
return &Smb2ScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 单个连接超时
|
|
||||||
err := fmt.Errorf("连接超时")
|
|
||||||
logFailedAuth(info, credential.Username, credential.Password, credential.Hash, err)
|
|
||||||
return &Smb2ScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: err,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smb2Con 尝试SMB2连接并进行认证,检查共享访问权限
|
|
||||||
func Smb2Con(ctx context.Context, info *common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, shares []string) {
|
|
||||||
// 建立TCP连接,使用socks代理支持
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:445", info.Host), time.Duration(common.Timeout)*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("连接失败: %v", err), nil
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 配置NTLM认证
|
|
||||||
initiator := smb2.NTLMInitiator{
|
|
||||||
User: user,
|
|
||||||
Domain: common.Domain,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置认证方式(哈希或密码)
|
|
||||||
if len(hash) > 0 {
|
|
||||||
initiator.Hash = hash
|
|
||||||
} else {
|
|
||||||
initiator.Password = pass
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建SMB2会话
|
|
||||||
dialer := &smb2.Dialer{
|
|
||||||
Initiator: &initiator,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用context设置超时
|
|
||||||
session, err := dialer.Dial(conn)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("SMB2会话建立失败: %v", err), nil
|
|
||||||
}
|
|
||||||
defer session.Logoff()
|
|
||||||
|
|
||||||
// 检查上下文是否已取消
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err(), nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取共享列表
|
|
||||||
sharesList, err := session.ListSharenames()
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("获取共享列表失败: %v", err), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 再次检查上下文是否已取消
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err(), sharesList
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试访问C$共享以验证管理员权限
|
|
||||||
fs, err := session.Mount("C$")
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("挂载C$失败: %v", err), sharesList
|
|
||||||
}
|
|
||||||
defer fs.Umount()
|
|
||||||
|
|
||||||
// 最后检查上下文是否已取消
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err(), sharesList
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试读取系统文件以验证权限
|
|
||||||
path := `Windows\win.ini`
|
|
||||||
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("访问系统文件失败: %v", err), sharesList
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
return true, nil, sharesList
|
|
||||||
}
|
|
||||||
|
|
||||||
// logSuccessfulAuth 记录成功的认证
|
|
||||||
func logSuccessfulAuth(info *common.HostInfo, user, pass string, hash []byte) {
|
|
||||||
credential := pass
|
|
||||||
if len(hash) > 0 {
|
|
||||||
credential = common.HashValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存认证成功结果
|
|
||||||
result := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "success",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "smb2",
|
|
||||||
"username": user,
|
|
||||||
"domain": common.Domain,
|
|
||||||
"type": "weak-auth",
|
|
||||||
"credential": credential,
|
|
||||||
"auth_type": map[bool]string{true: "hash", false: "password"}[len(hash) > 0],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(result)
|
|
||||||
|
|
||||||
// 控制台输出
|
|
||||||
var msg string
|
|
||||||
if common.Domain != "" {
|
|
||||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s\\%s", info.Host, info.Ports, common.Domain, user)
|
|
||||||
} else {
|
|
||||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s", info.Host, info.Ports, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(hash) > 0 {
|
|
||||||
msg += fmt.Sprintf(" Hash:%s", common.HashValue)
|
|
||||||
} else {
|
|
||||||
msg += fmt.Sprintf(" Pass:%s", pass)
|
|
||||||
}
|
|
||||||
common.LogSuccess(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// logFailedAuth 记录失败的认证
|
|
||||||
func logFailedAuth(info *common.HostInfo, user, pass string, hash []byte, err error) {
|
|
||||||
var errlog string
|
|
||||||
if len(hash) > 0 {
|
|
||||||
errlog = fmt.Sprintf("SMB2认证失败 %s:%s %s Hash:%s %v",
|
|
||||||
info.Host, info.Ports, user, common.HashValue, err)
|
|
||||||
} else {
|
|
||||||
errlog = fmt.Sprintf("SMB2认证失败 %s:%s %s:%s %v",
|
|
||||||
info.Host, info.Ports, user, pass, err)
|
|
||||||
}
|
|
||||||
errlog = strings.ReplaceAll(errlog, "\n", " ")
|
|
||||||
common.LogError(errlog)
|
|
||||||
}
|
|
||||||
|
|
||||||
// logShareInfo 记录SMB共享信息
|
|
||||||
func logShareInfo(info *common.HostInfo, user string, pass string, hash []byte, shares []string) {
|
|
||||||
credential := pass
|
|
||||||
if len(hash) > 0 {
|
|
||||||
credential = common.HashValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存共享信息结果
|
|
||||||
result := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "shares-found",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "smb2",
|
|
||||||
"username": user,
|
|
||||||
"domain": common.Domain,
|
|
||||||
"shares": shares,
|
|
||||||
"credential": credential,
|
|
||||||
"auth_type": map[bool]string{true: "hash", false: "password"}[len(hash) > 0],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(result)
|
|
||||||
|
|
||||||
// 控制台输出
|
|
||||||
var msg string
|
|
||||||
if common.Domain != "" {
|
|
||||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s\\%s", info.Host, info.Ports, common.Domain, user)
|
|
||||||
} else {
|
|
||||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s", info.Host, info.Ports, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(hash) > 0 {
|
|
||||||
msg += fmt.Sprintf(" Hash:%s", common.HashValue)
|
|
||||||
} else {
|
|
||||||
msg += fmt.Sprintf(" Pass:%s", pass)
|
|
||||||
}
|
|
||||||
msg += fmt.Sprintf(" 共享:%v", shares)
|
|
||||||
common.LogBase(msg)
|
|
||||||
}
|
|
326
Plugins/SMTP.go
326
Plugins/SMTP.go
@ -1,326 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/smtp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SmtpCredential 表示一个SMTP凭据
|
|
||||||
type SmtpCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmtpScanResult 表示SMTP扫描结果
|
|
||||||
type SmtpScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential SmtpCredential
|
|
||||||
IsAnonymous bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmtpScan 执行 SMTP 服务扫描
|
|
||||||
func SmtpScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 先测试匿名访问
|
|
||||||
common.LogDebug("尝试匿名访问...")
|
|
||||||
anonymousResult := trySmtpCredential(ctx, info, SmtpCredential{"", ""}, common.Timeout, common.MaxRetries)
|
|
||||||
|
|
||||||
if anonymousResult.Success {
|
|
||||||
// 匿名访问成功
|
|
||||||
saveSmtpResult(info, target, anonymousResult)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []SmtpCredential
|
|
||||||
for _, user := range common.Userdict["smtp"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, SmtpCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["smtp"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentSmtpScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveSmtpResult(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("SMTP扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentSmtpScan 并发扫描SMTP服务
|
|
||||||
func concurrentSmtpScan(ctx context.Context, info *common.HostInfo, credentials []SmtpCredential, timeoutSeconds int64, maxRetries int) *SmtpScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *SmtpScanResult, 1)
|
|
||||||
workChan := make(chan SmtpCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := trySmtpCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("SMTP并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// trySmtpCredential 尝试单个SMTP凭据
|
|
||||||
func trySmtpCredential(ctx context.Context, info *common.HostInfo, credential SmtpCredential, timeoutSeconds int64, maxRetries int) *SmtpScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &SmtpScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
|
|
||||||
// 在协程中尝试连接
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
success, err := SmtpConn(info, credential.Username, credential.Password, timeoutSeconds)
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{success, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或超时
|
|
||||||
var success bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
success = result.success
|
|
||||||
err = result.err
|
|
||||||
case <-connCtx.Done():
|
|
||||||
cancel()
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
// 全局超时
|
|
||||||
return &SmtpScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 单个连接超时
|
|
||||||
err = fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() // 释放连接上下文
|
|
||||||
|
|
||||||
if success {
|
|
||||||
isAnonymous := credential.Username == "" && credential.Password == ""
|
|
||||||
return &SmtpScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
IsAnonymous: isAnonymous,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SmtpScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmtpConn 尝试 SMTP 连接
|
|
||||||
func SmtpConn(info *common.HostInfo, user string, pass string, timeoutSeconds int64) (bool, error) {
|
|
||||||
host, port := info.Host, info.Ports
|
|
||||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
|
||||||
addr := fmt.Sprintf("%s:%s", host, port)
|
|
||||||
|
|
||||||
// 设置连接超时
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 设置读写超时
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
|
|
||||||
client, err := smtp.NewClient(conn, host)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
// 尝试认证
|
|
||||||
if user != "" {
|
|
||||||
auth := smtp.PlainAuth("", user, pass, host)
|
|
||||||
err = client.Auth(auth)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试发送邮件(测试权限)
|
|
||||||
err = client.Mail("test@test.com")
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveSmtpResult 保存SMTP扫描结果
|
|
||||||
func saveSmtpResult(info *common.HostInfo, target string, result *SmtpScanResult) {
|
|
||||||
var successMsg string
|
|
||||||
var details map[string]interface{}
|
|
||||||
|
|
||||||
if result.IsAnonymous {
|
|
||||||
successMsg = fmt.Sprintf("SMTP服务 %s 允许匿名访问", target)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "smtp",
|
|
||||||
"type": "anonymous-access",
|
|
||||||
"anonymous": true,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("SMTP服务 %s 爆破成功 用户名: %v 密码: %v",
|
|
||||||
target, result.Credential.Username, result.Credential.Password)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "smtp",
|
|
||||||
"type": "weak-password",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
145
Plugins/SNMP.go
145
Plugins/SNMP.go
@ -1,145 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/gosnmp/gosnmp"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SNMPScan 执行SNMP服务扫描
|
|
||||||
func SNMPScan(info *common.HostInfo) (tmperr error) {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
maxRetries := common.MaxRetries
|
|
||||||
portNum, _ := strconv.Atoi(info.Ports)
|
|
||||||
defaultCommunities := []string{"public", "private", "cisco", "community"}
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
common.LogDebug(fmt.Sprintf("尝试默认 community 列表 (总数: %d)", len(defaultCommunities)))
|
|
||||||
|
|
||||||
tried := 0
|
|
||||||
total := len(defaultCommunities)
|
|
||||||
|
|
||||||
for _, community := range defaultCommunities {
|
|
||||||
tried++
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试 community: %s", tried, total, community))
|
|
||||||
|
|
||||||
for retryCount := 0; retryCount < maxRetries; retryCount++ {
|
|
||||||
if retryCount > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: community: %s", retryCount+1, community))
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct {
|
|
||||||
success bool
|
|
||||||
sysDesc string
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
go func(community string) {
|
|
||||||
success, sysDesc, err := SNMPConnect(info, community, portNum)
|
|
||||||
select {
|
|
||||||
case done <- struct {
|
|
||||||
success bool
|
|
||||||
sysDesc string
|
|
||||||
err error
|
|
||||||
}{success, sysDesc, err}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}(community)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
select {
|
|
||||||
case result := <-done:
|
|
||||||
err = result.err
|
|
||||||
if result.success && err == nil {
|
|
||||||
successMsg := fmt.Sprintf("SNMP服务 %s community: %v 连接成功", target, community)
|
|
||||||
if result.sysDesc != "" {
|
|
||||||
successMsg += fmt.Sprintf(" System: %v", result.sysDesc)
|
|
||||||
}
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "snmp",
|
|
||||||
"community": community,
|
|
||||||
"type": "weak-community",
|
|
||||||
"system": result.sysDesc,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case <-time.After(timeout):
|
|
||||||
err = fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
errlog := fmt.Sprintf("SNMP服务 %s 尝试失败 community: %v 错误: %v",
|
|
||||||
target, community, err)
|
|
||||||
common.LogError(errlog)
|
|
||||||
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr != nil {
|
|
||||||
if retryCount == maxRetries-1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个 community", tried))
|
|
||||||
return tmperr
|
|
||||||
}
|
|
||||||
|
|
||||||
// SNMPConnect 尝试SNMP连接
|
|
||||||
func SNMPConnect(info *common.HostInfo, community string, portNum int) (bool, string, error) {
|
|
||||||
host := info.Host
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
|
|
||||||
snmp := &gosnmp.GoSNMP{
|
|
||||||
Target: host,
|
|
||||||
Port: uint16(portNum),
|
|
||||||
Community: community,
|
|
||||||
Version: gosnmp.Version2c,
|
|
||||||
Timeout: timeout,
|
|
||||||
Retries: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := snmp.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
defer snmp.Conn.Close()
|
|
||||||
|
|
||||||
oids := []string{"1.3.6.1.2.1.1.1.0"}
|
|
||||||
result, err := snmp.Get(oids)
|
|
||||||
if err != nil {
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(result.Variables) > 0 {
|
|
||||||
var sysDesc string
|
|
||||||
if result.Variables[0].Type != gosnmp.NoSuchObject {
|
|
||||||
sysDesc = strings.TrimSpace(string(result.Variables[0].Value.([]byte)))
|
|
||||||
}
|
|
||||||
return true, sysDesc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, "", fmt.Errorf("认证失败")
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SshScan 扫描SSH服务弱密码
|
|
||||||
// 现在完全使用新的插件架构
|
|
||||||
func SshScan(info *common.HostInfo) error {
|
|
||||||
// 使用新的插件架构
|
|
||||||
if adapter.TryNewArchitecture("ssh", info) {
|
|
||||||
return nil // 新架构处理成功
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果新架构不支持,记录错误(理论上不应该发生)
|
|
||||||
common.LogError("SSH插件新架构不可用,请检查插件注册")
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
pkt = "\x00" + // session
|
|
||||||
"\x00\x00\xc0" + // legth
|
|
||||||
|
|
||||||
"\xfeSMB@\x00" + // protocol
|
|
||||||
|
|
||||||
//[MS-SMB2]: SMB2 NEGOTIATE Request
|
|
||||||
//https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5
|
|
||||||
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x1f\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
|
|
||||||
// [MS-SMB2]: SMB2 NEGOTIATE_CONTEXT
|
|
||||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/15332256-522e-4a53-8cd7-0bd17678a2f7
|
|
||||||
|
|
||||||
"$\x00" +
|
|
||||||
"\x08\x00" +
|
|
||||||
"\x01\x00" +
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x7f\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"x\x00" +
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x02\x00" +
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x02\x02" +
|
|
||||||
"\x10\x02" +
|
|
||||||
"\x22\x02" +
|
|
||||||
"$\x02" +
|
|
||||||
"\x00\x03" +
|
|
||||||
"\x02\x03" +
|
|
||||||
"\x10\x03" +
|
|
||||||
"\x11\x03" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
|
|
||||||
// [MS-SMB2]: SMB2_PREAUTH_INTEGRITY_CAPABILITIES
|
|
||||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a07bd66-4734-4af8-abcf-5a44ff7ee0e5
|
|
||||||
|
|
||||||
"\x01\x00" +
|
|
||||||
"&\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x01\x00" +
|
|
||||||
"\x20\x00" +
|
|
||||||
"\x01\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x00\x00" +
|
|
||||||
|
|
||||||
// [MS-SMB2]: SMB2_COMPRESSION_CAPABILITIES
|
|
||||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/78e0c942-ab41-472b-b117-4a95ebe88271
|
|
||||||
|
|
||||||
"\x03\x00" +
|
|
||||||
"\x0e\x00" +
|
|
||||||
"\x00\x00\x00\x00" +
|
|
||||||
"\x01\x00" + //CompressionAlgorithmCount
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x01\x00\x00\x00" +
|
|
||||||
"\x01\x00" + //LZNT1
|
|
||||||
"\x00\x00" +
|
|
||||||
"\x00\x00\x00\x00"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数
|
|
||||||
func SmbGhost(info *common.HostInfo) error {
|
|
||||||
// 如果开启了暴力破解模式,跳过该检测
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行实际的SMB Ghost漏洞扫描
|
|
||||||
err := SmbGhostScan(info)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmbGhostScan 执行具体的SMB Ghost漏洞检测逻辑
|
|
||||||
func SmbGhostScan(info *common.HostInfo) error {
|
|
||||||
// 设置扫描参数
|
|
||||||
ip := info.Host
|
|
||||||
port := 445 // SMB服务默认端口
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
|
|
||||||
// 构造目标地址
|
|
||||||
addr := fmt.Sprintf("%s:%v", ip, port)
|
|
||||||
|
|
||||||
// 建立TCP连接
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close() // 确保连接最终被关闭
|
|
||||||
|
|
||||||
// 发送SMB协议探测数据包
|
|
||||||
if _, err = conn.Write([]byte(pkt)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 准备接收响应
|
|
||||||
buff := make([]byte, 1024)
|
|
||||||
|
|
||||||
// 设置读取超时
|
|
||||||
if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应数据
|
|
||||||
n, err := conn.Read(buff)
|
|
||||||
if err != nil || n == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分析响应数据,检测是否存在漏洞
|
|
||||||
// 检查条件:
|
|
||||||
// 1. 响应包含"Public"字符串
|
|
||||||
// 2. 响应长度大于等于76字节
|
|
||||||
// 3. 特征字节匹配 (0x11,0x03) 和 (0x02,0x00)
|
|
||||||
if bytes.Contains(buff[:n], []byte("Public")) &&
|
|
||||||
len(buff[:n]) >= 76 &&
|
|
||||||
bytes.Equal(buff[72:74], []byte{0x11, 0x03}) &&
|
|
||||||
bytes.Equal(buff[74:76], []byte{0x02, 0x00}) {
|
|
||||||
|
|
||||||
// 发现漏洞,记录结果
|
|
||||||
result := fmt.Sprintf("%v CVE-2020-0796 SmbGhost Vulnerable", ip)
|
|
||||||
common.LogSuccess(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,770 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TelnetCredential 表示一个Telnet凭据
|
|
||||||
type TelnetCredential struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TelnetScanResult 表示Telnet扫描结果
|
|
||||||
type TelnetScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential TelnetCredential
|
|
||||||
NoAuth bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// TelnetScan 执行Telnet服务扫描和密码爆破
|
|
||||||
func TelnetScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建凭据列表
|
|
||||||
var credentials []TelnetCredential
|
|
||||||
for _, user := range common.Userdict["telnet"] {
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
|
||||||
credentials = append(credentials, TelnetCredential{
|
|
||||||
Username: user,
|
|
||||||
Password: actualPass,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
|
||||||
len(common.Userdict["telnet"]), len(common.Passwords), len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentTelnetScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveTelnetResult(info, target, result)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Telnet扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentTelnetScan 并发扫描Telnet服务
|
|
||||||
func concurrentTelnetScan(ctx context.Context, info *common.HostInfo, credentials []TelnetCredential, timeoutSeconds int64, maxRetries int) *TelnetScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *TelnetScanResult, 1)
|
|
||||||
workChan := make(chan TelnetCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryTelnetCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success || result.NoAuth {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据或无需认证,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && (result.Success || result.NoAuth) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("Telnet并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryTelnetCredential 尝试单个Telnet凭据
|
|
||||||
func tryTelnetCredential(ctx context.Context, info *common.HostInfo, credential TelnetCredential, timeoutSeconds int64, maxRetries int) *TelnetScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &TelnetScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
resultChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
noAuth bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 设置单个连接超时
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
go func() {
|
|
||||||
defer cancel()
|
|
||||||
noAuth, err := telnetConnWithContext(connCtx, info, credential.Username, credential.Password)
|
|
||||||
select {
|
|
||||||
case <-connCtx.Done():
|
|
||||||
// 连接已超时或取消
|
|
||||||
case resultChan <- struct {
|
|
||||||
success bool
|
|
||||||
noAuth bool
|
|
||||||
err error
|
|
||||||
}{err == nil, noAuth, err}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或超时
|
|
||||||
var success bool
|
|
||||||
var noAuth bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
select {
|
|
||||||
case result := <-resultChan:
|
|
||||||
success = result.success
|
|
||||||
noAuth = result.noAuth
|
|
||||||
err = result.err
|
|
||||||
case <-connCtx.Done():
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
// 全局超时
|
|
||||||
return &TelnetScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: ctx.Err(),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 单个连接超时
|
|
||||||
err = fmt.Errorf("连接超时")
|
|
||||||
}
|
|
||||||
|
|
||||||
if noAuth {
|
|
||||||
return &TelnetScanResult{
|
|
||||||
Success: false,
|
|
||||||
NoAuth: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &TelnetScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &TelnetScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// telnetConnWithContext 带上下文的Telnet连接尝试
|
|
||||||
func telnetConnWithContext(ctx context.Context, info *common.HostInfo, user, pass string) (bool, error) {
|
|
||||||
// 创建TCP连接(使用支持context的socks代理)
|
|
||||||
conn, err := common.WrapperTcpWithContext(ctx, "tcp", fmt.Sprintf("%s:%s", info.Host, info.Ports))
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &TelnetClient{
|
|
||||||
IPAddr: info.Host,
|
|
||||||
Port: info.Ports,
|
|
||||||
UserName: user,
|
|
||||||
Password: pass,
|
|
||||||
conn: conn,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置连接关闭
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
// 检查上下文是否已取消
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化连接
|
|
||||||
client.init()
|
|
||||||
|
|
||||||
client.ServerType = client.MakeServerType()
|
|
||||||
|
|
||||||
if client.ServerType == UnauthorizedAccess {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = client.Login()
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveTelnetResult 保存Telnet扫描结果
|
|
||||||
func saveTelnetResult(info *common.HostInfo, target string, result *TelnetScanResult) {
|
|
||||||
var successMsg string
|
|
||||||
var details map[string]interface{}
|
|
||||||
|
|
||||||
if result.NoAuth {
|
|
||||||
successMsg = fmt.Sprintf("Telnet服务 %s 无需认证", target)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "telnet",
|
|
||||||
"type": "unauthorized-access",
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
successMsg = fmt.Sprintf("Telnet服务 %s 用户名:%v 密码:%v",
|
|
||||||
target, result.Credential.Username, result.Credential.Password)
|
|
||||||
details = map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "telnet",
|
|
||||||
"type": "weak-password",
|
|
||||||
"username": result.Credential.Username,
|
|
||||||
"password": result.Credential.Password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(successMsg)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: details,
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TelnetClient Telnet客户端结构体
|
|
||||||
type TelnetClient struct {
|
|
||||||
IPAddr string // 服务器IP地址
|
|
||||||
Port string // 服务器端口
|
|
||||||
UserName string // 用户名
|
|
||||||
Password string // 密码
|
|
||||||
conn net.Conn // 网络连接
|
|
||||||
LastResponse string // 最近一次响应内容
|
|
||||||
ServerType int // 服务器类型
|
|
||||||
}
|
|
||||||
|
|
||||||
// init 初始化Telnet连接
|
|
||||||
func (c *TelnetClient) init() {
|
|
||||||
// 启动后台goroutine处理服务器响应
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// 读取服务器响应
|
|
||||||
buf, err := c.read()
|
|
||||||
if err != nil {
|
|
||||||
// 处理连接关闭和EOF情况
|
|
||||||
if strings.Contains(err.Error(), "closed") ||
|
|
||||||
strings.Contains(err.Error(), "EOF") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理响应数据
|
|
||||||
displayBuf, commandList := c.SerializationResponse(buf)
|
|
||||||
|
|
||||||
if len(commandList) > 0 {
|
|
||||||
// 有命令需要回复
|
|
||||||
replyBuf := c.MakeReplyFromList(commandList)
|
|
||||||
c.LastResponse += string(displayBuf)
|
|
||||||
_ = c.write(replyBuf)
|
|
||||||
} else {
|
|
||||||
// 仅保存显示内容
|
|
||||||
c.LastResponse += string(displayBuf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待连接初始化完成
|
|
||||||
time.Sleep(time.Second * 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteContext 写入数据到Telnet连接
|
|
||||||
func (c *TelnetClient) WriteContext(s string) {
|
|
||||||
// 写入字符串并添加回车及空字符
|
|
||||||
_ = c.write([]byte(s + "\x0d\x00"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadContext 读取Telnet连接返回的内容
|
|
||||||
func (c *TelnetClient) ReadContext() string {
|
|
||||||
// 读取完成后清空缓存
|
|
||||||
defer func() { c.Clear() }()
|
|
||||||
|
|
||||||
// 等待响应
|
|
||||||
if c.LastResponse == "" {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理特殊字符
|
|
||||||
c.LastResponse = strings.ReplaceAll(c.LastResponse, "\x0d\x00", "")
|
|
||||||
c.LastResponse = strings.ReplaceAll(c.LastResponse, "\x0d\x0a", "\n")
|
|
||||||
|
|
||||||
return c.LastResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// Netloc 获取网络地址字符串
|
|
||||||
func (c *TelnetClient) Netloc() string {
|
|
||||||
return fmt.Sprintf("%s:%s", c.IPAddr, c.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭Telnet连接
|
|
||||||
func (c *TelnetClient) Close() {
|
|
||||||
if c.conn != nil {
|
|
||||||
c.conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SerializationResponse 解析Telnet响应数据
|
|
||||||
func (c *TelnetClient) SerializationResponse(responseBuf []byte) (displayBuf []byte, commandList [][]byte) {
|
|
||||||
for {
|
|
||||||
// 查找IAC命令标记
|
|
||||||
index := bytes.IndexByte(responseBuf, IAC)
|
|
||||||
if index == -1 || len(responseBuf)-index < 2 {
|
|
||||||
displayBuf = append(displayBuf, responseBuf...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取选项字符
|
|
||||||
ch := responseBuf[index+1]
|
|
||||||
|
|
||||||
// 处理连续的IAC
|
|
||||||
if ch == IAC {
|
|
||||||
displayBuf = append(displayBuf, responseBuf[:index]...)
|
|
||||||
responseBuf = responseBuf[index+1:]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理DO/DONT/WILL/WONT命令
|
|
||||||
if ch == DO || ch == DONT || ch == WILL || ch == WONT {
|
|
||||||
commandBuf := responseBuf[index : index+3]
|
|
||||||
commandList = append(commandList, commandBuf)
|
|
||||||
displayBuf = append(displayBuf, responseBuf[:index]...)
|
|
||||||
responseBuf = responseBuf[index+3:]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理子协商命令
|
|
||||||
if ch == SB {
|
|
||||||
displayBuf = append(displayBuf, responseBuf[:index]...)
|
|
||||||
seIndex := bytes.IndexByte(responseBuf, SE)
|
|
||||||
if seIndex != -1 && seIndex > index {
|
|
||||||
commandList = append(commandList, responseBuf[index:seIndex+1])
|
|
||||||
responseBuf = responseBuf[seIndex+1:]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayBuf, commandList
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeReplyFromList 处理命令列表并生成回复
|
|
||||||
func (c *TelnetClient) MakeReplyFromList(list [][]byte) []byte {
|
|
||||||
var reply []byte
|
|
||||||
for _, command := range list {
|
|
||||||
reply = append(reply, c.MakeReply(command)...)
|
|
||||||
}
|
|
||||||
return reply
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeReply 根据命令生成对应的回复
|
|
||||||
func (c *TelnetClient) MakeReply(command []byte) []byte {
|
|
||||||
// 命令至少需要3字节
|
|
||||||
if len(command) < 3 {
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
|
|
||||||
verb := command[1] // 动作类型
|
|
||||||
option := command[2] // 选项码
|
|
||||||
|
|
||||||
// 处理回显(ECHO)和抑制继续进行(SGA)选项
|
|
||||||
if option == ECHO || option == SGA {
|
|
||||||
switch verb {
|
|
||||||
case DO:
|
|
||||||
return []byte{IAC, WILL, option}
|
|
||||||
case DONT:
|
|
||||||
return []byte{IAC, WONT, option}
|
|
||||||
case WILL:
|
|
||||||
return []byte{IAC, DO, option}
|
|
||||||
case WONT:
|
|
||||||
return []byte{IAC, DONT, option}
|
|
||||||
case SB:
|
|
||||||
// 处理子协商命令
|
|
||||||
// 命令格式: IAC + SB + option + modifier + IAC + SE
|
|
||||||
if len(command) >= 4 {
|
|
||||||
modifier := command[3]
|
|
||||||
if modifier == ECHO {
|
|
||||||
return []byte{IAC, SB, option, BINARY, IAC, SE}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 处理其他选项 - 拒绝所有请求
|
|
||||||
switch verb {
|
|
||||||
case DO, DONT:
|
|
||||||
return []byte{IAC, WONT, option}
|
|
||||||
case WILL, WONT:
|
|
||||||
return []byte{IAC, DONT, option}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// read 从Telnet连接读取数据
|
|
||||||
func (c *TelnetClient) read() ([]byte, error) {
|
|
||||||
var buf [2048]byte
|
|
||||||
// 设置读取超时为2秒
|
|
||||||
_ = c.conn.SetReadDeadline(time.Now().Add(time.Second * 2))
|
|
||||||
n, err := c.conn.Read(buf[0:])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buf[:n], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// write 向Telnet连接写入数据
|
|
||||||
func (c *TelnetClient) write(buf []byte) error {
|
|
||||||
// 设置写入超时
|
|
||||||
_ = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 3))
|
|
||||||
|
|
||||||
_, err := c.conn.Write(buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// 写入后短暂延迟,让服务器有时间处理
|
|
||||||
time.Sleep(TIME_DELAY_AFTER_WRITE)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login 根据服务器类型执行登录
|
|
||||||
func (c *TelnetClient) Login() error {
|
|
||||||
switch c.ServerType {
|
|
||||||
case Closed:
|
|
||||||
return errors.New("service is disabled")
|
|
||||||
case UnauthorizedAccess:
|
|
||||||
return nil
|
|
||||||
case OnlyPassword:
|
|
||||||
return c.LogBaserOnlyPassword()
|
|
||||||
case UsernameAndPassword:
|
|
||||||
return c.LogBaserUsernameAndPassword()
|
|
||||||
default:
|
|
||||||
return errors.New("unknown server type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeServerType 通过分析服务器响应判断服务器类型
|
|
||||||
func (c *TelnetClient) MakeServerType() int {
|
|
||||||
responseString := c.ReadContext()
|
|
||||||
|
|
||||||
// 空响应情况
|
|
||||||
if responseString == "" {
|
|
||||||
return Closed
|
|
||||||
}
|
|
||||||
|
|
||||||
response := strings.Split(responseString, "\n")
|
|
||||||
if len(response) == 0 {
|
|
||||||
return Closed
|
|
||||||
}
|
|
||||||
|
|
||||||
lastLine := strings.ToLower(response[len(response)-1])
|
|
||||||
|
|
||||||
// 检查是否需要用户名和密码
|
|
||||||
if containsAny(lastLine, []string{"user", "name", "login", "account", "用户名", "登录"}) {
|
|
||||||
return UsernameAndPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否只需要密码
|
|
||||||
if strings.Contains(lastLine, "pass") {
|
|
||||||
return OnlyPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否无需认证的情况
|
|
||||||
if isNoAuthRequired(lastLine) || c.isLoginSucceed(responseString) {
|
|
||||||
return UnauthorizedAccess
|
|
||||||
}
|
|
||||||
|
|
||||||
return Closed
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:检查字符串是否包含任意给定子串
|
|
||||||
func containsAny(s string, substrings []string) bool {
|
|
||||||
for _, sub := range substrings {
|
|
||||||
if strings.Contains(s, sub) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:检查是否无需认证
|
|
||||||
func isNoAuthRequired(line string) bool {
|
|
||||||
patterns := []string{
|
|
||||||
`^/ #.*`,
|
|
||||||
`^<[A-Za-z0-9_]+>`,
|
|
||||||
`^#`,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
if regexp.MustCompile(pattern).MatchString(line) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogBaserOnlyPassword 处理只需密码的登录
|
|
||||||
func (c *TelnetClient) LogBaserOnlyPassword() error {
|
|
||||||
c.Clear() // 清空之前的响应
|
|
||||||
|
|
||||||
// 发送密码并等待响应
|
|
||||||
c.WriteContext(c.Password)
|
|
||||||
time.Sleep(time.Second * 2)
|
|
||||||
|
|
||||||
// 验证登录结果
|
|
||||||
responseString := c.ReadContext()
|
|
||||||
if c.isLoginFailed(responseString) {
|
|
||||||
return errors.New("login failed")
|
|
||||||
}
|
|
||||||
if c.isLoginSucceed(responseString) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("login failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogBaserUsernameAndPassword 处理需要用户名和密码的登录
|
|
||||||
func (c *TelnetClient) LogBaserUsernameAndPassword() error {
|
|
||||||
// 发送用户名
|
|
||||||
c.WriteContext(c.UserName)
|
|
||||||
time.Sleep(time.Second * 2)
|
|
||||||
c.Clear()
|
|
||||||
|
|
||||||
// 发送密码
|
|
||||||
c.WriteContext(c.Password)
|
|
||||||
time.Sleep(time.Second * 3)
|
|
||||||
|
|
||||||
// 验证登录结果
|
|
||||||
responseString := c.ReadContext()
|
|
||||||
if c.isLoginFailed(responseString) {
|
|
||||||
return errors.New("login failed")
|
|
||||||
}
|
|
||||||
if c.isLoginSucceed(responseString) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("login failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear 清空最近一次响应
|
|
||||||
func (c *TelnetClient) Clear() {
|
|
||||||
c.LastResponse = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录失败的关键词列表
|
|
||||||
var loginFailedString = []string{
|
|
||||||
"wrong",
|
|
||||||
"invalid",
|
|
||||||
"fail",
|
|
||||||
"incorrect",
|
|
||||||
"error",
|
|
||||||
}
|
|
||||||
|
|
||||||
// isLoginFailed 检查是否登录失败
|
|
||||||
func (c *TelnetClient) isLoginFailed(responseString string) bool {
|
|
||||||
responseString = strings.ToLower(responseString)
|
|
||||||
|
|
||||||
// 空响应视为失败
|
|
||||||
if responseString == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查失败关键词
|
|
||||||
for _, str := range loginFailedString {
|
|
||||||
if strings.Contains(responseString, str) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否仍在要求输入凭证
|
|
||||||
patterns := []string{
|
|
||||||
"(?is).*pass(word)?:$",
|
|
||||||
"(?is).*user(name)?:$",
|
|
||||||
"(?is).*login:$",
|
|
||||||
}
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
if regexp.MustCompile(pattern).MatchString(responseString) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isLoginSucceed 检查是否登录成功
|
|
||||||
func (c *TelnetClient) isLoginSucceed(responseString string) bool {
|
|
||||||
// 空响应视为失败
|
|
||||||
if responseString == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取最后一行响应
|
|
||||||
lines := strings.Split(responseString, "\n")
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
lastLine := lines[len(lines)-1]
|
|
||||||
|
|
||||||
// 检查命令提示符
|
|
||||||
if regexp.MustCompile("^[#$>].*").MatchString(lastLine) ||
|
|
||||||
regexp.MustCompile("^<[a-zA-Z0-9_]+>.*").MatchString(lastLine) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查last login信息
|
|
||||||
if regexp.MustCompile("(?:s)last login").MatchString(responseString) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送测试命令验证
|
|
||||||
c.Clear()
|
|
||||||
c.WriteContext("?")
|
|
||||||
time.Sleep(time.Second * 2)
|
|
||||||
responseString = c.ReadContext()
|
|
||||||
|
|
||||||
// 检查响应长度
|
|
||||||
if strings.Count(responseString, "\n") > 6 || len([]rune(responseString)) > 100 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Telnet协议常量定义
|
|
||||||
const (
|
|
||||||
// 写入操作后的延迟时间
|
|
||||||
TIME_DELAY_AFTER_WRITE = 300 * time.Millisecond
|
|
||||||
|
|
||||||
// Telnet基础控制字符
|
|
||||||
IAC = byte(255) // 解释为命令(Interpret As Command)
|
|
||||||
DONT = byte(254) // 请求对方停止执行某选项
|
|
||||||
DO = byte(253) // 请求对方执行某选项
|
|
||||||
WONT = byte(252) // 拒绝执行某选项
|
|
||||||
WILL = byte(251) // 同意执行某选项
|
|
||||||
|
|
||||||
// 子协商相关控制字符
|
|
||||||
SB = byte(250) // 子协商开始(Subnegotiation Begin)
|
|
||||||
SE = byte(240) // 子协商结束(Subnegotiation End)
|
|
||||||
|
|
||||||
// 特殊功能字符
|
|
||||||
NULL = byte(0) // 空字符
|
|
||||||
EOF = byte(236) // 文档结束
|
|
||||||
SUSP = byte(237) // 暂停进程
|
|
||||||
ABORT = byte(238) // 停止进程
|
|
||||||
REOR = byte(239) // 记录结束
|
|
||||||
|
|
||||||
// Telnet选项代码
|
|
||||||
BINARY = byte(0) // 8位数据通道
|
|
||||||
ECHO = byte(1) // 回显
|
|
||||||
SGA = byte(3) // 禁止继续
|
|
||||||
|
|
||||||
// 服务器类型常量定义
|
|
||||||
Closed = iota // 连接关闭
|
|
||||||
UnauthorizedAccess // 无需认证
|
|
||||||
OnlyPassword // 仅需密码
|
|
||||||
UsernameAndPassword // 需要用户名和密码
|
|
||||||
)
|
|
274
Plugins/VNC.go
274
Plugins/VNC.go
@ -1,274 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-vnc"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
)
|
|
||||||
|
|
||||||
// VncCredential 表示VNC凭据
|
|
||||||
type VncCredential struct {
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// VncScanResult 表示VNC扫描结果
|
|
||||||
type VncScanResult struct {
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
Credential VncCredential
|
|
||||||
}
|
|
||||||
|
|
||||||
func VncScan(info *common.HostInfo) error {
|
|
||||||
if common.DisableBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
|
||||||
|
|
||||||
// 设置全局超时上下文
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// 构建密码列表
|
|
||||||
var credentials []VncCredential
|
|
||||||
for _, pass := range common.Passwords {
|
|
||||||
credentials = append(credentials, VncCredential{Password: pass})
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug(fmt.Sprintf("开始尝试密码组合 (总密码数: %d)", len(credentials)))
|
|
||||||
|
|
||||||
// 使用工作池并发扫描
|
|
||||||
result := concurrentVncScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
|
||||||
if result != nil {
|
|
||||||
// 记录成功结果
|
|
||||||
saveVncResult(info, target, result.Credential)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否因为全局超时而退出
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("VNC扫描全局超时")
|
|
||||||
return fmt.Errorf("全局超时")
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个密码", len(credentials)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// concurrentVncScan 并发扫描VNC服务
|
|
||||||
func concurrentVncScan(ctx context.Context, info *common.HostInfo, credentials []VncCredential, timeoutSeconds int64, maxRetries int) *VncScanResult {
|
|
||||||
// 使用ModuleThreadNum控制并发数
|
|
||||||
maxConcurrent := common.ModuleThreadNum
|
|
||||||
if maxConcurrent <= 0 {
|
|
||||||
maxConcurrent = 10 // 默认值
|
|
||||||
}
|
|
||||||
if maxConcurrent > len(credentials) {
|
|
||||||
maxConcurrent = len(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建工作池
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resultChan := make(chan *VncScanResult, 1)
|
|
||||||
workChan := make(chan VncCredential, maxConcurrent)
|
|
||||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
// 启动工作协程
|
|
||||||
for i := 0; i < maxConcurrent; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for credential := range workChan {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
result := tryVncCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
|
||||||
if result.Success {
|
|
||||||
select {
|
|
||||||
case resultChan <- result:
|
|
||||||
scanCancel() // 找到有效凭据,取消其他工作
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送工作
|
|
||||||
go func() {
|
|
||||||
for i, cred := range credentials {
|
|
||||||
select {
|
|
||||||
case <-scanCtx.Done():
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试密码: %s", i+1, len(credentials), cred.Password))
|
|
||||||
workChan <- cred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(workChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待结果或完成
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(resultChan)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取结果,考虑全局超时
|
|
||||||
select {
|
|
||||||
case result, ok := <-resultChan:
|
|
||||||
if ok && result != nil && result.Success {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
common.LogDebug("VNC并发扫描全局超时")
|
|
||||||
scanCancel() // 确保取消所有未完成工作
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryVncCredential 尝试单个VNC凭据
|
|
||||||
func tryVncCredential(ctx context.Context, info *common.HostInfo, credential VncCredential, timeoutSeconds int64, maxRetries int) *VncScanResult {
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
for retry := 0; retry < maxRetries; retry++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return &VncScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("全局超时"),
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if retry > 0 {
|
|
||||||
common.LogDebug(fmt.Sprintf("第%d次重试密码: %s", retry+1, credential.Password))
|
|
||||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连接超时上下文
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
|
||||||
success, err := VncConn(connCtx, info, credential.Password)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
if success {
|
|
||||||
return &VncScanResult{
|
|
||||||
Success: true,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否需要重试
|
|
||||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
|
||||||
break // 不需要重试的错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &VncScanResult{
|
|
||||||
Success: false,
|
|
||||||
Error: lastErr,
|
|
||||||
Credential: credential,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VncConn 尝试建立VNC连接
|
|
||||||
func VncConn(ctx context.Context, info *common.HostInfo, pass string) (bool, error) {
|
|
||||||
Host, Port := info.Host, info.Ports
|
|
||||||
timeout := time.Duration(common.Timeout) * time.Second
|
|
||||||
|
|
||||||
// 使用带上下文的TCP连接
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%s", Host, Port), timeout)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 设置读写超时
|
|
||||||
if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建完成通道
|
|
||||||
doneChan := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在协程中处理VNC认证
|
|
||||||
go func() {
|
|
||||||
// 配置VNC客户端
|
|
||||||
config := &vnc.ClientConfig{
|
|
||||||
Auth: []vnc.ClientAuth{
|
|
||||||
&vnc.PasswordAuth{
|
|
||||||
Password: pass,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试VNC认证
|
|
||||||
client, err := vnc.Client(conn, config)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case doneChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{false, err}:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 认证成功
|
|
||||||
defer client.Close()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case doneChan <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{true, nil}:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待认证结果或上下文取消
|
|
||||||
select {
|
|
||||||
case result := <-doneChan:
|
|
||||||
return result.success, result.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveVncResult 保存VNC扫描结果
|
|
||||||
func saveVncResult(info *common.HostInfo, target string, credential VncCredential) {
|
|
||||||
successLog := fmt.Sprintf("vnc://%s 密码: %v", target, credential.Password)
|
|
||||||
common.LogSuccess(successLog)
|
|
||||||
|
|
||||||
// 保存结果
|
|
||||||
vulnResult := &output.ScanResult{
|
|
||||||
Time: time.Now(),
|
|
||||||
Type: output.TypeVuln,
|
|
||||||
Target: info.Host,
|
|
||||||
Status: "vulnerable",
|
|
||||||
Details: map[string]interface{}{
|
|
||||||
"port": info.Ports,
|
|
||||||
"service": "vnc",
|
|
||||||
"password": credential.Password,
|
|
||||||
"type": "weak-password",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
common.SaveResult(vulnResult)
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package adapter
|
package adapters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,4 +1,4 @@
|
|||||||
package adapter
|
package adapters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -214,14 +214,14 @@ func read(text []byte, host string) error {
|
|||||||
if len(ipv4Addrs) > 0 {
|
if len(ipv4Addrs) > 0 {
|
||||||
output.WriteString("\n IPv4地址:")
|
output.WriteString("\n IPv4地址:")
|
||||||
for _, addr := range ipv4Addrs {
|
for _, addr := range ipv4Addrs {
|
||||||
output.WriteString(fmt.Sprintf("\n - %s", addr))
|
output.WriteString(fmt.Sprintf("\n └─ %s", addr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ipv6Addrs) > 0 {
|
if len(ipv6Addrs) > 0 {
|
||||||
output.WriteString("\n IPv6地址:")
|
output.WriteString("\n IPv6地址:")
|
||||||
for _, addr := range ipv6Addrs {
|
for _, addr := range ipv6Addrs {
|
||||||
output.WriteString(fmt.Sprintf("\n - %s", addr))
|
output.WriteString(fmt.Sprintf("\n └─ %s", addr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
|||||||
package elasticsearch
|
package elasticsearch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewElasticsearchPlugin 创建Elasticsearch弱密码检测插件
|
// NewElasticsearchPlugin 创建Elasticsearch弱密码检测插件
|
||||||
@ -21,7 +21,7 @@ func NewElasticsearchPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 适配器选项
|
// 适配器选项
|
||||||
options := &adapter.LegacyPluginOptions{
|
options := &adapters.LegacyPluginOptions{
|
||||||
CheckBruteFlag: true, // Elasticsearch依赖暴力破解标志
|
CheckBruteFlag: true, // Elasticsearch依赖暴力破解标志
|
||||||
IsVulnPlugin: false, // 这是服务检测插件,虽然包含安全检查
|
IsVulnPlugin: false, // 这是服务检测插件,虽然包含安全检查
|
||||||
IsInfoPlugin: true, // 包含信息收集功能
|
IsInfoPlugin: true, // 包含信息收集功能
|
||||||
@ -29,7 +29,7 @@ func NewElasticsearchPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建适配器,直接使用老版本的ElasticScan函数
|
// 创建适配器,直接使用老版本的ElasticScan函数
|
||||||
return adapter.NewLegacyPlugin(metadata, Plugins.ElasticScan, options)
|
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.ElasticScan, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init 自动注册Elasticsearch插件
|
// init 自动注册Elasticsearch插件
|
||||||
|
54
Plugins/legacy/findnet/plugin.go
Normal file
54
Plugins/legacy/findnet/plugin.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package findnet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewFindNetPlugin 创建FindNet网络发现插件
|
||||||
|
func NewFindNetPlugin() base.Plugin {
|
||||||
|
// 插件元数据
|
||||||
|
metadata := &base.PluginMetadata{
|
||||||
|
Name: "findnet",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "Windows网络接口发现和主机名解析 (通过RPC)",
|
||||||
|
Category: "information",
|
||||||
|
Ports: []int{135}, // RPC端口
|
||||||
|
Protocols: []string{"tcp"},
|
||||||
|
Tags: []string{"findnet", "rpc", "information-gathering", "windows", "network-discovery"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 适配器选项
|
||||||
|
options := &adapters.LegacyPluginOptions{
|
||||||
|
CheckBruteFlag: false, // FindNet不依赖暴力破解标志
|
||||||
|
IsVulnPlugin: false, // 这不是漏洞检测插件
|
||||||
|
IsInfoPlugin: true, // 这是信息收集插件
|
||||||
|
CustomPorts: []int{135}, // RPC端口
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建适配器,直接使用老版本的Findnet函数
|
||||||
|
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.Findnet, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// init 自动注册FindNet插件
|
||||||
|
func init() {
|
||||||
|
// 创建插件工厂
|
||||||
|
metadata := &base.PluginMetadata{
|
||||||
|
Name: "findnet",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "Windows网络接口发现和主机名解析 (通过RPC)",
|
||||||
|
Category: "information",
|
||||||
|
Ports: []int{135},
|
||||||
|
Protocols: []string{"tcp"},
|
||||||
|
Tags: []string{"findnet", "rpc", "information-gathering", "windows", "network-discovery"},
|
||||||
|
}
|
||||||
|
|
||||||
|
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||||
|
return NewFindNetPlugin()
|
||||||
|
})
|
||||||
|
|
||||||
|
base.GlobalPluginRegistry.Register("findnet", factory)
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package ms17010
|
package ms17010
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewMS17010Plugin 创建MS17010漏洞检测插件
|
// NewMS17010Plugin 创建MS17010漏洞检测插件
|
||||||
@ -21,7 +21,7 @@ func NewMS17010Plugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 适配器选项
|
// 适配器选项
|
||||||
options := &adapter.LegacyPluginOptions{
|
options := &adapters.LegacyPluginOptions{
|
||||||
CheckBruteFlag: false, // MS17010不依赖暴力破解标志
|
CheckBruteFlag: false, // MS17010不依赖暴力破解标志
|
||||||
IsVulnPlugin: true, // 这是漏洞检测插件
|
IsVulnPlugin: true, // 这是漏洞检测插件
|
||||||
IsInfoPlugin: false,
|
IsInfoPlugin: false,
|
||||||
@ -29,7 +29,7 @@ func NewMS17010Plugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建适配器,直接使用老版本的MS17010函数
|
// 创建适配器,直接使用老版本的MS17010函数
|
||||||
return adapter.NewLegacyPlugin(metadata, Plugins.MS17010, options)
|
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.MS17010, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init 自动注册MS17010插件
|
// init 自动注册MS17010插件
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package netbios
|
package netbios
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewNetBiosPlugin 创建NetBIOS信息收集插件
|
// NewNetBiosPlugin 创建NetBIOS信息收集插件
|
||||||
@ -21,7 +21,7 @@ func NewNetBiosPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 适配器选项
|
// 适配器选项
|
||||||
options := &adapter.LegacyPluginOptions{
|
options := &adapters.LegacyPluginOptions{
|
||||||
CheckBruteFlag: false, // NetBIOS信息收集不依赖暴力破解标志
|
CheckBruteFlag: false, // NetBIOS信息收集不依赖暴力破解标志
|
||||||
IsVulnPlugin: false, // 这不是漏洞检测插件
|
IsVulnPlugin: false, // 这不是漏洞检测插件
|
||||||
IsInfoPlugin: true, // 这是信息收集插件
|
IsInfoPlugin: true, // 这是信息收集插件
|
||||||
@ -29,7 +29,7 @@ func NewNetBiosPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建适配器,直接使用老版本的NetBIOS函数
|
// 创建适配器,直接使用老版本的NetBIOS函数
|
||||||
return adapter.NewLegacyPlugin(metadata, Plugins.NetBIOS, options)
|
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.NetBIOS, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init 自动注册NetBIOS插件
|
// init 自动注册NetBIOS插件
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package rdp
|
package rdp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRdpPlugin 创建RDP弱密码检测插件
|
// NewRdpPlugin 创建RDP弱密码检测插件
|
||||||
@ -21,7 +21,7 @@ func NewRdpPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 适配器选项
|
// 适配器选项
|
||||||
options := &adapter.LegacyPluginOptions{
|
options := &adapters.LegacyPluginOptions{
|
||||||
CheckBruteFlag: true, // RDP依赖暴力破解标志
|
CheckBruteFlag: true, // RDP依赖暴力破解标志
|
||||||
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
||||||
IsInfoPlugin: false, // 主要是弱密码检测
|
IsInfoPlugin: false, // 主要是弱密码检测
|
||||||
@ -29,7 +29,7 @@ func NewRdpPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建适配器,直接使用老版本的RdpScan函数
|
// 创建适配器,直接使用老版本的RdpScan函数
|
||||||
return adapter.NewLegacyPlugin(metadata, Plugins.RdpScan, options)
|
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.RdpScan, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init 自动注册RDP插件
|
// init 自动注册RDP插件
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package smb
|
package smb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSmbPlugin 创建SMB弱密码检测插件
|
// NewSmbPlugin 创建SMB弱密码检测插件
|
||||||
@ -21,7 +21,7 @@ func NewSmbPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 适配器选项
|
// 适配器选项
|
||||||
options := &adapter.LegacyPluginOptions{
|
options := &adapters.LegacyPluginOptions{
|
||||||
CheckBruteFlag: true, // SMB依赖暴力破解标志
|
CheckBruteFlag: true, // SMB依赖暴力破解标志
|
||||||
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
||||||
IsInfoPlugin: true, // 包含信息收集功能
|
IsInfoPlugin: true, // 包含信息收集功能
|
||||||
@ -29,7 +29,7 @@ func NewSmbPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建适配器,直接使用老版本的SmbScan函数
|
// 创建适配器,直接使用老版本的SmbScan函数
|
||||||
return adapter.NewLegacyPlugin(metadata, Plugins.SmbScan, options)
|
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.SmbScan, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init 自动注册SMB插件
|
// init 自动注册SMB插件
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package smb2
|
package smb2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSmb2Plugin 创建SMB2弱密码检测插件
|
// NewSmb2Plugin 创建SMB2弱密码检测插件
|
||||||
@ -21,7 +21,7 @@ func NewSmb2Plugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 适配器选项
|
// 适配器选项
|
||||||
options := &adapter.LegacyPluginOptions{
|
options := &adapters.LegacyPluginOptions{
|
||||||
CheckBruteFlag: true, // SMB2依赖暴力破解标志
|
CheckBruteFlag: true, // SMB2依赖暴力破解标志
|
||||||
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
||||||
IsInfoPlugin: true, // 包含信息收集功能
|
IsInfoPlugin: true, // 包含信息收集功能
|
||||||
@ -29,7 +29,7 @@ func NewSmb2Plugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建适配器,直接使用老版本的SmbScan2函数
|
// 创建适配器,直接使用老版本的SmbScan2函数
|
||||||
return adapter.NewLegacyPlugin(metadata, Plugins.SmbScan2, options)
|
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.SmbScan2, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init 自动注册SMB2插件
|
// init 自动注册SMB2插件
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package smbghost
|
package smbghost
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/plugins/adapter"
|
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSmbGhostPlugin 创建SMBGhost漏洞检测插件
|
// NewSmbGhostPlugin 创建SMBGhost漏洞检测插件
|
||||||
@ -21,7 +21,7 @@ func NewSmbGhostPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 适配器选项
|
// 适配器选项
|
||||||
options := &adapter.LegacyPluginOptions{
|
options := &adapters.LegacyPluginOptions{
|
||||||
CheckBruteFlag: false, // SMBGhost不依赖暴力破解标志
|
CheckBruteFlag: false, // SMBGhost不依赖暴力破解标志
|
||||||
IsVulnPlugin: true, // 这是漏洞检测插件
|
IsVulnPlugin: true, // 这是漏洞检测插件
|
||||||
IsInfoPlugin: false,
|
IsInfoPlugin: false,
|
||||||
@ -29,7 +29,7 @@ func NewSmbGhostPlugin() base.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建适配器,直接使用老版本的SmbGhost函数
|
// 创建适配器,直接使用老版本的SmbGhost函数
|
||||||
return adapter.NewLegacyPlugin(metadata, Plugins.SmbGhost, options)
|
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.SmbGhost, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init 自动注册SmbGhost插件
|
// init 自动注册SmbGhost插件
|
||||||
|
Loading…
Reference in New Issue
Block a user