From e3c14e9f8ed98a06463e5a5e793a44d2ab25d974 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Tue, 12 Aug 2025 23:06:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9ESMBInfo=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E5=A2=9E=E5=BC=BASMB=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E6=94=B6=E9=9B=86=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增smbinfo插件,专门用于SMB协议信息收集和操作系统检测 - 实现完整的NTLM Type 2消息解析,提取详细的系统信息 - 支持Windows版本识别、计算机名、域名等信息提取 - 采用标准插件输出格式,与其他插件保持一致 - 保留原始NetBIOS插件,两个插件功能互补 - 优化SMB协议数据包处理,提升兼容性和稳定性 --- core/Registry.go | 3 +- plugins/legacy/Base.go | 63 +-- plugins/legacy/netbios/plugin.go | 12 +- plugins/legacy/smbinfo.go | 701 +++++++++++++++++++++++++++++++ plugins/legacy/smbinfo/plugin.go | 54 +++ 5 files changed, 801 insertions(+), 32 deletions(-) create mode 100644 plugins/legacy/smbinfo.go create mode 100644 plugins/legacy/smbinfo/plugin.go diff --git a/core/Registry.go b/core/Registry.go index eb7fac9..563d022 100644 --- a/core/Registry.go +++ b/core/Registry.go @@ -34,7 +34,8 @@ import ( _ "github.com/shadow1ng/fscan/plugins/legacy/elasticsearch" // 跨平台服务 _ "github.com/shadow1ng/fscan/plugins/legacy/findnet" // 网络发现 _ "github.com/shadow1ng/fscan/plugins/legacy/ms17010" // Windows SMB漏洞(但扫描器可跨平台) - _ "github.com/shadow1ng/fscan/plugins/legacy/netbios" // NetBIOS协议(主要Windows但可跨平台扫描) + _ "github.com/shadow1ng/fscan/plugins/legacy/netbios" // NetBIOS协议(主要Windows但可跨平台扫描) + _ "github.com/shadow1ng/fscan/plugins/legacy/smbinfo" // SMB信息收集(主要Windows但可跨平台扫描) _ "github.com/shadow1ng/fscan/plugins/legacy/rdp" // RDP协议扫描(可跨平台) _ "github.com/shadow1ng/fscan/plugins/legacy/smb" // SMB协议扫描(可跨平台) _ "github.com/shadow1ng/fscan/plugins/legacy/smb2" // SMBv2协议扫描(可跨平台) diff --git a/plugins/legacy/Base.go b/plugins/legacy/Base.go index d315765..0815187 100644 --- a/plugins/legacy/Base.go +++ b/plugins/legacy/Base.go @@ -9,35 +9,48 @@ import ( "net" ) -// ReadBytes 从连接读取数据直到EOF或错误 +// ReadBytes 从连接读取数据直到EOF或错误 - 改进的SMB协议处理 func ReadBytes(conn net.Conn) ([]byte, error) { - size := 4096 // 缓冲区大小 - buf := make([]byte, size) - var result []byte - var lastErr error - - // 循环读取数据 - for { - count, err := conn.Read(buf) + // 首先读取NetBIOS头部(4字节)来确定消息长度 + headerBuf := make([]byte, 4) + n, err := conn.Read(headerBuf) + if err != nil { + return nil, fmt.Errorf("读取NetBIOS头部失败: %v", err) + } + if n != 4 { + return nil, fmt.Errorf("NetBIOS头部长度不足: %d", n) + } + + // 解析NetBIOS消息长度(大端序) + messageLength := int(headerBuf[0])<<24 | int(headerBuf[1])<<16 | int(headerBuf[2])<<8 | int(headerBuf[3]) + + // 防止过大的消息长度(安全检查) + if messageLength > 1024*1024 { // 1MB限制 + return nil, fmt.Errorf("消息长度过大: %d", messageLength) + } + + // 如果消息长度为0,只返回头部 + if messageLength == 0 { + return headerBuf, nil + } + + // 读取完整消息体 + messageBuf := make([]byte, messageLength) + totalRead := 0 + for totalRead < messageLength { + n, err := conn.Read(messageBuf[totalRead:]) if err != nil { - lastErr = err - break - } - - result = append(result, buf[0:count]...) - - // 如果读取的数据小于缓冲区,说明已经读完 - if count < size { - break + return nil, fmt.Errorf("读取消息体失败(已读取%d/%d字节): %v", totalRead, messageLength, err) } + totalRead += n } - - // 如果读到了数据,则忽略错误 - if len(result) > 0 { - return result, nil - } - - return result, lastErr + + // 返回完整消息(头部+消息体) + result := make([]byte, 0, 4+messageLength) + result = append(result, headerBuf...) + result = append(result, messageBuf...) + + return result, nil } // 默认AES加密密钥 diff --git a/plugins/legacy/netbios/plugin.go b/plugins/legacy/netbios/plugin.go index a3ad8a3..5543722 100644 --- a/plugins/legacy/netbios/plugin.go +++ b/plugins/legacy/netbios/plugin.go @@ -15,9 +15,9 @@ func NewNetBiosPlugin() base.Plugin { Author: "fscan-team", Description: "NetBIOS信息收集和主机名解析", Category: "service", - Ports: []int{139, 445}, // NetBIOS端口 + Ports: []int{137, 139}, // NetBIOS端口 Protocols: []string{"tcp", "udp"}, - Tags: []string{"netbios", "information-gathering", "hostname", "smb"}, + Tags: []string{"netbios", "information-gathering", "hostname"}, } // 适配器选项 @@ -25,10 +25,10 @@ func NewNetBiosPlugin() base.Plugin { CheckBruteFlag: false, // NetBIOS信息收集不依赖暴力破解标志 IsVulnPlugin: false, // 这不是漏洞检测插件 IsInfoPlugin: true, // 这是信息收集插件 - CustomPorts: []int{139, 445}, // NetBIOS/SMB端口 + CustomPorts: []int{137, 139}, // NetBIOS端口 } - // 创建适配器,直接使用老版本的NetBIOS函数 + // 创建适配器,使用NetBIOS函数 return adapters.NewLegacyPlugin(metadata, LegacyPlugins.NetBIOS, options) } @@ -41,9 +41,9 @@ func init() { Author: "fscan-team", Description: "NetBIOS信息收集和主机名解析", Category: "service", - Ports: []int{139, 445}, + Ports: []int{137, 139}, Protocols: []string{"tcp", "udp"}, - Tags: []string{"netbios", "information-gathering", "hostname", "smb"}, + Tags: []string{"netbios", "information-gathering", "hostname"}, } factory := base.NewSimplePluginFactory(metadata, func() base.Plugin { diff --git a/plugins/legacy/smbinfo.go b/plugins/legacy/smbinfo.go new file mode 100644 index 0000000..93bcbec --- /dev/null +++ b/plugins/legacy/smbinfo.go @@ -0,0 +1,701 @@ +package Plugins + +import ( + "bytes" + "encoding/hex" + "fmt" + "net" + "strings" + "time" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/output" +) + +// SMBInfo 主函数 - 基于最原始参考代码实现 +func SMBInfo(info *common.HostInfo) error { + if info.Ports != "445" && info.Ports != "139" { + return fmt.Errorf("SMBInfo插件仅支持139和445端口") + } + + realhost := fmt.Sprintf("%s:%s", info.Host, info.Ports) + conn, err := net.DialTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second) + if err != nil { + return fmt.Errorf("连接失败: %v", err) + } + defer conn.Close() + + // 发送SMBv1第一个协商包 + _, err = conn.Write(SMBInfoNegotiateSMBv1Data1) + if err != nil { + return fmt.Errorf("发送SMBv1协商包失败: %v", err) + } + + r1, err := ReadBytes(conn) + if err != nil { + common.LogDebug(fmt.Sprintf("读取SMBv1协商响应失败: %v", err)) + } + + var result string + + // ff534d42 SMBv1的标示 + // fe534d42 SMBv2的标示 + // 先发送探测SMBv1的payload,不支持的SMBv1的时候返回为空,然后尝试发送SMBv2的探测数据包 + if len(r1) > 0 { + // SMBv1 路径 + result = handleSMBv1(conn, info) + } else { + // SMBv2 路径 + result = handleSMBv2(realhost, info) + } + + // 显示和保存结果 + if result != "" { + displaySMBInfo(info, result, len(r1) > 0) + saveSMBInfo(info, result) + } else { + // 即使没有详细信息也显示基本连接信息 + displayBasicSMBInfo(info) + } + + return nil +} + +// handleSMBv1 处理SMBv1协议 +func handleSMBv1(conn net.Conn, info *common.HostInfo) string { + // 发送第二个SMBv1包 + _, err := conn.Write(SMBInfoNegotiateSMBv1Data2) + if err != nil { + common.LogDebug(fmt.Sprintf("发送SMBv1 Session Setup失败: %v", err)) + return "" + } + + ret, err := ReadBytes(conn) + if err != nil || len(ret) < 45 { + common.LogDebug(fmt.Sprintf("读取SMBv1 Session Setup响应失败: %v", err)) + return "" + } + + // 解析blob信息 + blob_length := uint16(bytesToUint16(ret[43:45])) + blob_count := uint16(bytesToUint16(ret[45:47])) + + if int(blob_count) > len(ret) { + common.LogDebug("blob_count超出数据范围") + return "" + } + + gss_native := ret[47:] + off_ntlm := bytes.Index(gss_native, []byte("NTLMSSP")) + if off_ntlm == -1 { + common.LogDebug("未找到NTLMSSP数据") + return "" + } + + // 提取native OS和LM信息 + native := gss_native[int(blob_length):blob_count] + ss := strings.Split(string(native), "\x00\x00") + + var nativeOS, nativeLM string + if len(ss) > 0 { + nativeOS = trimName(ss[0]) + } + if len(ss) > 1 { + nativeLM = trimName(ss[1]) + } + + // 解析NTLM信息 + bs := gss_native[off_ntlm:blob_length] + ntlmInfo := parseNTLMChallenge(bs) + + // 组合结果 + result := ntlmInfo + if nativeOS != "" { + result += fmt.Sprintf("NativeOS: %s\n", nativeOS) + } + if nativeLM != "" { + result += fmt.Sprintf("NativeLM: %s\n", nativeLM) + } + + return result +} + +// handleSMBv2 处理SMBv2协议 +func handleSMBv2(realhost string, info *common.HostInfo) string { + conn2, err := net.DialTimeout("tcp", realhost, time.Duration(common.Timeout)*time.Second) + if err != nil { + common.LogDebug(fmt.Sprintf("SMBv2连接失败: %v", err)) + return "" + } + defer conn2.Close() + + // 发送SMBv2第一个协商包 + _, err = conn2.Write(SMBInfoNegotiateSMBv2Data1) + if err != nil { + common.LogDebug(fmt.Sprintf("发送SMBv2协商包失败: %v", err)) + return "" + } + + r2, err := ReadBytes(conn2) + if err != nil { + common.LogDebug(fmt.Sprintf("读取SMBv2协商响应失败: %v", err)) + return "" + } + + // 根据响应构建NTLM数据包 + var ntlmSSPNegotiatev2Data []byte + if len(r2) > 70 && hex.EncodeToString(r2[70:71]) == "03" { + flags := []byte{0x15, 0x82, 0x08, 0xa0} + ntlmSSPNegotiatev2Data = getNTLMSSPNegotiateData(flags) + } else { + flags := []byte{0x05, 0x80, 0x08, 0xa0} + ntlmSSPNegotiatev2Data = getNTLMSSPNegotiateData(flags) + } + + // 发送第二个SMBv2包 + _, err = conn2.Write(SMBInfoNegotiateSMBv2Data2) + if err != nil { + common.LogDebug(fmt.Sprintf("发送SMBv2第二包失败: %v", err)) + return "" + } + + _, err = ReadBytes(conn2) + if err != nil { + common.LogDebug(fmt.Sprintf("读取SMBv2第二包响应失败: %v", err)) + return "" + } + + // 发送NTLM协商包 + _, err = conn2.Write(ntlmSSPNegotiatev2Data) + if err != nil { + common.LogDebug(fmt.Sprintf("发送SMBv2 NTLM包失败: %v", err)) + return "" + } + + ret, err := ReadBytes(conn2) + if err != nil { + common.LogDebug(fmt.Sprintf("读取SMBv2 NTLM响应失败: %v", err)) + return "" + } + + ntlmOff := bytes.Index(ret, []byte("NTLMSSP")) + if ntlmOff == -1 { + common.LogDebug("SMBv2响应中未找到NTLMSSP数据") + return "" + } + + return parseNTLMChallenge(ret[ntlmOff:]) +} + +// 原始参考代码中的数据包定义 +var SMBInfoNegotiateSMBv1Data1 = []byte{ + 0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F, + 0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02, + 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F, + 0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70, + 0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30, + 0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54, + 0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00, +} + +var SMBInfoNegotiateSMBv1Data2 = []byte{ + 0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE, + 0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60, + 0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30, + 0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04, + 0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08, + 0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, + 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00, + 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00, + 0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, + 0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00, + 0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, + 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, + 0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, +} + +var SMBInfoNegotiateSMBv2Data1 = []byte{ + 0x00, 0x00, 0x00, 0x45, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x01, 0x48, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0xAC, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x02, + 0x4E, 0x54, 0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, + 0x00, 0x02, 0x53, 0x4D, 0x42, 0x20, 0x32, 0x2E, 0x30, 0x30, + 0x32, 0x00, 0x02, 0x53, 0x4D, 0x42, 0x20, 0x32, 0x2E, 0x3F, + 0x3F, 0x3F, 0x00, +} + +var SMBInfoNegotiateSMBv2Data2 = []byte{ + 0x00, 0x00, 0x00, 0x68, 0xFE, 0x53, 0x4D, 0x42, 0x40, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, + 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x10, 0x02, +} + +func getNTLMSSPNegotiateData(flags []byte) []byte { + return []byte{ + 0x00, 0x00, 0x00, 0x9A, 0xFE, 0x53, 0x4D, 0x42, 0x40, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x58, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x60, 0x40, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, + 0x05, 0x02, 0xA0, 0x36, 0x30, 0x34, 0xA0, 0x0E, 0x30, 0x0C, + 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, + 0x02, 0x0A, 0xA2, 0x22, 0x04, 0x20, 0x4E, 0x54, 0x4C, 0x4D, + 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, + flags[0], flags[1], flags[2], flags[3], + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } +} + +// 辅助函数 +func bytesToUint16(b []byte) uint16 { + return uint16(b[0]) | uint16(b[1])<<8 +} + +func trimName(s string) string { + return strings.Trim(strings.TrimSpace(s), "\x00") +} + +// NTLM AV_PAIR类型常量 +const ( + MsvAvEOL = 0x0000 // End of list + MsvAvNbComputerName = 0x0001 // NetBIOS computer name + MsvAvNbDomainName = 0x0002 // NetBIOS domain name + MsvAvDnsComputerName = 0x0003 // DNS computer name + MsvAvDnsDomainName = 0x0004 // DNS domain name + MsvAvDnsTreeName = 0x0005 // DNS forest name + MsvAvFlags = 0x0006 // Server flags + MsvAvTimestamp = 0x0007 // Server timestamp + MsvAvSingleHost = 0x0008 // Single host data + MsvAvTargetName = 0x0009 // Target name + MsvAvChannelBindings = 0x000A // Channel bindings +) + +// parseNTLMChallenge 解析NTLM Type 2 (Challenge) 消息 +func parseNTLMChallenge(data []byte) string { + if len(data) < 32 { + return "" + } + + var result strings.Builder + + // 检查NTLM签名 "NTLMSSP\x00" + if !bytes.Equal(data[0:8], []byte("NTLMSSP\x00")) { + return "" + } + + // 检查消息类型 (应该是 Type 2 = 0x00000002) + if len(data) < 12 { + return "" + } + messageType := bytesToUint32(data[8:12]) + if messageType != 2 { + common.LogDebug(fmt.Sprintf("非Type 2 NTLM消息, 类型: %d", messageType)) + return "" + } + + result.WriteString("Protocol: NTLM Type 2 (Challenge)\n") + + // 解析Target Name (偏移12-20, 8字节的Security Buffer) + if len(data) >= 20 { + targetLength := bytesToUint16(data[12:14]) + targetOffset := bytesToUint32(data[16:20]) + + if targetLength > 0 && int(targetOffset) < len(data) && int(targetOffset+uint32(targetLength)) <= len(data) { + targetName := parseUnicodeString(data[targetOffset:targetOffset+uint32(targetLength)]) + if targetName != "" { + result.WriteString(fmt.Sprintf("Target Name: %s\n", targetName)) + } + } + } + + // 解析Flags (偏移20-24) + if len(data) >= 24 { + flags := bytesToUint32(data[20:24]) + parseNTLMFlags(flags, &result) + } + + // 解析Challenge (偏移24-32, 8字节) + if len(data) >= 32 { + challenge := data[24:32] + result.WriteString(fmt.Sprintf("Server Challenge: %s\n", hex.EncodeToString(challenge))) + } + + // 解析Target Info (AV_PAIR结构) - 偏移44开始的Security Buffer + if len(data) >= 52 { + targetInfoLength := bytesToUint16(data[40:42]) + targetInfoOffset := bytesToUint32(data[44:48]) + + if targetInfoLength > 0 && int(targetInfoOffset) < len(data) && + int(targetInfoOffset+uint32(targetInfoLength)) <= len(data) { + targetInfoData := data[targetInfoOffset:targetInfoOffset+uint32(targetInfoLength)] + parseTargetInfo(targetInfoData, &result) + } + } + + // 解析OS版本信息 (如果存在, 偏移48-56) + if len(data) >= 56 { + // 检查是否包含版本信息 (通过flags判断) + if len(data) >= 24 { + flags := bytesToUint32(data[20:24]) + // NTLMSSP_NEGOTIATE_VERSION = 0x02000000 + if flags&0x02000000 != 0 && len(data) >= 56 { + parseOSVersion(data[48:56], &result) + } + } + } + + return result.String() +} + +// parseUnicodeString 解析UTF-16LE编码的字符串 +func parseUnicodeString(data []byte) string { + if len(data)%2 != 0 { + return "" + } + + var runes []rune + for i := 0; i < len(data); i += 2 { + if i+1 >= len(data) { + break + } + // UTF-16LE: 低字节在前 + r := uint16(data[i]) | uint16(data[i+1])<<8 + if r == 0 { + break + } + runes = append(runes, rune(r)) + } + return string(runes) +} + +// parseNTLMFlags 解析NTLM标志位 +func parseNTLMFlags(flags uint32, result *strings.Builder) { + flagNames := map[uint32]string{ + 0x00000001: "NEGOTIATE_UNICODE", + 0x00000002: "NEGOTIATE_OEM", + 0x00000004: "REQUEST_TARGET", + 0x00000010: "NEGOTIATE_SIGN", + 0x00000020: "NEGOTIATE_SEAL", + 0x00000040: "NEGOTIATE_DATAGRAM", + 0x00000080: "NEGOTIATE_LM_KEY", + 0x00000200: "NEGOTIATE_NTLM", + 0x00001000: "NEGOTIATE_DOMAIN_SUPPLIED", + 0x00002000: "NEGOTIATE_WORKSTATION_SUPPLIED", + 0x00004000: "NEGOTIATE_LOCAL_CALL", + 0x00008000: "NEGOTIATE_ALWAYS_SIGN", + 0x00010000: "TARGET_TYPE_DOMAIN", + 0x00020000: "TARGET_TYPE_SERVER", + 0x00040000: "TARGET_TYPE_SHARE", + 0x00080000: "NEGOTIATE_EXTENDED_SESSIONSECURITY", + 0x00100000: "NEGOTIATE_IDENTIFY", + 0x02000000: "NEGOTIATE_VERSION", + 0x20000000: "NEGOTIATE_128", + 0x40000000: "NEGOTIATE_KEY_EXCH", + 0x80000000: "NEGOTIATE_56", + } + + var activeFlags []string + for flag, name := range flagNames { + if flags&flag != 0 { + activeFlags = append(activeFlags, name) + } + } + + if len(activeFlags) > 0 { + result.WriteString(fmt.Sprintf("NTLM Flags: %s\n", strings.Join(activeFlags, ", "))) + } +} + +// parseTargetInfo 解析NTLM Target Information (AV_PAIR结构) +func parseTargetInfo(data []byte, result *strings.Builder) { + offset := 0 + + for offset+4 <= len(data) { + // 读取AV_PAIR结构: AvId (2字节) + AvLen (2字节) + Value (AvLen字节) + avId := bytesToUint16(data[offset:offset+2]) + avLen := bytesToUint16(data[offset+2:offset+4]) + + if avId == MsvAvEOL { + break // 列表结束 + } + + if offset+4+int(avLen) > len(data) { + break // 数据不足 + } + + value := data[offset+4:offset+4+int(avLen)] + + switch avId { + case MsvAvNbComputerName: + computerName := parseUnicodeString(value) + if computerName != "" { + result.WriteString(fmt.Sprintf("NetBIOS Computer Name: %s\n", computerName)) + } + case MsvAvNbDomainName: + domainName := parseUnicodeString(value) + if domainName != "" { + result.WriteString(fmt.Sprintf("NetBIOS Domain Name: %s\n", domainName)) + } + case MsvAvDnsComputerName: + dnsComputerName := parseUnicodeString(value) + if dnsComputerName != "" { + result.WriteString(fmt.Sprintf("DNS Computer Name: %s\n", dnsComputerName)) + } + case MsvAvDnsDomainName: + dnsDomainName := parseUnicodeString(value) + if dnsDomainName != "" { + result.WriteString(fmt.Sprintf("DNS Domain Name: %s\n", dnsDomainName)) + } + case MsvAvDnsTreeName: + forestName := parseUnicodeString(value) + if forestName != "" { + result.WriteString(fmt.Sprintf("DNS Forest Name: %s\n", forestName)) + } + case MsvAvFlags: + if len(value) >= 4 { + serverFlags := bytesToUint32(value[0:4]) + parseServerFlags(serverFlags, result) + } + case MsvAvTimestamp: + if len(value) >= 8 { + timestamp := bytesToUint64(value[0:8]) + // Windows FILETIME: 100纳秒间隔自1601年1月1日 + if timestamp > 0 { + // 转换为Unix时间戳 (简化版本) + unixTime := int64((timestamp - 116444736000000000) / 10000000) + if unixTime > 0 { + t := time.Unix(unixTime, 0) + result.WriteString(fmt.Sprintf("Server Timestamp: %s\n", t.Format(time.RFC3339))) + } + } + } + case MsvAvTargetName: + targetName := parseUnicodeString(value) + if targetName != "" { + result.WriteString(fmt.Sprintf("Target SPN: %s\n", targetName)) + } + } + + offset += 4 + int(avLen) + } +} + +// parseServerFlags 解析服务器标志 +func parseServerFlags(flags uint32, result *strings.Builder) { + var serverFlags []string + + if flags&0x00000001 != 0 { + serverFlags = append(serverFlags, "CONSTRAINED_AUTHENTICATION") + } + if flags&0x00000002 != 0 { + serverFlags = append(serverFlags, "MIC_PROVIDED") + } + if flags&0x00000004 != 0 { + serverFlags = append(serverFlags, "UNTRUSTED_SPN_SOURCE") + } + + if len(serverFlags) > 0 { + result.WriteString(fmt.Sprintf("Server Flags: %s\n", strings.Join(serverFlags, ", "))) + } +} + +// parseOSVersion 解析操作系统版本信息 +func parseOSVersion(data []byte, result *strings.Builder) { + if len(data) < 8 { + return + } + + majorVersion := data[0] + minorVersion := data[1] + buildNumber := bytesToUint16(data[2:4]) + reserved := bytesToUint32(data[4:8]) + + // Windows版本映射 + var osName string + switch { + case majorVersion == 10 && minorVersion == 0: + if buildNumber >= 22000 { + osName = "Windows 11" + } else { + osName = "Windows 10" + } + case majorVersion == 6 && minorVersion == 3: + osName = "Windows 8.1 / Server 2012 R2" + case majorVersion == 6 && minorVersion == 2: + osName = "Windows 8 / Server 2012" + case majorVersion == 6 && minorVersion == 1: + osName = "Windows 7 / Server 2008 R2" + case majorVersion == 6 && minorVersion == 0: + osName = "Windows Vista / Server 2008" + case majorVersion == 5 && minorVersion == 2: + osName = "Windows XP x64 / Server 2003" + case majorVersion == 5 && minorVersion == 1: + osName = "Windows XP" + case majorVersion == 5 && minorVersion == 0: + osName = "Windows 2000" + default: + osName = fmt.Sprintf("Windows %d.%d", majorVersion, minorVersion) + } + + result.WriteString(fmt.Sprintf("OS Version: %s (Build %d)\n", osName, buildNumber)) + + if reserved != 0 { + result.WriteString(fmt.Sprintf("OS Reserved: 0x%08X\n", reserved)) + } +} + +// bytesToUint32 将字节数组转换为32位无符号整数 (小端序) +func bytesToUint32(b []byte) uint32 { + if len(b) < 4 { + return 0 + } + return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 +} + +// bytesToUint64 将字节数组转换为64位无符号整数 (小端序) +func bytesToUint64(b []byte) uint64 { + if len(b) < 8 { + return 0 + } + return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | + uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 +} + +// displaySMBInfo 显示SMB信息 +func displaySMBInfo(hostInfo *common.HostInfo, info string, isSMBv1 bool) { + target := fmt.Sprintf("%s:%s", hostInfo.Host, hostInfo.Ports) + smbVersion := "SMB2" + if isSMBv1 { + smbVersion = "SMB1" + } + + // 提取操作系统信息用于主显示行 + osInfo := extractOSInfo(info) + computerName := extractComputerName(info) + + var successMsg string + if osInfo != "" && computerName != "" { + successMsg = fmt.Sprintf("SMBInfo %s [%s] %s %s", target, osInfo, computerName, smbVersion) + } else if osInfo != "" { + successMsg = fmt.Sprintf("SMBInfo %s [%s] %s", target, osInfo, smbVersion) + } else if computerName != "" { + successMsg = fmt.Sprintf("SMBInfo %s %s %s", target, computerName, smbVersion) + } else { + successMsg = fmt.Sprintf("SMBInfo %s %s", target, smbVersion) + } + + common.LogSuccess(successMsg) +} + +// extractOSInfo 从信息中提取操作系统信息 +func extractOSInfo(info string) string { + lines := strings.Split(info, "\n") + for _, line := range lines { + if strings.Contains(line, "OS Version:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + osVersion := strings.TrimSpace(parts[1]) + // 简化OS版本显示,提取主要版本号 + if strings.Contains(osVersion, "Windows") { + if strings.Contains(osVersion, "Build") { + parts := strings.Split(osVersion, " (Build") + if len(parts) > 0 { + return strings.TrimSpace(parts[0]) + } + } + return osVersion + } + return osVersion + } + } + if strings.Contains(line, "Windows版本:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + return strings.TrimSpace(parts[1]) + } + } + if strings.Contains(line, "NativeOS:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + return strings.TrimSpace(parts[1]) + } + } + } + return "" +} + +// extractComputerName 从信息中提取计算机名 +func extractComputerName(info string) string { + lines := strings.Split(info, "\n") + for _, line := range lines { + if strings.Contains(line, "NetBIOS Computer Name:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + return strings.TrimSpace(parts[1]) + } + } + if strings.Contains(line, "NetBIOS计算机名:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + return strings.TrimSpace(parts[1]) + } + } + if strings.Contains(line, "DNS Computer Name:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + return strings.TrimSpace(parts[1]) + } + } + if strings.Contains(line, "DNS计算机名:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + return strings.TrimSpace(parts[1]) + } + } + } + return "" +} + +// displayBasicSMBInfo 显示基本SMB信息 +func displayBasicSMBInfo(hostInfo *common.HostInfo) { + target := fmt.Sprintf("%s:%s", hostInfo.Host, hostInfo.Ports) + msg := fmt.Sprintf("SMBInfo %s SMB service detected", target) + common.LogSuccess(msg) +} + +// saveSMBInfo 保存SMB信息 +func saveSMBInfo(hostInfo *common.HostInfo, info string) { + result := &output.ScanResult{ + Time: time.Now(), + Type: output.TypeService, + Target: hostInfo.Host, + Status: "open", + Details: map[string]interface{}{ + "port": hostInfo.Ports, + "protocol": "smb", + "info": info, + }, + } + common.SaveResult(result) +} \ No newline at end of file diff --git a/plugins/legacy/smbinfo/plugin.go b/plugins/legacy/smbinfo/plugin.go new file mode 100644 index 0000000..360897b --- /dev/null +++ b/plugins/legacy/smbinfo/plugin.go @@ -0,0 +1,54 @@ +package smbinfo + +import ( + "github.com/shadow1ng/fscan/plugins/adapters" + "github.com/shadow1ng/fscan/plugins/base" + LegacyPlugins "github.com/shadow1ng/fscan/plugins/legacy" +) + +// NewSMBInfoPlugin 创建SMB信息收集插件 +func NewSMBInfoPlugin() base.Plugin { + // 插件元数据 + metadata := &base.PluginMetadata{ + Name: "smbinfo", + Version: "1.0.0", + Author: "fscan-team", + Description: "SMB协议信息收集和操作系统检测", + Category: "service", + Ports: []int{139, 445}, // SMB端口 + Protocols: []string{"tcp"}, + Tags: []string{"smb", "information-gathering", "os-detection", "ntlm"}, + } + + // 适配器选项 + options := &adapters.LegacyPluginOptions{ + CheckBruteFlag: false, // SMB信息收集不依赖暴力破解标志 + IsVulnPlugin: false, // 这不是漏洞检测插件 + IsInfoPlugin: true, // 这是信息收集插件 + CustomPorts: []int{139, 445}, // SMB端口 + } + + // 创建适配器,使用SMBInfo函数 + return adapters.NewLegacyPlugin(metadata, LegacyPlugins.SMBInfo, options) +} + +// init 自动注册SMBInfo插件 +func init() { + // 创建插件工厂 + metadata := &base.PluginMetadata{ + Name: "smbinfo", + Version: "1.0.0", + Author: "fscan-team", + Description: "SMB协议信息收集和操作系统检测", + Category: "service", + Ports: []int{139, 445}, + Protocols: []string{"tcp"}, + Tags: []string{"smb", "information-gathering", "os-detection", "ntlm"}, + } + + factory := base.NewSimplePluginFactory(metadata, func() base.Plugin { + return NewSMBInfoPlugin() + }) + + base.GlobalPluginRegistry.Register("smbinfo", factory) +} \ No newline at end of file