From 55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 16 Apr 2025 12:11:58 -0500 Subject: rewrote everything lol --- Dockerfile | 1 + TODO.md | 4 +- cmd/args.go | 126 +++++++++++++++ cmd/dcom.go | 102 +++++++------ cmd/root.go | 172 ++++++++++----------- cmd/rpc.go | 86 ----------- cmd/scmr.go | 243 ++++++++++++++--------------- cmd/tsch.go | 303 +++++++++++++++++-------------------- cmd/wmi.go | 169 +++++++++++---------- go.mod | 2 +- go.sum | 2 + internal/client/dce/dce.go | 74 --------- internal/exec/dcom/dcom.go | 65 -------- internal/exec/dcom/exec.go | 181 ---------------------- internal/exec/dcom/module.go | 21 --- internal/exec/exec.go | 54 ------- internal/exec/scmr/exec.go | 345 ------------------------------------------ internal/exec/scmr/module.go | 44 ------ internal/exec/scmr/service.go | 25 --- internal/exec/tsch/exec.go | 189 ----------------------- internal/exec/tsch/module.go | 44 ------ internal/exec/tsch/task.go | 85 ----------- internal/exec/tsch/tsch.go | 141 ----------------- internal/exec/wmi/exec.go | 166 -------------------- internal/exec/wmi/module.go | 33 ---- internal/exec/wmi/wmi.go | 26 ---- internal/util/util.go | 5 + pkg/goexec/auth.go | 11 ++ pkg/goexec/clean.go | 31 ++++ pkg/goexec/client.go | 26 ++++ pkg/goexec/dce/client.go | 89 +++++++++++ pkg/goexec/dce/default.go | 5 + pkg/goexec/dce/options.go | 108 +++++++++++++ pkg/goexec/dcom/dcom.go | 34 +++++ pkg/goexec/dcom/mmc.go | 50 ++++++ pkg/goexec/dcom/module.go | 111 ++++++++++++++ pkg/goexec/dcom/util.go | 85 +++++++++++ pkg/goexec/exec.go | 14 ++ pkg/goexec/io.go | 87 +++++++++++ pkg/goexec/method.go | 72 +++++++++ pkg/goexec/module.go | 25 +++ pkg/goexec/proxy.go | 33 ++++ pkg/goexec/scmr/change.go | 144 ++++++++++++++++++ pkg/goexec/scmr/create.go | 125 +++++++++++++++ pkg/goexec/scmr/delete.go | 46 ++++++ pkg/goexec/scmr/module.go | 131 ++++++++++++++++ pkg/goexec/scmr/scmr.go | 19 +++ pkg/goexec/smb/client.go | 111 ++++++++++++++ pkg/goexec/smb/default.go | 10 ++ pkg/goexec/smb/options.go | 98 ++++++++++++ pkg/goexec/smb/output.go | 63 ++++++++ pkg/goexec/tsch/create.go | 113 ++++++++++++++ pkg/goexec/tsch/demand.go | 91 +++++++++++ pkg/goexec/tsch/module.go | 173 +++++++++++++++++++++ pkg/goexec/tsch/tsch.go | 166 ++++++++++++++++++++ pkg/goexec/wmi/call.go | 31 ++++ pkg/goexec/wmi/module.go | 140 +++++++++++++++++ pkg/goexec/wmi/proc.go | 64 ++++++++ pkg/goexec/wmi/wmi.go | 16 ++ 59 files changed, 2945 insertions(+), 2085 deletions(-) create mode 100644 cmd/args.go delete mode 100644 cmd/rpc.go delete mode 100644 internal/client/dce/dce.go delete mode 100644 internal/exec/dcom/dcom.go delete mode 100644 internal/exec/dcom/exec.go delete mode 100644 internal/exec/dcom/module.go delete mode 100644 internal/exec/exec.go delete mode 100644 internal/exec/scmr/exec.go delete mode 100644 internal/exec/scmr/module.go delete mode 100644 internal/exec/scmr/service.go delete mode 100644 internal/exec/tsch/exec.go delete mode 100644 internal/exec/tsch/module.go delete mode 100644 internal/exec/tsch/task.go delete mode 100644 internal/exec/tsch/tsch.go delete mode 100644 internal/exec/wmi/exec.go delete mode 100644 internal/exec/wmi/module.go delete mode 100644 internal/exec/wmi/wmi.go create mode 100644 pkg/goexec/auth.go create mode 100644 pkg/goexec/clean.go create mode 100644 pkg/goexec/client.go create mode 100644 pkg/goexec/dce/client.go create mode 100644 pkg/goexec/dce/default.go create mode 100644 pkg/goexec/dce/options.go create mode 100644 pkg/goexec/dcom/dcom.go create mode 100644 pkg/goexec/dcom/mmc.go create mode 100644 pkg/goexec/dcom/module.go create mode 100644 pkg/goexec/dcom/util.go create mode 100644 pkg/goexec/exec.go create mode 100644 pkg/goexec/io.go create mode 100644 pkg/goexec/method.go create mode 100644 pkg/goexec/module.go create mode 100644 pkg/goexec/proxy.go create mode 100644 pkg/goexec/scmr/change.go create mode 100644 pkg/goexec/scmr/create.go create mode 100644 pkg/goexec/scmr/delete.go create mode 100644 pkg/goexec/scmr/module.go create mode 100644 pkg/goexec/scmr/scmr.go create mode 100644 pkg/goexec/smb/client.go create mode 100644 pkg/goexec/smb/default.go create mode 100644 pkg/goexec/smb/options.go create mode 100644 pkg/goexec/smb/output.go create mode 100644 pkg/goexec/tsch/create.go create mode 100644 pkg/goexec/tsch/demand.go create mode 100644 pkg/goexec/tsch/module.go create mode 100644 pkg/goexec/tsch/tsch.go create mode 100644 pkg/goexec/wmi/call.go create mode 100644 pkg/goexec/wmi/module.go create mode 100644 pkg/goexec/wmi/proc.go create mode 100644 pkg/goexec/wmi/wmi.go diff --git a/Dockerfile b/Dockerfile index 6de9006..c28cd30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /go/src/ COPY cmd/ cmd/ COPY internal/ internal/ +COPY pkg/ pkg/ COPY main.go go.mod go.sum ./ ENV CGO_ENABLED=0 diff --git a/TODO.md b/TODO.md index 5800c7b..a73357a 100644 --- a/TODO.md +++ b/TODO.md @@ -8,13 +8,14 @@ - [X] Session hijacking - [ ] Add command to tsch - update task if it already exists. See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167 (`flags` argument) - [ ] Add more trigger types +- [ ] Generate random name/path ### SCMR - [X] Clean up SCMR module - [X] add dynamic string binding support - [X] general clean up. Use TSCH & WMI as reference -- [ ] Output +- [X] Output - [ ] Fix SCMR `change` method so that dependencies field isn't permanently overwritten ### DCOM @@ -41,6 +42,7 @@ ## Resolve Eventually +- [ ] Packet stub encryption for ncacn_ip_tcp doesn't appear to be working... - [ ] `--shell` option - [ ] Add Go tests - [ ] ability to specify multiple targets 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 + } } }, } diff --git a/cmd/wmi.go b/cmd/wmi.go index 59096ec..196ff82 100644 --- a/cmd/wmi.go +++ b/cmd/wmi.go @@ -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") } }, } diff --git a/go.mod b/go.mod index 793956b..7224fc0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/FalconOpsLLC/goexec go 1.24.1 require ( - github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877 + github.com/RedTeamPentesting/adauth v0.2.0 github.com/google/uuid v1.6.0 github.com/oiweiwei/go-msrpc v1.2.5 github.com/rs/zerolog v1.34.0 diff --git a/go.sum b/go.sum index b2629fd..d611052 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877 h1:n5V0EER+EvvmUZitR5zGFUMyoIHnI12SWFMciI7kh70= github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877/go.mod h1:iHf/fY7CueB7qLHZ5YgTZXvrVCSLJy4+tAifOSNLAFQ= +github.com/RedTeamPentesting/adauth v0.2.0 h1:pNb4xcEd/oG/JY8aXkBm8Y0veVaUA/CsKFhgEgNCPik= +github.com/RedTeamPentesting/adauth v0.2.0/go.mod h1:AhWZEgl98YkKEta6kXTOSocQh2SbJJZg4nKqF+uY4bc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/client/dce/dce.go b/internal/client/dce/dce.go deleted file mode 100644 index 0230c2a..0000000 --- a/internal/client/dce/dce.go +++ /dev/null @@ -1,74 +0,0 @@ -package dce - -import ( - "context" - "errors" - "fmt" - "github.com/RedTeamPentesting/adauth" - "github.com/RedTeamPentesting/adauth/dcerpcauth" - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/midl/uuid" - "github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3" - "github.com/oiweiwei/go-msrpc/ssp/gssapi" - "github.com/rs/zerolog" -) - -type ConnectionMethodDCEConfig struct { - NoEpm bool // NoEpm disables EPM - EpmAuto bool // EpmAuto will find any suitable endpoint, without any filter - Endpoint *dcerpc.StringBinding // Endpoint is the explicit endpoint passed to dcerpc.WithEndpoint for use without EPM - EpmFilter *dcerpc.StringBinding // EpmFilter is the rough filter used to pick an EPM endpoint - Options []dcerpc.Option // Options stores the options that will be passed to all dialers - DceOptions []dcerpc.Option // DceOptions are the options passed to dcerpc.Dial - EpmOptions []dcerpc.Option // EpmOptions are the options passed to epm.EndpointMapper - Resource string // Resource stores the target network resource (usually for DCOM) -} - -func (cfg *ConnectionMethodDCEConfig) GetDce(ctx context.Context, cred *adauth.Credential, target *adauth.Target, endpoint, object string, arbOpts ...dcerpc.Option) (cc dcerpc.Conn, err error) { - - log := zerolog.Ctx(ctx).With(). - Str("client", "DCERPC").Logger() - - // Mandatory logging - dceOpts := append(append(cfg.Options, arbOpts...), append(cfg.DceOptions, dcerpc.WithLogger(log))...) - epmOpts := append(cfg.Options, append(cfg.EpmOptions, dcerpc.WithLogger(log))...) - - ctx = gssapi.NewSecurityContext(ctx) - auth, err := dcerpcauth.AuthenticationOptions(ctx, cred, target, &dcerpcauth.Options{}) - if err != nil { - log.Error().Err(err).Msg("Failed to parse authentication options") - return nil, fmt.Errorf("parse auth options: %w", err) - } - addr := target.AddressWithoutPort() - log = log.With().Str("address", addr).Logger() - - if object != "" { - if id, err := uuid.Parse(object); err != nil { - log.Error().Err(err).Msg("Failed to parse input object UUID") - } else { - dceOpts = append(dceOpts, dcerpc.WithObjectUUID(id)) - } - } - if cfg.Endpoint != nil { - dceOpts = append(dceOpts, dcerpc.WithEndpoint(cfg.Endpoint.String())) - log.Debug().Str("binding", cfg.Endpoint.String()).Msg("Using endpoint") - - } else if !cfg.NoEpm { - dceOpts = append(dceOpts, epm.EndpointMapper(ctx, addr, append(epmOpts, auth...)...)) - log.Debug().Msg("Using endpoint mapper") - - if cfg.EpmFilter != nil { - dceOpts = append(dceOpts, dcerpc.WithEndpoint(cfg.EpmFilter.String())) - log.Debug().Str("filter", cfg.EpmFilter.String()).Msg("Using endpoint filter") - } - } else if endpoint != "" { - dceOpts = append(dceOpts, dcerpc.WithEndpoint(endpoint)) - log.Debug().Str("endpoint", endpoint).Msg("Using default endpoint") - - } else { - log.Err(err).Msg("Invalid DCE connection options") - return nil, errors.New("get DCE: invalid connection options") - } - - return dcerpc.Dial(ctx, target.AddressWithoutPort(), append(dceOpts, auth...)...) -} diff --git a/internal/exec/dcom/dcom.go b/internal/exec/dcom/dcom.go deleted file mode 100644 index b96bbc8..0000000 --- a/internal/exec/dcom/dcom.go +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index a34baf4..0000000 --- a/internal/exec/dcom/exec.go +++ /dev/null @@ -1,181 +0,0 @@ -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") - 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 errors.New("invalid configuration for DCE connection method") - } else { - opts := []dcerpc.Option{dcerpc.WithSign(), dcerpc.WithSecurityLevel(0)} - - // 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")...) // TODO - 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 deleted file mode 100644 index bbd50b5..0000000 --- a/internal/exec/dcom/module.go +++ /dev/null @@ -1,21 +0,0 @@ -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/exec.go b/internal/exec/exec.go deleted file mode 100644 index db83d91..0000000 --- a/internal/exec/exec.go +++ /dev/null @@ -1,54 +0,0 @@ -package exec - -import ( - "context" - "fmt" - "github.com/RedTeamPentesting/adauth" - "strings" -) - -const ( - ConnectionMethodDCE = "dcerpc" -) - -type ConnectionConfig struct { - ConnectionMethod string - ConnectionMethodConfig interface{} -} - -type CleanupConfig struct { - CleanupMethod string - CleanupMethodConfig interface{} -} - -type ExecutionConfig struct { - ExecutableName string // ExecutableName represents the name of the executable; i.e. "notepad.exe", "calc" - ExecutablePath string // ExecutablePath represents the full path to the executable; i.e. `C:\Windows\explorer.exe` - ExecutableArgs string // ExecutableArgs represents the arguments to be passed to the executable during execution; i.e. "/C whoami" - - ExecutionMethod string // ExecutionMethod represents the specific execution strategy used by the module. - ExecutionMethodConfig interface{} - ReturnOutput bool -} - -type ShellConfig struct { - ShellName string // ShellName specifies the name of the shell executable; i.e. "cmd.exe", "powershell" - ShellPath string // ShellPath is the full Windows path to the shell executable; i.e. `C:\Windows\System32\cmd.exe` -} - -type Module interface { - Connect(context.Context, *adauth.Credential, *adauth.Target, *ConnectionConfig) error - Exec(context.Context, *ExecutionConfig) error - Cleanup(context.Context, *CleanupConfig) error -} - -func (cfg *ExecutionConfig) GetRawCommand() string { - executable := cfg.ExecutablePath - if strings.Contains(executable, " ") { - executable = fmt.Sprintf("%q", executable) - } - if cfg.ExecutableArgs != "" { - return executable + " " + cfg.ExecutableArgs - } - return executable -} diff --git a/internal/exec/scmr/exec.go b/internal/exec/scmr/exec.go deleted file mode 100644 index 9d15dd5..0000000 --- a/internal/exec/scmr/exec.go +++ /dev/null @@ -1,345 +0,0 @@ -package scmrexec - -import ( - "context" - "errors" - "fmt" - "github.com/FalconOpsLLC/goexec/internal/client/dce" - "github.com/FalconOpsLLC/goexec/internal/exec" - "github.com/FalconOpsLLC/goexec/internal/util" - "github.com/FalconOpsLLC/goexec/internal/windows" - "github.com/RedTeamPentesting/adauth" - "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" - "github.com/rs/zerolog" -) - -const ( - ScmrDefaultEndpoint = "ncacn_np:[svcctl]" - ScmrDefaultObject = "367ABB81-9844-35F1-AD32-98F038001003" -) - -func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("func", "Connect").Logger() - - if ccfg.ConnectionMethod == exec.ConnectionMethodDCE { - 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") - mod.hostname = util.RandomHostname() - err = nil - } - connect := func(ctx context.Context) error { - // Create DCE connection - if mod.dce, err = cfg.GetDce(ctx, creds, target, ScmrDefaultEndpoint, ScmrDefaultObject); err != nil { - log.Error().Err(err).Msg("Failed to initialize DCE dialer") - return fmt.Errorf("create DCE dialer: %w", err) - } - log.Info().Msg("DCE dialer initialized") - - // Create SVCCTL client - mod.ctl, err = svcctl.NewSvcctlClient(ctx, mod.dce) - if err != nil { - log.Error().Err(err).Msg("Failed to initialize SCMR client") - return fmt.Errorf("init SCMR client: %w", err) - } - log.Info().Msg("DCE connection successful") - return nil - } - mod.reconnect = func(c context.Context) error { - mod.dce = nil - mod.ctl = nil - return connect(c) - } - return connect(ctx) - } - } else { - return errors.New("unsupported connection method") - } -} - -func (mod *Module) Cleanup(ctx context.Context, ccfg *exec.CleanupConfig) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("method", ccfg.CleanupMethod). - Str("func", "Cleanup").Logger() - - if len(mod.services) == 0 { - if cfg, ok := ccfg.CleanupMethodConfig.(CleanupMethodDeleteConfig); ok && len(cfg.ServiceNames) > 0 { - for _, svcName := range cfg.ServiceNames { - if svcName != "" { - mod.services = append(mod.services, remoteService{name: svcName}) - } - } - } - } - if mod.dce == nil || mod.ctl == nil { - // Try to reconnect - if err := mod.reconnect(ctx); err != nil { - log.Error().Err(err).Msg("Reconnect failed") - return err - } - log.Info().Msg("Reconnect successful") - } - if mod.scm == nil { - // Open a handle to SCM (again) - if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{ - MachineName: util.CheckNullString(mod.hostname), - DatabaseName: "ServicesActive\x00", - DesiredAccess: ServiceAllAccess, // TODO: Replace - }); err != nil { - log.Error().Err(err).Msg("Failed to open an SCM handle") - return err - } else { - mod.scm = resp.SCM - log.Info().Msg("Opened an SCM handle") - } - } - - for _, rsvc := range mod.services { - log = log.With().Str("service", rsvc.name).Logger() - - if rsvc.handle == nil { - // Open a handle to the service in question - if or, err := mod.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{ - ServiceManager: mod.scm, - ServiceName: rsvc.name, - DesiredAccess: windows.SERVICE_DELETE | windows.SERVICE_CHANGE_CONFIG, - }); err != nil { - log.Error().Err(err).Msg("Failed to open a service handle") - continue - } else { - rsvc.handle = or.Service - } - log.Info().Msg("Service handle opened") - } - if ccfg.CleanupMethod == CleanupMethodDelete { - // Delete the service - if _, err = mod.ctl.DeleteService(ctx, &svcctl.DeleteServiceRequest{Service: rsvc.handle}); err != nil { - log.Error().Err(err).Msg("Failed to delete service") - continue - } - log.Info().Msg("Service deleted successfully") - - } else if ccfg.CleanupMethod == CleanupMethodRevert { - // Revert the service configuration & state - log.Info().Msg("Attempting to revert service configuration") - if _, err = mod.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{ - Service: rsvc.handle, - //Dependencies: []byte(rsvc.originalConfig.Dependencies), // TODO: ensure this works - ServiceType: rsvc.originalConfig.ServiceType, - StartType: rsvc.originalConfig.StartType, - ErrorControl: rsvc.originalConfig.ErrorControl, - BinaryPathName: rsvc.originalConfig.BinaryPathName, - LoadOrderGroup: rsvc.originalConfig.LoadOrderGroup, - ServiceStartName: rsvc.originalConfig.ServiceStartName, - DisplayName: rsvc.originalConfig.DisplayName, - TagID: rsvc.originalConfig.TagID, - }); err != nil { - log.Error().Err(err).Msg("Failed to revert service configuration") - continue - } - log.Info().Msg("Service configuration reverted") - } - if _, err = mod.ctl.CloseService(ctx, &svcctl.CloseServiceRequest{ServiceObject: rsvc.handle}); err != nil { - log.Warn().Err(err).Msg("Failed to close service handle") - return nil - } - log.Info().Msg("Closed service handle") - } - 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 == MethodCreate { - if cfg, ok := ecfg.ExecutionMethodConfig.(MethodCreateConfig); !ok { - return errors.New("invalid configuration") - - } else { - svc := remoteService{ - name: cfg.ServiceName, - } - // Open a handle to SCM - if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{ - MachineName: util.CheckNullString(mod.hostname), - DatabaseName: "ServicesActive\x00", - DesiredAccess: ServiceAllAccess, // TODO: Replace - }); err != nil { - log.Debug().Err(err).Msg("Failed to open SCM handle") - return fmt.Errorf("open SCM handle: %w", err) - } else { - mod.scm = resp.SCM - log.Info().Msg("Opened SCM handle") - } - // Create service - serviceName := util.RandomStringIfBlank(svc.name) - resp, err := mod.ctl.CreateServiceW(ctx, &svcctl.CreateServiceWRequest{ - ServiceManager: mod.scm, - ServiceName: serviceName, - DisplayName: util.RandomStringIfBlank(cfg.DisplayName), - BinaryPathName: ecfg.GetRawCommand(), - ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, - StartType: windows.SERVICE_DEMAND_START, - DesiredAccess: ServiceAllAccess, // TODO: Replace - }) - if err != nil || resp == nil || resp.Return != 0 { - log.Error().Err(err).Msg("Failed to create service") - return fmt.Errorf("create service: %w", err) - } - defer func() { - mod.services = append(mod.services, svc) - }() - svc.handle = resp.Service - - log = log.With(). - Str("service", serviceName).Logger() - log.Info().Msg("Service created") - - // Start the service - sr, err := mod.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle}) - if err != nil { - - if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case) - log.Warn().Err(err).Msg("Service execution deadline exceeded") - // Connection closes, so we nullify the client variables and handles - mod.dce = nil - mod.ctl = nil - mod.scm = nil - svc.handle = nil - - } else if sr != nil && sr.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT { // Check for request timeout - log.Info().Msg("Received request timeout. Execution was likely successful") - - } else { - log.Error().Err(err).Msg("Failed to start service") - return fmt.Errorf("start service: %w", err) - } - // Inform the caller that execution was likely successful despite error - err = nil - } else { - log.Info().Msg("Started service") - } - } - } else if ecfg.ExecutionMethod == MethodChange { - if cfg, ok := ecfg.ExecutionMethodConfig.(MethodChangeConfig); !ok { - return errors.New("invalid configuration") - - } else { - svc := remoteService{ - name: cfg.ServiceName, - } - - // Open a handle to SCM - if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{ - MachineName: util.CheckNullString(mod.hostname), - DatabaseName: "ServicesActive\x00", - DesiredAccess: ServiceAllAccess, // TODO: Replace - }); err != nil { - log.Debug().Err(err).Msg("Failed to open SCM handle") - return fmt.Errorf("open SCM handle: %w", err) - } else { - mod.scm = resp.SCM - log.Info().Msg("Opened SCM handle") - } - - // Open a handle to the desired service - if resp, err := mod.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{ - ServiceManager: mod.scm, - ServiceName: svc.name, - DesiredAccess: ServiceAllAccess, // TODO: Replace - }); err != nil { - log.Error().Err(err).Msg("Failed to open service handle") - return fmt.Errorf("open service: %w", err) - } else { - svc.handle = resp.Service - } - - // Note original service status - if resp, err := mod.ctl.QueryServiceStatus(ctx, &svcctl.QueryServiceStatusRequest{ - Service: svc.handle, - }); err != nil { - log.Warn().Err(err).Msg("Failed to get service status") - } else { - svc.originalState = resp.ServiceStatus - } - - // Note original service configuration - if resp, err := mod.ctl.QueryServiceConfigW(ctx, &svcctl.QueryServiceConfigWRequest{ - Service: svc.handle, - BufferLength: 8 * 1024, - }); err != nil { - log.Error().Err(err).Msg("Failed to fetch service configuration") - return fmt.Errorf("get service config: %w", err) - } else { - log.Info().Str("binaryPath", resp.ServiceConfig.BinaryPathName).Msg("Fetched original service configuration") - svc.originalConfig = resp.ServiceConfig - } - - // Stop service if its running - if svc.originalState == nil || svc.originalState.CurrentState != windows.SERVICE_STOPPED { - if resp, err := mod.ctl.ControlService(ctx, &svcctl.ControlServiceRequest{ - Service: svc.handle, - Control: windows.SERVICE_STOPPED, - }); err != nil { - if resp != nil && resp.Return == windows.ERROR_SERVICE_NOT_ACTIVE { - log.Info().Msg("Service is already stopped") - } else { - log.Error().Err(err).Msg("Failed to stop service") - } - } else { - log.Info().Msg("Service stopped") - } - } - defer func() { - mod.services = append(mod.services, svc) - }() - // Change service configuration - if _, err = mod.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{ - Service: svc.handle, - BinaryPathName: ecfg.GetRawCommand(), - //Dependencies: []byte(svc.originalConfig.Dependencies), // TODO: ensure this works - ServiceType: svc.originalConfig.ServiceType, - StartType: windows.SERVICE_DEMAND_START, - ErrorControl: svc.originalConfig.ErrorControl, - LoadOrderGroup: svc.originalConfig.LoadOrderGroup, - ServiceStartName: svc.originalConfig.ServiceStartName, - DisplayName: svc.originalConfig.DisplayName, - TagID: svc.originalConfig.TagID, - }); err != nil { - log.Error().Err(err).Msg("Failed to change service configuration") - return fmt.Errorf("change service configuration: %w", err) - } - log.Info().Msg("Successfully altered service configuration") - - if !cfg.NoStart { - if resp, err := mod.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle}); err != nil { - - if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case) - log.Warn().Err(err).Msg("Service execution deadline exceeded") - // Connection closes, so we nullify the client variables and handles - mod.dce = nil - mod.ctl = nil - mod.scm = nil - svc.handle = nil - - } else if resp != nil && resp.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT { // Check for request timeout - log.Info().Err(err).Msg("Received request timeout. Execution was likely successful") - } else { - log.Error().Err(err).Msg("Failed to start service") - return fmt.Errorf("start service: %w", err) - } - } - } - } - } - return -} diff --git a/internal/exec/scmr/module.go b/internal/exec/scmr/module.go deleted file mode 100644 index 0372668..0000000 --- a/internal/exec/scmr/module.go +++ /dev/null @@ -1,44 +0,0 @@ -package scmrexec - -import ( - "context" - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" -) - -const ( - MethodCreate = "create" - MethodChange = "change" - - CleanupMethodDelete = "delete" - CleanupMethodRevert = "revert" -) - -type Module struct { - hostname string // The target hostname - dce dcerpc.Conn - reconnect func(context.Context) error - - ctl svcctl.SvcctlClient - scm *svcctl.Handle - services []remoteService -} - -type MethodCreateConfig struct { - NoDelete bool - ServiceName string - DisplayName string - ServiceType uint32 - StartType uint32 -} - -type MethodChangeConfig struct { - NoStart bool - ServiceName string -} - -type CleanupMethodDeleteConfig struct { - ServiceNames []string -} - -type CleanupMethodRevertConfig struct{} diff --git a/internal/exec/scmr/service.go b/internal/exec/scmr/service.go deleted file mode 100644 index 49c7506..0000000 --- a/internal/exec/scmr/service.go +++ /dev/null @@ -1,25 +0,0 @@ -package scmrexec - -import ( - "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 -) - -type remoteService struct { - name string - handle *svcctl.Handle - originalConfig *svcctl.QueryServiceConfigW - originalState *svcctl.ServiceStatus -} - -func (mod *Module) parseServiceDependencies(ctx context.Context) (err error) { - return nil // TODO -} diff --git a/internal/exec/tsch/exec.go b/internal/exec/tsch/exec.go deleted file mode 100644 index 49a2dc2..0000000 --- a/internal/exec/tsch/exec.go +++ /dev/null @@ -1,189 +0,0 @@ -package tschexec - -import ( - "context" - "errors" - "fmt" - "github.com/FalconOpsLLC/goexec/internal/client/dce" - "github.com/FalconOpsLLC/goexec/internal/exec" - "github.com/FalconOpsLLC/goexec/internal/util" - "github.com/RedTeamPentesting/adauth" - "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" - "github.com/rs/zerolog" - "time" -) - -const ( - TschDefaultEndpoint = "ncacn_np:[atsvc]" - TschDefaultObject = "86D35949-83C9-4044-B424-DB363231FD0C" -) - -// Connect to the target & initialize DCE & TSCH clients -func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("func", "Connect").Logger() - - if ccfg.ConnectionMethod == exec.ConnectionMethodDCE { - if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok { - return fmt.Errorf("invalid configuration for DCE connection method") - } else { - // Create DCERPC dialer - mod.dce, err = cfg.GetDce(ctx, creds, target, TschDefaultEndpoint, TschDefaultObject) - if err != nil { - log.Error().Err(err).Msg("Failed to create DCERPC dialer") - return fmt.Errorf("create DCERPC dialer: %w", err) - } - // Create ITaskSchedulerService client - mod.tsch, err = itaskschedulerservice.NewTaskSchedulerServiceClient(ctx, mod.dce) - if err != nil { - log.Error().Err(err).Msg("Failed to initialize TSCH client") - return fmt.Errorf("init TSCH client: %w", err) - } - log.Info().Msg("DCE connection successful") - } - } else { - return errors.New("unsupported connection method") - } - return -} - -func (mod *Module) Cleanup(ctx context.Context, ccfg *exec.CleanupConfig) (err error) { - log := zerolog.Ctx(ctx).With(). - Str("method", ccfg.CleanupMethod). - Str("func", "Cleanup").Logger() - - if ccfg.CleanupMethod == MethodDelete { - if cfg, ok := ccfg.CleanupMethodConfig.(MethodDeleteConfig); !ok { - return errors.New("invalid configuration") - - } else { - log = log.With().Str("task", cfg.TaskPath).Logger() - log.Info().Msg("Manually deleting task") - - if err = mod.deleteTask(ctx, cfg.TaskPath); err == nil { - log.Info().Msg("Task deleted successfully") - } - } - } else if ccfg.CleanupMethod == "" { - return nil - } else { - return fmt.Errorf("unsupported cleanup method") - } - 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 == MethodRegister { - if cfg, ok := ecfg.ExecutionMethodConfig.(MethodRegisterConfig); !ok { - return errors.New("invalid configuration") - - } else { - startTime := time.Now().UTC().Add(cfg.StartDelay) - stopTime := startTime.Add(cfg.StopDelay) - - tr := taskTimeTrigger{ - StartBoundary: startTime.Format(TaskXMLDurationFormat), - Enabled: true, - } - tk := newTask(nil, nil, triggers{TimeTriggers: []taskTimeTrigger{tr}}, ecfg.ExecutableName, ecfg.ExecutableArgs) - - if !cfg.NoDelete && !cfg.CallDelete { - if cfg.StopDelay == 0 { - cfg.StopDelay = time.Second - } - tk.Settings.DeleteExpiredTaskAfter = xmlDuration(cfg.DeleteDelay) - tk.Triggers.TimeTriggers[0].EndBoundary = stopTime.Format(TaskXMLDurationFormat) - } - taskPath := cfg.TaskPath - if taskPath == "" { - log.Debug().Msg("Task path not defined. Using random path") - taskPath = `\` + util.RandomString() - } - // The taskPath is changed here to the *actual path returned by SchRpcRegisterTask - taskPath, err = mod.registerTask(ctx, *tk, taskPath) - if err != nil { - return fmt.Errorf("call registerTask: %w", err) - } - - if !cfg.NoDelete { - if cfg.CallDelete { - defer mod.deleteTask(ctx, taskPath) - - log.Info().Dur("ms", cfg.StartDelay).Msg("Waiting for task to run") - select { - case <-ctx.Done(): - log.Warn().Msg("Cancelling execution") - return err - case <-time.After(cfg.StartDelay + time.Second): // + one second for good measure - for { - if stat, err := mod.tsch.GetLastRunInfo(ctx, &itaskschedulerservice.GetLastRunInfoRequest{Path: taskPath}); err != nil { - log.Warn().Err(err).Msg("Failed to get last run info. Assuming task was executed") - break - } else if stat.LastRuntime.AsTime().IsZero() { - log.Warn().Msg("Task was not yet run. Waiting 10 additional seconds") - time.Sleep(10 * time.Second) - } else { - break - } - } - break - } - } else { - log.Info().Time("when", stopTime).Msg("Task is scheduled to delete") - } - } - } - } else if ecfg.ExecutionMethod == MethodDemand { - if cfg, ok := ecfg.ExecutionMethodConfig.(MethodDemandConfig); !ok { - return errors.New("invalid configuration") - - } else { - taskPath := cfg.TaskPath - - if taskPath == "" { - log.Debug().Msg("Task path not defined. Using random path") - taskPath = `\` + util.RandomString() - } - - st := newSettings(true, true, false) - tk := newTask(st, nil, triggers{}, ecfg.ExecutableName, ecfg.ExecutableArgs) - - // The taskPath is changed here to the *actual path returned by SchRpcRegisterTask - taskPath, err = mod.registerTask(ctx, *tk, taskPath) - if err != nil { - return fmt.Errorf("call registerTask: %w", err) - } - - if !cfg.NoDelete { - defer mod.deleteTask(ctx, taskPath) - } - - var flags uint32 - - if cfg.SessionId != 0 { - flags |= 4 - } - _, err := mod.tsch.Run(ctx, &itaskschedulerservice.RunRequest{ - Path: taskPath, - Flags: flags, - SessionID: cfg.SessionId, - }) - if err != nil { - log.Error().Str("task", taskPath).Err(err).Msg("Failed to run task") - return fmt.Errorf("force run task: %w", err) - } - log.Info().Str("task", taskPath).Msg("Started task") - } - - } else { - return fmt.Errorf("method '%s' not implemented", ecfg.ExecutionMethod) - } - - return nil -} diff --git a/internal/exec/tsch/module.go b/internal/exec/tsch/module.go deleted file mode 100644 index 33c0723..0000000 --- a/internal/exec/tsch/module.go +++ /dev/null @@ -1,44 +0,0 @@ -package tschexec - -import ( - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" - "time" -) - -type Module struct { - // dce holds the working DCE connection interface - dce dcerpc.Conn - // tsch holds the ITaskSchedulerService client - tsch itaskschedulerservice.TaskSchedulerServiceClient -} - -type MethodRegisterConfig struct { - NoDelete bool - CallDelete bool - TaskPath string - StartDelay time.Duration - StopDelay time.Duration - DeleteDelay time.Duration -} - -type MethodDemandConfig struct { - NoDelete bool - CallDelete bool - SessionId uint32 - TaskName string - TaskPath string - StopDelay time.Duration - DeleteDelay time.Duration -} - -type MethodDeleteConfig struct { - TaskPath string -} - -const ( - MethodRegister string = "register" - MethodDemand string = "demand" - MethodDelete string = "delete" - MethodChange string = "update" -) diff --git a/internal/exec/tsch/task.go b/internal/exec/tsch/task.go deleted file mode 100644 index 928d029..0000000 --- a/internal/exec/tsch/task.go +++ /dev/null @@ -1,85 +0,0 @@ -package tschexec - -import ( - "fmt" - "regexp" - "time" -) - -var ( - TaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`) - TaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`) -) - -// newSettings just creates a settings instance with the necessary values + a few dynamic ones -func newSettings(terminate, onDemand, startWhenAvailable bool) *settings { - return &settings{ - MultipleInstancesPolicy: "IgnoreNew", - AllowHardTerminate: terminate, - IdleSettings: idleSettings{ - StopOnIdleEnd: true, - RestartOnIdle: false, - }, - AllowStartOnDemand: onDemand, - Enabled: true, - Hidden: true, - Priority: 7, // a pretty standard value for scheduled tasks - StartWhenAvailable: startWhenAvailable, - } -} - -// newTask creates a task with any static values filled -func newTask(se *settings, pr []principal, tr triggers, cmd, args string) *task { - if se == nil { - se = newSettings(true, true, false) - } - if pr == nil || len(pr) == 0 { - pr = []principal{ - { - ID: "1", - UserID: "S-1-5-18", - RunLevel: "HighestAvailable", - }, - } - } - return &task{ - TaskVersion: "1.2", - TaskNamespace: "http://schemas.microsoft.com/windows/2004/02/mit/task", - Triggers: tr, - Principals: principals{Principals: pr}, - Settings: *se, - Actions: actions{ - Context: pr[0].ID, - Exec: []actionExec{ - { - Command: cmd, - Arguments: args, - }, - }, - }, - } -} - -// xmlDuration is a *very* simple implementation of xs:duration - only accepts +seconds -func xmlDuration(dur time.Duration) string { - if s := int(dur.Seconds()); s >= 0 { - return fmt.Sprintf(`PT%dS`, s) - } - return `PT0S` -} - -// ValidateTaskName will validate the provided task name according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1 -func ValidateTaskName(taskName string) error { - if !TaskNameRegex.MatchString(taskName) { - return fmt.Errorf("invalid task name: %s", taskName) - } - return nil -} - -// ValidateTaskPath will validate the provided task path according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1 -func ValidateTaskPath(taskPath string) error { - if !TaskPathRegex.MatchString(taskPath) { - return fmt.Errorf("invalid task path: %s", taskPath) - } - return nil -} diff --git a/internal/exec/tsch/tsch.go b/internal/exec/tsch/tsch.go deleted file mode 100644 index d47e513..0000000 --- a/internal/exec/tsch/tsch.go +++ /dev/null @@ -1,141 +0,0 @@ -package tschexec - -import ( - "context" - "encoding/xml" - "fmt" - "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" - "github.com/rs/zerolog" -) - -const ( - TaskXMLDurationFormat = "2006-01-02T15:04:05.9999999Z" - TaskXMLHeader = `` -) - -// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f - -type triggers struct { - XMLName xml.Name `xml:"Triggers"` - TimeTriggers []taskTimeTrigger `xml:"TimeTrigger,omitempty"` -} - -type taskTimeTrigger struct { - XMLName xml.Name `xml:"TimeTrigger"` - StartBoundary string `xml:"StartBoundary,omitempty"` // Derived from time.Time - EndBoundary string `xml:"EndBoundary,omitempty"` // Derived from time.Time; must be > StartBoundary - Enabled bool `xml:"Enabled"` -} - -type idleSettings struct { - XMLName xml.Name `xml:"IdleSettings"` - StopOnIdleEnd bool `xml:"StopOnIdleEnd"` - RestartOnIdle bool `xml:"RestartOnIdle"` -} - -type settings struct { - XMLName xml.Name `xml:"Settings"` - Enabled bool `xml:"Enabled"` - Hidden bool `xml:"Hidden"` - DisallowStartIfOnBatteries bool `xml:"DisallowStartIfOnBatteries"` - StopIfGoingOnBatteries bool `xml:"StopIfGoingOnBatteries"` - AllowHardTerminate bool `xml:"AllowHardTerminate"` - RunOnlyIfNetworkAvailable bool `xml:"RunOnlyIfNetworkAvailable"` - AllowStartOnDemand bool `xml:"AllowStartOnDemand"` - WakeToRun bool `xml:"WakeToRun"` - RunOnlyIfIdle bool `xml:"RunOnlyIfIdle"` - StartWhenAvailable bool `xml:"StartWhenAvailable"` - Priority int `xml:"Priority,omitempty"` // 1 to 10 inclusive - MultipleInstancesPolicy string `xml:"MultipleInstancesPolicy,omitempty"` - ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` - DeleteExpiredTaskAfter string `xml:"DeleteExpiredTaskAfter,omitempty"` // Derived from time.Duration - IdleSettings idleSettings `xml:"IdleSettings,omitempty"` -} - -type actionExec struct { - XMLName xml.Name `xml:"Exec"` - Command string `xml:"Command"` - Arguments string `xml:"Arguments,omitempty"` -} - -type actions struct { - XMLName xml.Name `xml:"Actions"` - Context string `xml:"Context,attr"` - Exec []actionExec `xml:"Exec,omitempty"` -} - -type principals struct { - XMLName xml.Name `xml:"Principals"` - Principals []principal `xml:"Principal,omitempty"` -} - -type principal struct { - XMLName xml.Name `xml:"Principal"` - ID string `xml:"id,attr"` - UserID string `xml:"UserId"` - RunLevel string `xml:"RunLevel"` -} - -type task struct { - XMLName xml.Name `xml:"Task"` - TaskVersion string `xml:"version,attr"` - TaskNamespace string `xml:"xmlns,attr"` - Triggers triggers `xml:"Triggers"` - Actions actions `xml:"Actions"` - Principals principals `xml:"Principals"` - Settings settings `xml:"Settings"` -} - -// registerTask serializes and submits the provided task structure -func (mod *Module) registerTask(ctx context.Context, taskDef task, taskPath string) (path string, err error) { - - var taskXml string - - log := zerolog.Ctx(ctx).With(). - Str("module", "tsch"). - Str("func", "createTask").Logger() - - // Generate task XML content. See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f - { - doc, err := xml.Marshal(taskDef) - if err != nil { - log.Error().Err(err).Msg("failed to marshal task XML") - return "", fmt.Errorf("marshal task: %w", err) - } - taskXml = TaskXMLHeader + string(doc) - log.Debug().Str("content", taskXml).Msg("Generated task XML") - } - // Submit task - { - response, err := mod.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ - Path: taskPath, - XML: taskXml, - Flags: 0, // TODO - LogonType: 0, // TASK_LOGON_NONE - CredsCount: 0, - Creds: nil, - }) - if err != nil { - log.Error().Err(err).Msg("Failed to register task") - return "", fmt.Errorf("register task: %w", err) - } - log.Info().Str("path", taskPath).Msg("Task created successfully") - path = response.ActualPath - } - return -} - -func (mod *Module) deleteTask(ctx context.Context, taskPath string) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("module", "tsch"). - Str("path", taskPath). - Str("func", "deleteTask").Logger() - - if _, err = mod.tsch.Delete(ctx, &itaskschedulerservice.DeleteRequest{Path: taskPath}); err != nil { - log.Error().Err(err).Msg("Failed to delete task") - return fmt.Errorf("delete task: %w", err) - } - log.Info().Msg("Task deleted successfully") - return -} diff --git a/internal/exec/wmi/exec.go b/internal/exec/wmi/exec.go deleted file mode 100644 index 7ae33ba..0000000 --- a/internal/exec/wmi/exec.go +++ /dev/null @@ -1,166 +0,0 @@ -package wmiexec - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "github.com/FalconOpsLLC/goexec/internal/client/dce" - "github.com/FalconOpsLLC/goexec/internal/exec" - "github.com/RedTeamPentesting/adauth" - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/msrpc/dcom" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemlevel1login/v0" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0" - "github.com/rs/zerolog" -) - -const ( - ProtocolSequenceRPC uint16 = 7 - ProtocolSequenceNP uint16 = 15 - DefaultWmiEndpoint string = "ncacn_ip_tcp:[135]" -) - -var ( - ComVersion = &dcom.COMVersion{ - MajorVersion: 5, - MinorVersion: 7, - } - ORPCThis = &dcom.ORPCThis{Version: ComVersion} -) - -func (mod *Module) Cleanup(ctx context.Context, _ *exec.CleanupConfig) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("module", "tsch"). - Str("func", "Cleanup").Logger() - - if err = mod.dce.Close(ctx); err != nil { - log.Warn().Err(err).Msg("Failed to close DCERPC connection") - } - mod.sc = nil - mod.dce = nil - return -} - -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", "Connect").Logger() - - if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok { - return errors.New("invalid configuration for DCE connection method") - } else { - var dceOpts []dcerpc.Option - - // Create DCE connection - if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultWmiEndpoint, "", dceOpts...); err != nil { - log.Error().Err(err).Msg("Failed to initialize DCE dialer") - return fmt.Errorf("create DCE dialer: %w", err) - } - ia, err := iactivation.NewActivationClient(ctx, mod.dce) - if err != nil { - log.Error().Err(err).Msg("Failed to create activation client") - return fmt.Errorf("create activation client: %w", err) - } - act, err := ia.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ - ORPCThis: ORPCThis, - ClassID: wmi.Level1LoginClassID.GUID(), - IIDs: []*dcom.IID{iwbemlevel1login.Level1LoginIID}, - RequestedProtocolSequences: []uint16{ProtocolSequenceRPC}, // TODO: Named pipe support - }) - if err != nil { - return fmt.Errorf("request remote activation: %w", err) - } - if act.HResult != 0 { - return fmt.Errorf("remote activation failed with code %d", act.HResult) - } - retBinds := act.OXIDBindings.GetStringBindings() - if len(act.InterfaceData) < 1 || len(retBinds) < 1 { - return errors.New("remote activation failed") - } - ipid := act.InterfaceData[0].GetStandardObjectReference().Std.IPID - - for _, b := range retBinds { - sb, err := dcerpc.ParseStringBinding("ncacn_ip_tcp:" + b.NetworkAddr) - if err != nil { - log.Debug().Err(err).Msg("Failed to parse string binding") - } - sb.NetworkAddress = target.AddressWithoutPort() - dceOpts = append(dceOpts, dcerpc.WithEndpoint(sb.String())) - } - - if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultWmiEndpoint, "", dceOpts...); err != nil { - log.Error().Err(err).Msg("Failed to initialize secondary DCE dialer") - } - loginClient, err := iwbemlevel1login.NewLevel1LoginClient(ctx, mod.dce, dcom.WithIPID(ipid)) - if err != nil { - return fmt.Errorf("initialize wbem login client: %w", err) - } - login, err := loginClient.NTLMLogin(ctx, &iwbemlevel1login.NTLMLoginRequest{ - This: ORPCThis, - NetworkResource: cfg.Resource, - }) - if err != nil { - return fmt.Errorf("ntlm login: %w", err) - } - - mod.sc, err = iwbemservices.NewServicesClient(ctx, mod.dce, dcom.WithIPID(login.Namespace.InterfacePointer().IPID())) - if err != nil { - return fmt.Errorf("create services client: %w", err) - } - } - return -} - -func (mod *Module) Exec(ctx context.Context, ecfg *exec.ExecutionConfig) (err error) { - log := zerolog.Ctx(ctx).With(). - Str("module", "tsch"). - Str("method", ecfg.ExecutionMethod).Logger() - - if ecfg.ExecutionMethod == MethodCall { - if cfg, ok := ecfg.ExecutionMethodConfig.(MethodCallConfig); !ok { - return errors.New("invalid execution configuration") - - } else { - out, err := mod.query(ctx, cfg.Class, cfg.Method, cfg.Arguments) - if err != nil { - return fmt.Errorf("query: %w", err) - } - if outJson, err := json.Marshal(out); err != nil { - log.Error().Err(err).Msg("failed to marshal call output") - } else { - fmt.Println(string(outJson)) - } - } - } else if ecfg.ExecutionMethod == MethodProcess { - if cfg, ok := ecfg.ExecutionMethodConfig.(MethodProcessConfig); !ok { - return errors.New("invalid execution configuration") - } else { - out, err := mod.query(ctx, "Win32_Process", "Create", map[string]any{ - "CommandLine": cfg.Command, - "WorkingDir": cfg.WorkingDirectory, - }) - if err != nil { - return fmt.Errorf("query: %w", err) - } - if pid, ok := out["ProcessId"]; ok && pid != nil { - log.Info(). - Any("PID", pid). - Any("return", out["ReturnValue"]). - Msg("Process created") - } else { - log.Error(). - Any("return", out["ReturnValue"]). - Msg("Process creation failed") - return errors.New("failed to create process") - } - } - } else { - return errors.New("unsupported execution method") - } - return nil -} diff --git a/internal/exec/wmi/module.go b/internal/exec/wmi/module.go deleted file mode 100644 index 0e83aa8..0000000 --- a/internal/exec/wmi/module.go +++ /dev/null @@ -1,33 +0,0 @@ -package wmiexec - -import ( - "github.com/RedTeamPentesting/adauth" - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0" - "github.com/rs/zerolog" -) - -type Module struct { - creds *adauth.Credential - target *adauth.Target - - log zerolog.Logger - dce dcerpc.Conn - sc iwbemservices.ServicesClient -} - -type MethodCallConfig struct { - Class string - Method string - Arguments map[string]any -} - -type MethodProcessConfig struct { - Command string - WorkingDirectory string -} - -const ( - MethodCall = "call" - MethodProcess = "process" -) diff --git a/internal/exec/wmi/wmi.go b/internal/exec/wmi/wmi.go deleted file mode 100644 index dd003a3..0000000 --- a/internal/exec/wmi/wmi.go +++ /dev/null @@ -1,26 +0,0 @@ -package wmiexec - -import ( - "context" - "errors" - "fmt" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmio/query" -) - -func (mod *Module) query(ctx context.Context, class, method string, values map[string]any) (outValues map[string]any, err error) { - outValues = make(map[string]any) - if mod.sc == nil { - err = errors.New("module has not been initialized") - return - } - if out, err := query.NewBuilder(ctx, mod.sc, ComVersion). - Spawn(class). // The class to instantiate (i.e. Win32_Process) - Method(method). // The method to call (i.e. Create) - Values(values). // The values to pass to method - Exec(). - Object(); err == nil { - return out.Values(), err - } - err = fmt.Errorf("(*query.Builder).Spawn: %w", err) - return -} diff --git a/internal/util/util.go b/internal/util/util.go index f8f12ba..555297a 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,6 +1,7 @@ package util import ( + "github.com/google/uuid" "math/rand" // not crypto secure "regexp" "strings" @@ -23,6 +24,10 @@ func RandomHostname() (hostname string) { } } +func RandomWindowsTempFile() string { + return `\Windows\Temp\` + strings.ToUpper(uuid.New().String()) +} + func RandomString() string { return RandomStringFromCharset(randStringCharset, rand.Intn(10)+6) } diff --git a/pkg/goexec/auth.go b/pkg/goexec/auth.go new file mode 100644 index 0000000..e2e9c3e --- /dev/null +++ b/pkg/goexec/auth.go @@ -0,0 +1,11 @@ +package goexec + +import ( + "github.com/RedTeamPentesting/adauth" +) + +// AuthOptions holds Windows / Active Directory authentication parameters +type AuthOptions struct { + Target *adauth.Target + Credential *adauth.Credential +} diff --git a/pkg/goexec/clean.go b/pkg/goexec/clean.go new file mode 100644 index 0000000..a853dc4 --- /dev/null +++ b/pkg/goexec/clean.go @@ -0,0 +1,31 @@ +package goexec + +import ( + "context" + "github.com/rs/zerolog" +) + +type CleanProvider interface { + Clean(ctx context.Context) (err error) +} + +type Cleaner struct { + workers []func(ctx context.Context) error +} + +func (c *Cleaner) AddCleaner(worker func(ctx context.Context) error) { + c.workers = append(c.workers, worker) +} + +func (c *Cleaner) Clean(ctx context.Context) (err error) { + log := zerolog.Ctx(ctx).With(). + Str("component", "cleaner").Logger() + + for _, worker := range c.workers { + if err = worker(log.WithContext(ctx)); err != nil { + + log.Warn().Err(err).Msg("Clean worker failed") + } + } + return +} diff --git a/pkg/goexec/client.go b/pkg/goexec/client.go new file mode 100644 index 0000000..bee0399 --- /dev/null +++ b/pkg/goexec/client.go @@ -0,0 +1,26 @@ +package goexec + +import "context" + +// Client represents an application layer network client +type Client interface { + + // Connect establishes a connection to the remote server + Connect(ctx context.Context) error + + // Close terminates the active connection and frees allocated resources + Close(ctx context.Context) error +} + +// ClientOptions represents configuration options for a Client +type ClientOptions struct { + + // Proxy specifies the URI of the proxy server to route client requests through + Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"` + + // Host specifies the hostname or IP address that the client should connect to + Host string `json:"host" yaml:"host"` + + // Port specifies the network port on Host that the client will connect to + Port uint16 `json:"port" yaml:"port"` +} diff --git a/pkg/goexec/dce/client.go b/pkg/goexec/dce/client.go new file mode 100644 index 0000000..e8dadae --- /dev/null +++ b/pkg/goexec/dce/client.go @@ -0,0 +1,89 @@ +package dce + +import ( + "context" + "fmt" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3" + "github.com/rs/zerolog" +) + +type Client struct { + Options + + conn dcerpc.Conn + hostname string +} + +func NewClient() *Client { + return new(Client) +} + +func (c *Client) String() string { + return ClientName +} + +func (c *Client) Reconnect(ctx context.Context, opts ...dcerpc.Option) (err error) { + c.DcerpcOptions = append(c.DcerpcOptions, opts...) + + return c.Connect(ctx) +} + +func (c *Client) Dce() (dce dcerpc.Conn) { + return c.conn +} + +func (c *Client) Logger(ctx context.Context) (log zerolog.Logger) { + return zerolog.Ctx(ctx).With(). + Str("client", c.String()).Logger() +} + +func (c *Client) Connect(ctx context.Context) (err error) { + + log := c.Logger(ctx) + ctx = log.WithContext(ctx) + + var do, eo []dcerpc.Option + + do = append(do, c.DcerpcOptions...) + do = append(do, c.authOptions...) + + if !c.NoSign { + do = append(do, dcerpc.WithSign()) + eo = append(eo, dcerpc.WithSign()) + } + if !c.NoSeal { + do = append(do, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) + eo = append(eo, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) + } + + if !c.NoLog { + do = append(do, dcerpc.WithLogger(log)) + eo = append(eo, dcerpc.WithLogger(log)) + } + + if !c.NoEpm { + log.Debug().Msg("Using endpoint mapper") + + eo = append(eo, c.epmOptions...) + eo = append(eo, c.authOptions...) + + do = append(do, epm.EndpointMapper(ctx, c.Host, eo...)) + } + + for _, e := range c.stringBindings { + do = append(do, dcerpc.WithEndpoint(e.String())) + } + + if c.conn, err = dcerpc.Dial(ctx, c.Host, do...); err != nil { + + log.Error().Err(err).Msgf("Failed to connect to %s endpoint", c.String()) + return fmt.Errorf("dial %s: %w", c.String(), err) + } + + return +} + +func (c *Client) Close(ctx context.Context) (err error) { + return c.conn.Close(ctx) +} diff --git a/pkg/goexec/dce/default.go b/pkg/goexec/dce/default.go new file mode 100644 index 0000000..d3a912f --- /dev/null +++ b/pkg/goexec/dce/default.go @@ -0,0 +1,5 @@ +package dce + +const ( + ClientName = "DCE" +) diff --git a/pkg/goexec/dce/options.go b/pkg/goexec/dce/options.go new file mode 100644 index 0000000..e13ef43 --- /dev/null +++ b/pkg/goexec/dce/options.go @@ -0,0 +1,108 @@ +package dce + +import ( + "context" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/RedTeamPentesting/adauth/dcerpcauth" + "github.com/oiweiwei/go-msrpc/dcerpc" + "net" +) + +type Options struct { + goexec.ClientOptions + goexec.AuthOptions + + // NoSign disables packet signing by omitting dcerpc.WithSign() + NoSign bool `json:"no_sign" yaml:"no_sign"` + + // NoSeal disables packet stub encryption by omitting dcerpc.WithSeal() + NoSeal bool `json:"no_seal" yaml:"no_seal"` + + // NoLog disables logging by omitting dcerpc.WithLogger(...) + NoLog bool `json:"no_log" yaml:"no_log"` + + // NoEpm disables DCE endpoint mapper communications + NoEpm bool `json:"no_epm" yaml:"no_epm"` + + // Endpoint stores the explicit DCE string binding to use + Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"` + + // Filter stores the filter for returned endpoints from an endpoint mapper + Filter string `json:"filter,omitempty" yaml:"filter,omitempty"` + + netDialer goexec.Dialer + dialer dcerpc.Dialer + authOptions []dcerpc.Option + DcerpcOptions []dcerpc.Option + epmOptions []dcerpc.Option + stringBindings []*dcerpc.StringBinding +} + +func (c *Client) Parse(ctx context.Context) (err error) { + + // Reset internals + { + c.netDialer = nil + c.dialer = nil + c.stringBindings = []*dcerpc.StringBinding{} + c.authOptions = []dcerpc.Option{} + c.DcerpcOptions = []dcerpc.Option{} + c.epmOptions = []dcerpc.Option{ + dcerpc.WithSign(), // Require signing for EPM + } + } + + if !c.NoSeal { + // Enable encryption + c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) + c.epmOptions = append(c.epmOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) + } + if !c.NoSign { + // Enable signing + c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithSign()) + //c.epmOptions = append(c.epmOptions, dcerpc.WithSign()) + } + + // Parse DCERPC endpoint + if c.Endpoint != "" { + sb, err := dcerpc.ParseStringBinding(c.Endpoint) + if err != nil { + return err + } + c.stringBindings = append(c.stringBindings, sb) + } + + // Parse EPM filter + if c.Filter != "" { + sb, err := dcerpc.ParseStringBinding(c.Filter) + if err != nil { + return err + } + c.stringBindings = append(c.stringBindings, sb) + } + + if c.Proxy == "" { + c.netDialer = &net.Dialer{} // FUTURE: additional dial c + + } else { + // Parse proxy URL + d, err := goexec.ParseProxyURI(c.Proxy) + if err != nil { + return err + } + var ok bool + if c.dialer, ok = d.(dcerpc.Dialer); !ok { + return fmt.Errorf("cannot cast %T to dcerpc.Dialer", d) + } + } + + // Parse authentication parameters + if c.authOptions, err = dcerpcauth.AuthenticationOptions(ctx, c.Credential, c.Target, &dcerpcauth.Options{}); err != nil { + return fmt.Errorf("parse auth c: %w", err) + } + + c.Host = c.Target.AddressWithoutPort() + + return +} diff --git a/pkg/goexec/dcom/dcom.go b/pkg/goexec/dcom/dcom.go new file mode 100644 index 0000000..e66a382 --- /dev/null +++ b/pkg/goexec/dcom/dcom.go @@ -0,0 +1,34 @@ +package dcomexec + +import ( + guuid "github.com/google/uuid" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dtyp" +) + +const ( + LcEnglishUs uint32 = 0x409 +) + +var ( + ShellWindowsUuid = uuid.MustParse("9BA05972-F6A8-11CF-A442-00A0C90A8F39") + Mmc20Uuid = uuid.MustParse("49B2791A-B1AE-4C90-9B8E-E860BA07F889") + + 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(Mmc20Uuid)) + ORPCThis = &dcom.ORPCThis{ + Version: ComVersion, + CID: &RandCid, + } +) diff --git a/pkg/goexec/dcom/mmc.go b/pkg/goexec/dcom/mmc.go new file mode 100644 index 0000000..9c92af3 --- /dev/null +++ b/pkg/goexec/dcom/mmc.go @@ -0,0 +1,50 @@ +package dcomexec + +import ( + "context" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/rs/zerolog" +) + +const ( + MethodMmc = "MMC" // MMC20.Application::Document.ActiveView.ExecuteShellCommand +) + +type DcomMmc struct { + DcomExec + + WorkingDirectory string + WindowState string +} + +// Execute will perform command execution via the MMC20.Application DCOM object. +func (m *DcomMmc) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName). + Str("method", MethodMmc). + Logger() + + method := "Document.ActiveView.ExecuteShellCommand" + + var args = in.Arguments + if args == "" { + args = " " // the process arguments can't be a blank string + } + + // Arguments must be passed in reverse order + if _, err := callComMethod(ctx, + m.dispatchClient, + method, + stringToVariant(m.WindowState), + stringToVariant(in.Arguments), + stringToVariant(m.WorkingDirectory), + stringToVariant(in.Executable)); 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 +} diff --git a/pkg/goexec/dcom/module.go b/pkg/goexec/dcom/module.go new file mode 100644 index 0000000..47dc7ca --- /dev/null +++ b/pkg/goexec/dcom/module.go @@ -0,0 +1,111 @@ +package dcomexec + +import ( + "context" + "errors" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" + "github.com/rs/zerolog" +) + +const ( + ModuleName = "DCOM" +) + +type DcomExec struct { + client *dce.Client + dispatchClient idispatch.DispatchClient +} + +func (m *DcomExec) Init(ctx context.Context, c *dce.Client) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName).Logger() + + m.client = c + + if m.client.Dce() == nil { + return errors.New("DCE connection not initialized") + } + + opts := []dcerpc.Option{ + dcerpc.WithSign(), + } + + inst := &dcom.InstantiationInfoData{ + ClassID: &MmcClsid, + IID: []*dcom.IID{IDispatchIID}, + ClientCOMVersion: ComVersion, + } + ac := &dcom.ActivationContextInfoData{} + loc := &dcom.LocationInfoData{} + scm := &dcom.SCMRequestInfoData{ + RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{ + RequestedProtocolSequences: []uint16{7}, + }, + } + + 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, m.client.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 := new(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")...) // TODO + + err = c.Reconnect(ctx, opts...) + if err != nil { + return err + } + log.Info().Msg("created new DCERPC dialer") + + m.dispatchClient, err = idispatch.NewDispatchClient(ctx, c.Dce(), dcom.WithIPID(oIPID)) + if err != nil { + return err + } + log.Info().Msg("created IDispatch client") + + return +} diff --git a/pkg/goexec/dcom/util.go b/pkg/goexec/dcom/util.go new file mode 100644 index 0000000..5ed8a60 --- /dev/null +++ b/pkg/goexec/dcom/util.go @@ -0,0 +1,85 @@ +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" +) + +func callComMethod(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{}, + LocaleID: LcEnglishUs, + + Names: []string{obj + "\x00"}, + }, 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, + IID: &dcom.IID{}, + LocaleID: LcEnglishUs, + + DispatchIDMember: gr.DispatchID[0], + } + + 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 +} + +func stringToVariant(s string) *oaut.Variant { + return &oaut.Variant{ + Size: 5, + VT: 8, + VarUnion: &oaut.Variant_VarUnion{ + Value: &oaut.Variant_VarUnion_BSTR{ + BSTR: &oaut.String{ + Data: s, + }, + }, + }, + } +} diff --git a/pkg/goexec/exec.go b/pkg/goexec/exec.go new file mode 100644 index 0000000..cb44f19 --- /dev/null +++ b/pkg/goexec/exec.go @@ -0,0 +1,14 @@ +package goexec + +import "context" + +type ExecutionProvider interface { + Execute(ctx context.Context, in *ExecutionInput) (err error) +} + +type Executor struct{} + +type CleanExecutionProvider interface { + ExecutionProvider + CleanProvider +} diff --git a/pkg/goexec/io.go b/pkg/goexec/io.go new file mode 100644 index 0000000..846a1fd --- /dev/null +++ b/pkg/goexec/io.go @@ -0,0 +1,87 @@ +package goexec + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" +) + +type OutputProvider interface { + GetOutput(ctx context.Context) (out io.ReadCloser, err error) +} + +type ExecutionIO struct { + Cleaner + + Input *ExecutionInput + Output *ExecutionOutput +} + +type ExecutionOutput struct { + NoDelete bool + RemotePath string + Provider OutputProvider +} + +type ExecutionInput struct { + FilePath string + Executable string + ExecutablePath string + Arguments string + CommandLine string +} + +func (execIO *ExecutionIO) GetOutput(ctx context.Context) (out io.ReadCloser, err error) { + + if execIO.Output.Provider != nil { + return execIO.Output.Provider.GetOutput(ctx) + } + return nil, errors.New("output provider not set") +} + +func (execIO *ExecutionIO) CommandLine() string { + return execIO.Input.Command() +} + +func (execIO *ExecutionIO) String() (cmd string) { + + cmd = execIO.Input.Command() + + if execIO.Output.Provider != nil && execIO.Output.RemotePath != "" { + return fmt.Sprintf(`C:\Windows\System32\cmd.exe /C %s > %s`, cmd, execIO.Output.RemotePath) + } + return +} + +func (i *ExecutionInput) Command() string { + + if i.CommandLine == "" { + + if i.ExecutablePath != "" { + i.CommandLine = i.ExecutablePath + + } else if i.Executable != "" { + i.CommandLine = i.Executable + } + + if i.Arguments != "" { + i.CommandLine += " " + i.Arguments + } + } + return i.CommandLine +} + +func (i *ExecutionInput) String() string { + return i.Command() +} + +func (i *ExecutionInput) UploadReader(_ context.Context) (reader io.Reader, err error) { + + if i.FilePath != "" { + return os.OpenFile(i.FilePath, os.O_RDONLY, 0) + } + return bytes.NewBufferString(i.Command()), nil +} diff --git a/pkg/goexec/method.go b/pkg/goexec/method.go new file mode 100644 index 0000000..7ea0ddc --- /dev/null +++ b/pkg/goexec/method.go @@ -0,0 +1,72 @@ +package goexec + +import ( + "context" + "fmt" + "github.com/rs/zerolog" +) + +type Method interface{} + +type RemoteMethod interface { + Connect(ctx context.Context) error + Init(ctx context.Context) error +} + +type RemoteExecuteMethod interface { + RemoteMethod + Execute(ctx context.Context, io *ExecutionIO) error +} + +type RemoteCleanMethod interface { + RemoteMethod + Clean(ctx context.Context) error +} + +type RemoteExecuteCleanMethod interface { + RemoteExecuteMethod + Clean(ctx context.Context) error +} + +type RemoteExecuteCleanMethodWithOutput interface { + RemoteExecuteCleanMethod + OutputProvider +} + +func ExecuteMethod(ctx context.Context, module RemoteExecuteMethod, execIO *ExecutionIO) (err error) { + + log := zerolog.Ctx(ctx) + + if err = module.Connect(ctx); err != nil { + log.Error().Err(err).Msg("Connection failed") + return fmt.Errorf("connect: %w", err) + } + + if err = module.Init(ctx); err != nil { + log.Error().Err(err).Msg("Module initialization failed") + return fmt.Errorf("init module: %w", err) + } + + if err = module.Execute(ctx, execIO); err != nil { + log.Error().Err(err).Msg("Execution failed") + return fmt.Errorf("execute: %w", err) + } + return +} + +func ExecuteCleanMethod(ctx context.Context, module RemoteExecuteCleanMethod, execIO *ExecutionIO) (err error) { + + log := zerolog.Ctx(ctx) + + defer func() { + if err = module.Clean(ctx); err != nil { + log.Error().Err(err).Msg("Module cleanup failed") + } + }() + + return ExecuteMethod(ctx, module, execIO) +} + +func ExecuteCleanMethodWithOutput(ctx context.Context, module RemoteExecuteCleanMethodWithOutput, execIO *ExecutionIO) (err error) { + return ExecuteCleanMethod(ctx, module, execIO) +} diff --git a/pkg/goexec/module.go b/pkg/goexec/module.go new file mode 100644 index 0000000..6544b78 --- /dev/null +++ b/pkg/goexec/module.go @@ -0,0 +1,25 @@ +package goexec + +import "context" + +type Module interface { + Init(ctx context.Context) error +} + +type ExecutionModule interface { + Module + ExecutionProvider +} + +type CleanExecutionModule interface { + Module + ExecutionProvider + CleanProvider +} + +type CleanExecutionOutputModule interface { + Module + ExecutionProvider + CleanProvider + OutputProvider +} diff --git a/pkg/goexec/proxy.go b/pkg/goexec/proxy.go new file mode 100644 index 0000000..8f20758 --- /dev/null +++ b/pkg/goexec/proxy.go @@ -0,0 +1,33 @@ +package goexec + +import ( + "fmt" + "golang.org/x/net/proxy" + "net" + "net/url" +) + +// Dialer outlines a basic implementation for establishing network connections +type Dialer interface { + + // Dial establishes a network connection (net.Conn) using the provided parameters + Dial(network string, address string) (connection net.Conn, err error) +} + +// ParseProxyURI parses the provided proxy URI spec to a Dialer +func ParseProxyURI(uri string) (dialer Dialer, err error) { + + // Parse proxy spec as URL + u, err := url.Parse(uri) + if err != nil { + return nil, fmt.Errorf("parse proxy URI: %w", err) + } + + // Create dialer from URL + dialer, err = proxy.FromURL(u, nil) + if err != nil { + return nil, fmt.Errorf("init proxy: %w", err) + } + + return +} diff --git a/pkg/goexec/scmr/change.go b/pkg/goexec/scmr/change.go new file mode 100644 index 0000000..c8321f7 --- /dev/null +++ b/pkg/goexec/scmr/change.go @@ -0,0 +1,144 @@ +package scmrexec + +import ( + "context" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/windows" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" + "github.com/rs/zerolog" +) + +const ( + MethodChange = "Change" +) + +type ScmrChange struct { + Scmr + goexec.Cleaner + goexec.Executor + + NoStart bool + ServiceName string +} + +func (m *ScmrChange) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName). + Str("method", MethodChange). + Str("service", m.ServiceName). + Logger() + + svc := &service{name: m.ServiceName} + + openResponse, err := m.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{ + ServiceManager: m.scm, + ServiceName: svc.name, + DesiredAccess: ServiceAllAccess, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to open service handle") + return fmt.Errorf("open service request: %w", err) + } + if openResponse.Return != 0 { + log.Error().Err(err).Msg("Failed to open service handle") + return fmt.Errorf("create service: %w", err) + } + + svc.handle = openResponse.Service + log.Info().Msg("Opened service handle") + + defer m.AddCleaner(func(ctxInner context.Context) error { + + r, errInner := m.ctl.CloseService(ctxInner, &svcctl.CloseServiceRequest{ + ServiceObject: svc.handle, + }) + if errInner != nil { + return fmt.Errorf("close service: %w", errInner) + } + if r.Return != 0 { + return fmt.Errorf("close service returned non-zero exit code: %02x", r.Return) + } + log.Info().Msg("Closed service handle") + + return nil + }) + + // Note original service configuration + queryResponse, err := m.ctl.QueryServiceConfigW(ctx, &svcctl.QueryServiceConfigWRequest{ + Service: svc.handle, + BufferLength: 8 * 1024, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to fetch service configuration") + return fmt.Errorf("get service config: %w", err) + } + if queryResponse.Return != 0 { + log.Error().Err(err).Msg("Failed to query service configuration") + return fmt.Errorf("query service config: %w", err) + } + + log.Info().Str("binaryPath", queryResponse.ServiceConfig.BinaryPathName).Msg("Fetched original service configuration") + svc.originalConfig = queryResponse.ServiceConfig + + stopResponse, err := m.ctl.ControlService(ctx, &svcctl.ControlServiceRequest{ + Service: svc.handle, + Control: windows.SERVICE_CONTROL_STOP, + }) + + if err != nil { + if stopResponse == nil || stopResponse.Return != windows.ERROR_SERVICE_NOT_ACTIVE { + + log.Error().Err(err).Msg("Failed to stop existing service") + return fmt.Errorf("stop service: %w", err) + } + + log.Debug().Msg("Service is not running") + + // TODO: restore state + /* + defer m.AddCleaner(func(ctxInner context.Context) error { + // ... + return nil + }) + */ + + } else { + log.Info().Msg("Stopped existing service") + } + + changeResponse, err := m.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{ + Service: svc.handle, + BinaryPathName: in.String(), + DisplayName: svc.originalConfig.DisplayName, + ServiceType: svc.originalConfig.ServiceType, + StartType: windows.SERVICE_DEMAND_START, + ErrorControl: svc.originalConfig.ErrorControl, + LoadOrderGroup: svc.originalConfig.LoadOrderGroup, + ServiceStartName: svc.originalConfig.ServiceStartName, + TagID: svc.originalConfig.TagID, + //Dependencies: []byte(svc.originalConfig.Dependencies), // TODO + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to request service configuration change") + return fmt.Errorf("change service config request: %w", err) + } + if changeResponse.Return != 0 { + log.Error().Err(err).Msg("Failed to change service configuration") + return fmt.Errorf("change service config: %w", err) + } + + if !m.NoStart { + + err = m.startService(ctx, svc) + if err != nil { + log.Error().Err(err).Msg("Failed to start service") + } + } + + return +} diff --git a/pkg/goexec/scmr/create.go b/pkg/goexec/scmr/create.go new file mode 100644 index 0000000..6de50f8 --- /dev/null +++ b/pkg/goexec/scmr/create.go @@ -0,0 +1,125 @@ +package scmrexec + +import ( + "context" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/util" + "github.com/FalconOpsLLC/goexec/internal/windows" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" + "github.com/rs/zerolog" +) + +const ( + MethodCreate = "Create" +) + +type ScmrCreate struct { + Scmr + goexec.Cleaner + goexec.Executor + + NoDelete bool + NoStart bool + ServiceName string + DisplayName string +} + +func (m *ScmrCreate) ensure() { + if m.ServiceName == "" { + m.ServiceName = util.RandomString() + } + if m.DisplayName == "" { + m.DisplayName = m.ServiceName + } +} + +func (m *ScmrCreate) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) { + m.ensure() + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName). + Str("method", MethodCreate). + Str("service", m.ServiceName). + Logger() + + svc := &service{name: m.ServiceName} + + resp, err := m.ctl.CreateServiceW(ctx, &svcctl.CreateServiceWRequest{ + ServiceManager: m.scm, + ServiceName: m.ServiceName, + DisplayName: m.DisplayName, + BinaryPathName: in.String(), + ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, + StartType: windows.SERVICE_DEMAND_START, + DesiredAccess: ServiceAllAccess, // TODO: Replace + }) + + if err != nil { + log.Error().Err(err).Msg("Create service request failed") + return fmt.Errorf("create service request: %w", err) + } + + if resp.Return != 0 { + log.Error().Err(err).Msg("Failed to create service") + return fmt.Errorf("create service returned non-zero exit code: %02x", resp.Return) + } + + if !m.NoDelete { + m.AddCleaner(func(ctxInner context.Context) error { + + r, errInner := m.ctl.DeleteService(ctxInner, &svcctl.DeleteServiceRequest{ + Service: svc.handle, + }) + if errInner != nil { + return fmt.Errorf("delete service: %w", errInner) + } + if r.Return != 0 { + return fmt.Errorf("delete service returned non-zero exit code: %02x", r.Return) + } + log.Info().Msg("Deleted service") + + return nil + }) + } + + m.AddCleaner(func(ctxInner context.Context) error { + + r, errInner := m.ctl.CloseService(ctxInner, &svcctl.CloseServiceRequest{ + ServiceObject: svc.handle, + }) + if errInner != nil { + return fmt.Errorf("close service: %w", errInner) + } + if r.Return != 0 { + return fmt.Errorf("close service returned non-zero exit code: %02x", r.Return) + } + log.Info().Msg("Closed service handle") + + return nil + }) + + log.Info().Msg("Created service") + svc.handle = resp.Service + + if !m.NoStart { + + err = m.startService(ctx, svc) + if err != nil { + log.Error().Err(err).Msg("Failed to start service") + } + } + if svc.handle == nil { + + if err = m.Reconnect(ctx); err != nil { + return err + } + + svc, err = m.openService(ctx, svc.name) + if err != nil { + return fmt.Errorf("reopen service: %w", err) + } + } + + return +} diff --git a/pkg/goexec/scmr/delete.go b/pkg/goexec/scmr/delete.go new file mode 100644 index 0000000..d327a61 --- /dev/null +++ b/pkg/goexec/scmr/delete.go @@ -0,0 +1,46 @@ +package scmrexec + +import ( + "context" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" + "github.com/rs/zerolog" +) + +const ( + MethodDelete = "Delete" +) + +type ScmrDelete struct { + Scmr + goexec.Cleaner + + ServiceName string +} + +func (m *ScmrDelete) Clean(ctx context.Context) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName). + Str("method", MethodDelete). + Str("service", m.ServiceName). + Logger() + + svc, err := m.openService(ctx, m.ServiceName) + if err != nil { + return err + } + deleteResponse, err := m.ctl.DeleteService(ctx, &svcctl.DeleteServiceRequest{ + Service: svc.handle, + }) + if err != nil { + return fmt.Errorf("delete service: %w", err) + } + if deleteResponse.Return != 0 { + return fmt.Errorf("delete service returned non-zero exit code: %02x", deleteResponse.Return) + } + + log.Info().Msg("Deleted service") + return +} diff --git a/pkg/goexec/scmr/module.go b/pkg/goexec/scmr/module.go new file mode 100644 index 0000000..efa3d50 --- /dev/null +++ b/pkg/goexec/scmr/module.go @@ -0,0 +1,131 @@ +package scmrexec + +import ( + "context" + "errors" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/util" + "github.com/FalconOpsLLC/goexec/internal/windows" + "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" + "github.com/rs/zerolog" +) + +const ( + ModuleName = "SCMR" + + DefaultEndpoint = "ncacn_np:[svcctl]" + ScmrUuid = "367ABB81-9844-35F1-AD32-98F038001003" +) + +type Scmr struct { + client *dce.Client + ctl svcctl.SvcctlClient + scm *svcctl.Handle + + hostname string +} + +func (m *Scmr) Init(ctx context.Context, c *dce.Client) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName).Logger() + + m.client = c + + if m.client.Dce() == nil { + return errors.New("DCE connection not initialized") + } + + m.hostname, err = c.Target.Hostname(ctx) + if err != nil { + log.Debug().Err(err).Msg("Failed to determine target hostname") + } + if m.hostname == "" { + m.hostname = util.RandomHostname() + } + + m.ctl, err = svcctl.NewSvcctlClient(ctx, m.client.Dce(), dcerpc.WithObjectUUID(uuid.MustParse(ScmrUuid))) + if err != nil { + log.Error().Err(err).Msg("Failed to initialize SVCCTL client") + return fmt.Errorf("create SVCCTL client: %w", err) + } + log.Info().Msg("Created SVCCTL client") + + resp, err := m.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{ + MachineName: m.hostname, + DatabaseName: "ServicesActive\x00", + DesiredAccess: ServiceAllAccess, // TODO: Replace + }) + if err != nil { + log.Debug().Err(err).Msg("Failed to open SCM handle") + return fmt.Errorf("open SCM handle: %w", err) + } + log.Info().Msg("Opened SCM handle") + + m.scm = resp.SCM + + return +} + +func (m *Scmr) Reconnect(ctx context.Context) (err error) { + + if err = m.client.Reconnect(ctx); err != nil { + return fmt.Errorf("reconnect: %w", err) + } + if err = m.Init(ctx, m.client); err != nil { + return fmt.Errorf("reconnect SCMR: %w", err) + } + return +} + +// openService will a handle to the desired service +func (m *Scmr) openService(ctx context.Context, name string) (svc *service, err error) { + + log := zerolog.Ctx(ctx) + + resp, err := m.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{ + ServiceManager: m.scm, + ServiceName: name, + DesiredAccess: ServiceAllAccess, // TODO: dynamic + }) + if err != nil { + log.Error().Err(err).Msg("Failed to open service handle") + return nil, fmt.Errorf("open service: %w", err) + } + + log.Info().Msg("Opened service handle") + + svc = new(service) + svc.name = name + svc.handle = resp.Service + + return +} + +func (m *Scmr) startService(ctx context.Context, svc *service) error { + + log := zerolog.Ctx(ctx) + + sr, err := m.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle}) + + if err != nil { + + if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case) + log.Warn().Msg("Service execution deadline exceeded") + svc.handle = nil + return nil + + } else if sr.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT { + log.Info().Msg("Received request timeout. Execution was likely successful") + return nil + } + + log.Error().Err(err).Msg("Failed to start service") + return fmt.Errorf("start service: %w", err) + } + log.Info().Msg("Service started successfully") + return nil +} diff --git a/pkg/goexec/scmr/scmr.go b/pkg/goexec/scmr/scmr.go new file mode 100644 index 0000000..3ee8f9c --- /dev/null +++ b/pkg/goexec/scmr/scmr.go @@ -0,0 +1,19 @@ +package scmrexec + +import ( + "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 +) + +type service struct { + name string + handle *svcctl.Handle + originalConfig *svcctl.QueryServiceConfigW +} diff --git a/pkg/goexec/smb/client.go b/pkg/goexec/smb/client.go new file mode 100644 index 0000000..d9e8772 --- /dev/null +++ b/pkg/goexec/smb/client.go @@ -0,0 +1,111 @@ +package smb + +import ( + "context" + "errors" + "fmt" + "github.com/oiweiwei/go-smb2.fork" + "github.com/rs/zerolog" + "net" +) + +type Client struct { + ClientOptions + + conn net.Conn + sess *smb2.Session + mount *smb2.Share +} + +func (c *Client) Session() (sess *smb2.Session) { + return c.sess +} + +func (c *Client) String() string { + return ClientName +} + +func (c *Client) Logger(ctx context.Context) zerolog.Logger { + return zerolog.Ctx(ctx).With().Str("client", c.String()).Logger() +} + +func (c *Client) Mount(_ context.Context, share string) (err error) { + + if c.sess == nil { + return errors.New("SMB session not initialized") + } + + c.mount, err = c.sess.Mount(share) + return +} + +func (c *Client) Connect(ctx context.Context) (err error) { + + log := c.Logger(ctx) + { + if c.netDialer == nil { + panic(fmt.Errorf("TCP dialer not initialized")) + } + if c.dialer == nil { + panic(fmt.Errorf("%s dialer not initialized", c.String())) + } + } + + // Establish TCP connection + c.conn, err = c.netDialer.Dial("tcp", net.JoinHostPort(c.Host, fmt.Sprintf("%d", c.Port))) + + if err != nil { + return err + } + + log = log.With().Str("address", c.conn.RemoteAddr().String()).Logger() + log.Debug().Msgf("Connected to %s server", c.String()) + + // Open SMB session + c.sess, err = c.dialer.DialContext(ctx, c.conn) + + if err != nil { + log.Error().Err(err).Msgf("Failed to open %s session", c.String()) + return fmt.Errorf("dial %s: %w", c.String(), err) + } + + log.Debug().Msgf("Opened %s session", c.String()) + + return +} + +func (c *Client) Close(ctx context.Context) (err error) { + + log := c.Logger(ctx) + + // Close TCP connection + if c.conn != nil { + defer func() { + if err = c.conn.Close(); err != nil { + log.Error().Err(err).Msgf("Failed to close %s connection", c.String()) + } + log.Debug().Msgf("Closed %s connection", c.String()) + }() + } + + // Close SMB session + if c.sess != nil { + defer func() { + if err = c.sess.Logoff(); err != nil { + log.Error().Err(err).Msgf("Failed to discard %s session", c.String()) + } + log.Debug().Msgf("Discarded %s session", c.String()) + }() + } + + // Unmount SMB share + if c.mount != nil { + defer func() { + if err = c.mount.Umount(); err != nil { + log.Error().Err(err).Msg("Failed to unmount share") + } + log.Debug().Msg("Unmounted file share") + }() + } + return +} diff --git a/pkg/goexec/smb/default.go b/pkg/goexec/smb/default.go new file mode 100644 index 0000000..94f50de --- /dev/null +++ b/pkg/goexec/smb/default.go @@ -0,0 +1,10 @@ +package smb + +import "github.com/oiweiwei/go-msrpc/smb2" + +const ( + ClientName = "SMB" + + DefaultPort = 445 + DefaultDialect = smb2.SMB311 +) diff --git a/pkg/goexec/smb/options.go b/pkg/goexec/smb/options.go new file mode 100644 index 0000000..0c2ffb6 --- /dev/null +++ b/pkg/goexec/smb/options.go @@ -0,0 +1,98 @@ +package smb + +import ( + "context" + "errors" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/RedTeamPentesting/adauth/smbauth" + msrpcSMB2 "github.com/oiweiwei/go-msrpc/smb2" + "github.com/oiweiwei/go-smb2.fork" + "net" +) + +var supportedDialects = map[msrpcSMB2.Dialect]msrpcSMB2.Dialect{ + 2_0_2: msrpcSMB2.SMB202, + 2_1_0: msrpcSMB2.SMB210, + 3_0_0: msrpcSMB2.SMB300, + 3_0_2: msrpcSMB2.SMB302, + 3_1_1: msrpcSMB2.SMB311, + + 0x202: msrpcSMB2.SMB202, + 0x210: msrpcSMB2.SMB210, + 0x300: msrpcSMB2.SMB300, + 0x302: msrpcSMB2.SMB302, + 0x311: msrpcSMB2.SMB311, +} + +// ClientOptions holds configuration settings for an SMB client +type ClientOptions struct { + goexec.ClientOptions + goexec.AuthOptions + + // NoSign disables packet signing + NoSign bool `json:"no_sign" yaml:"no_sign"` + + // NoSeal disables packet encryption + NoSeal bool `json:"no_seal" yaml:"no_seal"` + + // Dialect sets the SMB dialect to be passed to smb2.WithDialect() + Dialect msrpcSMB2.Dialect `json:"dialect" yaml:"dialect"` + + netDialer goexec.Dialer + dialer *smb2.Dialer +} + +func (c *Client) Parse(ctx context.Context) (err error) { + + var do []msrpcSMB2.DialerOption + + if c.Port == 0 { + c.Port = DefaultPort + } + if c.Dialect == 0 { + c.Dialect = DefaultDialect + } + + // Validate SMB dialect/version + if d, ok := supportedDialects[c.Dialect]; ok { + do = append(do, msrpcSMB2.WithDialect(d)) + + } else { + return errors.New("unsupported SMB version") + } + + if c.Proxy == "" { + c.netDialer = &net.Dialer{} // FUTURE: additional dial c + + } else { + // Parse proxy URL + c.netDialer, err = goexec.ParseProxyURI(c.Proxy) + if err != nil { + return err + } + } + + if !c.NoSeal { + // Enable encryption + do = append(do, msrpcSMB2.WithSeal()) + } + if !c.NoSign { + // Enable signing + do = append(do, msrpcSMB2.WithSign()) + } + + // Validate authentication parameters + c.dialer, err = smbauth.Dialer(ctx, c.Credential, c.Target, + &smbauth.Options{ + SMBOptions: do, + }) + + if err != nil { + return fmt.Errorf("set %s auth: %w", ClientName, err) + } + + c.Host = c.Target.AddressWithoutPort() + + return nil +} diff --git a/pkg/goexec/smb/output.go b/pkg/goexec/smb/output.go new file mode 100644 index 0000000..35c40c8 --- /dev/null +++ b/pkg/goexec/smb/output.go @@ -0,0 +1,63 @@ +package smb + +import ( + "context" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "io" + "os" + "regexp" + "time" +) + +var ( + DefaultOutputPollInterval = 1 * time.Second + DefaultOutputPollTimeout = 60 * time.Second + pathPrefix = regexp.MustCompile(`^([a-zA-Z]:)?\\*`) +) + +type OutputFileFetcher struct { + goexec.Cleaner + + Client *Client + Share string + File string + PollInterval time.Duration + PollTimeout time.Duration + + relativePath string +} + +func (o *OutputFileFetcher) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) { + + if o.PollInterval == 0 { + o.PollInterval = DefaultOutputPollInterval + } + if o.PollTimeout == 0 { + o.PollTimeout = DefaultOutputPollTimeout + } + + o.relativePath = pathPrefix.ReplaceAllString(o.File, "") + + err = o.Client.Connect(ctx) + if err != nil { + return + } + + err = o.Client.Mount(ctx, o.Share) + if err != nil { + return + } + + stopAt := time.Now().Add(o.PollTimeout) + + for { + if time.Now().After(stopAt) { + return + } + if reader, err = o.Client.mount.OpenFile(o.relativePath, os.O_RDONLY, 0); err == nil { + return + } + time.Sleep(o.PollInterval) + } + return +} diff --git a/pkg/goexec/tsch/create.go b/pkg/goexec/tsch/create.go new file mode 100644 index 0000000..c0f24d1 --- /dev/null +++ b/pkg/goexec/tsch/create.go @@ -0,0 +1,113 @@ +package tschexec + +import ( + "context" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/FalconOpsLLC/goexec/pkg/goexec/smb" + "github.com/rs/zerolog" + "time" +) + +const ( + MethodCreate = "Create" +) + +type TschCreate struct { + Tsch + goexec.Executor + goexec.Cleaner + smb.OutputFileFetcher + + IO goexec.ExecutionIO + + NoDelete bool + CallDelete bool + StartDelay time.Duration + StopDelay time.Duration + DeleteDelay time.Duration + TimeOffset time.Duration + // FEATURE: more opts +} + +func (m *TschCreate) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName). + Str("method", MethodCreate). + Str("task", m.TaskName). + Logger() + + startTime := time.Now().UTC().Add(m.StartDelay) + stopTime := startTime.Add(m.StopDelay) + + trigger := taskTimeTrigger{ + StartBoundary: startTime.Format(TaskXmlDurationFormat), + Enabled: true, + } + + var deleteAfter string + + if !m.NoDelete && !m.CallDelete { + + if m.StopDelay == 0 { + m.StopDelay = time.Second // value is required, 1 second by default + } + trigger.EndBoundary = stopTime.Format(TaskXmlDurationFormat) + deleteAfter = xmlDuration(m.DeleteDelay) + } + + path, err := m.registerTask(ctx, + ®isterOptions{ + AllowStartOnDemand: true, + AllowHardTerminate: true, + Hidden: !m.NotHidden, + triggers: taskTriggers{ + TimeTriggers: []taskTimeTrigger{trigger}, + }, + DeleteAfter: deleteAfter, + }, + execIO, + ) + if err != nil { + return err + } + + if !m.NoDelete { + if m.CallDelete { + + m.AddCleaner(func(ctxInner context.Context) error { + + log.Info().Msg("Waiting for task to start...") + + select { + case <-ctxInner.Done(): + log.Warn().Msg("Task deletion cancelled") + + case <-time.After(m.StartDelay + (5 * time.Second)): // 5 second buffer + /* + for { + stat, err := m.tsch.GetLastRunInfo(ctx, &itaskschedulerservice.GetLastRunInfoRequest{ + Path: path, + }) + if err != nil { + log.Warn().Err(err).Msg("Failed to get last run info. Assuming task was executed") + + } else if stat.LastRuntime.AsTime().IsZero() { + log.Warn().Msg("Task was not yet executed. Waiting 5 additional seconds") + + time.Sleep(5 * time.Second) + continue + } + break + } + */ + } + return m.deleteTask(ctxInner, path) + }) + + } else { + log.Info().Time("when", stopTime).Msg("Task is scheduled to delete") + } + } + return +} diff --git a/pkg/goexec/tsch/demand.go b/pkg/goexec/tsch/demand.go new file mode 100644 index 0000000..cd2ccd5 --- /dev/null +++ b/pkg/goexec/tsch/demand.go @@ -0,0 +1,91 @@ +package tschexec + +import ( + "context" + "errors" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" + "github.com/rs/zerolog" + "io" +) + +const ( + MethodDemand = "Demand" +) + +type TschDemand struct { + Tsch + goexec.Executor + goexec.Cleaner + + IO goexec.ExecutionIO + + NoDelete bool + NoStart bool + SessionId uint32 +} + +func (m *TschDemand) Execute(ctx context.Context, in *goexec.ExecutionIO) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName). + Str("method", MethodDemand). + Str("task", m.TaskName). + Logger() + + path, err := m.registerTask(ctx, + ®isterOptions{ + AllowStartOnDemand: true, + AllowHardTerminate: true, + Hidden: !m.NotHidden, + triggers: taskTriggers{}, + }, + in, + ) + if err != nil { + return err + } + + log.Info().Msg("Task registered") + + if !m.NoDelete { + m.AddCleaner(func(ctxInner context.Context) error { + return m.deleteTask(ctxInner, path) + }) + } + + if !m.NoStart { + + var flags uint32 + if m.SessionId != 0 { + flags |= 4 + } + + runResponse, err := m.tsch.Run(ctx, &itaskschedulerservice.RunRequest{ + Path: path, + Flags: flags, + SessionID: m.SessionId, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to run task") + return fmt.Errorf("run task: %w", err) + } + if ret := uint32(runResponse.Return); ret != 0 { + log.Error().Str("code", fmt.Sprintf("0x%08x", ret)).Msg("Task returned non-zero exit code") + return fmt.Errorf("task returned non-zero exit code: 0x%08x", ret) + } + + log.Info().Msg("Task started successfully") + } + return +} + +func (m *TschDemand) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) { + + if m.IO.Output != nil { + return m.IO.GetOutput(ctx) + } + return nil, errors.New("no available output provider") +} diff --git a/pkg/goexec/tsch/module.go b/pkg/goexec/tsch/module.go new file mode 100644 index 0000000..13d7b24 --- /dev/null +++ b/pkg/goexec/tsch/module.go @@ -0,0 +1,173 @@ +package tschexec + +import ( + "context" + "encoding/xml" + "errors" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" + "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" + "github.com/rs/zerolog" + "strings" +) + +const ( + ModuleName = "TSCH" +) + +type Tsch struct { + goexec.Cleaner + + Client *dce.Client + tsch itaskschedulerservice.TaskSchedulerServiceClient + + TaskName string + TaskPath string + UserSid string + NotHidden bool +} + +type registerOptions struct { + AllowStartOnDemand bool + AllowHardTerminate bool + StartWhenAvailable bool + Hidden bool + DeleteAfter string + + triggers taskTriggers +} + +func (m *Tsch) Connect(ctx context.Context) (err error) { + + if err = m.Client.Connect(ctx); err == nil { + m.AddCleaner(m.Client.Close) + } + return +} + +func (m *Tsch) Init(ctx context.Context) (err error) { + + if m.Client.Dce() == nil { + return errors.New("DCE connection not initialized") + } + + // Create ITaskSchedulerService Client + m.tsch, err = itaskschedulerservice.NewTaskSchedulerServiceClient(ctx, m.Client.Dce()) + return +} + +func (m *Tsch) taskPath() string { + if m.TaskPath == "" { + m.TaskPath = `\` + m.TaskName + } + return m.TaskPath +} + +func (m *Tsch) registerTask(ctx context.Context, opts *registerOptions, in *goexec.ExecutionIO) (path string, err error) { + + log := zerolog.Ctx(ctx).With(). + Str("task", m.TaskName). + Logger() + + ctx = log.WithContext(ctx) + + principalId := "1" + + settings := taskSettings{ + MultipleInstancesPolicy: "IgnoreNew", + IdleSettings: taskIdleSettings{ + StopOnIdleEnd: true, + RestartOnIdle: false, + }, + Enabled: true, + Priority: 7, // a pretty standard value for scheduled tasks + + AllowHardTerminate: opts.AllowHardTerminate, + AllowStartOnDemand: opts.AllowStartOnDemand, + Hidden: opts.Hidden, + StartWhenAvailable: opts.StartWhenAvailable, + DeleteExpiredTaskAfter: opts.DeleteAfter, + } + + principals := taskPrincipals{ + Principals: []taskPrincipal{ + { + ID: principalId, // TODO: dynamic + UserID: m.UserSid, + RunLevel: "HighestAvailable", + }, + }} + + e := taskActionExec{} + + if ea := strings.SplitN(in.String(), " ", 2); len(ea) == 1 { + e.Command = ea[0] + } else { + e.Command = ea[0] + e.Arguments = ea[1] + } + + actions := taskActions{ + Context: principalId, + Exec: []taskActionExec{e}, + } + + def := task{ + TaskVersion: TaskXmlVersion, + TaskNamespace: TaskXmlNamespace, + Triggers: opts.triggers, + Actions: actions, + Principals: principals, + Settings: settings, + } + + // Generate task XML content. See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f + + doc, err := xml.Marshal(def) + + if err != nil { + log.Error().Err(err).Msg("failed to marshal task XML") + return "", fmt.Errorf("marshal task: %w", err) + } + + taskXml := TaskXmlHeader + string(doc) + + log.Debug().Str("content", taskXml).Msg("Generated task XML") + + registerResponse, err := m.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ + Path: m.taskPath(), + XML: taskXml, + Flags: 0, // FEATURE: dynamic + SDDL: "", + LogonType: 0, // FEATURE: dynamic + CredsCount: 0, + Creds: nil, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to register task") + return "", fmt.Errorf("register task: %w", err) + } + + return registerResponse.ActualPath, nil +} + +func (m *Tsch) deleteTask(ctx context.Context, taskPath string) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("path", taskPath).Logger() + + _, err = m.tsch.Delete(ctx, &itaskschedulerservice.DeleteRequest{ + Path: taskPath, + }) + + if err != nil { + log.Error().Err(err).Msg("Failed to delete task") + return fmt.Errorf("delete task: %w", err) + } + + log.Info().Msg("Task deleted") + + return +} diff --git a/pkg/goexec/tsch/tsch.go b/pkg/goexec/tsch/tsch.go new file mode 100644 index 0000000..e51433d --- /dev/null +++ b/pkg/goexec/tsch/tsch.go @@ -0,0 +1,166 @@ +package tschexec + +import ( + "encoding/xml" + "fmt" + "regexp" + "time" +) + +const ( + TaskXmlHeader = `` + TaskXmlNamespace = "http://schemas.microsoft.com/windows/2004/02/mit/task" + TaskXmlVersion = "1.2" + TaskXmlDurationFormat = "2006-01-02T15:04:05.9999999Z" +) + +var ( + TaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`) + TaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`) +) + +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f + +type taskTriggers struct { + XMLName xml.Name `xml:"Triggers"` + TimeTriggers []taskTimeTrigger `xml:"TimeTrigger,omitempty"` +} + +type taskTimeTrigger struct { + XMLName xml.Name `xml:"TimeTrigger"` + StartBoundary string `xml:"StartBoundary,omitempty"` // Derived from time.Time + EndBoundary string `xml:"EndBoundary,omitempty"` // Derived from time.Time; must be > StartBoundary + Enabled bool `xml:"Enabled"` +} + +type taskIdleSettings struct { + XMLName xml.Name `xml:"IdleSettings"` + StopOnIdleEnd bool `xml:"StopOnIdleEnd"` + RestartOnIdle bool `xml:"RestartOnIdle"` +} + +type taskSettings struct { + XMLName xml.Name `xml:"Settings"` + Enabled bool `xml:"Enabled"` + Hidden bool `xml:"Hidden"` + DisallowStartIfOnBatteries bool `xml:"DisallowStartIfOnBatteries"` + StopIfGoingOnBatteries bool `xml:"StopIfGoingOnBatteries"` + AllowHardTerminate bool `xml:"AllowHardTerminate"` + RunOnlyIfNetworkAvailable bool `xml:"RunOnlyIfNetworkAvailable"` + AllowStartOnDemand bool `xml:"AllowStartOnDemand"` + WakeToRun bool `xml:"WakeToRun"` + RunOnlyIfIdle bool `xml:"RunOnlyIfIdle"` + StartWhenAvailable bool `xml:"StartWhenAvailable"` + Priority int `xml:"Priority,omitempty"` // 1 to 10 inclusive + MultipleInstancesPolicy string `xml:"MultipleInstancesPolicy,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + DeleteExpiredTaskAfter string `xml:"DeleteExpiredTaskAfter,omitempty"` // Derived from time.Duration + IdleSettings taskIdleSettings `xml:"IdleSettings,omitempty"` +} + +type taskActionExec struct { + XMLName xml.Name `xml:"Exec"` + Command string `xml:"Command"` + Arguments string `xml:"Arguments,omitempty"` +} + +type taskActions struct { + XMLName xml.Name `xml:"Actions"` + Context string `xml:"Context,attr"` + Exec []taskActionExec `xml:"Exec,omitempty"` +} + +type taskPrincipals struct { + XMLName xml.Name `xml:"Principals"` + Principals []taskPrincipal `xml:"Principal,omitempty"` +} + +type taskPrincipal struct { + XMLName xml.Name `xml:"Principal"` + ID string `xml:"id,attr"` + UserID string `xml:"UserId"` + RunLevel string `xml:"RunLevel"` +} + +type task struct { + XMLName xml.Name `xml:"Task"` + TaskVersion string `xml:"version,attr"` + TaskNamespace string `xml:"xmlns,attr"` + Triggers taskTriggers `xml:"Triggers"` + Actions taskActions `xml:"Actions"` + Principals taskPrincipals `xml:"Principals"` + Settings taskSettings `xml:"Settings"` +} + +// newSettings just creates a taskSettings instance with the necessary values + a few dynamic ones +func newSettings(terminate, onDemand, startWhenAvailable bool) *taskSettings { + return &taskSettings{ + MultipleInstancesPolicy: "IgnoreNew", + AllowHardTerminate: terminate, + IdleSettings: taskIdleSettings{ + StopOnIdleEnd: true, + RestartOnIdle: false, + }, + AllowStartOnDemand: onDemand, + Enabled: true, + Hidden: true, + Priority: 7, // a pretty standard value for scheduled tasks + StartWhenAvailable: startWhenAvailable, + } +} + +// newTask creates a task with any static values filled +func newTask(se *taskSettings, pr []taskPrincipal, tr taskTriggers, cmd, args string) *task { + if se == nil { + se = newSettings(true, true, false) + } + if pr == nil || len(pr) == 0 { + pr = []taskPrincipal{ + { + ID: "1", + UserID: "S-1-5-18", + RunLevel: "HighestAvailable", + }, + } + } + return &task{ + TaskVersion: "1.2", + TaskNamespace: "http://schemas.microsoft.com/windows/2004/02/mit/task", + Triggers: tr, + Principals: taskPrincipals{Principals: pr}, + Settings: *se, + Actions: taskActions{ + Context: pr[0].ID, + Exec: []taskActionExec{ + { + Command: cmd, + Arguments: args, + }, + }, + }, + } +} + +// xmlDuration is a *very* simple implementation of xs:duration - only accepts +seconds +func xmlDuration(dur time.Duration) string { + if s := int(dur.Seconds()); s >= 0 { + return fmt.Sprintf(`PT%dS`, s) + } + return `PT0S` +} + +// ValidateTaskName will validate the provided task name according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1 +func ValidateTaskName(taskName string) error { + if !TaskNameRegex.MatchString(taskName) { + return fmt.Errorf("invalid task name: %s", taskName) + } + return nil +} + +// ValidateTaskPath will validate the provided task path according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1 +func ValidateTaskPath(taskPath string) error { + if !TaskPathRegex.MatchString(taskPath) { + return fmt.Errorf("invalid task path: %s", taskPath) + } + return nil +} diff --git a/pkg/goexec/wmi/call.go b/pkg/goexec/wmi/call.go new file mode 100644 index 0000000..e4386b5 --- /dev/null +++ b/pkg/goexec/wmi/call.go @@ -0,0 +1,31 @@ +package wmiexec + +import ( + "context" + "encoding/json" + "github.com/rs/zerolog" +) + +const ( + MethodCall = "Call" +) + +type WmiCall struct { + Wmi + + Class string + Method string + Args map[string]any +} + +func (m *WmiCall) Call(ctx context.Context) (out []byte, err error) { + var outMap map[string]any + + if outMap, err = m.query(ctx, m.Class, m.Method, m.Args); err != nil { + return + } + zerolog.Ctx(ctx).Info().Msg("Call succeeded") + + out, err = json.Marshal(outMap) + return +} diff --git a/pkg/goexec/wmi/module.go b/pkg/goexec/wmi/module.go new file mode 100644 index 0000000..20d687a --- /dev/null +++ b/pkg/goexec/wmi/module.go @@ -0,0 +1,140 @@ +package wmiexec + +import ( + "context" + "errors" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemlevel1login/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmio/query" + "github.com/rs/zerolog" +) + +const ( + ModuleName = "WMI" + DefaultEndpoint = "ncacn_ip_tcp:[135]" +) + +type Wmi struct { + goexec.Cleaner + Client *dce.Client + + Resource string + + servicesClient iwbemservices.ServicesClient +} + +func (m *Wmi) Connect(ctx context.Context) (err error) { + + if err = m.Client.Connect(ctx); err == nil { + m.AddCleaner(m.Client.Close) + } + return +} + +func (m *Wmi) Init(ctx context.Context) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName).Logger() + + if m.Client.Dce() == nil { + return errors.New("DCE connection not initialized") + } + + actClient, err := iactivation.NewActivationClient(ctx, m.Client.Dce()) + if err != nil { + log.Error().Err(err).Msg("Failed to initialize IActivation client") + return fmt.Errorf("create IActivation client: %w", err) + } + + actResponse, err := actClient.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ + ORPCThis: ORPCThis, + ClassID: wmi.Level1LoginClassID.GUID(), + IIDs: []*dcom.IID{iwbemlevel1login.Level1LoginIID}, + RequestedProtocolSequences: []uint16{ProtocolSequenceRPC}, // FEATURE: Named pipe support? + }) + if err != nil { + log.Error().Err(err).Msg("Failed to activate remote object") + return fmt.Errorf("request remote activation: %w", err) + } + if actResponse.HResult != 0 { + return fmt.Errorf("remote activation failed with code %d", actResponse.HResult) + } + + log.Info().Msg("Remote activation succeeded") + + var newOpts []dcerpc.Option + + for _, bind := range actResponse.OXIDBindings.GetStringBindings() { + stringBinding, err := dcerpc.ParseStringBinding("ncacn_ip_tcp:" + bind.NetworkAddr) // TODO: try bind.String() + + if err != nil { + log.Error().Err(err).Msg("Failed to parse string binding") + continue + } + stringBinding.NetworkAddress = m.Client.Target.AddressWithoutPort() + newOpts = append(newOpts, dcerpc.WithEndpoint(stringBinding.String())) + } + + if err = m.Client.Reconnect(ctx, newOpts...); err != nil { + log.Error().Err(err).Msg("Failed to connect to remote instance") + return fmt.Errorf("connect remote instance: %w", err) + } + + log.Info().Msg("Connected to remote instance") + + ipid := actResponse.InterfaceData[0].GetStandardObjectReference().Std.IPID + loginClient, err := iwbemlevel1login.NewLevel1LoginClient(ctx, m.Client.Dce(), dcom.WithIPID(ipid)) + + if err != nil { + log.Error().Err(err).Msg("Failed to create IWbemLevel1Login client") + return fmt.Errorf("create IWbemLevel1Login client: %w", err) + } + + login, err := loginClient.NTLMLogin(ctx, &iwbemlevel1login.NTLMLoginRequest{ + This: ORPCThis, + NetworkResource: m.Resource, + }) + + log.Info().Msg("Completed NTLMLogin operation") + + if err != nil { + log.Error().Err(err).Msg("Failed to login on remote instance") + return fmt.Errorf("login: IWbemLevel1Login::NTLMLogin: %w", err) + } + + ipid = login.Namespace.InterfacePointer().IPID() + m.servicesClient, err = iwbemservices.NewServicesClient(ctx, m.Client.Dce(), dcom.WithIPID(ipid)) + + if err != nil { + log.Error().Err(err).Msg("Failed to create services client") + return fmt.Errorf("create IWbemServices client: %w", err) + } + + log.Info().Msg("Initialized services client") + + return +} + +func (m *Wmi) query(ctx context.Context, class, method string, values map[string]any) (outValues map[string]any, err error) { + outValues = make(map[string]any) + + if m.servicesClient == nil { + return nil, errors.New("module has not been initialized") + } + if out, err := query.NewBuilder(ctx, m.servicesClient, ComVersion). + Spawn(class). // The class to instantiate (i.e. Win32_Process) + Method(method). // The method to call (i.e. Create) + Values(values). // The values to pass to method + Exec(). + Object(); err == nil { + return out.Values(), err + } + return nil, fmt.Errorf("spawn WMI query: %w", err) +} diff --git a/pkg/goexec/wmi/proc.go b/pkg/goexec/wmi/proc.go new file mode 100644 index 0000000..25e515c --- /dev/null +++ b/pkg/goexec/wmi/proc.go @@ -0,0 +1,64 @@ +package wmiexec + +import ( + "context" + "errors" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/rs/zerolog" + "io" +) + +const ( + MethodProc = "Proc" +) + +type WmiProc struct { + Wmi + IO goexec.ExecutionIO + WorkingDirectory string +} + +func (m *WmiProc) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { + + log := zerolog.Ctx(ctx).With(). + Str("module", ModuleName). + Str("method", MethodProc). + Logger() + ctx = log.WithContext(ctx) + + if execIO == nil { + return errors.New("execution IO is nil") + } + + out, err := m.query(ctx, + "Win32_Process", + "Create", + + map[string]any{ + "CommandLine": execIO.String(), + "WorkingDir": m.WorkingDirectory, + }, + ) + if err != nil { + return + } + + if pid := out["ProcessId"].(uint32); pid != 0 { + log = log.With().Uint32("pid", pid).Logger() + } + log.Info().Err(err).Msg("Process created") + + if ret := out["ReturnValue"].(uint32); ret != 0 { + log.Error().Err(err).Uint32("return", ret).Msg("Process returned non-zero exit code") + } + return +} + +func (m *WmiProc) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) { + + if m.IO.Output != nil { + + return m.IO.GetOutput(ctx) + } + return nil, errors.New("no available output provider") +} diff --git a/pkg/goexec/wmi/wmi.go b/pkg/goexec/wmi/wmi.go new file mode 100644 index 0000000..ad7d453 --- /dev/null +++ b/pkg/goexec/wmi/wmi.go @@ -0,0 +1,16 @@ +package wmiexec + +import "github.com/oiweiwei/go-msrpc/msrpc/dcom" + +const ( + ProtocolSequenceRPC uint16 = 7 + ProtocolSequenceNP uint16 = 15 +) + +var ( + ComVersion = &dcom.COMVersion{ + MajorVersion: 5, + MinorVersion: 7, + } + ORPCThis = &dcom.ORPCThis{Version: ComVersion} +) -- cgit v1.2.3