mirror of
https://github.com/projectdiscovery/nuclei
synced 2026-06-08 16:50:47 +00:00
20cbf0bd73
* feat(filepath): add IsPathWithinAnyDirectory func Signed-off-by: Dwi Siswanto <git@dw1.io> * refactor: use path-aware filesystem containment checks Several filesystem trust boundaries relied on lexical prefix checks to decide whether a path fell under an allowed directory. That let sibling paths be treated as if they were children of trusted directories. Replace those checks with canonical path containment checks for helper payload loading, template archive extraction, custom template metadata, template path classification, and headless screenshot output validation. Signed-off-by: Dwi Siswanto <git@dw1.io> * adding more tests * fixing tests * adding test --------- Signed-off-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Mzack9999 <mzack9999@protonmail.com>
303 lines
8.2 KiB
Go
303 lines
8.2 KiB
Go
package protocolstate
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/go-sql-driver/mysql"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/net/proxy"
|
|
|
|
"github.com/projectdiscovery/fastdialer/fastdialer"
|
|
"github.com/projectdiscovery/mapcidr/asn"
|
|
"github.com/projectdiscovery/networkpolicy"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/utils/expand"
|
|
"github.com/projectdiscovery/retryablehttp-go"
|
|
mapsutil "github.com/projectdiscovery/utils/maps"
|
|
)
|
|
|
|
var (
|
|
dialers *mapsutil.SyncLockMap[string, *Dialers]
|
|
)
|
|
|
|
func init() {
|
|
dialers = mapsutil.NewSyncLockMap[string, *Dialers]()
|
|
}
|
|
|
|
func GetDialers(ctx context.Context) *Dialers {
|
|
executionContext := GetExecutionContext(ctx)
|
|
dialers, ok := dialers.Get(executionContext.ExecutionID)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return dialers
|
|
}
|
|
|
|
func GetDialersWithId(id string) *Dialers {
|
|
dialers, ok := dialers.Get(id)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return dialers
|
|
}
|
|
|
|
func ShouldInit(id string) bool {
|
|
dialer, ok := dialers.Get(id)
|
|
if !ok {
|
|
return true
|
|
}
|
|
return dialer == nil
|
|
}
|
|
|
|
// Init creates the Dialers instance based on user configuration
|
|
func Init(options *types.Options) error {
|
|
if existingDialers := GetDialersWithId(options.ExecutionId); existingDialers != nil {
|
|
// Dialers already exist for this ExecutionId. Refresh the LFA /
|
|
// network-policy state derived from options so that a second
|
|
// Init call with different options (e.g. flipping
|
|
// AllowLocalFileAccess) is reflected in IsLfaAllowed and the
|
|
// per-execution dialer state. Without this refresh the second
|
|
// caller silently keeps the first caller's settings, which is a
|
|
// footgun for tests and SDK callers that share an execution id.
|
|
existingDialers.Lock()
|
|
existingDialers.LocalFileAccessAllowed = options.AllowLocalFileAccess
|
|
existingDialers.RestrictLocalNetworkAccess = options.RestrictLocalNetworkAccess
|
|
existingDialers.Unlock()
|
|
SetLfaAllowed(options)
|
|
return nil
|
|
}
|
|
|
|
return initDialers(options)
|
|
}
|
|
|
|
// initDialers is the internal implementation of Init
|
|
func initDialers(options *types.Options) error {
|
|
opts := fastdialer.DefaultOptions
|
|
opts.DialerTimeout = options.GetTimeouts().DialTimeout
|
|
if options.DialerKeepAlive > 0 {
|
|
opts.DialerKeepAlive = options.DialerKeepAlive
|
|
}
|
|
|
|
var expandedDenyList []string
|
|
for _, excludeTarget := range options.ExcludeTargets {
|
|
switch {
|
|
case asn.IsASN(excludeTarget):
|
|
expandedDenyList = append(expandedDenyList, expand.ASN(excludeTarget)...)
|
|
default:
|
|
expandedDenyList = append(expandedDenyList, excludeTarget)
|
|
}
|
|
}
|
|
|
|
if options.RestrictLocalNetworkAccess {
|
|
expandedDenyList = append(expandedDenyList, networkpolicy.DefaultIPv4DenylistRanges...)
|
|
expandedDenyList = append(expandedDenyList, networkpolicy.DefaultIPv6DenylistRanges...)
|
|
}
|
|
npOptions := &networkpolicy.Options{
|
|
DenyList: expandedDenyList,
|
|
}
|
|
opts.WithNetworkPolicyOptions = npOptions
|
|
|
|
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.AliveSocksProxy != "" {
|
|
proxyURL, err := url.Parse(options.AliveSocksProxy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var forward *net.Dialer
|
|
if opts.Dialer != nil {
|
|
forward = opts.Dialer
|
|
} else {
|
|
forward = &net.Dialer{
|
|
Timeout: opts.DialerTimeout,
|
|
KeepAlive: opts.DialerKeepAlive,
|
|
DualStack: true,
|
|
}
|
|
}
|
|
dialer, err := proxy.FromURL(proxyURL, forward)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.ProxyDialer = &dialer
|
|
}
|
|
|
|
if options.SystemResolvers {
|
|
opts.ResolversFile = true
|
|
opts.EnableFallback = true
|
|
}
|
|
if len(options.InternalResolversList) > 0 {
|
|
opts.BaseResolvers = options.InternalResolversList
|
|
}
|
|
|
|
opts.Deny = append(opts.Deny, expandedDenyList...)
|
|
|
|
opts.WithDialerHistory = true
|
|
opts.SNIName = options.SNI
|
|
// this instance is used in javascript protocol libraries and
|
|
// dial history is required to get dialed ip of a host
|
|
opts.WithDialerHistory = true
|
|
|
|
// fastdialer now by default fallbacks to ztls when there are tls related errors
|
|
dialer, err := fastdialer.NewDialer(opts)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not create dialer")
|
|
}
|
|
|
|
networkPolicy, _ := networkpolicy.New(*npOptions)
|
|
|
|
httpClientPool := mapsutil.NewSyncLockMap(
|
|
// evicts inactive httpclientpool entries after 24 hours
|
|
// of inactivity (long running instances)
|
|
mapsutil.WithEviction[string, *retryablehttp.Client](24*time.Hour, 12*time.Hour),
|
|
)
|
|
|
|
dialersInstance := &Dialers{
|
|
Fastdialer: dialer,
|
|
NetworkPolicy: networkPolicy,
|
|
HTTPClientPool: httpClientPool,
|
|
LocalFileAccessAllowed: options.AllowLocalFileAccess,
|
|
}
|
|
|
|
_ = dialers.Set(options.ExecutionId, dialersInstance)
|
|
|
|
// Set a custom dialer for the "nucleitcp" protocol. This is just plain TCP, but it's registered
|
|
// with a different name so that we do not clobber the "tcp" dialer in the event that nuclei is
|
|
// being included as a package in another application.
|
|
mysql.RegisterDialContext("nucleitcp", func(ctx context.Context, addr string) (net.Conn, error) {
|
|
// Because we're not using the default TCP workflow, quietly add the default port
|
|
// number if no port number was specified.
|
|
if _, _, err := net.SplitHostPort(addr); err != nil {
|
|
addr += ":3306"
|
|
}
|
|
|
|
var executionId string
|
|
if val := ctx.Value("executionId"); val != nil {
|
|
executionId = val.(string)
|
|
}
|
|
dialer := GetDialersWithId(executionId)
|
|
if dialer == nil {
|
|
return nil, fmt.Errorf("dialers not initialized for %s", executionId)
|
|
}
|
|
return dialer.Fastdialer.Dial(ctx, "tcp", addr)
|
|
})
|
|
|
|
StartActiveMemGuardian(context.Background())
|
|
|
|
SetLfaAllowed(options)
|
|
|
|
return nil
|
|
}
|
|
|
|
// isIpAssociatedWithInterface checks if the given IP is associated with the given interface.
|
|
func isIpAssociatedWithInterface(sourceIP, 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() == sourceIP {
|
|
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
|
|
break
|
|
}
|
|
}
|
|
}
|
|
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(executionId string) {
|
|
dialersInstance, ok := dialers.Get(executionId)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if dialersInstance != nil {
|
|
dialersInstance.Fastdialer.Close()
|
|
}
|
|
|
|
dialers.Delete(executionId)
|
|
|
|
if dialers.IsEmpty() {
|
|
StopActiveMemGuardian()
|
|
}
|
|
}
|