diff options
author | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-03-10 16:04:08 -0500 |
---|---|---|
committer | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-03-10 16:04:08 -0500 |
commit | 11741c4cde3d552211fbb04eddd719b3dc3bd472 (patch) | |
tree | 52f28ca2feacde039b7215fa3fd27b5a7ec02ed5 | |
parent | ab141f2076b141bf885f56cb5730252cc2880041 (diff) | |
download | goexec-11741c4cde3d552211fbb04eddd719b3dc3bd472.tar.gz goexec-11741c4cde3d552211fbb04eddd719b3dc3bd472.zip |
Added basic dcom execution module
-rw-r--r-- | cmd/dcom.go | 75 | ||||
-rw-r--r-- | cmd/root.go | 135 | ||||
-rw-r--r-- | cmd/rpc.go | 103 | ||||
-rw-r--r-- | cmd/wmi.go | 228 | ||||
-rw-r--r-- | go.mod | 4 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | internal/client/dce/dce.go | 2 | ||||
-rw-r--r-- | internal/exec/dcom/dcom.go | 65 | ||||
-rw-r--r-- | internal/exec/dcom/exec.go | 187 | ||||
-rw-r--r-- | internal/exec/dcom/module.go | 21 | ||||
-rw-r--r-- | internal/exec/scmr/exec.go | 2 | ||||
-rw-r--r-- | internal/exec/scmr/service.go | 26 |
12 files changed, 605 insertions, 247 deletions
diff --git a/cmd/dcom.go b/cmd/dcom.go new file mode 100644 index 0000000..d105b0c --- /dev/null +++ b/cmd/dcom.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "github.com/FalconOpsLLC/goexec/internal/exec" + dcomexec "github.com/FalconOpsLLC/goexec/internal/exec/dcom" + "github.com/spf13/cobra" +) + +func dcomCmdInit() { + registerRpcFlags(dcomCmd) + dcomMmcCmdInit() + dcomCmd.AddCommand(dcomMmcCmd) +} + +func dcomMmcCmdInit() { + dcomMmcCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke") + dcomMmcCmd.Flags().StringVarP(&workingDirectory, "directory", "d", `C:\`, "Working directory") + dcomMmcCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Process command line") + dcomMmcCmd.Flags().StringVar(&windowState, "window", "Minimized", "Window state") + dcomMmcCmd.Flags().StringVarP(&command, "command", "c", ``, "Windows executable & arguments to run") + + dcomMmcCmd.MarkFlagsOneRequired("executable", "command") + dcomMmcCmd.MarkFlagsMutuallyExclusive("executable", "command") +} + +var ( + dcomCmd = &cobra.Command{ + Use: "dcom", + Short: "Establish execution via DCOM", + Args: cobra.NoArgs, + } + dcomMmcCmd = &cobra.Command{ + Use: "mmc [target]", + Short: "Establish execution via the DCOM MMC20.Application object", + Long: `Description: + The mmc method uses the exposed MMC20.Application object to call Document.ActiveView.ShellExec, + and ultimately execute system commands. + +References: + https://www.scorpiones.io/articles/lateral-movement-using-dcom-objects + https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/ + https://github.com/fortra/impacket/blob/master/examples/dcomexec.py + https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mmc/view-executeshellcommand +`, + Args: needsRpcTarget("host"), + Run: func(cmd *cobra.Command, args []string) { + + ctx = log.With(). + Str("module", "dcom"). + Str("method", "mmc"). + Logger().WithContext(ctx) + + module := dcomexec.Module{} + connCfg := &exec.ConnectionConfig{ + ConnectionMethod: exec.ConnectionMethodDCE, + ConnectionMethodConfig: dceConfig, + } + execCfg := &exec.ExecutionConfig{ + ExecutableName: executable, + ExecutableArgs: executableArgs, + ExecutionMethod: dcomexec.MethodMmc, + + ExecutionMethodConfig: dcomexec.MethodMmcConfig{ + WorkingDirectory: workingDirectory, + WindowState: windowState, + }, + } + if err := module.Connect(ctx, creds, target, connCfg); err != nil { + log.Fatal().Err(err).Msg("Connection failed") + } else if err = module.Exec(ctx, execCfg); err != nil { + log.Fatal().Err(err).Msg("Execution failed") + } + }, + } +) diff --git a/cmd/root.go b/cmd/root.go index f083063..3f17253 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,83 +1,94 @@ package cmd import ( - "context" - "fmt" - "github.com/RedTeamPentesting/adauth" - "github.com/rs/zerolog" - "github.com/spf13/cobra" - "os" - "regexp" + "context" + "fmt" + "github.com/RedTeamPentesting/adauth" + "github.com/rs/zerolog" + "github.com/spf13/cobra" + "os" + "regexp" + "strings" ) var ( - //logFile string - log zerolog.Logger - ctx context.Context - authOpts *adauth.Options + //logFile string + log zerolog.Logger + ctx context.Context + authOpts *adauth.Options - debug bool - command string - executable string - executablePath string - executableArgs string - workingDirectory string + debug bool + command string + executable string + executablePath string + executableArgs string + workingDirectory string + windowState string - rootCmd = &cobra.Command{ - Use: "goexec", - PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { - // For modules that require a full executable path - if executablePath != "" && !regexp.MustCompile(`^([a-zA-Z]:)?\\`).MatchString(executablePath) { - return fmt.Errorf("executable path (-e) must be an absolute Windows path, i.e. C:\\Windows\\System32\\cmd.exe") - } - log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel).With().Timestamp().Logger() - if debug { - log = log.Level(zerolog.DebugLevel) - } - return - }, - } + rootCmd = &cobra.Command{ + Use: "goexec", + PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { + // For modules that require a full executable path + if executablePath != "" && !regexp.MustCompile(`^([a-zA-Z]:)?\\`).MatchString(executablePath) { + return fmt.Errorf("executable path (-e) must be an absolute Windows path, i.e. C:\\Windows\\System32\\cmd.exe") + } + if command != "" { + p := strings.SplitN(command, " ", 2) + executable = p[0] + if len(p) > 1 { + executableArgs = p[1] + } + } + log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel).With().Timestamp().Logger() + if debug { + log = log.Level(zerolog.DebugLevel) + } + return + }, + } ) func needsTarget(proto string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) (err error) { - if len(args) != 1 { - return fmt.Errorf("command require exactly one positional argument: [target]") - } - if creds, target, err = authOpts.WithTarget(ctx, proto, args[0]); err != nil { - return fmt.Errorf("failed to parse target: %w", err) - } - if creds == nil { - return fmt.Errorf("no credentials supplied") - } - if target == nil { - return fmt.Errorf("no target supplied") - } - return - } + return func(cmd *cobra.Command, args []string) (err error) { + if len(args) != 1 { + return fmt.Errorf("command require exactly one positional argument: [target]") + } + if creds, target, err = authOpts.WithTarget(ctx, proto, args[0]); err != nil { + return fmt.Errorf("failed to parse target: %w", err) + } + if creds == nil { + return fmt.Errorf("no credentials supplied") + } + if target == nil { + return fmt.Errorf("no target supplied") + } + return + } } func init() { - ctx = context.Background() + ctx = context.Background() - rootCmd.InitDefaultVersionFlag() - rootCmd.InitDefaultHelpCmd() - rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging") + rootCmd.InitDefaultVersionFlag() + rootCmd.InitDefaultHelpCmd() + rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging") - authOpts = &adauth.Options{Debug: log.Debug().Msgf} - authOpts.RegisterFlags(rootCmd.PersistentFlags()) + authOpts = &adauth.Options{Debug: log.Debug().Msgf} + authOpts.RegisterFlags(rootCmd.PersistentFlags()) - scmrCmdInit() - rootCmd.AddCommand(scmrCmd) - tschCmdInit() - rootCmd.AddCommand(tschCmd) - wmiCmdInit() - rootCmd.AddCommand(wmiCmd) + scmrCmdInit() + rootCmd.AddCommand(scmrCmd) + tschCmdInit() + rootCmd.AddCommand(tschCmd) + wmiCmdInit() + rootCmd.AddCommand(wmiCmd) + dcomCmdInit() + rootCmd.AddCommand(dcomCmd) } func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } } @@ -1,67 +1,70 @@ package cmd import ( - "fmt" - "github.com/FalconOpsLLC/goexec/internal/client/dce" - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/spf13/cobra" - "regexp" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/client/dce" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "regexp" ) func needsRpcTarget(proto string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) (err error) { + return func(cmd *cobra.Command, args []string) (err error) { - if argDceStringBinding != "" { - dceConfig.Endpoint, err = dcerpc.ParseStringBinding(argDceStringBinding) - if err != nil { - return fmt.Errorf("failed to parse RPC endpoint: %w", err) - } - dceConfig.NoEpm = true // If an explicit endpoint is set, don't use EPM + if argDceStringBinding != "" { + dceConfig.Endpoint, err = dcerpc.ParseStringBinding(argDceStringBinding) + if err != nil { + return fmt.Errorf("failed to parse RPC endpoint: %w", err) + } + dceConfig.NoEpm = true // If an explicit endpoint is set, don't use EPM - } else if argDceEpmFilter != "" { - // This ensures that filters like "ncacn_ip_tcp" will be made into a valid binding (i.e. "ncacn_ip_tcp:") - if ok, err := regexp.MatchString(`^\w+$`, argDceEpmFilter); err == nil && ok { - argDceEpmFilter += ":" - } - dceConfig.EpmFilter, err = dcerpc.ParseStringBinding(argDceEpmFilter) - if err != nil { - return fmt.Errorf("failed to parse EPM filter: %w", err) - } - } - if !argDceNoSign { - dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithSign()) - dceConfig.EpmOptions = append(dceConfig.EpmOptions, dcerpc.WithSign()) - } - if argDceNoSeal { - dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithInsecure()) - } else { - dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) - dceConfig.EpmOptions = append(dceConfig.EpmOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) - } - return needsTarget(proto)(cmd, args) - } + } else if argDceEpmFilter != "" { + // This ensures that filters like "ncacn_ip_tcp" will be made into a valid binding (i.e. "ncacn_ip_tcp:") + if ok, err := regexp.MatchString(`^\w+$`, argDceEpmFilter); err == nil && ok { + argDceEpmFilter += ":" + } + dceConfig.EpmFilter, err = dcerpc.ParseStringBinding(argDceEpmFilter) + if err != nil { + return fmt.Errorf("failed to parse EPM filter: %w", err) + } + } + if !argDceNoSign { + dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithSign()) + dceConfig.EpmOptions = append(dceConfig.EpmOptions, dcerpc.WithSign()) + } + if argDceNoSeal { + dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithInsecure()) + } else { + dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) + dceConfig.EpmOptions = append(dceConfig.EpmOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) + } + return needsTarget(proto)(cmd, args) + } } var ( - // DCE arguments - argDceStringBinding string - argDceEpmFilter string - argDceNoSeal bool - argDceNoSign bool + // DCE arguments + argDceStringBinding string + argDceEpmFilter string + argDceNoSeal bool + argDceNoSign bool - // DCE options - dceStringBinding *dcerpc.StringBinding - dceConfig dce.ConnectionMethodDCEConfig + // DCE options + dceStringBinding *dcerpc.StringBinding + dceConfig dce.ConnectionMethodDCEConfig ) func registerRpcFlags(cmd *cobra.Command) { - cmd.PersistentFlags().BoolVar(&dceConfig.NoEpm, "no-epm", false, "Do not use EPM to automatically detect endpoints") - cmd.PersistentFlags().BoolVar(&dceConfig.EpmAuto, "epm-auto", false, "Automatically detect endpoints instead of using the module defaults") - cmd.PersistentFlags().BoolVar(&argDceNoSign, "no-sign", false, "Disable signing on DCE messages") - cmd.PersistentFlags().BoolVar(&argDceNoSeal, "no-seal", false, "Disable packet stub encryption on DCE messages") - cmd.PersistentFlags().StringVarP(&argDceEpmFilter, "epm-filter", "F", "", "String binding to filter endpoints returned by EPM") - cmd.PersistentFlags().StringVar(&argDceStringBinding, "endpoint", "", "Explicit RPC endpoint definition") + rpcFlags := pflag.NewFlagSet("RPC", pflag.ExitOnError) + rpcFlags.BoolVar(&dceConfig.NoEpm, "no-epm", false, "Do not use EPM to automatically detect endpoints") + rpcFlags.BoolVar(&dceConfig.EpmAuto, "epm-auto", false, "Automatically detect endpoints instead of using the module defaults") + rpcFlags.BoolVar(&argDceNoSign, "no-sign", false, "Disable signing on DCE messages") + rpcFlags.BoolVar(&argDceNoSeal, "no-seal", false, "Disable packet stub encryption on DCE messages") + rpcFlags.StringVarP(&argDceEpmFilter, "epm-filter", "F", "", "String binding to filter endpoints returned by EPM") + rpcFlags.StringVar(&argDceStringBinding, "endpoint", "", "Explicit RPC endpoint definition") + cmd.PersistentFlags().AddFlagSet(rpcFlags) - cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter") - cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter") + cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter") + cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter") } @@ -1,142 +1,140 @@ package cmd import ( - "encoding/json" - "fmt" - "github.com/FalconOpsLLC/goexec/internal/exec" - wmiexec "github.com/FalconOpsLLC/goexec/internal/exec/wmi" - "github.com/spf13/cobra" - "regexp" - "strings" + "encoding/json" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/exec" + wmiexec "github.com/FalconOpsLLC/goexec/internal/exec/wmi" + "github.com/spf13/cobra" + "regexp" + "strings" ) func wmiCmdInit() { - wmiCustomCmdInit() - wmiCmd.AddCommand(wmiCustomCmd) - wmiProcessCmdInit() - wmiCmd.AddCommand(wmiProcessCmd) + wmiCustomCmdInit() + wmiCmd.AddCommand(wmiCustomCmd) + wmiProcessCmdInit() + wmiCmd.AddCommand(wmiProcessCmd) } func wmiCustomCmdInit() { - wmiCustomCmd.Flags().StringVarP(&wmiArgMethod, "method", "m", "", `WMI Method to use in the format CLASS.METHOD (i.e. "Win32_Process.Create")`) - wmiCustomCmd.Flags().StringVarP(&wmiArgMethodArgs, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`) - if err := wmiCustomCmd.MarkFlagRequired("method"); err != nil { - panic(err) - } + wmiCustomCmd.Flags().StringVarP(&wmiArgMethod, "method", "m", "", `WMI Method to use in the format CLASS.METHOD (i.e. "Win32_Process.Create")`) + wmiCustomCmd.Flags().StringVarP(&wmiArgMethodArgs, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`) + if err := wmiCustomCmd.MarkFlagRequired("method"); err != nil { + panic(err) + } } func wmiProcessCmdInit() { - wmiProcessCmd.Flags().StringVarP(&command, "command", "c", "", "Process command line") - wmiProcessCmd.Flags().StringVarP(&workingDirectory, "directory", "d", `C:\`, "Working directory") - if err := wmiProcessCmd.MarkFlagRequired("command"); err != nil { - panic(err) - } + wmiProcessCmd.Flags().StringVarP(&command, "command", "c", "", "Process command line") + wmiProcessCmd.Flags().StringVarP(&workingDirectory, "directory", "d", `C:\`, "Working directory") + if err := wmiProcessCmd.MarkFlagRequired("command"); err != nil { + panic(err) + } } var ( - // for custom method - wmiArgMethod string - wmiArgMethodArgs string - - wmiClass string - wmiMethod string - wmiMethodArgsMap map[string]any - methodRegex = regexp.MustCompile(`^\w+\.\w+$`) - - wmiCmd = &cobra.Command{ - Use: "wmi", - Short: "Establish execution via WMI", - Args: cobra.NoArgs, - } - wmiCustomCmd = &cobra.Command{ - Use: "custom", - Short: "Execute specified WMI method", - Long: `Description: + // for custom method + wmiArgMethod string + wmiArgMethodArgs string + + wmiClass string + wmiMethod string + wmiMethodArgsMap map[string]any + methodRegex = regexp.MustCompile(`^\w+\.\w+$`) + + wmiCmd = &cobra.Command{ + Use: "wmi", + Short: "Establish execution via WMI", + Args: cobra.NoArgs, + } + wmiCustomCmd = &cobra.Command{ + Use: "custom", + Short: "Execute specified WMI method", + Long: `Description: The custom method creates an instance of the specified WMI class (-c), then calls the provided method (-m) with the provided arguments (-A). References: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-classes `, - Args: func(cmd *cobra.Command, args []string) (err error) { - if err = needsTarget("cifs")(cmd, args); err == nil { - if wmiArgMethod != "" && !methodRegex.MatchString(wmiArgMethod) { - return fmt.Errorf("invalid CLASS.METHOD syntax: %s", wmiArgMethod) - } - if err = json.Unmarshal([]byte(wmiArgMethodArgs), &wmiMethodArgsMap); err != nil { - err = fmt.Errorf("failed to parse JSON arguments: %w", err) - } - } - return - }, - Run: func(cmd *cobra.Command, args []string) { - module := wmiexec.Module{} - - connCfg := &exec.ConnectionConfig{} - cleanCfg := &exec.CleanupConfig{} - - parts := strings.SplitN(wmiArgMethod, ".", 2) - wmiClass = parts[0] - wmiMethod = parts[1] - - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: wmiexec.MethodCustom, - ExecutionMethodConfig: wmiexec.MethodCustomConfig{ - Class: wmiClass, - Method: wmiMethod, - Arguments: wmiMethodArgsMap, - }, - } - if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil { - log.Fatal().Err(err).Msg("Connection failed") - - } else if err := module.Exec(log.WithContext(ctx), execCfg); err != nil { - log.Fatal().Err(err).Msg("Execution failed") - - } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { - log.Error().Err(err).Msg("Cleanup failed") - } - }, - } - - wmiProcessCmd = &cobra.Command{ - Use: "process", - Short: "Create a Windows process", - Long: `Description: - The process method creates an instance of the Win32_Process WMI class, - then calls the Win32_Process.Create method with the provided command (-c), + Args: func(cmd *cobra.Command, args []string) (err error) { + if err = needsTarget("cifs")(cmd, args); err == nil { + if wmiArgMethod != "" && !methodRegex.MatchString(wmiArgMethod) { + return fmt.Errorf("invalid CLASS.METHOD syntax: %s", wmiArgMethod) + } + if err = json.Unmarshal([]byte(wmiArgMethodArgs), &wmiMethodArgsMap); err != nil { + err = fmt.Errorf("failed to parse JSON arguments: %w", err) + } + } + return + }, + Run: func(cmd *cobra.Command, args []string) { + module := wmiexec.Module{} + + connCfg := &exec.ConnectionConfig{} + cleanCfg := &exec.CleanupConfig{} + + parts := strings.SplitN(wmiArgMethod, ".", 2) + wmiClass = parts[0] + wmiMethod = parts[1] + + execCfg := &exec.ExecutionConfig{ + ExecutableName: executable, + ExecutableArgs: executableArgs, + ExecutionMethod: wmiexec.MethodCustom, + ExecutionMethodConfig: wmiexec.MethodCustomConfig{ + Class: wmiClass, + Method: wmiMethod, + Arguments: wmiMethodArgsMap, + }, + } + if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil { + log.Fatal().Err(err).Msg("Connection failed") + } else if err := module.Exec(log.WithContext(ctx), execCfg); err != nil { + log.Fatal().Err(err).Msg("Execution failed") + } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { + log.Error().Err(err).Msg("Cleanup failed") + } + }, + } + + wmiProcessCmd = &cobra.Command{ + Use: "proc", + Short: "Start a Windows process", + Long: `Description: + The proc method creates an instance of the Win32_Process WMI class, then + calls the Win32_Process.Create method with the provided command (-c), and optional working directory (-d). References: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/create-method-in-class-win32-process `, - Args: needsTarget("cifs"), - Run: func(cmd *cobra.Command, args []string) { - log = log.With().Str("module", "wmi").Logger() - - module := wmiexec.Module{} - connCfg := &exec.ConnectionConfig{} - cleanCfg := &exec.CleanupConfig{} - - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: wmiexec.MethodProcess, - - ExecutionMethodConfig: wmiexec.MethodProcessConfig{ - Command: command, - WorkingDirectory: workingDirectory, - }, - } - if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil { - log.Fatal().Err(err).Msg("Connection failed") - } else if err := module.Exec(log.WithContext(ctx), execCfg); err != nil { - log.Fatal().Err(err).Msg("Execution failed") - } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { - log.Error().Err(err).Msg("Cleanup failed") - } - }, - } + Args: needsTarget("cifs"), + Run: func(cmd *cobra.Command, args []string) { + log = log.With().Str("module", "wmi").Logger() + + module := wmiexec.Module{} + connCfg := &exec.ConnectionConfig{} + cleanCfg := &exec.CleanupConfig{} + + execCfg := &exec.ExecutionConfig{ + ExecutableName: executable, + ExecutableArgs: executableArgs, + ExecutionMethod: wmiexec.MethodProcess, + + ExecutionMethodConfig: wmiexec.MethodProcessConfig{ + Command: command, + WorkingDirectory: workingDirectory, + }, + } + if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil { + log.Fatal().Err(err).Msg("Connection failed") + } else if err := module.Exec(log.WithContext(ctx), execCfg); err != nil { + log.Fatal().Err(err).Msg("Execution failed") + } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { + log.Error().Err(err).Msg("Cleanup failed") + } + }, + } ) @@ -5,9 +5,10 @@ go 1.24.0 require ( github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877 github.com/google/uuid v1.6.0 - github.com/oiweiwei/go-msrpc v1.2.1 + github.com/oiweiwei/go-msrpc v1.2.3 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 ) require ( @@ -25,7 +26,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/oiweiwei/go-smb2.fork v1.0.0 // indirect github.com/oiweiwei/gokrb5.fork/v9 v9.0.2 // indirect - github.com/spf13/pflag v1.0.6 // indirect golang.org/x/crypto v0.35.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect @@ -42,8 +42,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/oiweiwei/go-msrpc v1.2.1 h1:jpt7DYCefrJhDS+C9GvhgFls9zTh05eWDw3mwRToqKc= -github.com/oiweiwei/go-msrpc v1.2.1/go.mod h1:ev+Bg4HdktdaLvwQ2RcwTlgvx7boe+fskcdUlesepdM= +github.com/oiweiwei/go-msrpc v1.2.3 h1:Wluv8bB0+Gxo+sMcU+0+a5WThqAEm1l84eHBQOWXoBU= +github.com/oiweiwei/go-msrpc v1.2.3/go.mod h1:ev+Bg4HdktdaLvwQ2RcwTlgvx7boe+fskcdUlesepdM= github.com/oiweiwei/go-smb2.fork v1.0.0 h1:xHq/eYPM8hQEO/nwCez8YwHWHC8mlcsgw/Neu52fPN4= github.com/oiweiwei/go-smb2.fork v1.0.0/go.mod h1:h0CzLVvGAmq39izdYVHKyI5cLv6aHdbQAMKEe4dz4N8= github.com/oiweiwei/gokrb5.fork/v9 v9.0.2 h1:JNkvXMuOEWNXJKzLiyROGfdK31/1RQWA9e5gJxAsl50= diff --git a/internal/client/dce/dce.go b/internal/client/dce/dce.go index f58123b..85512ac 100644 --- a/internal/client/dce/dce.go +++ b/internal/client/dce/dce.go @@ -26,7 +26,7 @@ type ConnectionMethodDCEConfig struct { func (cfg *ConnectionMethodDCEConfig) GetDce(ctx context.Context, cred *adauth.Credential, target *adauth.Target, endpoint, object string, opts ...dcerpc.Option) (cc dcerpc.Conn, err error) { dceOpts := append(opts, cfg.DceOptions...) - epmOpts := append(opts, cfg.EpmOptions...) + epmOpts := cfg.EpmOptions log := zerolog.Ctx(ctx).With(). Str("client", "DCERPC").Logger() diff --git a/internal/exec/dcom/dcom.go b/internal/exec/dcom/dcom.go new file mode 100644 index 0000000..b96bbc8 --- /dev/null +++ b/internal/exec/dcom/dcom.go @@ -0,0 +1,65 @@ +package dcomexec + +import ( + "context" + "fmt" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" + "strings" +) + +const ( + LC_ENGLISH_US uint32 = 0x409 +) + +func callMethod(ctx context.Context, dc idispatch.DispatchClient, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) { + parts := strings.Split(method, ".") + + var id *dcom.IPID + var gr *idispatch.GetIDsOfNamesResponse + + for i, obj := range parts { + var opts []dcerpc.CallOption + if id != nil { + opts = append(opts, dcom.WithIPID(id)) + } + gr, err = dc.GetIDsOfNames(ctx, &idispatch.GetIDsOfNamesRequest{ + This: ORPCThis, + IID: &dcom.IID{}, + Names: []string{obj + "\x00"}, + LocaleID: LC_ENGLISH_US, + }, opts...) + + if err != nil { + return nil, fmt.Errorf("get dispatch ID of name %q: %w", obj, err) + } + if len(gr.DispatchID) < 1 { + return nil, fmt.Errorf("dispatch ID of name %q not found", obj) + } + irq := &idispatch.InvokeRequest{ + This: ORPCThis, + DispatchIDMember: gr.DispatchID[0], + IID: &dcom.IID{}, + LocaleID: LC_ENGLISH_US, + } + if i >= len(parts)-1 { + irq.Flags = 1 + irq.DispatchParams = &oaut.DispatchParams{ArgsCount: uint32(len(args)), Args: args} + return dc.Invoke(ctx, irq, opts...) + } + irq.Flags = 2 + ir, err = dc.Invoke(ctx, irq, opts...) + if err != nil { + return nil, fmt.Errorf("get properties of object %q: %w", obj, err) + } + di, ok := ir.VarResult.VarUnion.GetValue().(*oaut.Dispatch) + if !ok { + return nil, fmt.Errorf("invalid dispatch object for %q", obj) + } + id = di.InterfacePointer().GetStandardObjectReference().Std.IPID + } + + return +} diff --git a/internal/exec/dcom/exec.go b/internal/exec/dcom/exec.go new file mode 100644 index 0000000..297c26f --- /dev/null +++ b/internal/exec/dcom/exec.go @@ -0,0 +1,187 @@ +package dcomexec + +import ( + "context" + "errors" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/client/dce" + "github.com/FalconOpsLLC/goexec/internal/exec" + "github.com/RedTeamPentesting/adauth" + guuid "github.com/google/uuid" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dtyp" + "github.com/rs/zerolog" +) + +const ( + DefaultDcomEndpoint = "ncacn_ip_tcp:[135]" +) + +var ( + MmcUuid = uuid.MustParse("49B2791A-B1AE-4C90-9B8E-E860BA07F889") + ShellWindowsUuid = uuid.MustParse("9BA05972-F6A8-11CF-A442-00A0C90A8F39") + //RandUuid = uuid.MustParse("dc95cac4-0d74-49eb-8947-570ad52ef215") + RandCid = dcom.CID(*dtyp.GUIDFromUUID(uuid.MustParse(guuid.NewString()))) + IDispatchIID = &dcom.IID{ + Data1: 0x20400, + Data2: 0x0, + Data3: 0x0, + Data4: []byte{0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46}, + } + ComVersion = &dcom.COMVersion{ + MajorVersion: 5, + MinorVersion: 7, + } + MmcClsid = dcom.ClassID(*dtyp.GUIDFromUUID(MmcUuid)) + ORPCThis = &dcom.ORPCThis{ + Version: ComVersion, + CID: &RandCid, + } +) + +func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("method", ccfg.ConnectionMethod). + Str("func", "Exec").Logger() + + if ccfg.ConnectionMethod == exec.ConnectionMethodDCE { + if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok { + return fmt.Errorf("invalid configuration for DCE connection method") + } else { + opts := []dcerpc.Option{dcerpc.WithSign(), dcerpc.WithSecurityLevel(0)} + + // Fetch target hostname + if mod.hostname, err = target.Hostname(ctx); err != nil { + log.Debug().Err(err).Msg("Failed to get target hostname") + opts = append(opts, dcerpc.WithTargetName(mod.hostname)) + } + // Create DCE connection + if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultDcomEndpoint, "", opts...); err != nil { + log.Error().Err(err).Msg("Failed to initialize DCE dialer") + return fmt.Errorf("create DCE dialer: %w", err) + } + + inst := &dcom.InstantiationInfoData{ + ClassID: &MmcClsid, + IID: []*dcom.IID{IDispatchIID}, + ClientCOMVersion: ComVersion, + } + scm := &dcom.SCMRequestInfoData{ + RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{ + RequestedProtocolSequences: []uint16{7}, + }, + } + loc := &dcom.LocationInfoData{} + ac := &dcom.ActivationContextInfoData{} + ap := &dcom.ActivationProperties{ + DestinationContext: 2, + Properties: []dcom.ActivationProperty{inst, ac, loc, scm}, + } + apin, err := ap.ActivationPropertiesIn() + if err != nil { + return err + } + act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, mod.dce) + if err != nil { + return err + } + cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{ + ORPCThis: &dcom.ORPCThis{ + Version: ComVersion, + Flags: 1, + CID: &RandCid, + }, + ActPropertiesIn: apin, + }) + if err != nil { + return err + } + log.Info().Msg("RemoteCreateInstance succeeded") + + apout := &dcom.ActivationProperties{} + if err = apout.Parse(cr.ActPropertiesOut); err != nil { + return err + } + si := apout.SCMReplyInfoData() + pi := apout.PropertiesOutInfo() + + if si == nil { + return fmt.Errorf("remote create instance response: SCMReplyInfoData is nil") + } + if pi == nil { + return fmt.Errorf("remote create instance response: PropertiesOutInfo is nil") + } + oIPID := pi.InterfaceData[0].IPID() + + opts = append(opts, si.RemoteReply.OXIDBindings.EndpointsByProtocol("ncacn_ip_tcp")...) + mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultDcomEndpoint, "", opts...) + if err != nil { + return err + } + log.Info().Msg("created new DCERPC dialer") + + mod.dc, err = idispatch.NewDispatchClient(ctx, mod.dce, dcom.WithIPID(oIPID)) + if err != nil { + return err + } + log.Info().Msg("created IDispatch client") + } + } + return +} + +func (mod *Module) Exec(ctx context.Context, ecfg *exec.ExecutionConfig) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("method", ecfg.ExecutionMethod). + Str("func", "Exec").Logger() + + if ecfg.ExecutionMethod == MethodMmc { + if cfg, ok := ecfg.ExecutionMethodConfig.(MethodMmcConfig); !ok { + return errors.New("invalid configuration") + + } else { + // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mmc/view-executeshellcommand + method := "Document.ActiveView.ExecuteShellCommand" + log = log.With().Str("classMethod", method).Logger() + + log.Info(). + Str("executable", ecfg.ExecutableName). + Str("arguments", ecfg.ExecutableArgs).Msg("Attempting execution") + + command := &oaut.Variant{ + Size: 5, + VT: 8, + VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: ecfg.ExecutableName}}}, + } + directory := &oaut.Variant{ + Size: 5, + VT: 8, + VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: cfg.WorkingDirectory}}}, + } + parameters := &oaut.Variant{ + Size: 5, + VT: 8, + VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: ecfg.ExecutableArgs}}}, + } + windowState := &oaut.Variant{ + Size: 5, + VT: 8, + VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: cfg.WindowState}}}, + } + // Arguments must be passed in reverse order + if _, err := callMethod(ctx, mod.dc, method, windowState, parameters, directory, command); err != nil { + log.Error().Err(err).Msg("Failed to call method") + return fmt.Errorf("call %q: %w", method, err) + } + log.Info().Msg("Method call successful") + } + } + return nil +} diff --git a/internal/exec/dcom/module.go b/internal/exec/dcom/module.go new file mode 100644 index 0000000..bbd50b5 --- /dev/null +++ b/internal/exec/dcom/module.go @@ -0,0 +1,21 @@ +package dcomexec + +import ( + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" +) + +type Module struct { + dce dcerpc.Conn + dc idispatch.DispatchClient + hostname string +} + +type MethodMmcConfig struct { + WorkingDirectory string + WindowState string +} + +const ( + MethodMmc string = "mmc" +) diff --git a/internal/exec/scmr/exec.go b/internal/exec/scmr/exec.go index 7134df0..656b212 100644 --- a/internal/exec/scmr/exec.go +++ b/internal/exec/scmr/exec.go @@ -27,7 +27,6 @@ func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok { return fmt.Errorf("invalid configuration for DCE connection method") } else { - // Fetch target hostname - for opening SCM handle if mod.hostname, err = target.Hostname(ctx); err != nil { log.Debug().Err(err).Msg("Failed to get target hostname") @@ -157,7 +156,6 @@ func (mod *Module) Cleanup(ctx context.Context, ccfg *exec.CleanupConfig) (err e func (mod *Module) Exec(ctx context.Context, ecfg *exec.ExecutionConfig) (err error) { - //vctx := context.WithoutCancel(ctx) log := zerolog.Ctx(ctx).With(). Str("method", ecfg.ExecutionMethod). Str("func", "Exec").Logger() diff --git a/internal/exec/scmr/service.go b/internal/exec/scmr/service.go index 9a580cb..49c7506 100644 --- a/internal/exec/scmr/service.go +++ b/internal/exec/scmr/service.go @@ -1,25 +1,25 @@ package scmrexec import ( - "context" - "github.com/FalconOpsLLC/goexec/internal/windows" - "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" + "context" + "github.com/FalconOpsLLC/goexec/internal/windows" + "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" ) const ( - ServiceDeleteAccess uint32 = windows.SERVICE_DELETE - ServiceModifyAccess uint32 = windows.SERVICE_QUERY_CONFIG | windows.SERVICE_CHANGE_CONFIG | windows.SERVICE_STOP | windows.SERVICE_START | windows.SERVICE_DELETE - ServiceCreateAccess uint32 = windows.SC_MANAGER_CREATE_SERVICE | windows.SERVICE_START | windows.SERVICE_STOP | windows.SERVICE_DELETE - ServiceAllAccess uint32 = ServiceCreateAccess | ServiceModifyAccess + ServiceDeleteAccess uint32 = windows.SERVICE_DELETE + ServiceModifyAccess uint32 = windows.SERVICE_QUERY_CONFIG | windows.SERVICE_CHANGE_CONFIG | windows.SERVICE_STOP | windows.SERVICE_START | windows.SERVICE_DELETE + ServiceCreateAccess uint32 = windows.SC_MANAGER_CREATE_SERVICE | windows.SERVICE_START | windows.SERVICE_STOP | windows.SERVICE_DELETE + ServiceAllAccess uint32 = ServiceCreateAccess | ServiceModifyAccess ) type remoteService struct { - name string - handle *svcctl.Handle - originalConfig *svcctl.QueryServiceConfigW - originalState *svcctl.ServiceStatus + name string + handle *svcctl.Handle + originalConfig *svcctl.QueryServiceConfigW + originalState *svcctl.ServiceStatus } -func (mod *Module) parseServiceDependencies(ctx context.Context, ) (err error) { - return nil +func (mod *Module) parseServiceDependencies(ctx context.Context) (err error) { + return nil // TODO } |