From 855534e4375f3efaa8d575fa5ce4d4bb36cc91ec Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Apr 2026 13:24:10 -0300 Subject: [PATCH] add patch proxy --- config/config.go | 103 +++++++++++++++++++++++++++++++++++++++++++--- config/credmgr.go | 93 ++++++++++++++++++++++++++++++++++++++++- go.mod | 1 + go.sum | 6 +++ 4 files changed, 195 insertions(+), 8 deletions(-) diff --git a/config/config.go b/config/config.go index d3d963b..0ff71d3 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,8 @@ import ( "context" "fmt" "net" + "net/url" + "strings" "time" "github.com/RedTeamPentesting/adauth" @@ -29,6 +31,7 @@ type Config struct { ConfigPath string PprofEnabled bool VerbosityLevel int + SocksProxy string LdapAuthOptions *ldapauth.Options RuntimeOptions *RuntimeOptions @@ -61,14 +64,44 @@ const DNS_DIAL_TIMEOUT = 5 * time.Second // Timeout for dialing to DNS server const DNS_LOOKUP_TIMEOUT = 10 * time.Second // Timeout for DNS lookups // DialerWithResolver implements custom LDAP dialing with DNS resolver override. +// When SocksDial is set, it is used for every connection and local DNS +// resolution is bypassed so the proxy handles the name lookup. // TODO: Review if there's a better way (shouldn't ConnectTo respect my specified Resolver?) type DialerWithResolver struct { - Resolver *CustomResolver - Timeout time.Duration + Resolver *CustomResolver + Timeout time.Duration + SocksDial func(network, address string) (net.Conn, error) } // DialContext resolves the address using the custom resolver and dials using TCP. func (d *DialerWithResolver) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + // Route through SOCKS when configured. Hostname is passed through so the + // proxy performs the lookup (socks4a / socks5). For plain socks4, the + // caller must provide an IP. + if d.SocksDial != nil { + type result struct { + conn net.Conn + err error + } + ch := make(chan result, 1) + go func() { + c, err := d.SocksDial(network, addr) + ch <- result{c, err} + }() + select { + case <-ctx.Done(): + go func() { + r := <-ch + if r.conn != nil { + r.conn.Close() + } + }() + return nil, ctx.Err() + case r := <-ch: + return r.conn, r.err + } + } + // Use your resolver to resolve the address first host, port, err := net.SplitHostPort(addr) if err != nil { @@ -140,6 +173,10 @@ func ParseFlags() (*Config, error) { pflag.StringVarP(&config.LdapxAttrs, "ldapx-attrs", "a", "", "LDAP attributes obfuscation middleware chain (e.g., 'Owp', read the docs for details)") pflag.StringVarP(&config.LdapxBaseDN, "ldapx-basedn", "b", "", "LDAP baseDN obfuscation middleware chain (e.g., 'OX', read the docs for details)") + // SOCKS proxy flag. Supports socks4://, socks4a:// and socks5:// URIs. + // Example: socks5://user:pass@127.0.0.1:1080 + pflag.StringVarP(&config.SocksProxy, "socks", "x", "", "SOCKS proxy URI (socks4://, socks4a:// or socks5://) to tunnel LDAP, SMB, RPC and HTTP traffic") + // Register adauth flags for ingestion standardAuthOptions := &adauth.Options{} registerIngestionAuthFlags(standardAuthOptions, pflag.CommandLine) @@ -195,15 +232,29 @@ func ParseFlags() (*Config, error) { return nil, err } + // Validate SOCKS URI and build the shared socks dial function. + // h12.io/socks accepts socks4://, socks4a://, socks5:// with optional + // userinfo and a ?timeout= query param. + var socksLdapDial, socksKrbDial func(network, address string) (net.Conn, error) + if config.SocksProxy != "" { + if err := validateSocksURI(config.SocksProxy); err != nil { + return nil, fmt.Errorf("invalid --socks URI: %w", err) + } + socksLdapDial = socksDialWithClear(applySocksTimeout(config.SocksProxy, config.LdapAuthOptions.Timeout)) + socksKrbDial = socksDialWithClear(applySocksTimeout(config.SocksProxy, KERBEROS_TIMEOUT)) + } + // Used for LDAP connections config.LdapAuthOptions.LDAPDialer = &DialerWithResolver{ - Resolver: resolver, - Timeout: config.LdapAuthOptions.Timeout, + Resolver: resolver, + Timeout: config.LdapAuthOptions.Timeout, + SocksDial: socksLdapDial, } config.LdapAuthOptions.KerberosDialer = &DialerWithResolver{ - Resolver: resolver, - Timeout: KERBEROS_TIMEOUT, + Resolver: resolver, + Timeout: KERBEROS_TIMEOUT, + SocksDial: socksKrbDial, } isEmptyPassword := standardAuthOptions.Password == "" && pflag.CommandLine.Changed("password") @@ -219,6 +270,9 @@ func ParseFlags() (*Config, error) { if ingestAuth != nil { ingestAuth.SetDC(config.DomainController) + if config.SocksProxy != "" { + ingestAuth.SetSocksProxy(config.SocksProxy) + } config.IngestAuth = ingestAuth } @@ -231,6 +285,9 @@ func ParseFlags() (*Config, error) { // Not sure if it's needed to set the DC here, // setting just in case (maybe it's needed for kerberos?) remoteAuth.SetDC(config.DomainController) + if config.SocksProxy != "" { + remoteAuth.SetSocksProxy(config.SocksProxy) + } config.RemoteAuth = remoteAuth } else { config.RemoteAuth = ingestAuth @@ -308,3 +365,37 @@ func setupDNSResolver(customDNS string, useTCP bool) (*CustomResolver, error) { return customResolver, nil } + +// validateSocksURI rejects obviously malformed SOCKS proxy URIs early so the +// user gets a clear error instead of a cryptic handshake failure at dial time. +func validateSocksURI(raw string) error { + u, err := url.Parse(raw) + if err != nil { + return err + } + switch strings.ToLower(u.Scheme) { + case "socks4", "socks4a", "socks5": + default: + return fmt.Errorf("unsupported scheme %q (expected socks4, socks4a or socks5)", u.Scheme) + } + if u.Host == "" { + return fmt.Errorf("missing host:port") + } + if _, _, err := net.SplitHostPort(u.Host); err != nil { + return fmt.Errorf("invalid host:port: %w", err) + } + return nil +} + +// applySocksTimeout appends ?timeout= to the URI when not already present. +// h12.io/socks honors this during the SOCKS handshake. +func applySocksTimeout(uri string, timeout time.Duration) string { + if timeout <= 0 || strings.Contains(uri, "timeout=") { + return uri + } + sep := "?" + if strings.Contains(uri, "?") { + sep = "&" + } + return fmt.Sprintf("%s%stimeout=%s", uri, sep, timeout) +} diff --git a/config/credmgr.go b/config/credmgr.go index 4352842..05527fb 100644 --- a/config/credmgr.go +++ b/config/credmgr.go @@ -12,12 +12,18 @@ import ( "time" "github.com/RedTeamPentesting/adauth" + "h12.io/socks" ) -// CustomDialer wraps a net.Dialer to use CustomResolver's cache +// CustomDialer wraps a net.Dialer to use CustomResolver's cache. +// If socksDial is set, it is used for all connections and local DNS +// resolution is skipped so the SOCKS proxy resolves the hostname +// (socks5h-like behavior). For socks4 the hostname must already be +// an IP; use socks4a or socks5 for remote name resolution. type CustomDialer struct { net.Dialer - resolver *CustomResolver + resolver *CustomResolver + socksDial func(network, address string) (net.Conn, error) } func (cd *CustomDialer) Dial(network, address string) (net.Conn, error) { @@ -25,6 +31,35 @@ func (cd *CustomDialer) Dial(network, address string) (net.Conn, error) { } func (cd *CustomDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + // If a SOCKS dialer is configured, delegate everything to it. + // Hostnames are passed through so the proxy handles resolution. + if cd.socksDial != nil { + // h12.io/socks does not expose a DialContext; honor ctx via a + // goroutine + cancellation so ctx timeouts are respected. + type result struct { + conn net.Conn + err error + } + ch := make(chan result, 1) + go func() { + c, err := cd.socksDial(network, address) + ch <- result{c, err} + }() + select { + case <-ctx.Done(): + // Best effort: close the conn if it arrived after cancel + go func() { + r := <-ch + if r.conn != nil { + r.conn.Close() + } + }() + return nil, ctx.Err() + case r := <-ch: + return r.conn, r.err + } + } + host, port, err := net.SplitHostPort(address) if err != nil { return nil, err @@ -52,6 +87,7 @@ func (cd *CustomDialer) DialContext(ctx context.Context, network, address string type CredentialMgr struct { credential *adauth.Credential useKerberos bool + socksProxy string } func NewCredentialMgr(credential *adauth.Credential, useKerberos bool) *CredentialMgr { @@ -69,6 +105,17 @@ func (a *CredentialMgr) SetDC(dc string) { a.credential.SetDC(dc) } +// SetSocksProxy configures a SOCKS proxy URI (e.g. socks5://127.0.0.1:1080) +// to be used by every Dialer this manager produces. An empty string disables +// the proxy. +func (a *CredentialMgr) SetSocksProxy(uri string) { + a.socksProxy = uri +} + +func (a *CredentialMgr) SocksProxy() string { + return a.socksProxy +} + func (a *CredentialMgr) Kerberos() bool { return a.useKerberos } @@ -84,9 +131,51 @@ func (a *CredentialMgr) Dialer(timeout time.Duration) *CustomDialer { } } + if a.socksProxy != "" { + // Embed timeout into the URI so h12.io/socks honors it at dial time. + // socksDialWithClear strips the deadline that the SOCKS5 handshake + // leaves on the connection, so long-running reads (paged LDAP + // searches, large SMB/RPC transfers) are not killed by it. + dialer.socksDial = socksDialWithClear(appendSocksTimeout(a.socksProxy, timeout)) + } + return dialer } +// appendSocksTimeout injects ?timeout= into a SOCKS URI when the user +// has not specified one. h12.io/socks reads this query parameter to bound +// the proxy handshake. +func appendSocksTimeout(uri string, timeout time.Duration) string { + if timeout <= 0 { + return uri + } + if strings.Contains(uri, "timeout=") { + return uri + } + sep := "?" + if strings.Contains(uri, "?") { + sep = "&" + } + return fmt.Sprintf("%s%stimeout=%s", uri, sep, timeout) +} + +// socksDialWithClear wraps a SOCKS dial function to strip any residual +// Read/Write deadline left on the connection by the SOCKS5 handshake. +// h12.io/socks v1.0.3 clears deadlines on the SOCKS4 path but forgets to +// do so on the SOCKS5 path, which causes i/o timeouts on long-running +// LDAP paged searches that exceed the handshake timeout. +func socksDialWithClear(uri string) func(network, address string) (net.Conn, error) { + inner := socks.Dial(uri) + return func(network, address string) (net.Conn, error) { + c, err := inner(network, address) + if err != nil { + return nil, err + } + _ = c.SetDeadline(time.Time{}) + return c, nil + } +} + func (a *CredentialMgr) Resolver() *net.Resolver { if a.credential != nil && a.credential.Resolver != nil { if customResolver, ok := a.credential.Resolver.(*CustomResolver); ok { diff --git a/go.mod b/go.mod index 81a7ddc..9def1a2 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 golang.org/x/sync v0.19.0 gopkg.in/yaml.v3 v3.0.1 + h12.io/socks v1.0.3 ) require ( diff --git a/go.sum b/go.sum index 8a08a9a..3cbd808 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= +github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -93,6 +95,8 @@ github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -188,5 +192,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= +h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=