mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00
297 lines
8.1 KiB
Go
297 lines
8.1 KiB
Go
package modbus
|
||
|
||
import (
|
||
"context"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"net"
|
||
"time"
|
||
|
||
"github.com/shadow1ng/fscan/common"
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
"github.com/shadow1ng/fscan/plugins/base"
|
||
)
|
||
|
||
// ModbusPlugin Modbus插件实现
|
||
type ModbusPlugin struct {
|
||
*base.ServicePlugin
|
||
exploiter *ModbusExploiter
|
||
}
|
||
|
||
// NewModbusPlugin 创建Modbus插件
|
||
func NewModbusPlugin() *ModbusPlugin {
|
||
// 插件元数据
|
||
metadata := &base.PluginMetadata{
|
||
Name: "modbus",
|
||
Version: "2.0.0",
|
||
Author: "fscan-team",
|
||
Description: "Modbus工业协议扫描和利用插件",
|
||
Category: "service",
|
||
Ports: []int{502, 5020}, // 502: 标准Modbus TCP端口, 5020: 测试端口
|
||
Protocols: []string{"tcp"},
|
||
Tags: []string{"modbus", "industrial", "scada", "unauthorized"},
|
||
}
|
||
|
||
// 创建连接器和服务插件
|
||
connector := NewModbusConnector()
|
||
servicePlugin := base.NewServicePlugin(metadata, connector)
|
||
|
||
// 创建Modbus插件
|
||
plugin := &ModbusPlugin{
|
||
ServicePlugin: servicePlugin,
|
||
exploiter: NewModbusExploiter(),
|
||
}
|
||
|
||
// 设置能力
|
||
plugin.SetCapabilities([]base.Capability{
|
||
base.CapUnauthorized,
|
||
base.CapDataExtraction,
|
||
})
|
||
|
||
return plugin
|
||
}
|
||
|
||
// Scan 重写扫描方法,检测Modbus协议和未授权访问
|
||
func (p *ModbusPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||
// 如果禁用了暴力破解,只进行服务识别
|
||
if common.DisableBrute {
|
||
return p.performServiceIdentification(ctx, info)
|
||
}
|
||
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
|
||
// Modbus协议通常无认证,直接检查协议响应
|
||
unauthCred := &base.Credential{Username: "", Password: ""}
|
||
result, err := p.ScanCredential(ctx, info, unauthCred)
|
||
if err == nil && result.Success {
|
||
// 获取设备信息
|
||
deviceInfo := p.getDeviceInfo(ctx, info)
|
||
|
||
// 未授权访问成功
|
||
common.LogSuccess(i18n.GetText("modbus_unauth_access", target))
|
||
if deviceInfo != "" {
|
||
common.LogSuccess(i18n.GetText("modbus_device_info", deviceInfo))
|
||
}
|
||
|
||
return &base.ScanResult{
|
||
Success: true,
|
||
Service: "Modbus",
|
||
Credentials: []*base.Credential{unauthCred},
|
||
Extra: map[string]interface{}{
|
||
"service": "Modbus",
|
||
"port": info.Ports,
|
||
"unauthorized": true,
|
||
"access_type": "no_authentication",
|
||
"device_info": deviceInfo,
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
// 如果Modbus协议检测失败,返回失败结果
|
||
return &base.ScanResult{
|
||
Success: false,
|
||
Error: fmt.Errorf("非Modbus服务或连接失败"),
|
||
}, nil
|
||
}
|
||
|
||
// generateCredentials Modbus通常无需凭据,返回空凭据
|
||
func (p *ModbusPlugin) generateCredentials() []*base.Credential {
|
||
// Modbus协议通常无认证机制,只返回空凭据用于协议检测
|
||
return []*base.Credential{
|
||
{Username: "", Password: ""},
|
||
}
|
||
}
|
||
|
||
// getDeviceInfo 获取Modbus设备信息
|
||
func (p *ModbusPlugin) getDeviceInfo(ctx context.Context, info *common.HostInfo) string {
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
timeout := time.Duration(common.Timeout) * time.Second
|
||
|
||
// 尝试建立连接
|
||
conn, err := net.DialTimeout("tcp", target, timeout)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
defer conn.Close()
|
||
|
||
// 设置超时
|
||
conn.SetDeadline(time.Now().Add(timeout))
|
||
|
||
// 发送读取设备标识请求 (功能码0x11)
|
||
request := buildModbusDeviceIdentifyRequest()
|
||
if _, err := conn.Write(request); err != nil {
|
||
return ""
|
||
}
|
||
|
||
// 读取响应
|
||
response := make([]byte, 256)
|
||
n, err := conn.Read(response)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
|
||
return parseDeviceInfo(response[:n])
|
||
}
|
||
|
||
// buildModbusDeviceIdentifyRequest 构建Modbus设备标识请求
|
||
func buildModbusDeviceIdentifyRequest() []byte {
|
||
request := make([]byte, 8)
|
||
|
||
// Modbus TCP头部
|
||
binary.BigEndian.PutUint16(request[0:], 0x0002) // 事务标识符
|
||
binary.BigEndian.PutUint16(request[2:], 0x0000) // 协议标识符
|
||
binary.BigEndian.PutUint16(request[4:], 0x0002) // 长度
|
||
request[6] = 0x01 // 单元标识符
|
||
request[7] = 0x11 // 功能码: Report Server ID
|
||
|
||
return request
|
||
}
|
||
|
||
// parseDeviceInfo 解析设备信息
|
||
func parseDeviceInfo(response []byte) string {
|
||
if len(response) < 8 {
|
||
return "Unknown Device"
|
||
}
|
||
|
||
// 检查是否为有效响应
|
||
if !isValidModbusResponse(response) {
|
||
return "Unknown Device"
|
||
}
|
||
|
||
unitID := response[6]
|
||
funcCode := response[7]
|
||
|
||
info := fmt.Sprintf("Unit ID: %d, Function: 0x%02X", unitID, funcCode)
|
||
|
||
// 如果是设备标识响应,尝试解析设备信息
|
||
if funcCode == 0x11 && len(response) > 9 {
|
||
byteCount := response[8]
|
||
if byteCount > 0 && len(response) >= 9+int(byteCount) {
|
||
// 提取设备ID信息
|
||
deviceData := response[9 : 9+int(byteCount)]
|
||
if len(deviceData) > 0 {
|
||
info += fmt.Sprintf(", Device Data: %x", deviceData)
|
||
}
|
||
}
|
||
} else if funcCode == 0x01 && len(response) >= 10 {
|
||
// 读取线圈响应,显示线圈状态
|
||
byteCount := response[8]
|
||
if byteCount > 0 && len(response) >= 9+int(byteCount) {
|
||
coilValue := response[9] & 0x01
|
||
info += fmt.Sprintf(", Coil Status: %d", coilValue)
|
||
}
|
||
}
|
||
|
||
return info
|
||
}
|
||
|
||
// Exploit 使用exploiter执行利用
|
||
func (p *ModbusPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
|
||
return p.exploiter.Exploit(ctx, info, creds)
|
||
}
|
||
|
||
// GetExploitMethods 获取利用方法
|
||
func (p *ModbusPlugin) GetExploitMethods() []base.ExploitMethod {
|
||
return p.exploiter.GetExploitMethods()
|
||
}
|
||
|
||
// IsExploitSupported 检查利用支持
|
||
func (p *ModbusPlugin) IsExploitSupported(method base.ExploitType) bool {
|
||
return p.exploiter.IsExploitSupported(method)
|
||
}
|
||
|
||
// performServiceIdentification 执行Modbus服务识别(-nobr模式)
|
||
func (p *ModbusPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
|
||
// 尝试识别Modbus服务
|
||
modbusInfo, isModbus := p.identifyModbusService(ctx, info)
|
||
if isModbus {
|
||
// 记录服务识别成功
|
||
common.LogSuccess(i18n.GetText("modbus_service_identified", target, modbusInfo))
|
||
|
||
return &base.ScanResult{
|
||
Success: true,
|
||
Service: "Modbus",
|
||
Banner: modbusInfo,
|
||
Extra: map[string]interface{}{
|
||
"service": "Modbus",
|
||
"port": info.Ports,
|
||
"info": modbusInfo,
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
// 如果无法识别为Modbus,返回失败
|
||
return &base.ScanResult{
|
||
Success: false,
|
||
Error: fmt.Errorf("无法识别为Modbus服务"),
|
||
}, nil
|
||
}
|
||
|
||
// identifyModbusService 通过协议识别Modbus服务
|
||
func (p *ModbusPlugin) identifyModbusService(ctx context.Context, info *common.HostInfo) (string, bool) {
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
timeout := time.Duration(common.Timeout) * time.Second
|
||
|
||
// 尝试建立TCP连接
|
||
conn, err := net.DialTimeout("tcp", target, timeout)
|
||
if err != nil {
|
||
return "", false
|
||
}
|
||
defer conn.Close()
|
||
|
||
// 设置操作超时
|
||
conn.SetDeadline(time.Now().Add(timeout))
|
||
|
||
// 发送Modbus读取线圈请求
|
||
request := buildModbusReadCoilsRequest()
|
||
if _, err := conn.Write(request); err != nil {
|
||
return "", false
|
||
}
|
||
|
||
// 读取响应
|
||
response := make([]byte, 256)
|
||
n, err := conn.Read(response)
|
||
if err != nil {
|
||
return "", false
|
||
}
|
||
|
||
// 检查是否为有效Modbus响应
|
||
if isValidModbusResponse(response[:n]) {
|
||
deviceInfo := parseDeviceInfo(response[:n])
|
||
return fmt.Sprintf("Modbus TCP服务: %s", deviceInfo), true
|
||
}
|
||
|
||
return "", false
|
||
}
|
||
|
||
// =============================================================================
|
||
// 插件注册
|
||
// =============================================================================
|
||
|
||
// RegisterModbusPlugin 注册Modbus插件
|
||
func RegisterModbusPlugin() {
|
||
factory := base.NewSimplePluginFactory(
|
||
&base.PluginMetadata{
|
||
Name: "modbus",
|
||
Version: "2.0.0",
|
||
Author: "fscan-team",
|
||
Description: "Modbus工业协议扫描和利用插件",
|
||
Category: "service",
|
||
Ports: []int{502, 5020}, // 502: 标准Modbus TCP端口, 5020: 测试端口
|
||
Protocols: []string{"tcp"},
|
||
Tags: []string{"modbus", "industrial", "scada", "unauthorized"},
|
||
},
|
||
func() base.Plugin {
|
||
return NewModbusPlugin()
|
||
},
|
||
)
|
||
|
||
base.GlobalPluginRegistry.Register("modbus", factory)
|
||
}
|
||
|
||
// 自动注册
|
||
func init() {
|
||
RegisterModbusPlugin()
|
||
} |