mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00
Compare commits
2 Commits
da981cdbce
...
e3c14e9f8e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e3c14e9f8e | ||
![]() |
daa7fb2dcb |
@ -34,7 +34,8 @@ import (
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/elasticsearch" // 跨平台服务
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/findnet" // 网络发现
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/ms17010" // Windows SMB漏洞(但扫描器可跨平台)
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/netbios" // NetBIOS协议(主要Windows但可跨平台扫描)
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/netbios" // NetBIOS协议(主要Windows但可跨平台扫描)
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/smbinfo" // SMB信息收集(主要Windows但可跨平台扫描)
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/rdp" // RDP协议扫描(可跨平台)
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/smb" // SMB协议扫描(可跨平台)
|
||||
_ "github.com/shadow1ng/fscan/plugins/legacy/smb2" // SMBv2协议扫描(可跨平台)
|
||||
|
@ -9,35 +9,48 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// ReadBytes 从连接读取数据直到EOF或错误
|
||||
// ReadBytes 从连接读取数据直到EOF或错误 - 改进的SMB协议处理
|
||||
func ReadBytes(conn net.Conn) ([]byte, error) {
|
||||
size := 4096 // 缓冲区大小
|
||||
buf := make([]byte, size)
|
||||
var result []byte
|
||||
var lastErr error
|
||||
|
||||
// 循环读取数据
|
||||
for {
|
||||
count, err := conn.Read(buf)
|
||||
// 首先读取NetBIOS头部(4字节)来确定消息长度
|
||||
headerBuf := make([]byte, 4)
|
||||
n, err := conn.Read(headerBuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取NetBIOS头部失败: %v", err)
|
||||
}
|
||||
if n != 4 {
|
||||
return nil, fmt.Errorf("NetBIOS头部长度不足: %d", n)
|
||||
}
|
||||
|
||||
// 解析NetBIOS消息长度(大端序)
|
||||
messageLength := int(headerBuf[0])<<24 | int(headerBuf[1])<<16 | int(headerBuf[2])<<8 | int(headerBuf[3])
|
||||
|
||||
// 防止过大的消息长度(安全检查)
|
||||
if messageLength > 1024*1024 { // 1MB限制
|
||||
return nil, fmt.Errorf("消息长度过大: %d", messageLength)
|
||||
}
|
||||
|
||||
// 如果消息长度为0,只返回头部
|
||||
if messageLength == 0 {
|
||||
return headerBuf, nil
|
||||
}
|
||||
|
||||
// 读取完整消息体
|
||||
messageBuf := make([]byte, messageLength)
|
||||
totalRead := 0
|
||||
for totalRead < messageLength {
|
||||
n, err := conn.Read(messageBuf[totalRead:])
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
break
|
||||
}
|
||||
|
||||
result = append(result, buf[0:count]...)
|
||||
|
||||
// 如果读取的数据小于缓冲区,说明已经读完
|
||||
if count < size {
|
||||
break
|
||||
return nil, fmt.Errorf("读取消息体失败(已读取%d/%d字节): %v", totalRead, messageLength, err)
|
||||
}
|
||||
totalRead += n
|
||||
}
|
||||
|
||||
// 如果读到了数据,则忽略错误
|
||||
if len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return result, lastErr
|
||||
|
||||
// 返回完整消息(头部+消息体)
|
||||
result := make([]byte, 0, 4+messageLength)
|
||||
result = append(result, headerBuf...)
|
||||
result = append(result, messageBuf...)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 默认AES加密密钥
|
||||
|
@ -3,245 +3,84 @@ package Plugins
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
smbcommon "github.com/shadow1ng/fscan/plugins/legacy/smb/common"
|
||||
"github.com/shadow1ng/fscan/plugins/legacy/smb/smb1"
|
||||
"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
|
||||
}
|
||||
|
||||
// SmbScan 执行SMB1服务的认证扫描(重构版本)
|
||||
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))
|
||||
|
||||
|
||||
// 创建目标信息
|
||||
target := &smbcommon.TargetInfo{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
Domain: common.Domain,
|
||||
}
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建连接器、凭据管理器和扫描器
|
||||
connector := smb1.NewSmb1Connector()
|
||||
credMgr := smbcommon.NewPasswordCredentialManager()
|
||||
scanner := smbcommon.NewScanner()
|
||||
|
||||
// 配置扫描参数
|
||||
config := &smbcommon.ScanConfig{
|
||||
MaxConcurrent: common.ModuleThreadNum,
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
GlobalTimeout: time.Duration(common.GlobalTimeout) * time.Second,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 执行扫描
|
||||
result, err := scanner.Scan(ctx, target, connector, credMgr, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
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,
|
||||
}
|
||||
|
||||
// 处理扫描结果
|
||||
if result != nil && result.Success {
|
||||
saveSmbResult(info, result.Credential)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveSmbResult 保存SMB扫描结果
|
||||
func saveSmbResult(info *common.HostInfo, target string, credential SmbCredential) {
|
||||
func saveSmbResult(info *common.HostInfo, cred smbcommon.Credential) {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 构建结果消息
|
||||
var successMsg string
|
||||
details := map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"username": cred.Username,
|
||||
"password": cred.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
|
||||
|
||||
if common.Domain != "" {
|
||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s\\%s:%s", target, common.Domain, credential.Username, credential.Password)
|
||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s\\%s:%s",
|
||||
target, common.Domain, cred.Username, cred.Password)
|
||||
details["domain"] = common.Domain
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s:%s", target, credential.Username, credential.Password)
|
||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s:%s",
|
||||
target, cred.Username, cred.Password)
|
||||
}
|
||||
|
||||
|
||||
// 记录成功日志
|
||||
common.LogSuccess(successMsg)
|
||||
|
||||
|
||||
// 保存结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
@ -253,47 +92,3 @@ func saveSmbResult(info *common.HostInfo, target string, credential SmbCredentia
|
||||
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
|
||||
}
|
||||
|
@ -3,404 +3,79 @@ package Plugins
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
||||
smbcommon "github.com/shadow1ng/fscan/plugins/legacy/smb/common"
|
||||
"github.com/shadow1ng/fscan/plugins/legacy/smb/smb2"
|
||||
"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服务的认证扫描,支持密码和哈希两种认证方式
|
||||
// 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,
|
||||
|
||||
// 创建目标信息
|
||||
target := &smbcommon.TargetInfo{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
Domain: common.Domain,
|
||||
}
|
||||
|
||||
// 设置认证方式(哈希或密码)
|
||||
if len(hash) > 0 {
|
||||
initiator.Hash = hash
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(),
|
||||
time.Duration(common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 根据是否提供哈希选择认证模式
|
||||
var credMgr smbcommon.CredentialManager
|
||||
if len(common.HashBytes) > 0 {
|
||||
credMgr = smbcommon.NewHashCredentialManager()
|
||||
common.LogDebug(fmt.Sprintf("开始SMB2哈希认证扫描 (总用户数: %d, 总哈希数: %d)",
|
||||
len(common.Userdict["smb"]), len(common.HashBytes)))
|
||||
} else {
|
||||
initiator.Password = pass
|
||||
credMgr = smbcommon.NewPasswordCredentialManager()
|
||||
common.LogDebug(fmt.Sprintf("开始SMB2密码认证扫描 (总用户数: %d, 总密码数: %d)",
|
||||
len(common.Userdict["smb"]), len(common.Passwords)))
|
||||
}
|
||||
|
||||
// 创建SMB2会话
|
||||
dialer := &smb2.Dialer{
|
||||
Initiator: &initiator,
|
||||
|
||||
// 创建连接器和扫描器
|
||||
connector := smb2.NewSmb2Connector()
|
||||
scanner := smbcommon.NewScanner()
|
||||
|
||||
// 配置扫描参数
|
||||
config := &smbcommon.ScanConfig{
|
||||
MaxConcurrent: common.ModuleThreadNum,
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
GlobalTimeout: time.Duration(common.GlobalTimeout) * time.Second,
|
||||
}
|
||||
|
||||
// 使用context设置超时
|
||||
session, err := dialer.Dial(conn)
|
||||
|
||||
// 执行扫描
|
||||
result, err := scanner.Scan(ctx, target, connector, credMgr, config)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("SMB2会话建立失败: %v", err), nil
|
||||
return err
|
||||
}
|
||||
defer session.Logoff()
|
||||
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err(), nil
|
||||
default:
|
||||
|
||||
// 处理扫描结果
|
||||
if result != nil && result.Success {
|
||||
logSuccessfulAuth(info, result.Credential, result.Shares)
|
||||
if len(result.Shares) > 0 {
|
||||
logShareInfo(info, result.Credential, result.Shares)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取共享列表
|
||||
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
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// logSuccessfulAuth 记录成功的认证
|
||||
func logSuccessfulAuth(info *common.HostInfo, user, pass string, hash []byte) {
|
||||
credential := pass
|
||||
if len(hash) > 0 {
|
||||
func logSuccessfulAuth(info *common.HostInfo, cred smbcommon.Credential, shares []string) {
|
||||
credential := cred.Password
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
credential = common.HashValue
|
||||
}
|
||||
|
||||
|
||||
// 保存认证成功结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
@ -410,52 +85,41 @@ func logSuccessfulAuth(info *common.HostInfo, user, pass string, hash []byte) {
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb2",
|
||||
"username": user,
|
||||
"username": cred.Username,
|
||||
"domain": common.Domain,
|
||||
"type": "weak-auth",
|
||||
"credential": credential,
|
||||
"auth_type": map[bool]string{true: "hash", false: "password"}[len(hash) > 0],
|
||||
"auth_type": map[bool]string{true: "hash", false: "password"}[cred.IsHash],
|
||||
"shares": shares,
|
||||
},
|
||||
}
|
||||
common.SaveResult(result)
|
||||
|
||||
|
||||
// 控制台输出
|
||||
var msg string
|
||||
if common.Domain != "" {
|
||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s\\%s", info.Host, info.Ports, common.Domain, user)
|
||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s\\%s",
|
||||
info.Host, info.Ports, common.Domain, cred.Username)
|
||||
} else {
|
||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s", info.Host, info.Ports, user)
|
||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s",
|
||||
info.Host, info.Ports, cred.Username)
|
||||
}
|
||||
|
||||
if len(hash) > 0 {
|
||||
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
msg += fmt.Sprintf(" Hash:%s", common.HashValue)
|
||||
} else {
|
||||
msg += fmt.Sprintf(" Pass:%s", pass)
|
||||
msg += fmt.Sprintf(" Pass:%s", cred.Password)
|
||||
}
|
||||
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 {
|
||||
func logShareInfo(info *common.HostInfo, cred smbcommon.Credential, shares []string) {
|
||||
credential := cred.Password
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
credential = common.HashValue
|
||||
}
|
||||
|
||||
|
||||
// 保存共享信息结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
@ -465,28 +129,31 @@ func logShareInfo(info *common.HostInfo, user string, pass string, hash []byte,
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb2",
|
||||
"username": user,
|
||||
"username": cred.Username,
|
||||
"domain": common.Domain,
|
||||
"shares": shares,
|
||||
"credential": credential,
|
||||
"auth_type": map[bool]string{true: "hash", false: "password"}[len(hash) > 0],
|
||||
"auth_type": map[bool]string{true: "hash", false: "password"}[cred.IsHash],
|
||||
},
|
||||
}
|
||||
common.SaveResult(result)
|
||||
|
||||
|
||||
// 控制台输出
|
||||
var msg string
|
||||
if common.Domain != "" {
|
||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s\\%s", info.Host, info.Ports, common.Domain, user)
|
||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s\\%s",
|
||||
info.Host, info.Ports, common.Domain, cred.Username)
|
||||
} else {
|
||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s", info.Host, info.Ports, user)
|
||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s",
|
||||
info.Host, info.Ports, cred.Username)
|
||||
}
|
||||
|
||||
if len(hash) > 0 {
|
||||
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
msg += fmt.Sprintf(" Hash:%s", common.HashValue)
|
||||
} else {
|
||||
msg += fmt.Sprintf(" Pass:%s", pass)
|
||||
msg += fmt.Sprintf(" Pass:%s", cred.Password)
|
||||
}
|
||||
msg += fmt.Sprintf(" 共享:%v", shares)
|
||||
common.LogBase(msg)
|
||||
}
|
||||
|
||||
|
@ -15,9 +15,9 @@ func NewNetBiosPlugin() base.Plugin {
|
||||
Author: "fscan-team",
|
||||
Description: "NetBIOS信息收集和主机名解析",
|
||||
Category: "service",
|
||||
Ports: []int{139, 445}, // NetBIOS端口
|
||||
Ports: []int{137, 139}, // NetBIOS端口
|
||||
Protocols: []string{"tcp", "udp"},
|
||||
Tags: []string{"netbios", "information-gathering", "hostname", "smb"},
|
||||
Tags: []string{"netbios", "information-gathering", "hostname"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
@ -25,10 +25,10 @@ func NewNetBiosPlugin() base.Plugin {
|
||||
CheckBruteFlag: false, // NetBIOS信息收集不依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这不是漏洞检测插件
|
||||
IsInfoPlugin: true, // 这是信息收集插件
|
||||
CustomPorts: []int{139, 445}, // NetBIOS/SMB端口
|
||||
CustomPorts: []int{137, 139}, // NetBIOS端口
|
||||
}
|
||||
|
||||
// 创建适配器,直接使用老版本的NetBIOS函数
|
||||
// 创建适配器,使用NetBIOS函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.NetBIOS, options)
|
||||
}
|
||||
|
||||
@ -41,9 +41,9 @@ func init() {
|
||||
Author: "fscan-team",
|
||||
Description: "NetBIOS信息收集和主机名解析",
|
||||
Category: "service",
|
||||
Ports: []int{139, 445},
|
||||
Ports: []int{137, 139},
|
||||
Protocols: []string{"tcp", "udp"},
|
||||
Tags: []string{"netbios", "information-gathering", "hostname", "smb"},
|
||||
Tags: []string{"netbios", "information-gathering", "hostname"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
|
106
plugins/legacy/smb/common/credentials.go
Normal file
106
plugins/legacy/smb/common/credentials.go
Normal file
@ -0,0 +1,106 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// DefaultCredentialManager 默认凭据管理器实现
|
||||
type DefaultCredentialManager struct {
|
||||
credentials []Credential
|
||||
lockedUsers map[string]bool
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPasswordCredentialManager 创建密码认证凭据管理器
|
||||
func NewPasswordCredentialManager() *DefaultCredentialManager {
|
||||
mgr := &DefaultCredentialManager{
|
||||
lockedUsers: make(map[string]bool),
|
||||
}
|
||||
mgr.generatePasswordCredentials()
|
||||
return mgr
|
||||
}
|
||||
|
||||
// NewHashCredentialManager 创建哈希认证凭据管理器
|
||||
func NewHashCredentialManager() *DefaultCredentialManager {
|
||||
mgr := &DefaultCredentialManager{
|
||||
lockedUsers: make(map[string]bool),
|
||||
}
|
||||
mgr.generateHashCredentials()
|
||||
return mgr
|
||||
}
|
||||
|
||||
// generatePasswordCredentials 生成密码凭据列表
|
||||
func (m *DefaultCredentialManager) generatePasswordCredentials() {
|
||||
for _, user := range common.Userdict["smb"] {
|
||||
for _, pass := range common.Passwords {
|
||||
actualPass := strings.ReplaceAll(pass, "{user}", user)
|
||||
m.credentials = append(m.credentials, Credential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
Hash: []byte{},
|
||||
IsHash: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateHashCredentials 生成哈希凭据列表
|
||||
func (m *DefaultCredentialManager) generateHashCredentials() {
|
||||
for _, user := range common.Userdict["smb"] {
|
||||
for _, hash := range common.HashBytes {
|
||||
m.credentials = append(m.credentials, Credential{
|
||||
Username: user,
|
||||
Password: "",
|
||||
Hash: hash,
|
||||
IsHash: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateCredentials 获取所有凭据
|
||||
func (m *DefaultCredentialManager) GenerateCredentials() []Credential {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
result := make([]Credential, len(m.credentials))
|
||||
copy(result, m.credentials)
|
||||
return result
|
||||
}
|
||||
|
||||
// HandleAuthFailure 处理认证失败
|
||||
func (m *DefaultCredentialManager) HandleAuthFailure(username string, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
isLocked := strings.Contains(errMsg, "locked") ||
|
||||
strings.Contains(errMsg, "account has been automatically locked") ||
|
||||
strings.Contains(errMsg, "user account has been automatically locked")
|
||||
|
||||
if isLocked {
|
||||
m.mutex.Lock()
|
||||
m.lockedUsers[username] = true
|
||||
m.mutex.Unlock()
|
||||
|
||||
common.LogError("用户 " + username + " 已被锁定")
|
||||
}
|
||||
}
|
||||
|
||||
// IsUserLocked 检查用户是否被锁定
|
||||
func (m *DefaultCredentialManager) IsUserLocked(username string) bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.lockedUsers[username]
|
||||
}
|
||||
|
||||
// GetCredentialCount 获取凭据总数
|
||||
func (m *DefaultCredentialManager) GetCredentialCount() int {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return len(m.credentials)
|
||||
}
|
102
plugins/legacy/smb/common/errors.go
Normal file
102
plugins/legacy/smb/common/errors.go
Normal file
@ -0,0 +1,102 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 定义常见的SMB错误类型
|
||||
var (
|
||||
ErrAuthFailed = errors.New("认证失败")
|
||||
ErrAccountLocked = errors.New("账户锁定")
|
||||
ErrAccessDenied = errors.New("拒绝访问")
|
||||
ErrAccountDisabled = errors.New("账户禁用")
|
||||
ErrPasswordExpired = errors.New("密码过期")
|
||||
ErrConnectionFailed = errors.New("连接失败")
|
||||
ErrTimeout = errors.New("连接超时")
|
||||
ErrSessionDeleted = errors.New("会话断开")
|
||||
)
|
||||
|
||||
// ClassifySmbError 对SMB错误进行分类和标准化
|
||||
func ClassifySmbError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 清理错误信息中的换行符和多余空格
|
||||
errMsg := strings.TrimSpace(strings.ReplaceAll(err.Error(), "\n", " "))
|
||||
errMsgLower := strings.ToLower(errMsg)
|
||||
|
||||
// SMB1特定错误处理
|
||||
if strings.Contains(errMsg, "NT Status Error") {
|
||||
switch {
|
||||
case strings.Contains(errMsg, "STATUS_LOGON_FAILURE"):
|
||||
return ErrAuthFailed
|
||||
case strings.Contains(errMsg, "STATUS_ACCOUNT_LOCKED_OUT"):
|
||||
return ErrAccountLocked
|
||||
case strings.Contains(errMsg, "STATUS_ACCESS_DENIED"):
|
||||
return ErrAccessDenied
|
||||
case strings.Contains(errMsg, "STATUS_ACCOUNT_DISABLED"):
|
||||
return ErrAccountDisabled
|
||||
case strings.Contains(errMsg, "STATUS_PASSWORD_EXPIRED"):
|
||||
return ErrPasswordExpired
|
||||
case strings.Contains(errMsg, "STATUS_USER_SESSION_DELETED"):
|
||||
return ErrSessionDeleted
|
||||
default:
|
||||
return ErrAuthFailed
|
||||
}
|
||||
}
|
||||
|
||||
// SMB2特定错误处理
|
||||
switch {
|
||||
case strings.Contains(errMsgLower, "account has been automatically locked") ||
|
||||
strings.Contains(errMsgLower, "account has been locked") ||
|
||||
strings.Contains(errMsgLower, "user account has been automatically locked"):
|
||||
return ErrAccountLocked
|
||||
|
||||
case strings.Contains(errMsgLower, "access denied") ||
|
||||
strings.Contains(errMsgLower, "access is denied"):
|
||||
return ErrAccessDenied
|
||||
|
||||
case strings.Contains(errMsgLower, "account disabled") ||
|
||||
strings.Contains(errMsgLower, "account is disabled"):
|
||||
return ErrAccountDisabled
|
||||
|
||||
case strings.Contains(errMsgLower, "password expired") ||
|
||||
strings.Contains(errMsgLower, "password has expired"):
|
||||
return ErrPasswordExpired
|
||||
|
||||
case strings.Contains(errMsgLower, "connection refused") ||
|
||||
strings.Contains(errMsgLower, "connection failed") ||
|
||||
strings.Contains(errMsgLower, "no connection could be made"):
|
||||
return ErrConnectionFailed
|
||||
|
||||
case strings.Contains(errMsgLower, "timeout") ||
|
||||
strings.Contains(errMsgLower, "timed out"):
|
||||
return ErrTimeout
|
||||
|
||||
case strings.Contains(errMsgLower, "session") && strings.Contains(errMsgLower, "deleted"):
|
||||
return ErrSessionDeleted
|
||||
|
||||
case strings.Contains(errMsgLower, "logon failure") ||
|
||||
strings.Contains(errMsgLower, "authentication failed") ||
|
||||
strings.Contains(errMsgLower, "login failed"):
|
||||
return ErrAuthFailed
|
||||
}
|
||||
|
||||
// 默认返回原始错误
|
||||
return err
|
||||
}
|
||||
|
||||
// IsAccountLockError 判断是否为账户锁定错误
|
||||
func IsAccountLockError(err error) bool {
|
||||
return errors.Is(err, ErrAccountLocked) ||
|
||||
strings.Contains(strings.ToLower(err.Error()), "locked")
|
||||
}
|
||||
|
||||
// IsFatalError 判断是否为致命错误(应该停止尝试该用户)
|
||||
func IsFatalError(err error) bool {
|
||||
return errors.Is(err, ErrAccountLocked) ||
|
||||
errors.Is(err, ErrAccountDisabled) ||
|
||||
errors.Is(err, ErrPasswordExpired)
|
||||
}
|
78
plugins/legacy/smb/common/interfaces.go
Normal file
78
plugins/legacy/smb/common/interfaces.go
Normal file
@ -0,0 +1,78 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TargetInfo SMB目标信息
|
||||
type TargetInfo struct {
|
||||
Host string
|
||||
Port int
|
||||
Domain string
|
||||
}
|
||||
|
||||
// Credential SMB认证凭据
|
||||
type Credential struct {
|
||||
Username string
|
||||
Password string
|
||||
Hash []byte
|
||||
IsHash bool
|
||||
}
|
||||
|
||||
// ConnectionResult 连接结果
|
||||
type ConnectionResult struct {
|
||||
Success bool
|
||||
Shares []string
|
||||
HasAdminAccess bool
|
||||
Error error
|
||||
}
|
||||
|
||||
// ScanConfig 扫描配置
|
||||
type ScanConfig struct {
|
||||
MaxConcurrent int
|
||||
Timeout time.Duration
|
||||
GlobalTimeout time.Duration
|
||||
}
|
||||
|
||||
// SmbConnector SMB连接器接口
|
||||
type SmbConnector interface {
|
||||
// Connect 建立SMB连接并进行认证
|
||||
Connect(ctx context.Context, target *TargetInfo, cred *Credential) (*ConnectionResult, error)
|
||||
|
||||
// GetProtocolName 获取协议名称
|
||||
GetProtocolName() string
|
||||
|
||||
// GetDefaultPort 获取默认端口
|
||||
GetDefaultPort() int
|
||||
}
|
||||
|
||||
// CredentialManager 凭据管理器接口
|
||||
type CredentialManager interface {
|
||||
// GenerateCredentials 生成认证凭据列表
|
||||
GenerateCredentials() []Credential
|
||||
|
||||
// HandleAuthFailure 处理认证失败
|
||||
HandleAuthFailure(username string, err error)
|
||||
|
||||
// IsUserLocked 检查用户是否被锁定
|
||||
IsUserLocked(username string) bool
|
||||
|
||||
// GetCredentialCount 获取凭据总数
|
||||
GetCredentialCount() int
|
||||
}
|
||||
|
||||
// ScanResult 扫描结果
|
||||
type ScanResult struct {
|
||||
Success bool
|
||||
Credential Credential
|
||||
Shares []string
|
||||
Error error
|
||||
}
|
||||
|
||||
// Scanner 并发扫描器接口
|
||||
type Scanner interface {
|
||||
// Scan 执行并发扫描
|
||||
Scan(ctx context.Context, target *TargetInfo, connector SmbConnector,
|
||||
credMgr CredentialManager, config *ScanConfig) (*ScanResult, error)
|
||||
}
|
198
plugins/legacy/smb/common/scanner.go
Normal file
198
plugins/legacy/smb/common/scanner.go
Normal file
@ -0,0 +1,198 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// DefaultScanner 默认并发扫描器实现
|
||||
type DefaultScanner struct{}
|
||||
|
||||
// NewScanner 创建新的扫描器
|
||||
func NewScanner() *DefaultScanner {
|
||||
return &DefaultScanner{}
|
||||
}
|
||||
|
||||
// Scan 执行并发扫描
|
||||
func (s *DefaultScanner) Scan(ctx context.Context, target *TargetInfo,
|
||||
connector SmbConnector, credMgr CredentialManager, config *ScanConfig) (*ScanResult, error) {
|
||||
|
||||
credentials := credMgr.GenerateCredentials()
|
||||
if len(credentials) == 0 {
|
||||
return nil, fmt.Errorf("没有可用的认证凭据")
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("开始%s扫描 %s:%d (总用户数: %d, 总组合数: %d)",
|
||||
connector.GetProtocolName(), target.Host, target.Port,
|
||||
len(common.Userdict["smb"]), len(credentials)))
|
||||
|
||||
// 确定并发数
|
||||
maxConcurrent := config.MaxConcurrent
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *ScanResult, 1)
|
||||
workChan := make(chan Credential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for cred := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
// 检查用户是否已锁定
|
||||
if credMgr.IsUserLocked(cred.Username) {
|
||||
common.LogDebug(fmt.Sprintf("跳过已锁定用户: %s", cred.Username))
|
||||
continue
|
||||
}
|
||||
|
||||
// 尝试连接
|
||||
result := s.tryCredential(scanCtx, target, connector, cred, config.Timeout)
|
||||
|
||||
// 处理认证失败
|
||||
if !result.Success && result.Error != nil {
|
||||
credMgr.HandleAuthFailure(cred.Username, result.Error)
|
||||
}
|
||||
|
||||
// 如果成功,发送结果并取消其他工作
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel()
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
defer close(workChan)
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
// 跳过已锁定用户
|
||||
if credMgr.IsUserLocked(cred.Username) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 记录尝试日志
|
||||
s.logAttempt(connector.GetProtocolName(), i+1, len(credentials), cred)
|
||||
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result, nil
|
||||
}
|
||||
return nil, nil
|
||||
case <-ctx.Done():
|
||||
common.LogDebug(fmt.Sprintf("%s扫描全局超时", connector.GetProtocolName()))
|
||||
scanCancel()
|
||||
return nil, fmt.Errorf("全局超时")
|
||||
}
|
||||
}
|
||||
|
||||
// tryCredential 尝试单个凭据
|
||||
func (s *DefaultScanner) tryCredential(ctx context.Context, target *TargetInfo,
|
||||
connector SmbConnector, cred Credential, timeout time.Duration) *ScanResult {
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
// 在协程中尝试连接
|
||||
resultChan := make(chan *ConnectionResult, 1)
|
||||
go func() {
|
||||
result, err := connector.Connect(connCtx, target, &cred)
|
||||
if err != nil {
|
||||
result = &ConnectionResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- result:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.Success {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Credential: cred,
|
||||
Shares: result.Shares,
|
||||
}
|
||||
}
|
||||
|
||||
// 分类错误
|
||||
classifiedError := ClassifySmbError(result.Error)
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Error: classifiedError,
|
||||
Credential: cred,
|
||||
Shares: result.Shares,
|
||||
}
|
||||
|
||||
case <-connCtx.Done():
|
||||
var err error
|
||||
if ctx.Err() != nil {
|
||||
err = ctx.Err() // 全局超时
|
||||
} else {
|
||||
err = ErrTimeout // 连接超时
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
Credential: cred,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logAttempt 记录尝试日志
|
||||
func (s *DefaultScanner) logAttempt(protocol string, current, total int, cred Credential) {
|
||||
if cred.IsHash {
|
||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试%s: %s Hash:%s",
|
||||
current, total, protocol, cred.Username, common.HashValue))
|
||||
} else {
|
||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试%s: %s:%s",
|
||||
current, total, protocol, cred.Username, cred.Password))
|
||||
}
|
||||
}
|
93
plugins/legacy/smb/smb.go
Normal file
93
plugins/legacy/smb/smb.go
Normal file
@ -0,0 +1,93 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/plugins/legacy/smb/common"
|
||||
"github.com/shadow1ng/fscan/plugins/legacy/smb/smb1"
|
||||
fscanCommon "github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
)
|
||||
|
||||
// SmbScan 执行SMB1服务的认证扫描(重构版本)
|
||||
func SmbScan(info *fscanCommon.HostInfo) error {
|
||||
if fscanCommon.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建目标信息
|
||||
target := &common.TargetInfo{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
Domain: fscanCommon.Domain,
|
||||
}
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(),
|
||||
time.Duration(fscanCommon.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建连接器、凭据管理器和扫描器
|
||||
connector := smb1.NewSmb1Connector()
|
||||
credMgr := common.NewPasswordCredentialManager()
|
||||
scanner := common.NewScanner()
|
||||
|
||||
// 配置扫描参数
|
||||
config := &common.ScanConfig{
|
||||
MaxConcurrent: fscanCommon.ModuleThreadNum,
|
||||
Timeout: time.Duration(fscanCommon.Timeout) * time.Second,
|
||||
GlobalTimeout: time.Duration(fscanCommon.GlobalTimeout) * time.Second,
|
||||
}
|
||||
|
||||
// 执行扫描
|
||||
result, err := scanner.Scan(ctx, target, connector, credMgr, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理扫描结果
|
||||
if result != nil && result.Success {
|
||||
saveSmbResult(info, result.Credential)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveSmbResult 保存SMB扫描结果
|
||||
func saveSmbResult(info *fscanCommon.HostInfo, cred common.Credential) {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 构建结果消息
|
||||
var successMsg string
|
||||
details := map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb",
|
||||
"username": cred.Username,
|
||||
"password": cred.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
|
||||
if fscanCommon.Domain != "" {
|
||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s\\%s:%s",
|
||||
target, fscanCommon.Domain, cred.Username, cred.Password)
|
||||
details["domain"] = fscanCommon.Domain
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s:%s",
|
||||
target, cred.Username, cred.Password)
|
||||
}
|
||||
|
||||
// 记录成功日志
|
||||
fscanCommon.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeVuln,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
fscanCommon.SaveResult(result)
|
||||
}
|
86
plugins/legacy/smb/smb1/connector.go
Normal file
86
plugins/legacy/smb/smb1/connector.go
Normal file
@ -0,0 +1,86 @@
|
||||
package smb1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shadow1ng/fscan/plugins/legacy/smb/common"
|
||||
"github.com/stacktitan/smb/smb"
|
||||
)
|
||||
|
||||
// Smb1Connector SMB1连接器实现
|
||||
type Smb1Connector struct{}
|
||||
|
||||
// NewSmb1Connector 创建SMB1连接器
|
||||
func NewSmb1Connector() *Smb1Connector {
|
||||
return &Smb1Connector{}
|
||||
}
|
||||
|
||||
// Connect 建立SMB1连接并进行认证
|
||||
func (c *Smb1Connector) Connect(ctx context.Context, target *common.TargetInfo,
|
||||
cred *common.Credential) (*common.ConnectionResult, error) {
|
||||
|
||||
// SMB1不支持哈希认证
|
||||
if cred.IsHash {
|
||||
return nil, fmt.Errorf("SMB1不支持哈希认证")
|
||||
}
|
||||
|
||||
// 创建信号通道用于与原有代码兼容
|
||||
signal := make(chan struct{}, 1)
|
||||
|
||||
// 调用原有的SMB连接函数
|
||||
success, err := c.smbConnect(target.Host, target.Port, target.Domain,
|
||||
cred.Username, cred.Password, signal)
|
||||
|
||||
result := &common.ConnectionResult{
|
||||
Success: success,
|
||||
Error: err,
|
||||
}
|
||||
|
||||
// SMB1暂时不获取共享列表,保持原有行为
|
||||
if success {
|
||||
result.Shares = []string{}
|
||||
result.HasAdminAccess = true // SMB1连接成功通常意味着有访问权限
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetProtocolName 获取协议名称
|
||||
func (c *Smb1Connector) GetProtocolName() string {
|
||||
return "SMB"
|
||||
}
|
||||
|
||||
// GetDefaultPort 获取默认端口
|
||||
func (c *Smb1Connector) GetDefaultPort() int {
|
||||
return 445
|
||||
}
|
||||
|
||||
// smbConnect 原有的SMB连接实现(改进版本)
|
||||
func (c *Smb1Connector) smbConnect(host string, port int, domain, user, pass string,
|
||||
signal chan struct{}) (bool, error) {
|
||||
|
||||
options := smb.Options{
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: user,
|
||||
Password: pass,
|
||||
Domain: domain,
|
||||
Workstation: "",
|
||||
}
|
||||
|
||||
session, err := smb.NewSession(options, false)
|
||||
if err == nil {
|
||||
defer session.Close()
|
||||
if session.IsAuthenticated {
|
||||
return true, nil
|
||||
}
|
||||
return false, common.ErrAuthFailed
|
||||
}
|
||||
|
||||
// 分类和处理错误
|
||||
classifiedError := common.ClassifySmbError(err)
|
||||
|
||||
signal <- struct{}{}
|
||||
return false, classifiedError
|
||||
}
|
158
plugins/legacy/smb/smb2.go
Normal file
158
plugins/legacy/smb/smb2.go
Normal file
@ -0,0 +1,158 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/plugins/legacy/smb/common"
|
||||
"github.com/shadow1ng/fscan/plugins/legacy/smb/smb2"
|
||||
fscanCommon "github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
)
|
||||
|
||||
// SmbScan2 执行SMB2服务的认证扫描(重构版本)
|
||||
func SmbScan2(info *fscanCommon.HostInfo) error {
|
||||
if fscanCommon.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建目标信息
|
||||
target := &common.TargetInfo{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
Domain: fscanCommon.Domain,
|
||||
}
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(),
|
||||
time.Duration(fscanCommon.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 根据是否提供哈希选择认证模式
|
||||
var credMgr common.CredentialManager
|
||||
if len(fscanCommon.HashBytes) > 0 {
|
||||
credMgr = common.NewHashCredentialManager()
|
||||
fscanCommon.LogDebug(fmt.Sprintf("开始SMB2哈希认证扫描 (总用户数: %d, 总哈希数: %d)",
|
||||
len(fscanCommon.Userdict["smb"]), len(fscanCommon.HashBytes)))
|
||||
} else {
|
||||
credMgr = common.NewPasswordCredentialManager()
|
||||
fscanCommon.LogDebug(fmt.Sprintf("开始SMB2密码认证扫描 (总用户数: %d, 总密码数: %d)",
|
||||
len(fscanCommon.Userdict["smb"]), len(fscanCommon.Passwords)))
|
||||
}
|
||||
|
||||
// 创建连接器和扫描器
|
||||
connector := smb2.NewSmb2Connector()
|
||||
scanner := common.NewScanner()
|
||||
|
||||
// 配置扫描参数
|
||||
config := &common.ScanConfig{
|
||||
MaxConcurrent: fscanCommon.ModuleThreadNum,
|
||||
Timeout: time.Duration(fscanCommon.Timeout) * time.Second,
|
||||
GlobalTimeout: time.Duration(fscanCommon.GlobalTimeout) * time.Second,
|
||||
}
|
||||
|
||||
// 执行扫描
|
||||
result, err := scanner.Scan(ctx, target, connector, credMgr, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理扫描结果
|
||||
if result != nil && result.Success {
|
||||
logSuccessfulAuth(info, result.Credential, result.Shares)
|
||||
if len(result.Shares) > 0 {
|
||||
logShareInfo(info, result.Credential, result.Shares)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// logSuccessfulAuth 记录成功的认证
|
||||
func logSuccessfulAuth(info *fscanCommon.HostInfo, cred common.Credential, shares []string) {
|
||||
credential := cred.Password
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
credential = fscanCommon.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": cred.Username,
|
||||
"domain": fscanCommon.Domain,
|
||||
"type": "weak-auth",
|
||||
"credential": credential,
|
||||
"auth_type": map[bool]string{true: "hash", false: "password"}[cred.IsHash],
|
||||
"shares": shares,
|
||||
},
|
||||
}
|
||||
fscanCommon.SaveResult(result)
|
||||
|
||||
// 控制台输出
|
||||
var msg string
|
||||
if fscanCommon.Domain != "" {
|
||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s\\%s",
|
||||
info.Host, info.Ports, fscanCommon.Domain, cred.Username)
|
||||
} else {
|
||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s",
|
||||
info.Host, info.Ports, cred.Username)
|
||||
}
|
||||
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
msg += fmt.Sprintf(" Hash:%s", fscanCommon.HashValue)
|
||||
} else {
|
||||
msg += fmt.Sprintf(" Pass:%s", cred.Password)
|
||||
}
|
||||
fscanCommon.LogSuccess(msg)
|
||||
}
|
||||
|
||||
// logShareInfo 记录SMB共享信息
|
||||
func logShareInfo(info *fscanCommon.HostInfo, cred common.Credential, shares []string) {
|
||||
credential := cred.Password
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
credential = fscanCommon.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": cred.Username,
|
||||
"domain": fscanCommon.Domain,
|
||||
"shares": shares,
|
||||
"credential": credential,
|
||||
"auth_type": map[bool]string{true: "hash", false: "password"}[cred.IsHash],
|
||||
},
|
||||
}
|
||||
fscanCommon.SaveResult(result)
|
||||
|
||||
// 控制台输出
|
||||
var msg string
|
||||
if fscanCommon.Domain != "" {
|
||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s\\%s",
|
||||
info.Host, info.Ports, fscanCommon.Domain, cred.Username)
|
||||
} else {
|
||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s",
|
||||
info.Host, info.Ports, cred.Username)
|
||||
}
|
||||
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
msg += fmt.Sprintf(" Hash:%s", fscanCommon.HashValue)
|
||||
} else {
|
||||
msg += fmt.Sprintf(" Pass:%s", cred.Password)
|
||||
}
|
||||
msg += fmt.Sprintf(" 共享:%v", shares)
|
||||
fscanCommon.LogBase(msg)
|
||||
}
|
146
plugins/legacy/smb/smb2/connector.go
Normal file
146
plugins/legacy/smb/smb2/connector.go
Normal file
@ -0,0 +1,146 @@
|
||||
package smb2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/plugins/legacy/smb/common"
|
||||
fscanCommon "github.com/shadow1ng/fscan/common"
|
||||
"github.com/hirochachacha/go-smb2"
|
||||
)
|
||||
|
||||
// Smb2Connector SMB2连接器实现
|
||||
type Smb2Connector struct{}
|
||||
|
||||
// NewSmb2Connector 创建SMB2连接器
|
||||
func NewSmb2Connector() *Smb2Connector {
|
||||
return &Smb2Connector{}
|
||||
}
|
||||
|
||||
// Connect 建立SMB2连接并进行认证
|
||||
func (c *Smb2Connector) Connect(ctx context.Context, target *common.TargetInfo,
|
||||
cred *common.Credential) (*common.ConnectionResult, error) {
|
||||
|
||||
// 建立TCP连接,使用socks代理支持
|
||||
conn, err := fscanCommon.WrapperTcpWithTimeout("tcp",
|
||||
fmt.Sprintf("%s:%d", target.Host, target.Port),
|
||||
time.Duration(fscanCommon.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接失败: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 配置NTLM认证
|
||||
initiator := smb2.NTLMInitiator{
|
||||
User: cred.Username,
|
||||
Domain: target.Domain,
|
||||
}
|
||||
|
||||
// 设置认证方式(哈希或密码)
|
||||
if cred.IsHash && len(cred.Hash) > 0 {
|
||||
initiator.Hash = cred.Hash
|
||||
} else {
|
||||
initiator.Password = cred.Password
|
||||
}
|
||||
|
||||
// 创建SMB2会话
|
||||
dialer := &smb2.Dialer{
|
||||
Initiator: &initiator,
|
||||
}
|
||||
|
||||
// 建立会话
|
||||
session, err := dialer.Dial(conn)
|
||||
if err != nil {
|
||||
classifiedError := common.ClassifySmbError(err)
|
||||
return &common.ConnectionResult{
|
||||
Success: false,
|
||||
Error: classifiedError,
|
||||
}, nil
|
||||
}
|
||||
defer session.Logoff()
|
||||
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &common.ConnectionResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
}, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// 获取共享列表
|
||||
shares, err := session.ListSharenames()
|
||||
if err != nil {
|
||||
return &common.ConnectionResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("获取共享列表失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &common.ConnectionResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Shares: shares,
|
||||
}, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// 尝试验证管理员权限
|
||||
hasAdminAccess := c.validateAdminAccess(ctx, session)
|
||||
|
||||
return &common.ConnectionResult{
|
||||
Success: true,
|
||||
Shares: shares,
|
||||
HasAdminAccess: hasAdminAccess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProtocolName 获取协议名称
|
||||
func (c *Smb2Connector) GetProtocolName() string {
|
||||
return "SMB2"
|
||||
}
|
||||
|
||||
// GetDefaultPort 获取默认端口
|
||||
func (c *Smb2Connector) GetDefaultPort() int {
|
||||
return 445
|
||||
}
|
||||
|
||||
// validateAdminAccess 验证管理员权限
|
||||
func (c *Smb2Connector) validateAdminAccess(ctx context.Context, session *smb2.Session) bool {
|
||||
// 检查上下文
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
// 尝试挂载C$共享
|
||||
fs, err := session.Mount("C$")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer fs.Umount()
|
||||
|
||||
// 检查上下文
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
// 尝试读取系统文件以验证权限
|
||||
path := `Windows\win.ini`
|
||||
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return true
|
||||
}
|
701
plugins/legacy/smbinfo.go
Normal file
701
plugins/legacy/smbinfo.go
Normal file
@ -0,0 +1,701 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
)
|
||||
|
||||
// SMBInfo 主函数 - 基于最原始参考代码实现
|
||||
func SMBInfo(info *common.HostInfo) error {
|
||||
if info.Ports != "445" && info.Ports != "139" {
|
||||
return fmt.Errorf("SMBInfo插件仅支持139和445端口")
|
||||
}
|
||||
|
||||
realhost := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
conn, err := net.DialTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接失败: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 发送SMBv1第一个协商包
|
||||
_, err = conn.Write(SMBInfoNegotiateSMBv1Data1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送SMBv1协商包失败: %v", err)
|
||||
}
|
||||
|
||||
r1, err := ReadBytes(conn)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("读取SMBv1协商响应失败: %v", err))
|
||||
}
|
||||
|
||||
var result string
|
||||
|
||||
// ff534d42 SMBv1的标示
|
||||
// fe534d42 SMBv2的标示
|
||||
// 先发送探测SMBv1的payload,不支持的SMBv1的时候返回为空,然后尝试发送SMBv2的探测数据包
|
||||
if len(r1) > 0 {
|
||||
// SMBv1 路径
|
||||
result = handleSMBv1(conn, info)
|
||||
} else {
|
||||
// SMBv2 路径
|
||||
result = handleSMBv2(realhost, info)
|
||||
}
|
||||
|
||||
// 显示和保存结果
|
||||
if result != "" {
|
||||
displaySMBInfo(info, result, len(r1) > 0)
|
||||
saveSMBInfo(info, result)
|
||||
} else {
|
||||
// 即使没有详细信息也显示基本连接信息
|
||||
displayBasicSMBInfo(info)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSMBv1 处理SMBv1协议
|
||||
func handleSMBv1(conn net.Conn, info *common.HostInfo) string {
|
||||
// 发送第二个SMBv1包
|
||||
_, err := conn.Write(SMBInfoNegotiateSMBv1Data2)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("发送SMBv1 Session Setup失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
ret, err := ReadBytes(conn)
|
||||
if err != nil || len(ret) < 45 {
|
||||
common.LogDebug(fmt.Sprintf("读取SMBv1 Session Setup响应失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析blob信息
|
||||
blob_length := uint16(bytesToUint16(ret[43:45]))
|
||||
blob_count := uint16(bytesToUint16(ret[45:47]))
|
||||
|
||||
if int(blob_count) > len(ret) {
|
||||
common.LogDebug("blob_count超出数据范围")
|
||||
return ""
|
||||
}
|
||||
|
||||
gss_native := ret[47:]
|
||||
off_ntlm := bytes.Index(gss_native, []byte("NTLMSSP"))
|
||||
if off_ntlm == -1 {
|
||||
common.LogDebug("未找到NTLMSSP数据")
|
||||
return ""
|
||||
}
|
||||
|
||||
// 提取native OS和LM信息
|
||||
native := gss_native[int(blob_length):blob_count]
|
||||
ss := strings.Split(string(native), "\x00\x00")
|
||||
|
||||
var nativeOS, nativeLM string
|
||||
if len(ss) > 0 {
|
||||
nativeOS = trimName(ss[0])
|
||||
}
|
||||
if len(ss) > 1 {
|
||||
nativeLM = trimName(ss[1])
|
||||
}
|
||||
|
||||
// 解析NTLM信息
|
||||
bs := gss_native[off_ntlm:blob_length]
|
||||
ntlmInfo := parseNTLMChallenge(bs)
|
||||
|
||||
// 组合结果
|
||||
result := ntlmInfo
|
||||
if nativeOS != "" {
|
||||
result += fmt.Sprintf("NativeOS: %s\n", nativeOS)
|
||||
}
|
||||
if nativeLM != "" {
|
||||
result += fmt.Sprintf("NativeLM: %s\n", nativeLM)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// handleSMBv2 处理SMBv2协议
|
||||
func handleSMBv2(realhost string, info *common.HostInfo) string {
|
||||
conn2, err := net.DialTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("SMBv2连接失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
defer conn2.Close()
|
||||
|
||||
// 发送SMBv2第一个协商包
|
||||
_, err = conn2.Write(SMBInfoNegotiateSMBv2Data1)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("发送SMBv2协商包失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
r2, err := ReadBytes(conn2)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("读取SMBv2协商响应失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
// 根据响应构建NTLM数据包
|
||||
var ntlmSSPNegotiatev2Data []byte
|
||||
if len(r2) > 70 && hex.EncodeToString(r2[70:71]) == "03" {
|
||||
flags := []byte{0x15, 0x82, 0x08, 0xa0}
|
||||
ntlmSSPNegotiatev2Data = getNTLMSSPNegotiateData(flags)
|
||||
} else {
|
||||
flags := []byte{0x05, 0x80, 0x08, 0xa0}
|
||||
ntlmSSPNegotiatev2Data = getNTLMSSPNegotiateData(flags)
|
||||
}
|
||||
|
||||
// 发送第二个SMBv2包
|
||||
_, err = conn2.Write(SMBInfoNegotiateSMBv2Data2)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("发送SMBv2第二包失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
_, err = ReadBytes(conn2)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("读取SMBv2第二包响应失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
// 发送NTLM协商包
|
||||
_, err = conn2.Write(ntlmSSPNegotiatev2Data)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("发送SMBv2 NTLM包失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
ret, err := ReadBytes(conn2)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("读取SMBv2 NTLM响应失败: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
ntlmOff := bytes.Index(ret, []byte("NTLMSSP"))
|
||||
if ntlmOff == -1 {
|
||||
common.LogDebug("SMBv2响应中未找到NTLMSSP数据")
|
||||
return ""
|
||||
}
|
||||
|
||||
return parseNTLMChallenge(ret[ntlmOff:])
|
||||
}
|
||||
|
||||
// 原始参考代码中的数据包定义
|
||||
var SMBInfoNegotiateSMBv1Data1 = []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,
|
||||
}
|
||||
|
||||
var SMBInfoNegotiateSMBv1Data2 = []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,
|
||||
}
|
||||
|
||||
var SMBInfoNegotiateSMBv2Data1 = []byte{
|
||||
0x00, 0x00, 0x00, 0x45, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00,
|
||||
0x00, 0x00, 0x00, 0x18, 0x01, 0x48, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
|
||||
0xAC, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x02,
|
||||
0x4E, 0x54, 0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32,
|
||||
0x00, 0x02, 0x53, 0x4D, 0x42, 0x20, 0x32, 0x2E, 0x30, 0x30,
|
||||
0x32, 0x00, 0x02, 0x53, 0x4D, 0x42, 0x20, 0x32, 0x2E, 0x3F,
|
||||
0x3F, 0x3F, 0x00,
|
||||
}
|
||||
|
||||
var SMBInfoNegotiateSMBv2Data2 = []byte{
|
||||
0x00, 0x00, 0x00, 0x68, 0xFE, 0x53, 0x4D, 0x42, 0x40, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00,
|
||||
0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x10, 0x02,
|
||||
}
|
||||
|
||||
func getNTLMSSPNegotiateData(flags []byte) []byte {
|
||||
return []byte{
|
||||
0x00, 0x00, 0x00, 0x9A, 0xFE, 0x53, 0x4D, 0x42, 0x40, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x58, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x60, 0x40, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05,
|
||||
0x05, 0x02, 0xA0, 0x36, 0x30, 0x34, 0xA0, 0x0E, 0x30, 0x0C,
|
||||
0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02,
|
||||
0x02, 0x0A, 0xA2, 0x22, 0x04, 0x20, 0x4E, 0x54, 0x4C, 0x4D,
|
||||
0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
flags[0], flags[1], flags[2], flags[3],
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func bytesToUint16(b []byte) uint16 {
|
||||
return uint16(b[0]) | uint16(b[1])<<8
|
||||
}
|
||||
|
||||
func trimName(s string) string {
|
||||
return strings.Trim(strings.TrimSpace(s), "\x00")
|
||||
}
|
||||
|
||||
// NTLM AV_PAIR类型常量
|
||||
const (
|
||||
MsvAvEOL = 0x0000 // End of list
|
||||
MsvAvNbComputerName = 0x0001 // NetBIOS computer name
|
||||
MsvAvNbDomainName = 0x0002 // NetBIOS domain name
|
||||
MsvAvDnsComputerName = 0x0003 // DNS computer name
|
||||
MsvAvDnsDomainName = 0x0004 // DNS domain name
|
||||
MsvAvDnsTreeName = 0x0005 // DNS forest name
|
||||
MsvAvFlags = 0x0006 // Server flags
|
||||
MsvAvTimestamp = 0x0007 // Server timestamp
|
||||
MsvAvSingleHost = 0x0008 // Single host data
|
||||
MsvAvTargetName = 0x0009 // Target name
|
||||
MsvAvChannelBindings = 0x000A // Channel bindings
|
||||
)
|
||||
|
||||
// parseNTLMChallenge 解析NTLM Type 2 (Challenge) 消息
|
||||
func parseNTLMChallenge(data []byte) string {
|
||||
if len(data) < 32 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
|
||||
// 检查NTLM签名 "NTLMSSP\x00"
|
||||
if !bytes.Equal(data[0:8], []byte("NTLMSSP\x00")) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 检查消息类型 (应该是 Type 2 = 0x00000002)
|
||||
if len(data) < 12 {
|
||||
return ""
|
||||
}
|
||||
messageType := bytesToUint32(data[8:12])
|
||||
if messageType != 2 {
|
||||
common.LogDebug(fmt.Sprintf("非Type 2 NTLM消息, 类型: %d", messageType))
|
||||
return ""
|
||||
}
|
||||
|
||||
result.WriteString("Protocol: NTLM Type 2 (Challenge)\n")
|
||||
|
||||
// 解析Target Name (偏移12-20, 8字节的Security Buffer)
|
||||
if len(data) >= 20 {
|
||||
targetLength := bytesToUint16(data[12:14])
|
||||
targetOffset := bytesToUint32(data[16:20])
|
||||
|
||||
if targetLength > 0 && int(targetOffset) < len(data) && int(targetOffset+uint32(targetLength)) <= len(data) {
|
||||
targetName := parseUnicodeString(data[targetOffset:targetOffset+uint32(targetLength)])
|
||||
if targetName != "" {
|
||||
result.WriteString(fmt.Sprintf("Target Name: %s\n", targetName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析Flags (偏移20-24)
|
||||
if len(data) >= 24 {
|
||||
flags := bytesToUint32(data[20:24])
|
||||
parseNTLMFlags(flags, &result)
|
||||
}
|
||||
|
||||
// 解析Challenge (偏移24-32, 8字节)
|
||||
if len(data) >= 32 {
|
||||
challenge := data[24:32]
|
||||
result.WriteString(fmt.Sprintf("Server Challenge: %s\n", hex.EncodeToString(challenge)))
|
||||
}
|
||||
|
||||
// 解析Target Info (AV_PAIR结构) - 偏移44开始的Security Buffer
|
||||
if len(data) >= 52 {
|
||||
targetInfoLength := bytesToUint16(data[40:42])
|
||||
targetInfoOffset := bytesToUint32(data[44:48])
|
||||
|
||||
if targetInfoLength > 0 && int(targetInfoOffset) < len(data) &&
|
||||
int(targetInfoOffset+uint32(targetInfoLength)) <= len(data) {
|
||||
targetInfoData := data[targetInfoOffset:targetInfoOffset+uint32(targetInfoLength)]
|
||||
parseTargetInfo(targetInfoData, &result)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析OS版本信息 (如果存在, 偏移48-56)
|
||||
if len(data) >= 56 {
|
||||
// 检查是否包含版本信息 (通过flags判断)
|
||||
if len(data) >= 24 {
|
||||
flags := bytesToUint32(data[20:24])
|
||||
// NTLMSSP_NEGOTIATE_VERSION = 0x02000000
|
||||
if flags&0x02000000 != 0 && len(data) >= 56 {
|
||||
parseOSVersion(data[48:56], &result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// parseUnicodeString 解析UTF-16LE编码的字符串
|
||||
func parseUnicodeString(data []byte) string {
|
||||
if len(data)%2 != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var runes []rune
|
||||
for i := 0; i < len(data); i += 2 {
|
||||
if i+1 >= len(data) {
|
||||
break
|
||||
}
|
||||
// UTF-16LE: 低字节在前
|
||||
r := uint16(data[i]) | uint16(data[i+1])<<8
|
||||
if r == 0 {
|
||||
break
|
||||
}
|
||||
runes = append(runes, rune(r))
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// parseNTLMFlags 解析NTLM标志位
|
||||
func parseNTLMFlags(flags uint32, result *strings.Builder) {
|
||||
flagNames := map[uint32]string{
|
||||
0x00000001: "NEGOTIATE_UNICODE",
|
||||
0x00000002: "NEGOTIATE_OEM",
|
||||
0x00000004: "REQUEST_TARGET",
|
||||
0x00000010: "NEGOTIATE_SIGN",
|
||||
0x00000020: "NEGOTIATE_SEAL",
|
||||
0x00000040: "NEGOTIATE_DATAGRAM",
|
||||
0x00000080: "NEGOTIATE_LM_KEY",
|
||||
0x00000200: "NEGOTIATE_NTLM",
|
||||
0x00001000: "NEGOTIATE_DOMAIN_SUPPLIED",
|
||||
0x00002000: "NEGOTIATE_WORKSTATION_SUPPLIED",
|
||||
0x00004000: "NEGOTIATE_LOCAL_CALL",
|
||||
0x00008000: "NEGOTIATE_ALWAYS_SIGN",
|
||||
0x00010000: "TARGET_TYPE_DOMAIN",
|
||||
0x00020000: "TARGET_TYPE_SERVER",
|
||||
0x00040000: "TARGET_TYPE_SHARE",
|
||||
0x00080000: "NEGOTIATE_EXTENDED_SESSIONSECURITY",
|
||||
0x00100000: "NEGOTIATE_IDENTIFY",
|
||||
0x02000000: "NEGOTIATE_VERSION",
|
||||
0x20000000: "NEGOTIATE_128",
|
||||
0x40000000: "NEGOTIATE_KEY_EXCH",
|
||||
0x80000000: "NEGOTIATE_56",
|
||||
}
|
||||
|
||||
var activeFlags []string
|
||||
for flag, name := range flagNames {
|
||||
if flags&flag != 0 {
|
||||
activeFlags = append(activeFlags, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(activeFlags) > 0 {
|
||||
result.WriteString(fmt.Sprintf("NTLM Flags: %s\n", strings.Join(activeFlags, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
// parseTargetInfo 解析NTLM Target Information (AV_PAIR结构)
|
||||
func parseTargetInfo(data []byte, result *strings.Builder) {
|
||||
offset := 0
|
||||
|
||||
for offset+4 <= len(data) {
|
||||
// 读取AV_PAIR结构: AvId (2字节) + AvLen (2字节) + Value (AvLen字节)
|
||||
avId := bytesToUint16(data[offset:offset+2])
|
||||
avLen := bytesToUint16(data[offset+2:offset+4])
|
||||
|
||||
if avId == MsvAvEOL {
|
||||
break // 列表结束
|
||||
}
|
||||
|
||||
if offset+4+int(avLen) > len(data) {
|
||||
break // 数据不足
|
||||
}
|
||||
|
||||
value := data[offset+4:offset+4+int(avLen)]
|
||||
|
||||
switch avId {
|
||||
case MsvAvNbComputerName:
|
||||
computerName := parseUnicodeString(value)
|
||||
if computerName != "" {
|
||||
result.WriteString(fmt.Sprintf("NetBIOS Computer Name: %s\n", computerName))
|
||||
}
|
||||
case MsvAvNbDomainName:
|
||||
domainName := parseUnicodeString(value)
|
||||
if domainName != "" {
|
||||
result.WriteString(fmt.Sprintf("NetBIOS Domain Name: %s\n", domainName))
|
||||
}
|
||||
case MsvAvDnsComputerName:
|
||||
dnsComputerName := parseUnicodeString(value)
|
||||
if dnsComputerName != "" {
|
||||
result.WriteString(fmt.Sprintf("DNS Computer Name: %s\n", dnsComputerName))
|
||||
}
|
||||
case MsvAvDnsDomainName:
|
||||
dnsDomainName := parseUnicodeString(value)
|
||||
if dnsDomainName != "" {
|
||||
result.WriteString(fmt.Sprintf("DNS Domain Name: %s\n", dnsDomainName))
|
||||
}
|
||||
case MsvAvDnsTreeName:
|
||||
forestName := parseUnicodeString(value)
|
||||
if forestName != "" {
|
||||
result.WriteString(fmt.Sprintf("DNS Forest Name: %s\n", forestName))
|
||||
}
|
||||
case MsvAvFlags:
|
||||
if len(value) >= 4 {
|
||||
serverFlags := bytesToUint32(value[0:4])
|
||||
parseServerFlags(serverFlags, result)
|
||||
}
|
||||
case MsvAvTimestamp:
|
||||
if len(value) >= 8 {
|
||||
timestamp := bytesToUint64(value[0:8])
|
||||
// Windows FILETIME: 100纳秒间隔自1601年1月1日
|
||||
if timestamp > 0 {
|
||||
// 转换为Unix时间戳 (简化版本)
|
||||
unixTime := int64((timestamp - 116444736000000000) / 10000000)
|
||||
if unixTime > 0 {
|
||||
t := time.Unix(unixTime, 0)
|
||||
result.WriteString(fmt.Sprintf("Server Timestamp: %s\n", t.Format(time.RFC3339)))
|
||||
}
|
||||
}
|
||||
}
|
||||
case MsvAvTargetName:
|
||||
targetName := parseUnicodeString(value)
|
||||
if targetName != "" {
|
||||
result.WriteString(fmt.Sprintf("Target SPN: %s\n", targetName))
|
||||
}
|
||||
}
|
||||
|
||||
offset += 4 + int(avLen)
|
||||
}
|
||||
}
|
||||
|
||||
// parseServerFlags 解析服务器标志
|
||||
func parseServerFlags(flags uint32, result *strings.Builder) {
|
||||
var serverFlags []string
|
||||
|
||||
if flags&0x00000001 != 0 {
|
||||
serverFlags = append(serverFlags, "CONSTRAINED_AUTHENTICATION")
|
||||
}
|
||||
if flags&0x00000002 != 0 {
|
||||
serverFlags = append(serverFlags, "MIC_PROVIDED")
|
||||
}
|
||||
if flags&0x00000004 != 0 {
|
||||
serverFlags = append(serverFlags, "UNTRUSTED_SPN_SOURCE")
|
||||
}
|
||||
|
||||
if len(serverFlags) > 0 {
|
||||
result.WriteString(fmt.Sprintf("Server Flags: %s\n", strings.Join(serverFlags, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
// parseOSVersion 解析操作系统版本信息
|
||||
func parseOSVersion(data []byte, result *strings.Builder) {
|
||||
if len(data) < 8 {
|
||||
return
|
||||
}
|
||||
|
||||
majorVersion := data[0]
|
||||
minorVersion := data[1]
|
||||
buildNumber := bytesToUint16(data[2:4])
|
||||
reserved := bytesToUint32(data[4:8])
|
||||
|
||||
// Windows版本映射
|
||||
var osName string
|
||||
switch {
|
||||
case majorVersion == 10 && minorVersion == 0:
|
||||
if buildNumber >= 22000 {
|
||||
osName = "Windows 11"
|
||||
} else {
|
||||
osName = "Windows 10"
|
||||
}
|
||||
case majorVersion == 6 && minorVersion == 3:
|
||||
osName = "Windows 8.1 / Server 2012 R2"
|
||||
case majorVersion == 6 && minorVersion == 2:
|
||||
osName = "Windows 8 / Server 2012"
|
||||
case majorVersion == 6 && minorVersion == 1:
|
||||
osName = "Windows 7 / Server 2008 R2"
|
||||
case majorVersion == 6 && minorVersion == 0:
|
||||
osName = "Windows Vista / Server 2008"
|
||||
case majorVersion == 5 && minorVersion == 2:
|
||||
osName = "Windows XP x64 / Server 2003"
|
||||
case majorVersion == 5 && minorVersion == 1:
|
||||
osName = "Windows XP"
|
||||
case majorVersion == 5 && minorVersion == 0:
|
||||
osName = "Windows 2000"
|
||||
default:
|
||||
osName = fmt.Sprintf("Windows %d.%d", majorVersion, minorVersion)
|
||||
}
|
||||
|
||||
result.WriteString(fmt.Sprintf("OS Version: %s (Build %d)\n", osName, buildNumber))
|
||||
|
||||
if reserved != 0 {
|
||||
result.WriteString(fmt.Sprintf("OS Reserved: 0x%08X\n", reserved))
|
||||
}
|
||||
}
|
||||
|
||||
// bytesToUint32 将字节数组转换为32位无符号整数 (小端序)
|
||||
func bytesToUint32(b []byte) uint32 {
|
||||
if len(b) < 4 {
|
||||
return 0
|
||||
}
|
||||
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
||||
}
|
||||
|
||||
// bytesToUint64 将字节数组转换为64位无符号整数 (小端序)
|
||||
func bytesToUint64(b []byte) uint64 {
|
||||
if len(b) < 8 {
|
||||
return 0
|
||||
}
|
||||
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
|
||||
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
|
||||
}
|
||||
|
||||
// displaySMBInfo 显示SMB信息
|
||||
func displaySMBInfo(hostInfo *common.HostInfo, info string, isSMBv1 bool) {
|
||||
target := fmt.Sprintf("%s:%s", hostInfo.Host, hostInfo.Ports)
|
||||
smbVersion := "SMB2"
|
||||
if isSMBv1 {
|
||||
smbVersion = "SMB1"
|
||||
}
|
||||
|
||||
// 提取操作系统信息用于主显示行
|
||||
osInfo := extractOSInfo(info)
|
||||
computerName := extractComputerName(info)
|
||||
|
||||
var successMsg string
|
||||
if osInfo != "" && computerName != "" {
|
||||
successMsg = fmt.Sprintf("SMBInfo %s [%s] %s %s", target, osInfo, computerName, smbVersion)
|
||||
} else if osInfo != "" {
|
||||
successMsg = fmt.Sprintf("SMBInfo %s [%s] %s", target, osInfo, smbVersion)
|
||||
} else if computerName != "" {
|
||||
successMsg = fmt.Sprintf("SMBInfo %s %s %s", target, computerName, smbVersion)
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMBInfo %s %s", target, smbVersion)
|
||||
}
|
||||
|
||||
common.LogSuccess(successMsg)
|
||||
}
|
||||
|
||||
// extractOSInfo 从信息中提取操作系统信息
|
||||
func extractOSInfo(info string) string {
|
||||
lines := strings.Split(info, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "OS Version:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
osVersion := strings.TrimSpace(parts[1])
|
||||
// 简化OS版本显示,提取主要版本号
|
||||
if strings.Contains(osVersion, "Windows") {
|
||||
if strings.Contains(osVersion, "Build") {
|
||||
parts := strings.Split(osVersion, " (Build")
|
||||
if len(parts) > 0 {
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
}
|
||||
return osVersion
|
||||
}
|
||||
return osVersion
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "Windows版本:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "NativeOS:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractComputerName 从信息中提取计算机名
|
||||
func extractComputerName(info string) string {
|
||||
lines := strings.Split(info, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "NetBIOS Computer Name:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "NetBIOS计算机名:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "DNS Computer Name:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "DNS计算机名:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// displayBasicSMBInfo 显示基本SMB信息
|
||||
func displayBasicSMBInfo(hostInfo *common.HostInfo) {
|
||||
target := fmt.Sprintf("%s:%s", hostInfo.Host, hostInfo.Ports)
|
||||
msg := fmt.Sprintf("SMBInfo %s SMB service detected", target)
|
||||
common.LogSuccess(msg)
|
||||
}
|
||||
|
||||
// saveSMBInfo 保存SMB信息
|
||||
func saveSMBInfo(hostInfo *common.HostInfo, info string) {
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeService,
|
||||
Target: hostInfo.Host,
|
||||
Status: "open",
|
||||
Details: map[string]interface{}{
|
||||
"port": hostInfo.Ports,
|
||||
"protocol": "smb",
|
||||
"info": info,
|
||||
},
|
||||
}
|
||||
common.SaveResult(result)
|
||||
}
|
54
plugins/legacy/smbinfo/plugin.go
Normal file
54
plugins/legacy/smbinfo/plugin.go
Normal file
@ -0,0 +1,54 @@
|
||||
package smbinfo
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewSMBInfoPlugin 创建SMB信息收集插件
|
||||
func NewSMBInfoPlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "smbinfo",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "SMB协议信息收集和操作系统检测",
|
||||
Category: "service",
|
||||
Ports: []int{139, 445}, // SMB端口
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb", "information-gathering", "os-detection", "ntlm"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: false, // SMB信息收集不依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这不是漏洞检测插件
|
||||
IsInfoPlugin: true, // 这是信息收集插件
|
||||
CustomPorts: []int{139, 445}, // SMB端口
|
||||
}
|
||||
|
||||
// 创建适配器,使用SMBInfo函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.SMBInfo, options)
|
||||
}
|
||||
|
||||
// init 自动注册SMBInfo插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "smbinfo",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "SMB协议信息收集和操作系统检测",
|
||||
Category: "service",
|
||||
Ports: []int{139, 445},
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb", "information-gathering", "os-detection", "ntlm"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewSMBInfoPlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("smbinfo", factory)
|
||||
}
|
Loading…
Reference in New Issue
Block a user