diff --git a/README.md b/README.md index d1700905..161b82e2 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ OUTPUT: CONFIGURATIONS: -config string path to the nuclei configuration file + -config-directory string override the default config path ($home/.config) -fr, -follow-redirects enable following redirects for http templates -mr, -max-redirects int max number of redirects to follow for http templates (default 10) -dr, -disable-redirects disable redirects for http templates @@ -158,6 +159,8 @@ CONFIGURATIONS: -sml, -show-match-line show match lines for file templates, works with extractors only -ztls use ztls library with autofallback to standard one for tls13 -sni string tls sni hostname to use (default: input domain name) + -i, -interface string network interface to use for network scan + -sip, -source-ip string source ip address to use for network scan INTERACTSH: -iserver, -interactsh-server string interactsh server url for self-hosted instance (default: oast.pro,oast.live,oast.site,oast.online,oast.fun,oast.me) diff --git a/v2/cmd/nuclei/main.go b/v2/cmd/nuclei/main.go index f066ffa1..72dc9810 100644 --- a/v2/cmd/nuclei/main.go +++ b/v2/cmd/nuclei/main.go @@ -183,6 +183,8 @@ on extensive configurability, massive extensibility and ease of use.`) flagSet.BoolVarP(&options.ShowMatchLine, "show-match-line", "sml", false, "show match lines for file templates, works with extractors only"), flagSet.BoolVar(&options.ZTLS, "ztls", false, "use ztls library with autofallback to standard one for tls13"), flagSet.StringVar(&options.SNI, "sni", "", "tls sni hostname to use (default: input domain name)"), + flagSet.StringVarP(&options.Interface, "interface", "i", "", "network interface to use for network scan"), + flagSet.StringVarP(&options.SourceIP, "source-ip", "sip", "", "source ip address to use for network scan"), flagSet.StringVar(&options.CustomConfigDir, "config-directory", "", "Override the default config path ($home/.config)"), ) diff --git a/v2/pkg/protocols/common/protocolstate/state.go b/v2/pkg/protocols/common/protocolstate/state.go index a08cb707..e422c0d1 100644 --- a/v2/pkg/protocols/common/protocolstate/state.go +++ b/v2/pkg/protocols/common/protocolstate/state.go @@ -1,6 +1,9 @@ package protocolstate import ( + "fmt" + "net" + "github.com/pkg/errors" "github.com/projectdiscovery/fastdialer/fastdialer" @@ -16,6 +19,48 @@ func Init(options *types.Options) error { return nil } opts := fastdialer.DefaultOptions + + switch { + case options.SourceIP != "" && options.Interface != "": + isAssociated, err := isIpAssociatedWithInterface(options.SourceIP, options.Interface) + if err != nil { + return err + } + if isAssociated { + opts.Dialer = &net.Dialer{ + LocalAddr: &net.TCPAddr{ + IP: net.ParseIP(options.SourceIP), + }, + } + } else { + return fmt.Errorf("source ip (%s) is not associated with the interface (%s)", options.SourceIP, options.Interface) + } + case options.SourceIP != "": + isAssociated, err := isIpAssociatedWithInterface(options.SourceIP, "any") + if err != nil { + return err + } + if isAssociated { + opts.Dialer = &net.Dialer{ + LocalAddr: &net.TCPAddr{ + IP: net.ParseIP(options.SourceIP), + }, + } + } else { + return fmt.Errorf("source ip (%s) is not associated with any network interface", options.SourceIP) + } + case options.Interface != "": + ifadrr, err := interfaceAddress(options.Interface) + if err != nil { + return err + } + opts.Dialer = &net.Dialer{ + LocalAddr: &net.TCPAddr{ + IP: ifadrr, + }, + } + } + if options.SystemResolvers { opts.EnableFallback = true } @@ -33,6 +78,58 @@ func Init(options *types.Options) error { return nil } +// isIpAssociatedWithInterface checks if the given IP is associated with the given interface. +func isIpAssociatedWithInterface(souceIP, interfaceName string) (bool, error) { + addrs, err := interfaceAddresses(interfaceName) + if err != nil { + return false, err + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ipnet.IP.String() == souceIP { + return true, nil + } + } + } + return false, nil +} + +// interfaceAddress returns the first IPv4 address of the given interface. +func interfaceAddress(interfaceName string) (net.IP, error) { + addrs, err := interfaceAddresses(interfaceName) + if err != nil { + return nil, err + } + var address net.IP + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + address = ipnet.IP + } + } + } + if address == nil { + return nil, fmt.Errorf("no suitable address found for interface: `%s`", interfaceName) + } + return address, nil +} + +// interfaceAddresses returns all interface addresses. +func interfaceAddresses(interfaceName string) ([]net.Addr, error) { + if interfaceName == "any" { + return net.InterfaceAddrs() + } + ief, err := net.InterfaceByName(interfaceName) + if err != nil { + return nil, errors.Wrapf(err, "failed to get interface: `%s`", interfaceName) + } + addrs, err := ief.Addrs() + if err != nil { + return nil, errors.Wrapf(err, "failed to get interface addresses for: `%s`", interfaceName) + } + return addrs, nil +} + // Close closes the global shared fastdialer func Close() { if Dialer != nil { diff --git a/v2/pkg/types/types.go b/v2/pkg/types/types.go index e9938863..8d710789 100644 --- a/v2/pkg/types/types.go +++ b/v2/pkg/types/types.go @@ -230,6 +230,10 @@ type Options struct { DisableRedirects bool // SNI custom hostname SNI string + // Interface to use for network scan + Interface string + // SourceIP sets custom source IP address for network requests + SourceIP string // Health Check HealthCheck bool // Time to wait between each input read operation before closing the stream