diff --git a/.gitignore b/.gitignore index ac3ecc6..b6016aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /examples/dcerpc/dcerpc /ldap /examples/ldap/ldap -/pkinit /examples/pkinit/pkinit +/smb +/examples/smb/smb *.exe diff --git a/README.md b/README.md index 69bf495..4db19e6 100644 --- a/README.md +++ b/README.md @@ -64,20 +64,24 @@ PKINIT implementation as well as helpers for creating and writing CCache files * UnPAC-the-Hash * Pass-the-Hash (RC4/NT or AES key) * CCache (containing TGT or ST) + * SOCKS5 support * NTLM: * Pass-the-Hash * LDAP: * Kerberos, NTLM, Simple Bind * mTLS Authentication / Pass-the-Certificate (LDAPS or LDAP+StartTLS) * Channel Binding (Kerberos and NTLM) + * SOCKS5 support * SMB: * Kerberos, NTLM * Signing and Sealing + * SOCKS5 support * DCERPC: * Kerberos, NTLM * Raw endpoits (with port mapping) * Named pipes (SMB) * Signing and Sealing + * SOCKS5 support ## Caveats diff --git a/dcerpcauth/dcerpcauth.go b/dcerpcauth/dcerpcauth.go index 49d216f..569e68b 100644 --- a/dcerpcauth/dcerpcauth.go +++ b/dcerpcauth/dcerpcauth.go @@ -3,6 +3,7 @@ package dcerpcauth import ( "context" "fmt" + "net" "strings" "github.com/RedTeamPentesting/adauth" @@ -25,8 +26,11 @@ type Options struct { // will be enabled for the SMB dialer, specify an empty slice to disable // this default. SMBOptions []smb2.DialerOption - // PKINITOptions can be used to modify the Kerberos PKINIT behavior. - PKINITOptions []pkinit.Option + + // KerberosDialer is a custom dialer that is used to request Kerberos + // tickets. + KerberosDialer adauth.Dialer + // Debug can be set to enable debug output, for example with // adauth.NewDebugFunc(...). Debug func(string, ...any) @@ -85,6 +89,7 @@ func AuthenticationOptions( CCachePath: creds.CCache, DisablePAFXFAST: true, DCEStyle: true, + KDCDialer: upstreamOptions.KerberosDialer, }), )) @@ -96,6 +101,7 @@ func AuthenticationOptions( CCachePath: creds.CCache, DisablePAFXFAST: true, DCEStyle: true, + KDCDialer: upstreamOptions.KerberosDialer, }), dcerpc.WithSMBDialer(smb2.NewDialer(smbOptions...)), ) @@ -148,8 +154,13 @@ func DCERPCCredentials(ctx context.Context, creds *adauth.Credential, options *O return nil, fmt.Errorf("generate kerberos config: %w", err) } + dialer := options.KerberosDialer + if dialer == nil { + dialer = &net.Dialer{Timeout: pkinit.DefaultKerberosRoundtripDeadline} + } + ccache, err := pkinit.Authenticate(ctx, creds.Username, strings.ToUpper(creds.Domain), - creds.ClientCert, creds.ClientCertKey, krbConf, options.PKINITOptions...) + creds.ClientCert, creds.ClientCertKey, krbConf, pkinit.WithDialer(adauth.AsContextDialer(dialer))) if err != nil { return nil, fmt.Errorf("PKINIT: %w", err) } diff --git a/dialer.go b/dialer.go new file mode 100644 index 0000000..0f561e9 --- /dev/null +++ b/dialer.go @@ -0,0 +1,66 @@ +package adauth + +import ( + "context" + "net" + "strings" + + "golang.org/x/net/proxy" +) + +type Dialer interface { + Dial(net string, addr string) (net.Conn, error) +} + +type ContextDialer interface { + DialContext(ctx context.Context, net string, addr string) (net.Conn, error) + Dial(net string, addr string) (net.Conn, error) +} + +type nopContextDialer func(string, string) (net.Conn, error) + +func (f nopContextDialer) DialContext(ctx context.Context, net string, addr string) (net.Conn, error) { + return f(net, addr) +} + +func (f nopContextDialer) Dial(net string, addr string) (net.Conn, error) { + return f(net, addr) +} + +// AsContextDialer converts a Dialer into a ContextDialer that either uses the +// dialer's DialContext method if implemented or it uses a DialContext method +// that simply calls Dial ignoring the context. +func AsContextDialer(d Dialer) ContextDialer { + ctxDialer, ok := d.(ContextDialer) + if !ok { + ctxDialer = nopContextDialer(d.Dial) + } + + return ctxDialer +} + +// SOCKS5Dialer returns a SOCKS5 dialer. +func SOCKS5Dialer(network string, address string, auth *proxy.Auth, forward *net.Dialer) ContextDialer { + proxyDialer, err := proxy.SOCKS5(network, address, auth, forward) + if err != nil { + return nopContextDialer(func(s1, s2 string) (net.Conn, error) { + return nil, err + }) + } + + return AsContextDialer(proxyDialer) +} + +// DialerWithSOCKS5ProxyIfSet returns a SOCKS5 dialer if socks5Server is not +// empty and it returns the forward dialer otherwise. +func DialerWithSOCKS5ProxyIfSet(socks5Server string, forward *net.Dialer) ContextDialer { + if forward == nil { + forward = &net.Dialer{} + } + + if strings.TrimSpace(socks5Server) == "" { + return AsContextDialer(forward) + } + + return SOCKS5Dialer("tcp", socks5Server, nil, forward) +} diff --git a/examples/dcerpc/main.go b/examples/dcerpc/main.go index 6b64add..d246994 100644 --- a/examples/dcerpc/main.go +++ b/examples/dcerpc/main.go @@ -18,8 +18,9 @@ import ( func run() error { var ( - debug bool - authOpts = &adauth.Options{ + debug bool + socksServer = os.Getenv("SOCKS5_SERVER") + authOpts = &adauth.Options{ Debug: adauth.NewDebugFunc(&debug, os.Stderr, true), } dcerpcauthOpts = &dcerpcauth.Options{ @@ -29,6 +30,7 @@ func run() error { ) pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output") + pflag.CommandLine.StringVar(&socksServer, "socks", socksServer, "SOCKS5 proxy server") pflag.CommandLine.BoolVar(&namedPipe, "named-pipe", false, "Use named pipe (SMB) as transport") authOpts.RegisterFlags(pflag.CommandLine) pflag.Parse() @@ -37,6 +39,8 @@ func run() error { return fmt.Errorf("usage: %s [options] ", binaryName()) } + dcerpcauthOpts.KerberosDialer = adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil) + creds, target, err := authOpts.WithTarget(context.Background(), "host", pflag.Arg(0)) if err != nil { return err @@ -49,10 +53,13 @@ func run() error { return err } - dcerpcOpts = append(dcerpcOpts, epm.EndpointMapper(ctx, - net.JoinHostPort(target.AddressWithoutPort(), "135"), - dcerpc.WithInsecure(), - )) + dcerpcOpts = append(dcerpcOpts, + epm.EndpointMapper(ctx, + net.JoinHostPort(target.AddressWithoutPort(), "135"), + dcerpc.WithInsecure(), + ), + dcerpc.WithDialer(adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil)), + ) proto := "ncacn_ip_tcp:" if namedPipe { diff --git a/examples/ldap/main.go b/examples/ldap/main.go index a0a3202..3891916 100644 --- a/examples/ldap/main.go +++ b/examples/ldap/main.go @@ -12,8 +12,9 @@ import ( func run() error { var ( - debug bool - authOpts = &adauth.Options{ + debug bool + socksServer = os.Getenv("SOCKS5_SERVER") + authOpts = &adauth.Options{ Debug: adauth.NewDebugFunc(&debug, os.Stderr, true), } ldapOpts = &ldapauth.Options{ @@ -22,10 +23,13 @@ func run() error { ) pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output") + pflag.CommandLine.StringVar(&socksServer, "socks", socksServer, "SOCKS5 proxy server") authOpts.RegisterFlags(pflag.CommandLine) ldapOpts.RegisterFlags(pflag.CommandLine) pflag.Parse() + ldapOpts.SetDialer(adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil)) + conn, err := ldapauth.Connect(context.Background(), authOpts, ldapOpts) if err != nil { return fmt.Errorf("%s connect: %w", ldapOpts.Scheme, err) diff --git a/examples/pkinit/main.go b/examples/pkinit/main.go index 30879db..71a8d0d 100644 --- a/examples/pkinit/main.go +++ b/examples/pkinit/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/RedTeamPentesting/adauth" "github.com/RedTeamPentesting/adauth/ccachetools" "github.com/RedTeamPentesting/adauth/pkinit" "github.com/spf13/pflag" @@ -18,6 +19,7 @@ func run() error { pfxPassword string ccacheName string dc string + socksServer = os.Getenv("SOCKS5_SERVER") ) pflag.StringVarP(&username, "username", "u", "", "Username (overrides UPN in PFX)") @@ -26,9 +28,12 @@ func run() error { pflag.StringVarP(&pfxPassword, "pfx-password", "p", "", "PFX file password") pflag.StringVar(&ccacheName, "cache", "", "CCache output file name") pflag.StringVar(&dc, "dc", "", "Domain controller (optional)") + pflag.StringVar(&socksServer, "socks", socksServer, "SOCKS5 server") + pflag.Parse() - ccache, hash, err := pkinit.UnPACTheHashFromPFX(context.Background(), username, domain, pfxFile, pfxPassword, dc) + ccache, hash, err := pkinit.UnPACTheHashFromPFX(context.Background(), username, domain, pfxFile, pfxPassword, dc, + pkinit.WithDialer(adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil))) if err != nil { return fmt.Errorf("UnPAC-the-Hash: %w", err) } diff --git a/examples/smb/main.go b/examples/smb/main.go index 099c2f4..055f400 100644 --- a/examples/smb/main.go +++ b/examples/smb/main.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "net" "os" "path/filepath" @@ -15,8 +14,9 @@ import ( func run() error { var ( - debug bool - authOpts = &adauth.Options{ + debug bool + socksServer = os.Getenv("SOCKS5_SERVER") + authOpts = &adauth.Options{ Debug: adauth.NewDebugFunc(&debug, os.Stderr, true), } smbauthOpts = &smbauth.Options{ @@ -25,6 +25,7 @@ func run() error { ) pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output") + pflag.CommandLine.StringVar(&socksServer, "socks", socksServer, "SOCKS5 proxy server") authOpts.RegisterFlags(pflag.CommandLine) pflag.Parse() @@ -43,12 +44,14 @@ func run() error { ctx := context.Background() + smbauthOpts.KerberosDialer = adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil) + smbDialer, err := smbauth.Dialer(ctx, creds, target, smbauthOpts) if err != nil { return fmt.Errorf("setup SMB authentication: %w", err) } - conn, err := net.Dial("tcp", target.Address()) + conn, err := adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil).DialContext(ctx, "tcp", target.Address()) if err != nil { return fmt.Errorf("dial: %w", err) } diff --git a/go.mod b/go.mod index d830bb0..f2475a9 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/oiweiwei/gokrb5.fork/v9 v9.0.2 github.com/spf13/pflag v1.0.6 github.com/vadimi/go-ntlm v1.2.1 + golang.org/x/net v0.39.0 software.sslmate.com/src/go-pkcs12 v0.5.0 ) @@ -29,7 +30,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/rs/zerolog v1.34.0 // indirect golang.org/x/crypto v0.37.0 // indirect - golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect ) diff --git a/ldapauth/gssapi.go b/ldapauth/gssapi.go index a7a2d6e..828dd7c 100644 --- a/ldapauth/gssapi.go +++ b/ldapauth/gssapi.go @@ -9,9 +9,9 @@ import ( "encoding/hex" "errors" "fmt" - "net" "strings" + "github.com/RedTeamPentesting/adauth" "github.com/RedTeamPentesting/adauth/compat" "github.com/RedTeamPentesting/adauth/pkinit" "github.com/jcmturner/gokrb5/v8/config" @@ -45,7 +45,7 @@ type gssapiClient struct { } func newClientFromCCache( - username string, domain string, ccachePath string, krb5Conf *config.Config, dialer Dialer, + username string, domain string, ccachePath string, krb5Conf *config.Config, dialer adauth.Dialer, ) (*gssapiClient, error) { ccache, err := credentials.LoadCCache(ccachePath) if err != nil { @@ -76,9 +76,9 @@ func newClientFromCCache( func newPKINITClient( ctx context.Context, username string, domain string, cert *x509.Certificate, key *rsa.PrivateKey, - krb5Conf *config.Config, dialer Dialer, + krb5Conf *config.Config, dialer adauth.Dialer, ) (*gssapiClient, error) { - ctxDialer := ContextDialer(dialer) + ctxDialer := adauth.AsContextDialer(dialer) ccache, err := pkinit.Authenticate(ctx, username, domain, cert, key, krb5Conf, pkinit.WithDialer(ctxDialer)) if err != nil { @@ -374,18 +374,3 @@ func krb5TokenAuthenticator( return auth, nil } - -type nopContextDialer func(string, string) (net.Conn, error) - -func (f nopContextDialer) DialContext(ctx context.Context, net string, addr string) (net.Conn, error) { - return f(net, addr) -} - -func ContextDialer(d Dialer) pkinit.ContextDialer { - ctxDialer, ok := d.(pkinit.ContextDialer) - if !ok { - ctxDialer = nopContextDialer(d.Dial) - } - - return ctxDialer -} diff --git a/ldapauth/ldap.go b/ldapauth/ldap.go index 18fe28f..6764b8a 100644 --- a/ldapauth/ldap.go +++ b/ldapauth/ldap.go @@ -61,10 +61,10 @@ type Options struct { DialOptions []ldap.DialOpt // KerberosDialer is a custom dialer that is used to request Kerberos // tickets. DialContext is used if implemented. - KerberosDialer Dialer + KerberosDialer adauth.Dialer // LDAPDialer is a custom dialer that is used to establish LDAP connections. // DialContext is used if implemented. - LDAPDialer Dialer + LDAPDialer adauth.Dialer } // RegisterFlags registers LDAP specific flags to a pflag.FlagSet such as the @@ -78,6 +78,12 @@ func (opts *Options) RegisterFlags(flagset *pflag.FlagSet) { "Negotiate StartTLS before authenticating on regular LDAP connection") } +// SetDialer configures a dialer for LDAP and Kerberos. +func (opts *Options) SetDialer(dialer adauth.Dialer) { + opts.KerberosDialer = dialer + opts.LDAPDialer = dialer +} + // Connect returns an authenticated LDAP connection to the domain controller's // LDAP server. func Connect(ctx context.Context, authOpts *adauth.Options, ldapOpts *Options) (conn *ldap.Conn, err error) { @@ -145,7 +151,7 @@ func connect(ctx context.Context, target *adauth.Target, opts *Options) (conn *l return nil, fmt.Errorf("LDAPS dial: %w", err) } } else { - tcpConn, err := ContextDialer(opts.LDAPDialer).DialContext(ctx, "tcp", target.Address()) + tcpConn, err := adauth.AsContextDialer(opts.LDAPDialer).DialContext(ctx, "tcp", target.Address()) if err != nil { return nil, fmt.Errorf("dial with custom dialer: %w", err) } @@ -175,7 +181,7 @@ func connect(ctx context.Context, target *adauth.Target, opts *Options) (conn *l return nil, fmt.Errorf("LDAP dial: %w", err) } } else { - tcpConn, err := ContextDialer(opts.LDAPDialer).DialContext(ctx, "tcp", target.Address()) + tcpConn, err := adauth.AsContextDialer(opts.LDAPDialer).DialContext(ctx, "tcp", target.Address()) if err != nil { return nil, fmt.Errorf("dial with custom dialer: %w", err) } @@ -533,7 +539,3 @@ func ChannelBindingHash(cert *x509.Certificate) []byte { return hash } - -type Dialer interface { - Dial(net string, addr string) (net.Conn, error) -} diff --git a/pkinit/exchange.go b/pkinit/exchange.go index 3b5ff49..1f3bdce 100644 --- a/pkinit/exchange.go +++ b/pkinit/exchange.go @@ -10,6 +10,7 @@ import ( "net" "time" + "github.com/RedTeamPentesting/adauth" "github.com/RedTeamPentesting/adauth/ccachetools" "github.com/jcmturner/gokrb5/v8/config" "github.com/jcmturner/gokrb5/v8/credentials" @@ -62,7 +63,7 @@ func Authenticate( // ASRep. func ASExchange( ctx context.Context, asReq messages.ASReq, domain string, config *config.Config, - dialer ContextDialer, roundtripDeadline time.Duration, + dialer adauth.ContextDialer, roundtripDeadline time.Duration, ) (asRep messages.ASRep, err error) { asReqBytes, err := asReq.Marshal() if err != nil { @@ -86,7 +87,7 @@ func ASExchange( // TGSRep. func TGSExchange( ctx context.Context, tgsReq messages.TGSReq, config *config.Config, domain string, - dialer ContextDialer, roundtripDeadline time.Duration, + dialer adauth.ContextDialer, roundtripDeadline time.Duration, ) (tgsRep messages.TGSRep, err error) { asReqBytes, err := tgsReq.Marshal() if err != nil { @@ -108,7 +109,7 @@ func TGSExchange( func roundtrip( ctx context.Context, request []byte, config *config.Config, domain string, - dialer ContextDialer, roundtripDeadline time.Duration, + dialer adauth.ContextDialer, roundtripDeadline time.Duration, ) (response []byte, err error) { _, kdcs, err := config.GetKDCs(domain, true) if err != nil { @@ -138,15 +139,9 @@ func roundtrip( } } -// ContextDialer is a context aware dialer such as net.Dialer or the SOCKS5 -// dialer returned by proxy.SOCKS5. -type ContextDialer interface { - DialContext(ctx context.Context, net string, addr string) (net.Conn, error) -} - func roundtripForSingleKDC( ctx context.Context, request []byte, address string, - dialer ContextDialer, roundtripDeadline time.Duration, + dialer adauth.ContextDialer, roundtripDeadline time.Duration, ) ([]byte, error) { if dialer == nil { dialer = &net.Dialer{Timeout: roundtripDeadline} @@ -237,11 +232,11 @@ func (option) isPKINITOption() {} type dialerOption struct { option - ContextDialer ContextDialer + ContextDialer adauth.ContextDialer } // WithDialer can be used to set a custom dialer for communication with a DC. -func WithDialer(dialer ContextDialer) Option { +func WithDialer(dialer adauth.ContextDialer) Option { return dialerOption{ContextDialer: dialer} } @@ -256,7 +251,7 @@ func WithRoundtripDeadline(deadline time.Duration) Option { return deadlineOption{Deadline: deadline} } -func processOptions(opts []Option) (dialer ContextDialer, roundtripDeadline time.Duration, err error) { +func processOptions(opts []Option) (dialer adauth.ContextDialer, roundtripDeadline time.Duration, err error) { roundtripDeadline = DefaultKerberosRoundtripDeadline for _, opt := range opts { diff --git a/smbauth/smbauth.go b/smbauth/smbauth.go index 3284479..84cc304 100644 --- a/smbauth/smbauth.go +++ b/smbauth/smbauth.go @@ -6,7 +6,6 @@ import ( "github.com/RedTeamPentesting/adauth" "github.com/RedTeamPentesting/adauth/dcerpcauth" - "github.com/RedTeamPentesting/adauth/pkinit" "github.com/oiweiwei/go-smb2.fork" @@ -23,8 +22,9 @@ type Options struct { // this default. SMBOptions []msrpcSMB2.DialerOption - // PKINITOptions can be used to modify the Kerberos PKINIT behavior. - PKINITOptions []pkinit.Option + // KerberosDialer is a custom dialer that is used to request Kerberos + // tickets. + KerberosDialer adauth.Dialer // Debug can be set to enable debug output, for example with // adauth.NewDebugFunc(...). @@ -46,8 +46,7 @@ func Dialer( ctx context.Context, creds *adauth.Credential, target *adauth.Target, options *Options, ) (*smb2.Dialer, error) { smbCreds, err := dcerpcauth.DCERPCCredentials(ctx, creds, &dcerpcauth.Options{ - PKINITOptions: options.PKINITOptions, - Debug: options.debug, + Debug: options.debug, }) if err != nil { return nil, err