diff options
author | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-04-16 12:11:58 -0500 |
---|---|---|
committer | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-04-16 12:11:58 -0500 |
commit | 55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c (patch) | |
tree | edf4ec3b814fb10ccdbf759a62819a865d3e8141 /cmd | |
parent | a827b67d47cba7b02ea9599fe6bb88ffb3a6967d (diff) | |
download | goexec-55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c.tar.gz goexec-55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c.zip |
rewrote everything lol
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/args.go | 126 | ||||
-rw-r--r-- | cmd/dcom.go | 102 | ||||
-rw-r--r-- | cmd/root.go | 172 | ||||
-rw-r--r-- | cmd/rpc.go | 86 | ||||
-rw-r--r-- | cmd/scmr.go | 243 | ||||
-rw-r--r-- | cmd/tsch.go | 303 | ||||
-rw-r--r-- | cmd/wmi.go | 169 |
7 files changed, 611 insertions, 590 deletions
diff --git a/cmd/args.go b/cmd/args.go new file mode 100644 index 0000000..50e7c74 --- /dev/null +++ b/cmd/args.go @@ -0,0 +1,126 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func registerRpcFlags(cmd *cobra.Command) { + rpcFlags := pflag.NewFlagSet("RPC", pflag.ExitOnError) + + rpcFlags.BoolVar(&rpcClient.NoEpm, "no-epm", false, "Do not use EPM to automatically detect endpoints") + //rpcFlags.BoolVar(&rpcClient.Options.EpmAuto, "epm-auto", false, "Automatically detect endpoints instead of using the module defaults") + rpcFlags.BoolVar(&rpcClient.NoSign, "no-sign", false, "Disable signing on DCE messages") + rpcFlags.BoolVar(&rpcClient.NoSeal, "no-seal", false, "Disable packet stub encryption on DCE messages") + rpcFlags.StringVar(&rpcClient.Filter, "epm-filter", "", "String binding to filter endpoints returned by EPM") + rpcFlags.StringVar(&rpcClient.Endpoint, "endpoint", "", "Explicit RPC endpoint definition") + + cmd.PersistentFlags().AddFlagSet(rpcFlags) + + cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter") + cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter") +} + +func registerProcessExecutionArgs(cmd *cobra.Command) { + group := pflag.NewFlagSet("Execution", pflag.ExitOnError) + + group.StringVarP(&exec.Input.Arguments, "args", "a", "", "Command line arguments") + group.StringVarP(&exec.Input.CommandLine, "command", "c", "", "Windows process command line (executable & arguments)") + group.StringVarP(&exec.Input.Executable, "executable", "e", "", "Windows executable to invoke") + + cmd.PersistentFlags().AddFlagSet(group) + + cmd.MarkFlagsOneRequired("executable", "command") + cmd.MarkFlagsMutuallyExclusive("executable", "command") +} + +func registerExecutionOutputArgs(cmd *cobra.Command) { + group := pflag.NewFlagSet("Output", pflag.ExitOnError) + + group.StringVarP(&outputPath, "output", "o", "", `Fetch execution output to file or "-" for standard output`) + group.StringVarP(&outputMethod, "output-method", "m", "smb", "Method to fetch execution output") + group.StringVar(&exec.Output.RemotePath, "remote-output", "", "Location to temporarily store output on remote filesystem") + group.BoolVar(&exec.Output.NoDelete, "no-delete-output", false, "Preserve output file on remote filesystem") + + cmd.PersistentFlags().AddFlagSet(group) +} + +func args(reqs ...func(*cobra.Command, []string) error) (fn func(*cobra.Command, []string) error) { + + return func(cmd *cobra.Command, args []string) (err error) { + + for _, req := range reqs { + if err = req(cmd, args); err != nil { + return + } + } + return + } +} + +func argsTarget(proto string) func(cmd *cobra.Command, args []string) error { + + return func(cmd *cobra.Command, args []string) (err error) { + + if len(args) != 1 { + return errors.New("command require exactly one positional argument: [target]") + } + + if credential, target, err = authOpts.WithTarget(context.TODO(), proto, args[0]); err != nil { + return fmt.Errorf("failed to parse target: %w", err) + } + + if credential == nil { + return errors.New("no credentials supplied") + } + if target == nil { + return errors.New("no target supplied") + } + return + } +} + +func argsSmbClient() func(cmd *cobra.Command, args []string) error { + return args( + argsTarget("cifs"), + + func(_ *cobra.Command, _ []string) error { + + smbClient.Credential = credential + smbClient.Target = target + smbClient.Proxy = proxy + + return smbClient.Parse(context.TODO()) + }, + ) +} + +func argsRpcClient(proto string) func(cmd *cobra.Command, args []string) error { + return args( + argsTarget(proto), + + func(cmd *cobra.Command, args []string) (err error) { + + rpcClient.Target = target + rpcClient.Credential = credential + rpcClient.Proxy = proxy + + return rpcClient.Parse(context.TODO()) + }, + ) +} + +func argsOutput(methods ...string) func(cmd *cobra.Command, args []string) error { + + var as []func(*cobra.Command, []string) error + + for _, method := range methods { + if method == "smb" { + as = append(as, argsSmbClient()) + } + } + return args(as...) +} diff --git a/cmd/dcom.go b/cmd/dcom.go index d105b0c..5bcec78 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -1,38 +1,37 @@ package cmd import ( - "github.com/FalconOpsLLC/goexec/internal/exec" - dcomexec "github.com/FalconOpsLLC/goexec/internal/exec/dcom" - "github.com/spf13/cobra" + "context" + dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" + "github.com/spf13/cobra" ) func dcomCmdInit() { - registerRpcFlags(dcomCmd) - dcomMmcCmdInit() - dcomCmd.AddCommand(dcomMmcCmd) + 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.Flags().StringVarP(&dcomMmc.WorkingDirectory, "directory", "d", `C:\`, "Working directory") + dcomMmcCmd.Flags().StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state") - dcomMmcCmd.MarkFlagsOneRequired("executable", "command") - dcomMmcCmd.MarkFlagsMutuallyExclusive("executable", "command") + registerProcessExecutionArgs(dcomMmcCmd) } 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: + dcomMmc dcomexec.DcomMmc + + 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. @@ -42,34 +41,39 @@ References: 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) { + Args: argsRpcClient("host"), + Run: func(cmd *cobra.Command, args []string) { + var err error + + ctx := gssapi.NewSecurityContext(context.Background()) + + ctx = log.With(). + Str("module", "dcom"). + Str("method", "mmc"). + Logger(). + WithContext(ctx) + + if err = rpcClient.Connect(ctx); err != nil { + log.Fatal().Err(err).Msg("Connection failed") + } - ctx = log.With(). - Str("module", "dcom"). - Str("method", "mmc"). - Logger().WithContext(ctx) + defer func() { + closeErr := rpcClient.Close(ctx) + if closeErr != nil { + log.Error().Err(closeErr).Msg("Failed to close connection") + } + }() - module := dcomexec.Module{} - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, - } - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: dcomexec.MethodMmc, + if err = dcomMmc.Init(ctx, &rpcClient); err != nil { + log.Error().Err(err).Msg("Module initialization failed") + returnCode = 1 + return + } - 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") - } - }, - } + if err = dcomMmc.Execute(ctx, exec.Input); err != nil { + log.Error().Err(err).Msg("Execution failed") + returnCode = 1 + } + }, + } ) diff --git a/cmd/root.go b/cmd/root.go index cbc38c0..733bb75 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,122 +1,105 @@ package cmd import ( - "context" "fmt" + "github.com/FalconOpsLLC/goexec/internal/util" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" + "github.com/FalconOpsLLC/goexec/pkg/goexec/smb" "github.com/RedTeamPentesting/adauth" + "github.com/oiweiwei/go-msrpc/ssp" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" "github.com/rs/zerolog" "github.com/spf13/cobra" - "net/url" "os" - "regexp" - "strings" ) var ( - //logFile string - log zerolog.Logger - ctx context.Context - authOpts *adauth.Options - - hostname string - proxyStr string - proxyUrl *url.URL - - // Root flags - unsafe bool // not implemented - debug bool - - // Generic flags - command string - executable string - executablePath string - executableArgs string - workingDirectory string - windowState string + debug bool + logJson bool + returnCode int + outputMethod string + outputPath string + proxy string + + log zerolog.Logger + + rpcClient dce.Client + smbClient smb.Client + + exec = goexec.ExecutionIO{ + Input: new(goexec.ExecutionInput), + Output: new(goexec.ExecutionOutput), + } + + authOpts *adauth.Options + credential *adauth.Credential + target *adauth.Target 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] - } + Use: "goexec", + Short: `Windows remote execution multitool`, + Long: `TODO`, + + PersistentPreRun: func(cmd *cobra.Command, args []string) { + + if logJson { + log = zerolog.New(os.Stderr) + } else { + log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}) } - log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel).With().Timestamp().Logger() + + log = log.Level(zerolog.InfoLevel).With().Timestamp().Logger() if debug { log = log.Level(zerolog.DebugLevel) } - return + + if outputMethod == "smb" { + if exec.Output.RemotePath == "" { + exec.Output.RemotePath = util.RandomWindowsTempFile() + } + exec.Output.Provider = &smb.OutputFileFetcher{ + Client: &smbClient, + Share: `C$`, + File: exec.Output.RemotePath, + } + } }, } ) -func needs(reqs ...func(*cobra.Command, []string) error) (fn func(*cobra.Command, []string) error) { - return func(cmd *cobra.Command, args []string) (err error) { - for _, req := range reqs { - if err = req(cmd, args); err != nil { - return - } - } - return - } -} +func init() { + // Cobra init + { + cobra.EnableCommandSorting = false -func needsTarget(proto string) func(cmd *cobra.Command, args []string) error { + rootCmd.InitDefaultVersionFlag() + rootCmd.InitDefaultHelpCmd() + rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging") + rootCmd.PersistentFlags().BoolVar(&logJson, "log-json", false, "Log in JSON format") - return func(cmd *cobra.Command, args []string) (err error) { - if proxyStr != "" { - if proxyUrl, err = url.Parse(proxyStr); err != nil { - return fmt.Errorf("failed to parse proxy URL %q: %w", proxyStr, err) - } - } - 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") - } - if hostname, err = target.Hostname(ctx); err != nil { - log.Debug().Err(err).Msg("Could not get target hostname") - } - return + dcomCmdInit() + rootCmd.AddCommand(dcomCmd) + + wmiCmdInit() + rootCmd.AddCommand(wmiCmd) + + scmrCmdInit() + rootCmd.AddCommand(scmrCmd) + + tschCmdInit() + rootCmd.AddCommand(tschCmd) } -} -func init() { - ctx = context.Background() - - cobra.EnableCommandSorting = false - - rootCmd.InitDefaultVersionFlag() - rootCmd.InitDefaultHelpCmd() - rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging") - rootCmd.PersistentFlags().StringVarP(&proxyStr, "proxy", "x", "", "Proxy URL") - rootCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "[NOT IMPLEMENTED] Don't ask for permission to run unsafe actions") - - authOpts = &adauth.Options{Debug: log.Debug().Msgf} - authOpts.RegisterFlags(rootCmd.PersistentFlags()) - - scmrCmdInit() - rootCmd.AddCommand(scmrCmd) - tschCmdInit() - rootCmd.AddCommand(tschCmd) - wmiCmdInit() - rootCmd.AddCommand(wmiCmd) - dcomCmdInit() - rootCmd.AddCommand(dcomCmd) + // Auth init + { + gssapi.AddMechanism(ssp.SPNEGO) + gssapi.AddMechanism(ssp.NTLM) + gssapi.AddMechanism(ssp.KRB5) + + authOpts = &adauth.Options{Debug: log.Debug().Msgf} + authOpts.RegisterFlags(rootCmd.PersistentFlags()) + } } func Execute() { @@ -124,4 +107,5 @@ func Execute() { fmt.Println(err) os.Exit(1) } + os.Exit(returnCode) } diff --git a/cmd/rpc.go b/cmd/rpc.go deleted file mode 100644 index cfc7a9d..0000000 --- a/cmd/rpc.go +++ /dev/null @@ -1,86 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/FalconOpsLLC/goexec/internal/client/dce" - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/net/proxy" - "regexp" -) - -func needsRpcTarget(proto string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) (err error) { - - if err = needsTarget(proto)(cmd, args); err != nil { - return err - } - if proxyUrl != nil { - if netDialer, err := proxy.FromURL(proxyUrl, nil); err != nil { - return fmt.Errorf("proxy dialer from URL: %w", err) - } else if dceDialer, ok := netDialer.(dcerpc.Dialer); !ok { - return fmt.Errorf("failed to cast %T to dcerpc.Dialer", netDialer) - } else { - dceConfig.Options = append(dceConfig.Options, dcerpc.WithDialer(dceDialer)) - } - } - 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 hostname != "" { - dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithTargetName(fmt.Sprintf("%s/%s", proto, hostname))) - } - 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 nil - } -} - -var ( - // DCE arguments - argDceStringBinding string - argDceEpmFilter string - argDceNoSeal bool - argDceNoSign bool - - // DCE options - dceStringBinding *dcerpc.StringBinding - dceConfig dce.ConnectionMethodDCEConfig -) - -func registerRpcFlags(cmd *cobra.Command) { - 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") -} diff --git a/cmd/scmr.go b/cmd/scmr.go index 11e1379..08e23d7 100644 --- a/cmd/scmr.go +++ b/cmd/scmr.go @@ -1,14 +1,12 @@ package cmd import ( - "github.com/FalconOpsLLC/goexec/internal/exec" + "context" "github.com/FalconOpsLLC/goexec/internal/util" - "github.com/FalconOpsLLC/goexec/internal/windows" - "github.com/RedTeamPentesting/adauth" - "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" "github.com/spf13/cobra" - scmrexec "github.com/FalconOpsLLC/goexec/internal/exec/scmr" + scmrexec "github.com/FalconOpsLLC/goexec/pkg/goexec/scmr" ) func scmrCmdInit() { @@ -22,53 +20,55 @@ func scmrCmdInit() { } func scmrCreateCmdInit() { - scmrCreateCmd.Flags().StringVarP(&scmrDisplayName, "display-name", "n", "", "Display name of service to create") - scmrCreateCmd.Flags().StringVarP(&scmrServiceName, "service-name", "s", "", "Name of service to create") - scmrCreateCmd.Flags().BoolVar(&scmrNoDelete, "no-delete", false, "Don't delete service after execution") - scmrCreateCmd.Flags().BoolVar(&scmrNoStart, "no-start", false, "Don't start service") - scmrCreateCmd.Flags().StringVarP(&executablePath, "executable-path", "f", "", "Full path to a remote Windows executable file") - scmrCreateCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to the executable") - scmrCreateCmd.Flags().BoolVarP(&scmrOutput, "output", "O", false, "Fetch program output") + scmrCreateCmd.Flags().StringVarP(&scmrCreate.DisplayName, "display-name", "n", "", "Display name of service to create") + scmrCreateCmd.Flags().StringVarP(&scmrCreate.ServiceName, "service-name", "s", "", "Name of service to create") + scmrCreateCmd.Flags().BoolVar(&scmrCreate.NoDelete, "no-delete", false, "Don't delete service after execution") + scmrCreateCmd.Flags().BoolVar(&scmrCreate.NoStart, "no-start", false, "Don't start service") + + scmrCreateCmd.Flags().StringVarP(&exec.Input.ExecutablePath, "executable-path", "f", "", "Full path to a remote Windows executable") + scmrCreateCmd.Flags().StringVarP(&exec.Input.Arguments, "args", "a", "", "Arguments to pass to the executable") + + scmrCreateCmd.MarkFlagsMutuallyExclusive("no-delete", "no-start") + if err := scmrCreateCmd.MarkFlagRequired("executable-path"); err != nil { panic(err) } } func scmrChangeCmdInit() { - scmrChangeCmd.Flags().StringVarP(&scmrDisplayName, "display-name", "n", "", "Display name of service to create") - scmrChangeCmd.Flags().BoolVar(&scmrNoStart, "no-start", false, "Don't start service") - scmrChangeCmd.Flags().StringVarP(&scmrServiceName, "service-name", "s", "", "Name of service to modify") - scmrChangeCmd.Flags().StringVarP(&executablePath, "executable-path", "f", "", "Full path to remote Windows executable") - scmrChangeCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable") + scmrChangeCmd.Flags().BoolVar(&scmrChange.NoStart, "no-start", false, "Don't start service") + scmrChangeCmd.Flags().StringVarP(&scmrChange.ServiceName, "service-name", "s", "", "Name of service to modify") + + scmrChangeCmd.Flags().StringVarP(&exec.Input.ExecutablePath, "executable-path", "f", "", "Full path to remote Windows executable") + scmrChangeCmd.Flags().StringVarP(&exec.Input.Arguments, "args", "a", "", "Arguments to pass to executable") + if err := scmrChangeCmd.MarkFlagRequired("service-name"); err != nil { panic(err) } + if err := scmrCreateCmd.MarkFlagRequired("executable-path"); err != nil { + panic(err) + } } func scmrDeleteCmdInit() { - scmrDeleteCmd.Flags().StringArrayVarP(&scmrServiceNames, "service-name", "s", scmrServiceNames, "Name of service(s) to delete") + scmrDeleteCmd.Flags().StringVarP(&scmrDelete.ServiceName, "service-name", "s", scmrDelete.ServiceName, "Name of service to delete") + if err := scmrDeleteCmd.MarkFlagRequired("service-name"); err != nil { panic(err) } } var ( - // scmr arguments - scmrServiceName string - scmrServiceNames []string - scmrDisplayName string - scmrNoDelete bool - scmrNoStart bool - scmrOutput bool - - creds *adauth.Credential - target *adauth.Target + scmrCreate scmrexec.ScmrCreate + scmrChange scmrexec.ScmrChange + scmrDelete scmrexec.ScmrDelete scmrCmd = &cobra.Command{ Use: "scmr", Short: "Establish execution via SCMR", Args: cobra.NoArgs, } + scmrCreateCmd = &cobra.Command{ Use: "create [target]", Short: "Create & run a new Windows service to gain execution", @@ -79,138 +79,145 @@ var ( References: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/6a8ca926-9477-4dd4-b766-692fab07227e `, - Args: needs(needsTarget("host"), needsRpcTarget("host")), + Args: argsRpcClient("cifs"), + Run: func(cmd *cobra.Command, args []string) { + var err error + + ctx := gssapi.NewSecurityContext(context.Background()) - if scmrServiceName == "" { - log.Warn().Msg("No service name was specified, using random string") - scmrServiceName = util.RandomString() + ctx = log.With(). + Str("module", "scmr"). + Str("method", "create"). + Logger(). + WithContext(ctx) + + if scmrCreate.ServiceName == "" { + log.Warn().Msg("No service name was provided. Using a random string") + scmrCreate.ServiceName = util.RandomString() } - if scmrNoDelete { + + if scmrCreate.NoDelete { log.Warn().Msg("Service will not be deleted after execution") } - if scmrDisplayName == "" { + + if scmrCreate.DisplayName == "" { log.Debug().Msg("No display name specified, using service name as display name") - scmrDisplayName = scmrServiceName + scmrCreate.DisplayName = scmrCreate.ServiceName } - executor := scmrexec.Module{} - cleanCfg := &exec.CleanupConfig{ - CleanupMethod: scmrexec.CleanupMethodDelete, - CleanupMethodConfig: scmrexec.CleanupMethodDeleteConfig{}, - } - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, - } - execCfg := &exec.ExecutionConfig{ - ExecutablePath: executablePath, - ExecutableArgs: executableArgs, - ReturnOutput: scmrOutput, - ExecutionMethod: scmrexec.MethodCreate, - - ExecutionMethodConfig: scmrexec.MethodCreateConfig{ - NoDelete: scmrNoDelete, - ServiceName: util.RandomStringIfBlank(scmrServiceName), - DisplayName: scmrDisplayName, - ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, - StartType: windows.SERVICE_DEMAND_START, - }, - } - ctx = log.With(). - Str("module", "scmr"). - Str("method", "create"). - Logger().WithContext(ctx) - - if err := executor.Connect(ctx, creds, target, connCfg); err != nil { + if err = rpcClient.Connect(ctx); err != nil { log.Fatal().Err(err).Msg("Connection failed") } - if !scmrNoDelete { - defer func() { - if err := executor.Cleanup(ctx, cleanCfg); err != nil { - log.Error().Err(err).Msg("Cleanup failed") - } - }() + + defer func() { + closeErr := rpcClient.Close(ctx) + if closeErr != nil { + log.Error().Err(closeErr).Msg("Failed to close connection") + } + }() + + defer func() { + cleanErr := scmrCreate.Clean(ctx) + if cleanErr != nil { + log.Warn().Err(cleanErr).Msg("Clean operation failed") + } + }() + + if err = scmrCreate.Init(ctx, &rpcClient); err != nil { + log.Error().Err(err).Msg("Module initialization failed") + returnCode = 2 + return } - if err := executor.Exec(ctx, execCfg); err != nil { + + if err = scmrCreate.Execute(ctx, exec.Input); err != nil { log.Error().Err(err).Msg("Execution failed") + returnCode = 4 } }, } + scmrChangeCmd = &cobra.Command{ Use: "change [target]", Short: "Change an existing Windows service to gain execution", - Args: needs(needsTarget("host"), needsRpcTarget("host")), + Args: argsRpcClient("cifs"), Run: func(cmd *cobra.Command, args []string) { + var err error + + ctx := gssapi.NewSecurityContext(context.Background()) - executor := scmrexec.Module{} - cleanCfg := &exec.CleanupConfig{ - CleanupMethod: scmrexec.CleanupMethodRevert, - CleanupMethodConfig: scmrexec.CleanupMethodRevertConfig{}, - } - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, - } - execCfg := &exec.ExecutionConfig{ - ExecutablePath: executablePath, - ExecutableArgs: executableArgs, - ExecutionMethod: scmrexec.MethodChange, - - ExecutionMethodConfig: scmrexec.MethodChangeConfig{ - NoStart: scmrNoStart, - ServiceName: scmrServiceName, - }, - } ctx = log.With(). Str("module", "scmr"). Str("method", "change"). - Logger().WithContext(ctx) + Logger(). + WithContext(ctx) - if err := executor.Connect(ctx, creds, target, connCfg); err != nil { + if err = rpcClient.Connect(ctx); err != nil { log.Fatal().Err(err).Msg("Connection failed") } - if !scmrNoDelete { - defer func() { - if err := executor.Cleanup(ctx, cleanCfg); err != nil { - log.Error().Err(err).Msg("Cleanup failed") - } - }() + + defer func() { + closeErr := rpcClient.Close(ctx) + if closeErr != nil { + log.Error().Err(closeErr).Msg("Failed to close connection") + } + }() + + defer func() { + cleanErr := scmrChange.Clean(ctx) + if cleanErr != nil { + log.Warn().Err(cleanErr).Msg("Clean operation failed") + } + }() + + if err = scmrChange.Init(ctx, &rpcClient); err != nil { + log.Error().Err(err).Msg("Module initialization failed") + returnCode = 2 + return } - if err := executor.Exec(ctx, execCfg); err != nil { + + if err = scmrChange.Execute(ctx, exec.Input); err != nil { log.Error().Err(err).Msg("Execution failed") + returnCode = 4 } }, } scmrDeleteCmd = &cobra.Command{ Use: "delete [target]", Short: "Delete an existing Windows service", - Long: `Description: -TODO -`, - Args: needs(needsTarget("host"), needsRpcTarget("host")), + Long: `TODO`, + + Args: argsRpcClient("cifs"), Run: func(cmd *cobra.Command, args []string) { - dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithInsecure()) + var err error + + ctx := gssapi.NewSecurityContext(context.Background()) - executor := scmrexec.Module{} - cleanCfg := &exec.CleanupConfig{ - CleanupMethod: scmrexec.CleanupMethodDelete, - CleanupMethodConfig: scmrexec.CleanupMethodDeleteConfig{ServiceNames: scmrServiceNames}, - } - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, - } ctx = log.With(). Str("module", "scmr"). Str("method", "delete"). - Logger().WithContext(ctx) + Logger(). + WithContext(ctx) - if err := executor.Connect(ctx, creds, target, connCfg); err != nil { + if err = rpcClient.Connect(ctx); err != nil { log.Fatal().Err(err).Msg("Connection failed") + } + + defer func() { + closeErr := rpcClient.Close(ctx) + if closeErr != nil { + log.Error().Err(closeErr).Msg("Failed to close connection") + } + }() + + if err = scmrDelete.Init(ctx, &rpcClient); err != nil { + log.Error().Err(err).Msg("Module initialization failed") + returnCode = 2 + } - } else if err = executor.Cleanup(ctx, cleanCfg); err != nil { - log.Fatal().Err(err).Msg("Delete failed") + if err = scmrDelete.Clean(ctx); err != nil { + log.Warn().Err(err).Msg("Clean failed") + returnCode = 4 } }, } diff --git a/cmd/tsch.go b/cmd/tsch.go index f377d56..7c0fdde 100644 --- a/cmd/tsch.go +++ b/cmd/tsch.go @@ -1,154 +1,89 @@ package cmd import ( - "fmt" - "github.com/FalconOpsLLC/goexec/internal/exec" - "github.com/FalconOpsLLC/goexec/internal/exec/tsch" + "context" + "github.com/FalconOpsLLC/goexec/internal/util" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + tschexec "github.com/FalconOpsLLC/goexec/pkg/goexec/tsch" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" "github.com/spf13/cobra" - "regexp" + "io" + "os" "time" ) func tschCmdInit() { registerRpcFlags(tschCmd) - tschDeleteCmdInit() - tschCmd.AddCommand(tschDeleteCmd) - tschRegisterCmdInit() - tschCmd.AddCommand(tschRegisterCmd) tschDemandCmdInit() tschCmd.AddCommand(tschDemandCmd) + + tschCreateCmdInit() + tschCmd.AddCommand(tschCreateCmd) } -func tschDeleteCmdInit() { - tschDeleteCmd.Flags().StringVarP(&tschTaskPath, "path", "t", "", "Scheduled task path") - if err := tschDeleteCmd.MarkFlagRequired("path"); err != nil { - panic(err) +func argsTschTaskIdentifiers(name, path string) error { + switch { + case path != "": + return tschexec.ValidateTaskPath(path) + case name != "": + return tschexec.ValidateTaskName(name) + default: } + return nil } -func tschDemandCmdInit() { - tschDemandCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke") - tschDemandCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable") - tschDemandCmd.Flags().StringVarP(&tschTaskName, "name", "n", "", "Target task name") - tschDemandCmd.Flags().BoolVar(&tschNoDelete, "no-delete", false, "Don't delete task after execution") - tschDemandCmd.Flags().Uint32Var(&tschSessionId, "session-id", 0, "Hijack existing session") - if err := tschDemandCmd.MarkFlagRequired("executable"); err != nil { - panic(err) - } +func argsTschDemand(_ *cobra.Command, _ []string) error { + return argsTschTaskIdentifiers(tschDemand.TaskName, tschDemand.TaskPath) } -func tschRegisterCmdInit() { - tschRegisterCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke") - tschRegisterCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable") - tschRegisterCmd.Flags().StringVarP(&tschTaskName, "name", "n", "", "Target task name") - tschRegisterCmd.Flags().DurationVar(&tschStopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This will not stop the process spawned by the task") - tschRegisterCmd.Flags().DurationVarP(&tschDelay, "delay-start", "d", 5*time.Second, "Delay between task registration and execution") - tschRegisterCmd.Flags().DurationVarP(&tschDeleteDelay, "delay-delete", "D", 0*time.Second, "Delay between task termination and deletion") - tschRegisterCmd.Flags().BoolVar(&tschNoDelete, "no-delete", false, "Don't delete task after execution") - tschRegisterCmd.Flags().BoolVar(&tschCallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task") - - tschRegisterCmd.MarkFlagsMutuallyExclusive("no-delete", "delay-delete") - tschRegisterCmd.MarkFlagsMutuallyExclusive("no-delete", "call-delete") - tschRegisterCmd.MarkFlagsMutuallyExclusive("delay-delete", "call-delete") - - if err := tschRegisterCmd.MarkFlagRequired("executable"); err != nil { - panic(err) - } +func argsTschCreate(_ *cobra.Command, _ []string) error { + return argsTschTaskIdentifiers(tschCreate.TaskName, tschCreate.TaskPath) } -func tschArgs(principal string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - if tschTaskPath != "" && !tschTaskPathRegex.MatchString(tschTaskPath) { - return fmt.Errorf("invalid task path: %s", tschTaskPath) - } - if tschTaskName != "" { - if !tschTaskNameRegex.MatchString(tschTaskName) { - return fmt.Errorf("invalid task name: %s", tschTaskName) - - } else if tschTaskPath == "" { - tschTaskPath = `\` + tschTaskName - } - } - return needsRpcTarget(principal)(cmd, args) - } +func tschDemandCmdInit() { + tschDemandCmd.Flags().StringVarP(&tschDemand.TaskName, "name", "t", "", "Name of task to register") + tschDemandCmd.Flags().StringVarP(&tschDemand.TaskPath, "path", "P", "", "Path of task to register") + tschDemandCmd.Flags().Uint32Var(&tschDemand.SessionId, "session", 0, "Hijack existing session given the session ID") + tschDemandCmd.Flags().BoolVar(&tschDemand.NoDelete, "no-delete", false, "Don't delete task after execution") + tschDemandCmd.Flags().StringVar(&tschDemand.UserSid, "sid", "S-1-5-18", "User SID to impersonate") + + registerProcessExecutionArgs(tschDemandCmd) + registerExecutionOutputArgs(tschDemandCmd) + + tschDemandCmd.MarkFlagsMutuallyExclusive("name", "path") +} + +func tschCreateCmdInit() { + tschCreateCmd.Flags().StringVarP(&tschCreate.TaskName, "name", "t", "", "Name of task to register") + tschCreateCmd.Flags().StringVarP(&tschCreate.TaskPath, "path", "P", "", "Path of task to register") + tschCreateCmd.Flags().DurationVar(&tschCreate.StopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This will not stop the process spawned by the task") + tschCreateCmd.Flags().DurationVar(&tschCreate.StartDelay, "start-delay", 5*time.Second, "Delay between task registration and execution") + tschCreateCmd.Flags().DurationVar(&tschCreate.DeleteDelay, "delete-delay", 0*time.Second, "Delay between task termination and deletion") + tschCreateCmd.Flags().BoolVar(&tschCreate.NoDelete, "no-delete", false, "Don't delete task after execution") + tschCreateCmd.Flags().BoolVar(&tschCreate.CallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task") + tschCreateCmd.Flags().StringVar(&tschCreate.UserSid, "sid", "S-1-5-18", "User SID to impersonate") + + registerProcessExecutionArgs(tschCreateCmd) + + tschCreateCmd.MarkFlagsMutuallyExclusive("name", "path") } var ( - tschSessionId uint32 - tschNoDelete bool - tschCallDelete bool - tschDeleteDelay time.Duration - tschStopDelay time.Duration - tschDelay time.Duration - tschTaskName string - tschTaskPath string - - tschTaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`) - tschTaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`) + tschDemand tschexec.TschDemand + tschCreate tschexec.TschCreate tschCmd = &cobra.Command{ Use: "tsch", - Short: "Establish execution via TSCH", + Short: "Establish execution via Windows Task Scheduler (MS-TSCH)", Args: cobra.NoArgs, } - tschRegisterCmd = &cobra.Command{ - Use: "register [target]", - Short: "Register a remote scheduled task with an automatic start time", - Long: `Description: - The register method calls SchRpcRegisterTask to register a scheduled task - with an automatic start time.This method avoids directly calling SchRpcRun, - and can even avoid calling SchRpcDelete by populating the DeleteExpiredTaskAfter - Setting. -References: - SchRpcRegisterTask - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167 - SchRpcRun - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/77f2250d-500a-40ee-be18-c82f7079c4f0 - SchRpcDelete - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/360bb9b1-dd2a-4b36-83ee-21f12cb97cff - DeleteExpiredTaskAfter - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/6bfde6fe-440e-4ddd-b4d6-c8fc0bc06fae -`, - Args: tschArgs("cifs"), - Run: func(cmd *cobra.Command, args []string) { - - log = log.With(). - Str("module", "tsch"). - Str("method", "register"). - Logger() - if tschNoDelete { - log.Warn().Msg("Task will not be deleted after execution") - } - - module := tschexec.Module{} - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, - } - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: tschexec.MethodRegister, - - ExecutionMethodConfig: tschexec.MethodRegisterConfig{ - NoDelete: tschNoDelete, - CallDelete: tschCallDelete, - StartDelay: tschDelay, - StopDelay: tschStopDelay, - DeleteDelay: tschDeleteDelay, - TaskPath: tschTaskPath, - }, - } - 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") - } - }, - } tschDemandCmd = &cobra.Command{ Use: "demand [target]", Short: "Register a remote scheduled task and demand immediate start", Long: `Description: - Similar to the register method, the demand method will call SchRpcRegisterTask, + Similar to the create method, the demand method will call SchRpcRegisterTask, But rather than setting a defined time when the task will start, it will additionally call SchRpcRun to forcefully start the task. @@ -156,68 +91,110 @@ References: SchRpcRegisterTask - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167 SchRpcRun - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/77f2250d-500a-40ee-be18-c82f7079c4f0 `, - Args: tschArgs("cifs"), + Args: args( + argsRpcClient("cifs"), + argsSmbClient(), + argsTschDemand, + ), + Run: func(cmd *cobra.Command, args []string) { + var err error + + tschDemand.Client = &rpcClient + tschDemand.IO = exec - log = log.With(). - Str("module", "tsch"). - Str("method", "register"). - Logger() - if tschNoDelete { - log.Warn().Msg("Task will not be deleted after execution") + if tschDemand.TaskName == "" && tschDemand.TaskPath == "" { + tschDemand.TaskPath = `\` + util.RandomString() } - module := tschexec.Module{} - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, + + ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO())) + + var writer io.WriteCloser + + if outputPath == "-" { + writer = os.Stdout + + } else if outputPath != "" { + + if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil { + log.Fatal().Err(err).Msg("Failed to open output file") + } + defer writer.Close() } - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: tschexec.MethodDemand, - - ExecutionMethodConfig: tschexec.MethodDemandConfig{ - NoDelete: tschNoDelete, - TaskPath: tschTaskPath, - SessionId: tschSessionId, - }, + + if err = goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") } - 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") + + if outputPath != "" { + if reader, err := tschDemand.GetOutput(ctx); err == nil { + _, err = io.Copy(writer, reader) + + } else { + log.Error().Err(err).Msg("Failed to get process execution output") + returnCode = 2 + } } }, } - tschDeleteCmd = &cobra.Command{ - Use: "delete [target]", - Short: "Manually delete a scheduled task", + tschCreateCmd = &cobra.Command{ + Use: "create [target]", + Short: "Create a remote scheduled task with an automatic start time", Long: `Description: - The delete method manually deletes a scheduled task by calling SchRpcDelete + The create method calls SchRpcRegisterTask to register a scheduled task + with an automatic start time.This method avoids directly calling SchRpcRun, + and can even avoid calling SchRpcDelete by populating the DeleteExpiredTaskAfter + Setting. References: + SchRpcRegisterTask - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167 + SchRpcRun - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/77f2250d-500a-40ee-be18-c82f7079c4f0 SchRpcDelete - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/360bb9b1-dd2a-4b36-83ee-21f12cb97cff + DeleteExpiredTaskAfter - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/6bfde6fe-440e-4ddd-b4d6-c8fc0bc06fae `, - Args: tschArgs("cifs"), + Args: args( + argsRpcClient("cifs"), + argsSmbClient(), + argsTschCreate, + ), + Run: func(cmd *cobra.Command, args []string) { - log = log.With(). - Str("module", "tsch"). - Str("method", "delete"). - Logger() - - module := tschexec.Module{} - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, + var err error + + tschCreate.Tsch.Client = &rpcClient + tschCreate.IO = exec + + if tschCreate.TaskName == "" && tschDemand.TaskPath == "" { + tschCreate.TaskPath = `\` + util.RandomString() } - cleanCfg := &exec.CleanupConfig{ - CleanupMethod: tschexec.MethodDelete, - CleanupMethodConfig: tschexec.MethodDeleteConfig{TaskPath: tschTaskPath}, + + ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO())) + + var writer io.WriteCloser + + if outputPath == "-" { + writer = os.Stdout + + } else if outputPath != "" { + + if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil { + log.Fatal().Err(err).Msg("Failed to open output file") + } + defer writer.Close() } - if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil { - log.Fatal().Err(err).Msg("Connection failed") - } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { - log.Fatal().Err(err).Msg("Cleanup failed") + + if err = goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + + if outputPath != "" { + if reader, err := tschDemand.GetOutput(ctx); err == nil { + _, err = io.Copy(writer, reader) + + } else { + log.Error().Err(err).Msg("Failed to get process execution output") + returnCode = 2 + } } }, } @@ -1,52 +1,65 @@ package cmd import ( + "context" "encoding/json" "fmt" - "github.com/FalconOpsLLC/goexec/internal/exec" - wmiexec "github.com/FalconOpsLLC/goexec/internal/exec/wmi" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + wmiexec "github.com/FalconOpsLLC/goexec/pkg/goexec/wmi" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" "github.com/spf13/cobra" + "io" + "os" ) func wmiCmdInit() { registerRpcFlags(wmiCmd) + wmiCallCmdInit() wmiCmd.AddCommand(wmiCallCmd) - wmiProcessCmdInit() - wmiCmd.AddCommand(wmiProcessCmd) + + wmiProcCmdInit() + wmiCmd.AddCommand(wmiProcCmd) +} + +func wmiCallArgs(_ *cobra.Command, _ []string) error { + return json.Unmarshal([]byte(wmiArguments), &wmiCall.Args) } func wmiCallCmdInit() { - wmiCallCmd.Flags().StringVarP(&dceConfig.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace") - wmiCallCmd.Flags().StringVarP(&wmi.Class, "class", "C", "", `WMI class to instantiate (i.e. "Win32_Process")`) - wmiCallCmd.Flags().StringVarP(&wmi.Method, "method", "m", "", `WMI Method to call (i.e. "Create")`) - wmiCallCmd.Flags().StringVarP(&wmi.Args, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`) + wmiCallCmd.Flags().StringVarP(&wmiCall.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace") + wmiCallCmd.Flags().StringVarP(&wmiCall.Class, "class", "C", "", `WMI class to instantiate (i.e. "Win32_Process")`) + wmiCallCmd.Flags().StringVarP(&wmiCall.Method, "method", "m", "", `WMI Method to call (i.e. "Create")`) + wmiCallCmd.Flags().StringVarP(&wmiArguments, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`) + + if err := wmiCallCmd.MarkFlagRequired("class"); err != nil { + panic(err) + } if err := wmiCallCmd.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) - } +func wmiProcCmdInit() { + wmiProcCmd.Flags().StringVarP(&wmiProc.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace") + wmiProcCmd.Flags().StringVarP(&wmiProc.WorkingDirectory, "directory", "d", `C:\`, "Working directory") + + registerProcessExecutionArgs(wmiProcCmd) + registerExecutionOutputArgs(wmiProcCmd) } var ( - wmi struct { - Class string - Method string - Args string - } - wmiMethodArgsMap map[string]any + wmiCall = wmiexec.WmiCall{} + wmiProc = wmiexec.WmiProc{} + + wmiArguments string wmiCmd = &cobra.Command{ Use: "wmi", - Short: "Establish execution via WMI", + Short: "Establish execution via wmi", Args: cobra.NoArgs, } + wmiCallCmd = &cobra.Command{ Use: "call", Short: "Execute specified WMI method", @@ -56,52 +69,48 @@ var ( References: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-classes - `, - Args: needs(needsTarget("cifs"), needsRpcTarget("cifs"), func(cmd *cobra.Command, args []string) (err error) { - if err = json.Unmarshal([]byte(wmi.Args), &wmiMethodArgsMap); err != nil { - err = fmt.Errorf("parse JSON arguments: %w", err) - } - return - }), +`, + Args: args(argsRpcClient("host"), wmiCallArgs), + Run: func(cmd *cobra.Command, args []string) { - executor := wmiexec.Module{} - cleanCfg := &exec.CleanupConfig{} // TODO - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, - } + var err error - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: wmiexec.MethodCall, - ExecutionMethodConfig: wmiexec.MethodCallConfig{ - Class: wmi.Class, - Method: wmi.Method, - Arguments: wmiMethodArgsMap, - }, - } + ctx := gssapi.NewSecurityContext(context.Background()) ctx = log.With(). Str("module", "wmi"). - Str("method", "proc"). - Logger().WithContext(ctx) + Str("method", "call"). + Logger(). + WithContext(ctx) - if err := executor.Connect(ctx, creds, target, connCfg); err != nil { + if err = rpcClient.Connect(ctx); err != nil { log.Fatal().Err(err).Msg("Connection failed") } + defer func() { - if err := executor.Cleanup(ctx, cleanCfg); err != nil { - log.Error().Err(err).Msg("Cleanup failed") + closeErr := rpcClient.Close(ctx) + if closeErr != nil { + log.Error().Err(closeErr).Msg("Failed to close connection") } }() - if err := executor.Exec(ctx, execCfg); err != nil { - log.Error().Err(err).Msg("Execution failed") + + if err = wmiCall.Init(ctx); err != nil { + log.Error().Err(err).Msg("Module initialization failed") + returnCode = 2 + return } + + out, err := wmiCall.Call(ctx) + if err != nil { + log.Error().Err(err).Msg("Call failed") + returnCode = 4 + return + } + fmt.Println(string(out)) }, } - wmiProcessCmd = &cobra.Command{ + wmiProcCmd = &cobra.Command{ Use: "proc", Short: "Start a Windows process", Long: `Description: @@ -112,41 +121,41 @@ References: References: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/create-method-in-class-win32-process `, - Args: needs(needsTarget("cifs"), needsRpcTarget("cifs")), + Args: args(argsOutput("smb"), argsRpcClient("host")), + Run: func(cmd *cobra.Command, args []string) { + var err error + + wmiProc.Client = &rpcClient + wmiProc.IO = exec + + ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO())) + + var writer io.WriteCloser + + if outputPath == "-" { + writer = os.Stdout - executor := wmiexec.Module{} - cleanCfg := &exec.CleanupConfig{} // TODO - connCfg := &exec.ConnectionConfig{ - ConnectionMethod: exec.ConnectionMethodDCE, - ConnectionMethodConfig: dceConfig, + } else if outputPath != "" { + + if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil { + log.Fatal().Err(err).Msg("Failed to open output file") + } + defer writer.Close() } - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: wmiexec.MethodProcess, - - ExecutionMethodConfig: wmiexec.MethodProcessConfig{ - Command: command, - WorkingDirectory: workingDirectory, - }, + + if err = goexec.ExecuteCleanMethod(ctx, &wmiProc, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") } - ctx = log.With(). - Str("module", "wmi"). - Str("method", "proc"). - Logger().WithContext(ctx) + if outputPath != "" { + if reader, err := wmiProc.GetOutput(ctx); err == nil { + _, err = io.Copy(writer, reader) - if err := executor.Connect(ctx, creds, target, connCfg); err != nil { - log.Fatal().Err(err).Msg("Connection failed") - } - defer func() { - if err := executor.Cleanup(ctx, cleanCfg); err != nil { - log.Error().Err(err).Msg("Cleanup failed") + } else { + log.Error().Err(err).Msg("Failed to get process execution output") + returnCode = 2 } - }() - if err := executor.Exec(ctx, execCfg); err != nil { - log.Error().Err(err).Msg("Execution failed") } }, } |