mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
Add native protocol service plugins
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
//go:build plugin_bacnet || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
var bacnetWhoIs = []byte{0x81, 0x0a, 0x00, 0x0c, 0x01, 0x20, 0xff, 0xff, 0x00, 0xff, 0x10, 0x08}
|
||||
|
||||
type BACnetPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewBACnetPlugin() *BACnetPlugin {
|
||||
return &BACnetPlugin{BasePlugin: plugins.NewBasePlugin("bacnet")}
|
||||
}
|
||||
|
||||
func (p *BACnetPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||
timeout := session.Config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
conn, err := session.DialUDP(ctx, target, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "bacnet"}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := conn.Write(bacnetWhoIs); err != nil {
|
||||
return &ScanResult{Success: false, Service: "bacnet"}
|
||||
}
|
||||
|
||||
buf := make([]byte, 1476)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "bacnet"}
|
||||
}
|
||||
|
||||
banner, ok := parseBACnetResponse(buf[:n])
|
||||
if !ok {
|
||||
return &ScanResult{Success: false, Service: "bacnet"}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "bacnet",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func parseBACnetResponse(data []byte) (string, bool) {
|
||||
if len(data) < 6 || data[0] != 0x81 {
|
||||
return "", false
|
||||
}
|
||||
length := int(binary.BigEndian.Uint16(data[2:4]))
|
||||
if length != len(data) {
|
||||
return "", false
|
||||
}
|
||||
for i := 4; i+1 < len(data); i++ {
|
||||
if data[i] == 0x10 && data[i+1] == 0x00 {
|
||||
return "BACnet I-Am response", true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterUDPPluginWithPorts("bacnet", func() Plugin {
|
||||
return NewBACnetPlugin()
|
||||
}, []int{47808})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build plugin_bacnet || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseBACnetResponse(t *testing.T) {
|
||||
banner, ok := parseBACnetResponse([]byte{0x81, 0x0a, 0x00, 0x08, 0x01, 0x20, 0x10, 0x00})
|
||||
if !ok || banner != "BACnet I-Am response" {
|
||||
t.Fatalf("unexpected bacnet banner: %q ok=%v", banner, ok)
|
||||
}
|
||||
|
||||
if _, ok := parseBACnetResponse([]byte{0x81, 0x0a, 0x00, 0x05, 0x00}); ok {
|
||||
t.Fatal("unexpected match for malformed bacnet packet")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//go:build plugin_dns || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type DNSPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewDNSPlugin() *DNSPlugin {
|
||||
return &DNSPlugin{BasePlugin: plugins.NewBasePlugin("dns")}
|
||||
}
|
||||
|
||||
func (p *DNSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||
timeout := session.Config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
queryID := randomUint16()
|
||||
query := buildDNSRootNSQuery(queryID)
|
||||
|
||||
conn, err := session.DialUDP(ctx, target, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := conn.Write(query); err != nil {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil || n < 12 {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
|
||||
banner, ok := parseDNSResponse(buf[:n], queryID)
|
||||
if !ok {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "dns",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterUDPPluginWithPorts("dns", func() Plugin {
|
||||
return NewDNSPlugin()
|
||||
}, []int{53})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//go:build plugin_dns || plugin_dnstcp || plugin_modbus || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func randomUint16() uint16 {
|
||||
var b [2]byte
|
||||
if _, err := rand.Read(b[:]); err == nil {
|
||||
return binary.BigEndian.Uint16(b[:])
|
||||
}
|
||||
return uint16(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func buildDNSRootNSQuery(id uint16) []byte {
|
||||
query := make([]byte, 17)
|
||||
binary.BigEndian.PutUint16(query[0:2], id)
|
||||
binary.BigEndian.PutUint16(query[2:4], 0x0100)
|
||||
binary.BigEndian.PutUint16(query[4:6], 1)
|
||||
query[12] = 0x00
|
||||
binary.BigEndian.PutUint16(query[13:15], 2)
|
||||
binary.BigEndian.PutUint16(query[15:17], 1)
|
||||
return query
|
||||
}
|
||||
|
||||
func parseDNSResponse(data []byte, id uint16) (string, bool) {
|
||||
if len(data) < 12 || binary.BigEndian.Uint16(data[0:2]) != id {
|
||||
return "", false
|
||||
}
|
||||
flags := binary.BigEndian.Uint16(data[2:4])
|
||||
if flags&0x8000 == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
rcode := flags & 0x000f
|
||||
qd := binary.BigEndian.Uint16(data[4:6])
|
||||
an := binary.BigEndian.Uint16(data[6:8])
|
||||
ns := binary.BigEndian.Uint16(data[8:10])
|
||||
ar := binary.BigEndian.Uint16(data[10:12])
|
||||
return fmt.Sprintf("DNS response rcode=%d qd=%d an=%d ns=%d ar=%d", rcode, qd, an, ns, ar), true
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build plugin_dns || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDNSQueryAndResponse(t *testing.T) {
|
||||
const id uint16 = 0x1234
|
||||
query := buildDNSRootNSQuery(id)
|
||||
if len(query) != 17 {
|
||||
t.Fatalf("unexpected dns query length: %d", len(query))
|
||||
}
|
||||
if binary.BigEndian.Uint16(query[0:2]) != id || binary.BigEndian.Uint16(query[13:15]) != 2 {
|
||||
t.Fatalf("unexpected dns query: %#v", query)
|
||||
}
|
||||
|
||||
resp := make([]byte, 12)
|
||||
binary.BigEndian.PutUint16(resp[0:2], id)
|
||||
binary.BigEndian.PutUint16(resp[2:4], 0x8180)
|
||||
binary.BigEndian.PutUint16(resp[4:6], 1)
|
||||
binary.BigEndian.PutUint16(resp[6:8], 2)
|
||||
binary.BigEndian.PutUint16(resp[8:10], 3)
|
||||
binary.BigEndian.PutUint16(resp[10:12], 4)
|
||||
|
||||
banner, ok := parseDNSResponse(resp, id)
|
||||
if !ok || !strings.Contains(banner, "rcode=0") || !strings.Contains(banner, "an=2") {
|
||||
t.Fatalf("unexpected dns banner: %q ok=%v", banner, ok)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//go:build plugin_dnstcp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type DNSTCPPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewDNSTCPPlugin() *DNSTCPPlugin {
|
||||
return &DNSTCPPlugin{BasePlugin: plugins.NewBasePlugin("dnstcp")}
|
||||
}
|
||||
|
||||
func (p *DNSTCPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||
timeout := session.Config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
queryID := randomUint16()
|
||||
query := buildDNSRootNSQuery(queryID)
|
||||
frame := make([]byte, 2, len(query)+2)
|
||||
binary.BigEndian.PutUint16(frame, uint16(len(query)))
|
||||
frame = append(frame, query...)
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write(frame); err != nil {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
|
||||
var lenBuf [2]byte
|
||||
if _, err := io.ReadFull(conn, lenBuf[:]); err != nil {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
respLen := int(binary.BigEndian.Uint16(lenBuf[:]))
|
||||
if respLen < 12 || respLen > 4096 {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
|
||||
resp := make([]byte, respLen)
|
||||
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
|
||||
banner, ok := parseDNSResponse(resp, queryID)
|
||||
if !ok {
|
||||
return &ScanResult{Success: false, Service: "dns"}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "dns",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("dnstcp", func() Plugin {
|
||||
return NewDNSTCPPlugin()
|
||||
}, []int{53})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build plugin_dnstcp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDNSTCPFrame(t *testing.T) {
|
||||
query := buildDNSRootNSQuery(0x4321)
|
||||
frame := make([]byte, 2, len(query)+2)
|
||||
binary.BigEndian.PutUint16(frame, uint16(len(query)))
|
||||
frame = append(frame, query...)
|
||||
|
||||
if binary.BigEndian.Uint16(frame[:2]) != uint16(len(query)) {
|
||||
t.Fatalf("unexpected dns tcp length prefix: %#v", frame[:2])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//go:build plugin_modbus || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type ModbusPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewModbusPlugin() *ModbusPlugin {
|
||||
return &ModbusPlugin{BasePlugin: plugins.NewBasePlugin("modbus")}
|
||||
}
|
||||
|
||||
func (p *ModbusPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||
timeout := session.Config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "modbus"}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
txID := randomUint16()
|
||||
req := buildModbusDeviceIDRequest(txID)
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write(req); err != nil {
|
||||
return &ScanResult{Success: false, Service: "modbus"}
|
||||
}
|
||||
|
||||
header := make([]byte, 7)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return &ScanResult{Success: false, Service: "modbus"}
|
||||
}
|
||||
length := int(binary.BigEndian.Uint16(header[4:6]))
|
||||
if length < 2 || length > 260 {
|
||||
return &ScanResult{Success: false, Service: "modbus"}
|
||||
}
|
||||
body := make([]byte, length-1)
|
||||
if _, err := io.ReadFull(conn, body); err != nil {
|
||||
return &ScanResult{Success: false, Service: "modbus"}
|
||||
}
|
||||
|
||||
banner, ok := parseModbusResponse(header, body, txID)
|
||||
if !ok {
|
||||
return &ScanResult{Success: false, Service: "modbus"}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "modbus",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func buildModbusDeviceIDRequest(txID uint16) []byte {
|
||||
req := make([]byte, 11)
|
||||
binary.BigEndian.PutUint16(req[0:2], txID)
|
||||
binary.BigEndian.PutUint16(req[2:4], 0)
|
||||
binary.BigEndian.PutUint16(req[4:6], 5)
|
||||
req[6] = 0xff
|
||||
req[7] = 0x2b
|
||||
req[8] = 0x0e
|
||||
req[9] = 0x01
|
||||
req[10] = 0x00
|
||||
return req
|
||||
}
|
||||
|
||||
func parseModbusResponse(header, body []byte, txID uint16) (string, bool) {
|
||||
if len(header) < 7 || len(body) < 1 {
|
||||
return "", false
|
||||
}
|
||||
if binary.BigEndian.Uint16(header[0:2]) != txID || binary.BigEndian.Uint16(header[2:4]) != 0 {
|
||||
return "", false
|
||||
}
|
||||
switch body[0] {
|
||||
case 0x2b:
|
||||
return "Modbus TCP device identification response", true
|
||||
case 0xab:
|
||||
return "Modbus TCP exception response", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("modbus", func() Plugin {
|
||||
return NewModbusPlugin()
|
||||
}, []int{502})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//go:build plugin_modbus || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestModbusDeviceIDRequestAndResponse(t *testing.T) {
|
||||
req := buildModbusDeviceIDRequest(0x1001)
|
||||
if len(req) != 11 || binary.BigEndian.Uint16(req[0:2]) != 0x1001 || req[7] != 0x2b {
|
||||
t.Fatalf("unexpected modbus request: %#v", req)
|
||||
}
|
||||
|
||||
header := []byte{0x10, 0x01, 0x00, 0x00, 0x00, 0x03, 0xff}
|
||||
banner, ok := parseModbusResponse(header, []byte{0x2b, 0x0e}, 0x1001)
|
||||
if !ok || !strings.Contains(banner, "Modbus TCP") {
|
||||
t.Fatalf("unexpected modbus banner: %q ok=%v", banner, ok)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//go:build plugin_mqtt || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
var mqttConnectPacket = []byte{
|
||||
0x10, 0x0c,
|
||||
0x00, 0x04, 'M', 'Q', 'T', 'T',
|
||||
0x04,
|
||||
0x02,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
}
|
||||
|
||||
type MQTTPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewMQTTPlugin() *MQTTPlugin {
|
||||
return &MQTTPlugin{BasePlugin: plugins.NewBasePlugin("mqtt")}
|
||||
}
|
||||
|
||||
func (p *MQTTPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||
timeout := session.Config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "mqtt"}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if info.Port == 8883 {
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
conn, err = p.wrapTLS(ctx, conn)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "mqtt"}
|
||||
}
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write(mqttConnectPacket); err != nil {
|
||||
return &ScanResult{Success: false, Service: "mqtt"}
|
||||
}
|
||||
|
||||
header := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return &ScanResult{Success: false, Service: "mqtt"}
|
||||
}
|
||||
|
||||
banner, ok := parseMQTTConnack(header)
|
||||
if !ok {
|
||||
return &ScanResult{Success: false, Service: "mqtt"}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "mqtt",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MQTTPlugin) wrapTLS(ctx context.Context, conn net.Conn) (net.Conn, error) {
|
||||
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func parseMQTTConnack(data []byte) (string, bool) {
|
||||
if len(data) < 4 || data[0] != 0x20 || data[1] != 0x02 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
switch data[3] {
|
||||
case 0x00:
|
||||
return "MQTT CONNACK accepted", true
|
||||
case 0x01:
|
||||
return "MQTT CONNACK unacceptable protocol version", true
|
||||
case 0x02:
|
||||
return "MQTT CONNACK identifier rejected", true
|
||||
case 0x03:
|
||||
return "MQTT CONNACK server unavailable", true
|
||||
case 0x04:
|
||||
return "MQTT CONNACK bad username or password", true
|
||||
case 0x05:
|
||||
return "MQTT CONNACK not authorized", true
|
||||
default:
|
||||
return fmt.Sprintf("MQTT CONNACK return_code=%d", data[3]), true
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("mqtt", func() Plugin {
|
||||
return NewMQTTPlugin()
|
||||
}, []int{1883, 8883})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build plugin_mqtt || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseMQTTConnack(t *testing.T) {
|
||||
banner, ok := parseMQTTConnack([]byte{0x20, 0x02, 0x00, 0x05})
|
||||
if !ok || !strings.Contains(banner, "not authorized") {
|
||||
t.Fatalf("unexpected mqtt banner: %q ok=%v", banner, ok)
|
||||
}
|
||||
|
||||
if _, ok := parseMQTTConnack([]byte{0x10, 0x02, 0x00, 0x00}); ok {
|
||||
t.Fatal("unexpected match for non-connack packet")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//go:build plugin_tftp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type TFTPPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewTFTPPlugin() *TFTPPlugin {
|
||||
return &TFTPPlugin{BasePlugin: plugins.NewBasePlugin("tftp")}
|
||||
}
|
||||
|
||||
func (p *TFTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||
timeout := session.Config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
conn, err := session.DialUDP(ctx, target, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "tftp"}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := conn.Write(buildTFTPReadRequest("probe")); err != nil {
|
||||
return &ScanResult{Success: false, Service: "tftp"}
|
||||
}
|
||||
|
||||
buf := make([]byte, 516)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil || n < 4 {
|
||||
return &ScanResult{Success: false, Service: "tftp"}
|
||||
}
|
||||
|
||||
banner, ok := parseTFTPResponse(buf[:n])
|
||||
if !ok {
|
||||
return &ScanResult{Success: false, Service: "tftp"}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "tftp",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func buildTFTPReadRequest(filename string) []byte {
|
||||
req := []byte{0x00, 0x01}
|
||||
req = append(req, filename...)
|
||||
req = append(req, 0x00)
|
||||
req = append(req, "octet"...)
|
||||
req = append(req, 0x00)
|
||||
return req
|
||||
}
|
||||
|
||||
func parseTFTPResponse(data []byte) (string, bool) {
|
||||
if len(data) < 4 || data[0] != 0x00 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
opcode := data[1]
|
||||
switch opcode {
|
||||
case 0x03:
|
||||
return "TFTP DATA response", true
|
||||
case 0x05:
|
||||
msg := strings.TrimRight(string(data[4:]), "\x00")
|
||||
if len(msg) > 160 {
|
||||
msg = msg[:160]
|
||||
}
|
||||
if msg == "" {
|
||||
msg = "error response"
|
||||
}
|
||||
return fmt.Sprintf("TFTP %s", msg), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterUDPPluginWithPorts("tftp", func() Plugin {
|
||||
return NewTFTPPlugin()
|
||||
}, []int{69})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build plugin_tftp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTFTPReadRequestAndResponse(t *testing.T) {
|
||||
req := buildTFTPReadRequest("probe")
|
||||
want := []byte{0x00, 0x01, 'p', 'r', 'o', 'b', 'e', 0x00, 'o', 'c', 't', 'e', 't', 0x00}
|
||||
if string(req) != string(want) {
|
||||
t.Fatalf("unexpected tftp request: %#v", req)
|
||||
}
|
||||
|
||||
banner, ok := parseTFTPResponse([]byte{0x00, 0x05, 0x00, 0x01, 'n', 'o', 't', ' ', 'f', 'o', 'u', 'n', 'd', 0x00})
|
||||
if !ok || !strings.Contains(banner, "not found") {
|
||||
t.Fatalf("unexpected tftp banner: %q ok=%v", banner, ok)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//go:build plugin_zookeeper || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type ZooKeeperPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewZooKeeperPlugin() *ZooKeeperPlugin {
|
||||
return &ZooKeeperPlugin{BasePlugin: plugins.NewBasePlugin("zookeeper")}
|
||||
}
|
||||
|
||||
func (p *ZooKeeperPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||
timeout := session.Config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{Success: false, Service: "zookeeper"}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte("ruok")); err != nil {
|
||||
return &ScanResult{Success: false, Service: "zookeeper"}
|
||||
}
|
||||
|
||||
buf := make([]byte, 512)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil || n == 0 {
|
||||
return &ScanResult{Success: false, Service: "zookeeper"}
|
||||
}
|
||||
|
||||
banner, ok := parseZooKeeperResponse(buf[:n])
|
||||
if !ok {
|
||||
return &ScanResult{Success: false, Service: "zookeeper"}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "zookeeper",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func parseZooKeeperResponse(data []byte) (string, bool) {
|
||||
resp := strings.TrimSpace(string(data))
|
||||
if resp == "imok" {
|
||||
return "ZooKeeper ruok=imok", true
|
||||
}
|
||||
lower := strings.ToLower(resp)
|
||||
if strings.Contains(lower, "zookeeper") || strings.Contains(lower, "zk_version") ||
|
||||
strings.Contains(lower, "mode:") || strings.Contains(lower, "not in the whitelist") {
|
||||
if len(resp) > 200 {
|
||||
resp = resp[:200]
|
||||
}
|
||||
return resp, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("zookeeper", func() Plugin {
|
||||
return NewZooKeeperPlugin()
|
||||
}, []int{2181})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build plugin_zookeeper || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseZooKeeperResponse(t *testing.T) {
|
||||
banner, ok := parseZooKeeperResponse([]byte("imok"))
|
||||
if !ok || banner != "ZooKeeper ruok=imok" {
|
||||
t.Fatalf("unexpected zookeeper banner: %q ok=%v", banner, ok)
|
||||
}
|
||||
|
||||
if _, ok := parseZooKeeperResponse([]byte("hello")); ok {
|
||||
t.Fatal("unexpected match for non-zookeeper response")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user