From 09ec08f3d7d258d2ce0ca5763350695df595e5ad Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Thu, 10 Apr 2025 06:47:13 -0500 Subject: [PATCH] Add barebones smbauth module w/ basic example --- examples/smb/main.go | 97 +++++++++++++++++++++++++ smbauth/smbauth.go | 167 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 examples/smb/main.go create mode 100644 smbauth/smbauth.go diff --git a/examples/smb/main.go b/examples/smb/main.go new file mode 100644 index 0000000..fd5958a --- /dev/null +++ b/examples/smb/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "fmt" + "github.com/RedTeamPentesting/adauth/smbauth" + "github.com/oiweiwei/go-msrpc/smb2" + "github.com/oiweiwei/go-msrpc/ssp" + "net" + "os" + "path/filepath" + + "github.com/RedTeamPentesting/adauth" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" + "github.com/spf13/pflag" +) + +var ( + debug bool + authOpts = &adauth.Options{ + Debug: adauth.NewDebugFunc(&debug, os.Stderr, true), + } +) + +func init() { + pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output") + authOpts.RegisterFlags(pflag.CommandLine) + gssapi.AddMechanism(ssp.SPNEGO) + gssapi.AddMechanism(ssp.NTLM) +} + +func run() error { + pflag.Parse() + + if len(pflag.Args()) != 1 { + return fmt.Errorf("usage: %s [--debug]", binaryName()) + } + + creds, target, err := authOpts.WithTarget(context.Background(), "host", pflag.Arg(0)) + if err != nil { + return err + } + + ctx := gssapi.NewSecurityContext(context.Background()) + + smbOpts, secOpts, err := smbauth.AuthenticationOptions(ctx, creds, target, &smbauth.Options{}) + if err != nil { + return err + } + + // Create go-smb2 Dialer + dialer := smb2.NewDialer(append(smbOpts, smb2.WithSecurity(secOpts...))...) + + conn, err := net.Dial("tcp", net.JoinHostPort(target.AddressWithoutPort(), "445")) + if err != nil { + return err + } + defer conn.Close() + + sess, err := dialer.Dial(conn) + if err != nil { + return err + } + defer sess.Logoff() + + names, err := sess.ListSharenames() + if err != nil { + return err + } + + for _, name := range names { + fmt.Println(name) + } + return nil +} + +func binaryName() string { + executable, err := os.Executable() + if err == nil { + return filepath.Base(executable) + } + + if len(os.Args) > 0 { + return filepath.Base(os.Args[0]) + } + + return "list-shares" +} + +func main() { + err := run() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + + os.Exit(1) + } +} diff --git a/smbauth/smbauth.go b/smbauth/smbauth.go new file mode 100644 index 0000000..2415a7a --- /dev/null +++ b/smbauth/smbauth.go @@ -0,0 +1,167 @@ +package smbauth + +import ( + "context" + "fmt" + "strings" + + "github.com/RedTeamPentesting/adauth" + "github.com/RedTeamPentesting/adauth/pkinit" + + "github.com/jcmturner/gokrb5/v8/keytab" + "github.com/oiweiwei/go-msrpc/smb2" + "github.com/oiweiwei/go-msrpc/ssp" + "github.com/oiweiwei/go-msrpc/ssp/credential" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" + "github.com/oiweiwei/go-msrpc/ssp/krb5" +) + +// Options holds options that modify the behavior of the AuthenticationOptions +// function. +type Options struct { + // SMBOptions holds options for the SMB dialer. If SMBOptions is nil, + // sealing 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 + + // Debug can be set to enable debug output, for example with + // adauth.NewDebugFunc(...). + Debug func(string, ...any) +} + +func (opts *Options) debug(format string, a ...any) { + if opts == nil || opts.Debug == nil { + return + } + + opts.Debug(format, a...) +} + +// AuthenticationOptions returns the security context options for +// smb2.WithSecurity. It is possible to configure the SMB, PKINIT +// and debug behavior using the optional upstreamOptions argument. +func AuthenticationOptions( + ctx context.Context, creds *adauth.Credential, target *adauth.Target, + upstreamOptions *Options, +) (dialOptions []smb2.DialerOption, secOptions []gssapi.ContextOption, err error) { + + if dialOptions = upstreamOptions.SMBOptions; dialOptions == nil { + dialOptions = []smb2.DialerOption{smb2.WithSeal()} + } + + smbCredentials, err := SMBCredentials(ctx, creds, upstreamOptions) + if err != nil { + return nil, nil, err + } + + switch { + case target.UseKerberos || creds.ClientCert != nil: + spn, err := target.SPN(ctx) + if err != nil { + return nil, nil, fmt.Errorf("build SPN: %w", err) + } + upstreamOptions.debug("Using Kerberos with SPN %q", spn) + + krbConf, err := creds.KerberosConfig(ctx) + if err != nil { + return nil, nil, fmt.Errorf("generate Kerberos config: %w", err) + } + + secOptions = append(secOptions, + gssapi.WithTargetName(spn), + gssapi.WithCredential(smbCredentials), + gssapi.WithMechanismFactory(ssp.KRB5, &krb5.Config{ + KRB5Config: krbConf, + CCachePath: creds.CCache, + DisablePAFXFAST: true, + DCEStyle: true, + }), + ) + default: + upstreamOptions.debug("Using NTLM") + + secOptions = append(secOptions, + gssapi.WithCredential(smbCredentials), + gssapi.WithMechanismFactory(ssp.NTLM), + ) + // Try fetching SPN + if spn, err := target.SPN(ctx); err == nil { + secOptions = append(secOptions, gssapi.WithTargetName(spn)) + } + } + return +} + +func SMBCredentials(ctx context.Context, creds *adauth.Credential, options *Options) (credential.Credential, error) { + switch { + case creds.Password != "": + options.debug("Authenticating with password") + + return credential.NewFromPassword(creds.LogonNameWithUpperCaseDomain(), creds.Password), nil + case creds.AESKey != "": + options.debug("Authenticating with AES key") + + keyTab, err := creds.Keytab() + if err != nil { + return nil, fmt.Errorf("create keytab: %w", err) + } + + return &keytabCredentials{username: creds.Username, domain: creds.Domain, keytab: keyTab}, nil + case creds.NTHash != "": + options.debug("Authenticating with NT hash") + + return credential.NewFromNTHash(creds.LogonNameWithUpperCaseDomain(), creds.NTHash), nil + case creds.PasswordIsEmtpyString: + options.debug("Authenticating with empty password") + + return credential.NewFromPassword(strings.ToUpper(creds.Domain)+`\`+creds.Username, ""), nil + case creds.ClientCert != nil: + options.debug("Authenticating with client certificate (PKINIT)") + + krbConf, err := creds.KerberosConfig(ctx) + if err != nil { + return nil, fmt.Errorf("generate kerberos config: %w", err) + } + + ccache, err := pkinit.Authenticate(ctx, creds.Username, strings.ToUpper(creds.Domain), + creds.ClientCert, creds.ClientCertKey, krbConf, options.PKINITOptions...) + if err != nil { + return nil, fmt.Errorf("PKINIT: %w", err) + } + + return credential.NewFromCCache(creds.LogonNameWithUpperCaseDomain(), ccache), nil + case creds.CCache != "": + options.debug("Authenticating with ccache") + + return credential.NewFromPassword(creds.LogonNameWithUpperCaseDomain(), ""), nil + default: + return nil, fmt.Errorf("no credentials available") + } +} + +type keytabCredentials struct { + keytab *keytab.Keytab + username string + domain string +} + +var _ credential.KeytabV8 = &keytabCredentials{} + +func (ktc *keytabCredentials) DomainName() string { + return strings.ToUpper(ktc.domain) +} + +func (ktc *keytabCredentials) Workstation() string { + return "" +} + +func (ktc *keytabCredentials) UserName() string { + return ktc.username +} + +func (ktc *keytabCredentials) Keytab() *keytab.Keytab { + return ktc.keytab +}