mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00
refactor: 最终整理插件目录结构,分离已迁移的老版本插件
目录结构: - plugins/legacy/ - 存放已迁移到新架构的老版本插件 - adapters/legacy_plugins/ - 对应的适配器实现 - plugins/services/ - 新架构服务插件 已迁移的老版本插件: - MS17010.go, MS17010-Exp.go - MS17010漏洞检测 - SmbGhost.go - SMBGhost漏洞检测 - SMB.go, SMB2.go - SMB服务检测 - RDP.go - RDP服务检测 - NetBIOS.go - NetBIOS信息收集 - Elasticsearch.go - Elasticsearch服务检测 - Base.go - 工具函数模块 架构特点: - 老版本插件代码完全不变 - 通过适配器实现与新架构的桥接 - 清晰的职责分离和目录组织 - 为后续Web插件和本地插件整理预留空间 测试验证:✓ 所有功能正常
This commit is contained in:
parent
32223db6e3
commit
f06bd3c1ac
127
Plugins/legacy/Base.go
Normal file
127
Plugins/legacy/Base.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package Plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadBytes 从连接读取数据直到EOF或错误
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, buf[0:count]...)
|
||||||
|
|
||||||
|
// 如果读取的数据小于缓冲区,说明已经读完
|
||||||
|
if count < size {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果读到了数据,则忽略错误
|
||||||
|
if len(result) > 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认AES加密密钥
|
||||||
|
var key = "0123456789abcdef"
|
||||||
|
|
||||||
|
// AesEncrypt 使用AES-CBC模式加密字符串
|
||||||
|
func AesEncrypt(orig string, key string) (string, error) {
|
||||||
|
// 转为字节数组
|
||||||
|
origData := []byte(orig)
|
||||||
|
keyBytes := []byte(key)
|
||||||
|
|
||||||
|
// 创建加密块,要求密钥长度必须为16/24/32字节
|
||||||
|
block, err := aes.NewCipher(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建加密块失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取块大小并填充数据
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
origData = PKCS7Padding(origData, blockSize)
|
||||||
|
|
||||||
|
// 创建CBC加密模式
|
||||||
|
blockMode := cipher.NewCBCEncrypter(block, keyBytes[:blockSize])
|
||||||
|
|
||||||
|
// 加密数据
|
||||||
|
encrypted := make([]byte, len(origData))
|
||||||
|
blockMode.CryptBlocks(encrypted, origData)
|
||||||
|
|
||||||
|
// base64编码
|
||||||
|
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AesDecrypt 使用AES-CBC模式解密字符串
|
||||||
|
func AesDecrypt(crypted string, key string) (string, error) {
|
||||||
|
// base64解码
|
||||||
|
cryptedBytes, err := base64.StdEncoding.DecodeString(crypted)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("base64解码失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes := []byte(key)
|
||||||
|
|
||||||
|
// 创建解密块
|
||||||
|
block, err := aes.NewCipher(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建解密块失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建CBC解密模式
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
blockMode := cipher.NewCBCDecrypter(block, keyBytes[:blockSize])
|
||||||
|
|
||||||
|
// 解密数据
|
||||||
|
origData := make([]byte, len(cryptedBytes))
|
||||||
|
blockMode.CryptBlocks(origData, cryptedBytes)
|
||||||
|
|
||||||
|
// 去除填充
|
||||||
|
origData, err = PKCS7UnPadding(origData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("去除PKCS7填充失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(origData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCS7Padding 对数据进行PKCS7填充
|
||||||
|
func PKCS7Padding(data []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(data)%blockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(data, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCS7UnPadding 去除PKCS7填充
|
||||||
|
func PKCS7UnPadding(data []byte) ([]byte, error) {
|
||||||
|
length := len(data)
|
||||||
|
if length == 0 {
|
||||||
|
return nil, errors.New("数据长度为0")
|
||||||
|
}
|
||||||
|
|
||||||
|
padding := int(data[length-1])
|
||||||
|
if padding > length {
|
||||||
|
return nil, errors.New("填充长度无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[:length-padding], nil
|
||||||
|
}
|
307
Plugins/legacy/Elasticsearch.go
Normal file
307
Plugins/legacy/Elasticsearch.go
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
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)
|
||||||
|
}
|
1422
Plugins/legacy/MS17010-Exp.go
Normal file
1422
Plugins/legacy/MS17010-Exp.go
Normal file
File diff suppressed because it is too large
Load Diff
289
Plugins/legacy/MS17010.go
Normal file
289
Plugins/legacy/MS17010.go
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
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
|
||||||
|
}
|
400
Plugins/legacy/NetBIOS.go
Normal file
400
Plugins/legacy/NetBIOS.go
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
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
|
||||||
|
}
|
401
Plugins/legacy/RDP.go
Normal file
401
Plugins/legacy/RDP.go
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
47
Plugins/legacy/README.md
Normal file
47
Plugins/legacy/README.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Legacy Plugins
|
||||||
|
|
||||||
|
这个目录包含了已迁移到新架构的老版本插件代码。这些插件通过适配器接入新的插件系统。
|
||||||
|
|
||||||
|
## 包含的插件
|
||||||
|
|
||||||
|
### 漏洞检测类
|
||||||
|
- **MS17010.go** - MS17010 SMB远程代码执行漏洞检测 (EternalBlue)
|
||||||
|
- **MS17010-Exp.go** - MS17010漏洞利用模块
|
||||||
|
- **SmbGhost.go** - CVE-2020-0796 SMBGhost远程代码执行漏洞检测
|
||||||
|
|
||||||
|
### 服务检测类
|
||||||
|
- **SMB.go** - SMB服务弱密码检测和共享枚举
|
||||||
|
- **SMB2.go** - SMB2服务弱密码检测 (支持NTLM哈希)
|
||||||
|
- **RDP.go** - RDP远程桌面服务弱密码检测
|
||||||
|
- **Elasticsearch.go** - Elasticsearch弱密码检测和未授权访问检测
|
||||||
|
|
||||||
|
### 信息收集类
|
||||||
|
- **NetBIOS.go** - NetBIOS信息收集和主机名解析
|
||||||
|
|
||||||
|
### 工具模块
|
||||||
|
- **Base.go** - 通用工具函数 (ReadBytes, AES加密解密, PKCS7填充等)
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
这些插件通过适配器自动集成到新的插件系统中:
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins/legacy/ # 老版本插件代码 (此目录)
|
||||||
|
↓
|
||||||
|
adapters/legacy_plugins/ # 适配器实现
|
||||||
|
↓
|
||||||
|
plugins/base/ # 新架构插件系统
|
||||||
|
```
|
||||||
|
|
||||||
|
## 维护说明
|
||||||
|
|
||||||
|
- 这些插件代码保持不变,确保兼容性
|
||||||
|
- 所有功能通过 `adapters/legacy_plugins/` 中的适配器访问
|
||||||
|
- 用户使用时与新插件完全相同,无需关心底层实现
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 本地信息收集插件 (DCInfo, FindNet, LocalInfo, MiniDump)
|
||||||
|
- Web相关插件 (WebPoc, WebTitle)
|
||||||
|
|
||||||
|
这些插件暂未包含在此目录中,后续会进行单独整理。
|
299
Plugins/legacy/SMB.go
Normal file
299
Plugins/legacy/SMB.go
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
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/legacy/SMB2.go
Normal file
492
Plugins/legacy/SMB2.go
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
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)
|
||||||
|
}
|
161
Plugins/legacy/SmbGhost.go
Normal file
161
Plugins/legacy/SmbGhost.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
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
|
||||||
|
}
|
3
Plugins/legacy/init.go
Normal file
3
Plugins/legacy/init.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Package Plugins contains legacy plugin implementations that are adapted to new architecture
|
||||||
|
// These plugins are accessed through adapters to maintain compatibility
|
||||||
|
package Plugins
|
@ -3,7 +3,7 @@ package elasticsearch
|
|||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/adapters"
|
"github.com/shadow1ng/fscan/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
Plugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewElasticsearchPlugin 创建Elasticsearch弱密码检测插件
|
// NewElasticsearchPlugin 创建Elasticsearch弱密码检测插件
|
||||||
|
@ -3,7 +3,7 @@ package ms17010
|
|||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/adapters"
|
"github.com/shadow1ng/fscan/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
Plugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewMS17010Plugin 创建MS17010漏洞检测插件
|
// NewMS17010Plugin 创建MS17010漏洞检测插件
|
||||||
|
@ -3,7 +3,7 @@ package netbios
|
|||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/adapters"
|
"github.com/shadow1ng/fscan/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
Plugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewNetBiosPlugin 创建NetBIOS信息收集插件
|
// NewNetBiosPlugin 创建NetBIOS信息收集插件
|
||||||
|
@ -3,7 +3,7 @@ package rdp
|
|||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/adapters"
|
"github.com/shadow1ng/fscan/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
Plugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRdpPlugin 创建RDP弱密码检测插件
|
// NewRdpPlugin 创建RDP弱密码检测插件
|
||||||
|
@ -3,7 +3,7 @@ package smb
|
|||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/adapters"
|
"github.com/shadow1ng/fscan/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
Plugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSmbPlugin 创建SMB弱密码检测插件
|
// NewSmbPlugin 创建SMB弱密码检测插件
|
||||||
|
@ -3,7 +3,7 @@ package smb2
|
|||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/adapters"
|
"github.com/shadow1ng/fscan/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
Plugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSmb2Plugin 创建SMB2弱密码检测插件
|
// NewSmb2Plugin 创建SMB2弱密码检测插件
|
||||||
|
@ -3,7 +3,7 @@ package smbghost
|
|||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/adapters"
|
"github.com/shadow1ng/fscan/adapters"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
Plugins "github.com/shadow1ng/fscan/plugins"
|
Plugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSmbGhostPlugin 创建SMBGhost漏洞检测插件
|
// NewSmbGhostPlugin 创建SMBGhost漏洞检测插件
|
||||||
|
Loading…
Reference in New Issue
Block a user