Overhaul dialer handling

This commit is contained in:
Erik Geiser
2025-04-15 11:03:00 +02:00
parent 6560051668
commit fda6dc91bf
13 changed files with 145 additions and 63 deletions
+2 -1
View File
@@ -2,6 +2,7 @@
/examples/dcerpc/dcerpc
/ldap
/examples/ldap/ldap
/pkinit
/examples/pkinit/pkinit
/smb
/examples/smb/smb
*.exe
+4
View File
@@ -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
+14 -3
View File
@@ -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)
}
+66
View File
@@ -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)
}
+13 -6
View File
@@ -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] <target>", 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 {
+6 -2
View File
@@ -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)
+6 -1
View File
@@ -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)
}
+7 -4
View File
@@ -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)
}
+1 -1
View File
@@ -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
)
+4 -19
View File
@@ -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
}
+10 -8
View File
@@ -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)
}
+8 -13
View File
@@ -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 {
+4 -5
View File
@@ -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