Files
sif/internal/modules/yaml.go
T
TigahandGitHub bf8c6a8643 feat(modules): add tcp module executor and recon modules (#308)
* feat(modules): tcp module executor

dial+probe+banner with word/regex/size matchers, ctx-honest watcher,
port required.

* feat(modules): add redis unauthenticated access tcp module

first shipped tcp module: sends INFO to redis (6379) and matches
redis_version in the reply, which an auth-required server never returns
(it answers -NOAUTH). extracts the leaked version.

* docs(modules): document the tcp module type

flip the http-only note and add a tcp config section (port, data,
word/regex/size matchers and regex extractors against the banner),
pointing at the redis demo module.

* feat(modules): support matchers-condition in tcp modules

tcp matchers were and-only; add the matchers-condition key (and default,
or) mirroring the http executor's checkMatchers, validated at load via the
shared validateMatchersCondition. documents the override in the combining
matchers section.

* feat(modules): decode escape sequences in tcp data payloads

decode C-style escapes (\\ \a \b \f \n \r \t \v \xHH) in a tcp module's
data value before it goes on the wire, so a single-quoted or plain yaml
scalar reaches the service with the same control bytes a double-quoted
one already would. an unrecognized escape is kept verbatim so no bytes
are silently lost.

* feat(modules): add tcp recon modules for common services

add five built-in tcp modules alongside the redis one so the executor
ships a real service set: memcached (unauth version probe), ssh and ftp
(passive banner version disclosure), mysql/mariadb (handshake exposure,
matched with matchers-condition: or across both auth plugin names), and
vnc (rfb handshake exposure). each fingerprint tracks the documented
on-wire protocol and pulls the version into an extractor.

* feat(modules): add mail and rsync tcp recon modules

add four more passive-banner tcp modules: smtp, pop3, and imap read the
greeting each mail service sends on connect (the smtp matcher requires
an esmtp/smtp marker so it does not fire on a bare 220 ftp greeting),
and rsync detects an exposed daemon by its @RSYNCD handshake. each pulls
the banner or protocol version into an extractor.
2026-07-22 12:49:22 -07:00

186 lines
6.7 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
-------------------------------------------------------------------------------------------------
: :
: SIF - Blazing-fast pentesting suite :
: Blaze - BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
-------------------------------------------------------------------------------------------------
*/
package modules
import (
"context"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// YAMLModule represents a parsed YAML module file
type YAMLModule struct {
ID string `yaml:"id"`
Info YAMLModuleInfo `yaml:"info"`
Type ModuleType `yaml:"type"`
HTTP *HTTPConfig `yaml:"http,omitempty"`
DNS *DNSConfig `yaml:"dns,omitempty"`
TCP *TCPConfig `yaml:"tcp,omitempty"`
}
// YAMLModuleInfo contains module metadata
type YAMLModuleInfo struct {
Name string `yaml:"name"`
Author string `yaml:"author"`
Severity string `yaml:"severity"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
}
// HTTPConfig defines HTTP module settings
type HTTPConfig struct {
Method string `yaml:"method"`
Paths []string `yaml:"paths"`
Wordlist string `yaml:"wordlist,omitempty"`
Payloads []string `yaml:"payloads,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
Body string `yaml:"body,omitempty"`
Attack string `yaml:"attack,omitempty"` // clusterbomb (default), pitchfork
Threads int `yaml:"threads,omitempty"`
DisableRedirects bool `yaml:"disable-redirects,omitempty"` // stop at the first response; don't follow 3xx
Matchers []Matcher `yaml:"matchers"`
MatchersCondition string `yaml:"matchers-condition,omitempty"` // and (default), or
Extractors []Extractor `yaml:"extractors,omitempty"`
}
// DNSConfig defines DNS module settings
type DNSConfig struct {
Type string `yaml:"type"` // A, AAAA, MX, TXT, NS, etc.
Name string `yaml:"name"`
Matchers []Matcher `yaml:"matchers"`
Extractors []Extractor `yaml:"extractors,omitempty"`
}
// TCPConfig defines TCP module settings
type TCPConfig struct {
Port int `yaml:"port"`
Data string `yaml:"data,omitempty"`
Matchers []Matcher `yaml:"matchers"`
MatchersCondition string `yaml:"matchers-condition,omitempty"` // and (default), or
Extractors []Extractor `yaml:"extractors,omitempty"`
}
// ParseYAMLModule parses a YAML file into a module definition
func ParseYAMLModule(path string) (*YAMLModule, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read module file: %w", err)
}
var ym YAMLModule
if err := yaml.Unmarshal(data, &ym); err != nil {
return nil, fmt.Errorf("parse yaml: %w", err)
}
if ym.ID == "" {
return nil, fmt.Errorf("module missing required field: id")
}
if ym.Type == "" {
return nil, fmt.Errorf("module missing required field: type")
}
if ym.HTTP != nil {
if err := validateAttack(ym.HTTP.Attack); err != nil {
return nil, fmt.Errorf("module %q: %w", ym.ID, err)
}
if err := validateMatchersCondition(ym.HTTP.MatchersCondition); err != nil {
return nil, fmt.Errorf("module %q: %w", ym.ID, err)
}
}
if ym.TCP != nil {
if err := validateTCP(ym.TCP); err != nil {
return nil, fmt.Errorf("module %q: %w", ym.ID, err)
}
}
var matchers []Matcher
switch {
case ym.HTTP != nil:
matchers = ym.HTTP.Matchers
case ym.DNS != nil:
matchers = ym.DNS.Matchers
case ym.TCP != nil:
matchers = ym.TCP.Matchers
}
if err := validateMatchers(matchers); err != nil {
return nil, fmt.Errorf("module %q: %w", ym.ID, err)
}
return &ym, nil
}
// yamlModuleWrapper wraps YAMLModule to implement the Module interface
type yamlModuleWrapper struct {
def *YAMLModule
path string
}
// newYAMLModuleWrapper creates a Module from a YAMLModule definition
func newYAMLModuleWrapper(def *YAMLModule, path string) *yamlModuleWrapper {
return &yamlModuleWrapper{def: def, path: path}
}
// Info returns the module metadata
func (m *yamlModuleWrapper) Info() Info {
return Info{
ID: m.def.ID,
Name: m.def.Info.Name,
Author: m.def.Info.Author,
Severity: m.def.Info.Severity,
Description: m.def.Info.Description,
Tags: m.def.Info.Tags,
}
}
// Type returns the module type
func (m *yamlModuleWrapper) Type() ModuleType {
return m.def.Type
}
// Execute runs the module (delegates to appropriate executor)
func (m *yamlModuleWrapper) Execute(ctx context.Context, target string, opts Options) (*Result, error) {
switch m.def.Type {
case TypeHTTP:
if m.def.HTTP == nil {
return nil, fmt.Errorf("HTTP module missing http configuration")
}
return ExecuteHTTPModule(ctx, target, m.def, opts)
case TypeDNS:
if m.def.DNS == nil {
return nil, fmt.Errorf("DNS module missing dns configuration")
}
return ExecuteDNSModule(ctx, target, m.def, opts)
case TypeTCP:
if m.def.TCP == nil {
return nil, fmt.Errorf("TCP module missing tcp configuration")
}
return ExecuteTCPModule(ctx, target, m.def, opts)
default:
return nil, fmt.Errorf("unsupported module type: %s", m.def.Type)
}
}