Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 97 additions & 6 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"context"
"fmt"
"net"
"net/url"
"strings"
"time"

"github.com/RedTeamPentesting/adauth"
Expand All @@ -29,6 +31,7 @@ type Config struct {
ConfigPath string
PprofEnabled bool
VerbosityLevel int
SocksProxy string
LdapAuthOptions *ldapauth.Options
RuntimeOptions *RuntimeOptions

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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=<dur> 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")
Expand All @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -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=<dur> 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)
}
93 changes: 91 additions & 2 deletions config/credmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,54 @@ 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) {
return cd.DialContext(context.Background(), network, address)
}

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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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=<dur> 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 {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=