diff options
author | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-03-08 06:07:29 -0600 |
---|---|---|
committer | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-03-08 06:07:29 -0600 |
commit | 7574b7370be083ff563fa8ad6d01d5ac776d7e4d (patch) | |
tree | 78aeb5d671e35aee8780fd92b2511cad00efd67d | |
parent | d8eca7209d000609fea08d2973a402a41a4cf921 (diff) | |
download | goexec-7574b7370be083ff563fa8ad6d01d5ac776d7e4d.tar.gz goexec-7574b7370be083ff563fa8ad6d01d5ac776d7e4d.zip |
Add a bunch of DCE related options to TSCH module
-rw-r--r-- | TODO.md | 2 | ||||
-rw-r--r-- | cmd/root.go | 30 | ||||
-rw-r--r-- | cmd/rpc.go | 62 | ||||
-rw-r--r-- | cmd/tsch.go | 282 | ||||
-rw-r--r-- | cmd/wmi.go | 10 | ||||
-rw-r--r-- | internal/client/dce/dce.go | 21 | ||||
-rw-r--r-- | internal/exec/exec.go | 4 | ||||
-rw-r--r-- | internal/exec/tsch/exec.go | 499 | ||||
-rw-r--r-- | internal/exec/tsch/module.go | 56 | ||||
-rw-r--r-- | internal/exec/tsch/task.go | 85 | ||||
-rw-r--r-- | internal/exec/tsch/tsch.go | 183 | ||||
-rw-r--r-- | internal/exec/wmi/exec.go | 6 |
12 files changed, 734 insertions, 506 deletions
@@ -12,12 +12,12 @@ - [ ] Fix SCMR `change` method so that dependencies field isn't permanently overwritten - [ ] Add `delete` command to all modules that may involve cleanup - use `tsch delete` for reference - [ ] Standardize modules to interface for future use +- [ ] 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) ## Resolve Eventually ### Higher Priority - [ ] Add dcom module -- [ ] 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) ### Lower Priority - [ ] `--ctf` option - allow unsafe OPSEC (i.e. fetching execution output via file write/read) diff --git a/cmd/root.go b/cmd/root.go index 9a84e28..441cafc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,20 +23,22 @@ var ( executableArgs string workingDirectory string - needsTarget = func(cmd *cobra.Command, args []string) (err error) { - if len(args) != 1 { - return fmt.Errorf("command require exactly one positional argument: [target]") - } - if creds, target, err = authOpts.WithTarget(ctx, "cifs", 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") + needsTarget = func(proto string) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) (err error) { + if len(args) != 1 { + return fmt.Errorf("command require exactly one positional argument: [target]") + } + if creds, target, err = authOpts.WithTarget(ctx, proto, args[0]); err != nil { + return fmt.Errorf("failed to parse target: %w", err) + } + if creds == nil { + return fmt.Errorf("no credentials supplied") + } + if target == nil { + return fmt.Errorf("no target supplied") + } + return } - return } rootCmd = &cobra.Command{ @@ -67,10 +69,8 @@ func init() { scmrCmdInit() rootCmd.AddCommand(scmrCmd) - tschCmdInit() rootCmd.AddCommand(tschCmd) - wmiCmdInit() rootCmd.AddCommand(wmiCmd) } diff --git a/cmd/rpc.go b/cmd/rpc.go new file mode 100644 index 0000000..9a92a89 --- /dev/null +++ b/cmd/rpc.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "fmt" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/spf13/cobra" + "regexp" +) + +var ( + // DCE options + argDceStringBinding string + argDceEpmFilter string + argDceEpmAuto bool + argDceNoEpm bool + argDceNoSeal bool + argDceNoSign bool + dceStringBinding *dcerpc.StringBinding + dceOptions []dcerpc.Option + + needsRpcTarget = func(proto string) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) (err error) { + if argDceStringBinding != "" { + dceStringBinding, err = dcerpc.ParseStringBinding(argDceStringBinding) + if err != nil { + return fmt.Errorf("failed to parse RPC endpoint: %w", err) + } + argDceNoEpm = 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 += ":" + } + dceStringBinding, err = dcerpc.ParseStringBinding(argDceEpmFilter) + if err != nil { + return fmt.Errorf("failed to parse EPM filter: %w", err) + } + } + if !argDceNoSign { + dceOptions = append(dceOptions, dcerpc.WithSign()) + } + if argDceNoSeal { + dceOptions = append(dceOptions, dcerpc.WithInsecure()) + } else { + dceOptions = append(dceOptions, dcerpc.WithSeal()) + } + return needsTarget(proto)(cmd, args) + } + } +) + +func registerRpcFlags(cmd *cobra.Command) { + cmd.PersistentFlags().StringVarP(&argDceEpmFilter, "epm-filter", "F", "", "String binding to filter endpoints returned by EPM") + cmd.PersistentFlags().StringVar(&argDceStringBinding, "endpoint", "", "Explicit RPC endpoint definition") + cmd.PersistentFlags().BoolVar(&argDceNoEpm, "no-epm", false, "Do not use EPM to automatically detect endpoints") + cmd.PersistentFlags().BoolVar(&argDceNoSign, "no-sign", false, "Disable signing on DCE messages") + cmd.PersistentFlags().BoolVar(&argDceNoSeal, "no-seal", false, "Disable packet stub encryption on DCE messages") + cmd.PersistentFlags().BoolVar(&argDceEpmAuto, "epm-auto", false, "Automatically detect endpoints instead of using the module defaults") + cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter") + cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter") +} diff --git a/cmd/tsch.go b/cmd/tsch.go index 05c55cf..8011cf2 100644 --- a/cmd/tsch.go +++ b/cmd/tsch.go @@ -1,70 +1,78 @@ package cmd import ( - "github.com/FalconOpsLLC/goexec/internal/exec" - "github.com/FalconOpsLLC/goexec/internal/exec/tsch" - "github.com/spf13/cobra" - "time" + "github.com/FalconOpsLLC/goexec/internal/client/dce" + "github.com/FalconOpsLLC/goexec/internal/exec" + "github.com/FalconOpsLLC/goexec/internal/exec/tsch" + "github.com/spf13/cobra" + "time" ) func tschCmdInit() { - tschDeleteCmdInit() - tschCmd.AddCommand(tschDeleteCmd) + registerRpcFlags(tschCmd) - tschRegisterCmdInit() - tschCmd.AddCommand(tschRegisterCmd) - - tschDemandCmdInit() - tschCmd.AddCommand(tschDemandCmd) + tschDeleteCmdInit() + tschCmd.AddCommand(tschDeleteCmd) + tschRegisterCmdInit() + tschCmd.AddCommand(tschRegisterCmd) + tschDemandCmdInit() + tschCmd.AddCommand(tschDemandCmd) } func tschDeleteCmdInit() { - tschDeleteCmd.Flags().StringVarP(&tschTaskPath, "path", "t", "", "Scheduled task path") - tschDeleteCmd.MarkFlagRequired("path") + tschDeleteCmd.Flags().StringVarP(&tschTaskPath, "path", "t", "", "Scheduled task path") + if err := tschDeleteCmd.MarkFlagRequired("path"); err != nil { + panic(err) + } } 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(&tschName, "name", "n", "", "Target task name") - tschDemandCmd.Flags().BoolVar(&tschNoDelete, "no-delete", false, "Don't delete task after execution") - tschDemandCmd.MarkFlagRequired("executable") + 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(&tschName, "name", "n", "", "Target task name") + tschDemandCmd.Flags().BoolVar(&tschNoDelete, "no-delete", false, "Don't delete task after execution") + if err := tschDemandCmd.MarkFlagRequired("executable"); err != nil { + panic(err) + } } 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(&tschName, "name", "n", "", "Target task name") - tschRegisterCmd.Flags().DurationVar(&tschStopDelay, "delay-stop", time.Duration(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", time.Duration(5*time.Second), "Delay between task registration and execution") - tschRegisterCmd.Flags().DurationVarP(&tschDeleteDelay, "delay-delete", "D", time.Duration(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") - tschRegisterCmd.MarkFlagRequired("executable") + 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(&tschName, "name", "n", "", "Target task name") + tschRegisterCmd.Flags().DurationVar(&tschStopDelay, "delay-stop", time.Duration(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", time.Duration(5*time.Second), "Delay between task registration and execution") + tschRegisterCmd.Flags().DurationVarP(&tschDeleteDelay, "delay-delete", "D", time.Duration(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) + } } var ( - tschNoDelete bool - tschCallDelete bool - tschDeleteDelay time.Duration - tschStopDelay time.Duration - tschDelay time.Duration - tschName string - tschTaskPath string - - tschCmd = &cobra.Command{ - Use: "tsch", - Short: "Establish execution via TSCH (ITaskSchedulerService)", - Args: cobra.NoArgs, - } - tschRegisterCmd = &cobra.Command{ - Use: "register [target]", - Short: "Register a remote scheduled task with an automatic start time", - Long: `Description: + tschNoDelete bool + tschCallDelete bool + tschDeleteDelay time.Duration + tschStopDelay time.Duration + tschDelay time.Duration + tschName string + tschTaskPath string + + tschCmd = &cobra.Command{ + Use: "tsch", + Short: "Establish execution via TSCH (ITaskSchedulerService)", + 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 @@ -76,35 +84,51 @@ References: 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: needsTarget, - Run: func(cmd *cobra.Command, args []string) { - if tschNoDelete { - log.Warn().Msg("Task will not be deleted after execution") - } - module := tschexec.Module{} - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: tschexec.MethodRegister, - - ExecutionMethodConfig: tschexec.MethodRegisterConfig{ - NoDelete: tschNoDelete, - CallDelete: tschCallDelete, - StartDelay: tschDelay, - StopDelay: tschStopDelay, - DeleteDelay: tschDeleteDelay, - TaskName: tschName, - }, - } - if err := module.Exec(log.WithContext(ctx), creds, target, execCfg); err != nil { - log.Fatal().Err(err).Msg("TSCH execution failed") - } - }, - } - tschDemandCmd = &cobra.Command{ - Use: "demand [target]", - Short: "Register a remote scheduled task and demand immediate start", - Long: `Description: + Args: needsRpcTarget("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: dce.ConnectionMethodDCEConfig{ + Endpoint: dceStringBinding, + Options: dceOptions, + EpmAuto: argDceEpmAuto, + }, + } + 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, But rather than setting a defined time when the task will start, it will additionally call SchRpcRun to forcefully start the task. @@ -113,46 +137,76 @@ 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: needsTarget, - Run: func(cmd *cobra.Command, args []string) { - if tschNoDelete { - log.Warn().Msg("Task will not be deleted after execution") - } - module := tschexec.Module{} - execCfg := &exec.ExecutionConfig{ - ExecutableName: executable, - ExecutableArgs: executableArgs, - ExecutionMethod: tschexec.MethodDemand, - - ExecutionMethodConfig: tschexec.MethodDemandConfig{ - NoDelete: tschNoDelete, - TaskName: tschName, - }, - } - if err := module.Exec(log.WithContext(ctx), creds, target, execCfg); err != nil { - log.Fatal().Err(err).Msg("TSCH execution failed") - } - }, - } - tschDeleteCmd = &cobra.Command{ - Use: "delete [target]", - Short: "Manually delete a scheduled task", - Long: `Description: + Args: needsTarget("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: dce.ConnectionMethodDCEConfig{ + Endpoint: dceStringBinding, + Options: dceOptions, + EpmAuto: argDceEpmAuto, + }, + } + execCfg := &exec.ExecutionConfig{ + ExecutableName: executable, + ExecutableArgs: executableArgs, + ExecutionMethod: tschexec.MethodDemand, + + ExecutionMethodConfig: tschexec.MethodDemandConfig{ + NoDelete: tschNoDelete, + TaskName: tschName, + }, + } + 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") + } + }, + } + tschDeleteCmd = &cobra.Command{ + Use: "delete [target]", + Short: "Manually delete a scheduled task", + Long: `Description: The delete method manually deletes a scheduled task by calling SchRpcDelete References: SchRpcDelete - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/360bb9b1-dd2a-4b36-83ee-21f12cb97cff `, - Args: needsTarget, - Run: func(cmd *cobra.Command, args []string) { - module := tschexec.Module{} - cleanCfg := &exec.CleanupConfig{ - CleanupMethod: tschexec.MethodDelete, - CleanupMethodConfig: tschexec.MethodDeleteConfig{TaskPath: tschTaskPath}, - } - if err := module.Cleanup(log.WithContext(ctx), creds, target, cleanCfg); err != nil { - log.Fatal().Err(err).Msg("TSCH cleanup failed") - } - }, - } + Args: needsTarget("cifs"), + 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: dce.ConnectionMethodDCEConfig{ + Endpoint: dceStringBinding, + Options: dceOptions, + EpmAuto: argDceEpmAuto, + }, + } + cleanCfg := &exec.CleanupConfig{ + CleanupMethod: tschexec.MethodDelete, + CleanupMethodConfig: tschexec.MethodDeleteConfig{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.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { + log.Fatal().Err(err).Msg("Cleanup failed") + } + }, + } ) @@ -59,7 +59,7 @@ References: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-classes `, Args: func(cmd *cobra.Command, args []string) (err error) { - if err = needsTarget(cmd, args); err == nil { + if err = needsTarget("cifs")(cmd, args); err == nil { if wmiArgMethod != "" && !methodRegex.MatchString(wmiArgMethod) { return fmt.Errorf("invalid CLASS.METHOD syntax: %s", wmiArgMethod) } @@ -112,10 +112,11 @@ References: References: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/create-method-in-class-win32-process `, - Args: needsTarget, + Args: needsTarget("cifs"), Run: func(cmd *cobra.Command, args []string) { - module := wmiexec.Module{} + log = log.With().Str("module", "wmi").Logger() + module := wmiexec.Module{} connCfg := &exec.ConnectionConfig{} cleanCfg := &exec.CleanupConfig{} @@ -123,6 +124,7 @@ References: ExecutableName: executable, ExecutableArgs: executableArgs, ExecutionMethod: wmiexec.MethodProcess, + ExecutionMethodConfig: wmiexec.MethodProcessConfig{ Command: command, WorkingDirectory: workingDirectory, @@ -130,10 +132,8 @@ References: } if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil { log.Fatal().Err(err).Msg("Connection failed") - } else if err := module.Exec(log.WithContext(ctx), execCfg); err != nil { log.Fatal().Err(err).Msg("Execution failed") - } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { log.Error().Err(err).Msg("Cleanup failed") } diff --git a/internal/client/dce/dce.go b/internal/client/dce/dce.go new file mode 100644 index 0000000..2be5a1e --- /dev/null +++ b/internal/client/dce/dce.go @@ -0,0 +1,21 @@ +package dce + +import "github.com/oiweiwei/go-msrpc/dcerpc" + +var ( + NP = "ncacn_np" + TCP = "ncacn_ip_tcp" + HTTP = "ncacn_http" + DefaultPorts = map[string]uint16{ + NP: 445, + TCP: 135, + HTTP: 593, + } +) + +type ConnectionMethodDCEConfig struct { + NoEpm bool // NoEpm disables EPM + EpmAuto bool // EpmAuto will find any suitable endpoint, without any filter + Endpoint *dcerpc.StringBinding + Options []dcerpc.Option +} diff --git a/internal/exec/exec.go b/internal/exec/exec.go index a89cf7b..56edead 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -5,6 +5,10 @@ import ( "github.com/RedTeamPentesting/adauth" ) +const ( + ConnectionMethodDCE = "dcerpc" +) + type ConnectionConfig struct { ConnectionMethod string ConnectionMethodConfig interface{} diff --git a/internal/exec/tsch/exec.go b/internal/exec/tsch/exec.go index 51f157c..d205776 100644 --- a/internal/exec/tsch/exec.go +++ b/internal/exec/tsch/exec.go @@ -1,289 +1,248 @@ package tschexec import ( - "context" - "encoding/xml" - "errors" - "fmt" - dcerpc2 "github.com/FalconOpsLLC/goexec/internal/client/dcerpc" - "github.com/FalconOpsLLC/goexec/internal/exec" - "github.com/FalconOpsLLC/goexec/internal/util" - "github.com/RedTeamPentesting/adauth" - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" - "github.com/rs/zerolog" - "regexp" - "time" + "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/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/msrpc/tsch/itaskschedulerservice/v1" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" + "github.com/rs/zerolog" + "time" ) const ( - TaskXMLDurationFormat = "2006-01-02T15:04:05.9999999Z" - TaskXMLHeader = `<?xml version="1.0" encoding="UTF-16"?>` + DefaultEndpoint = "ncacn_np:[atsvc]" ) var ( - TaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`) // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1 - TaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`) + TschRpcUuid = uuid.MustParse("86D35949-83C9-4044-B424-DB363231FD0C") + SupportedEndpointProtocols = []string{"ncacn_np", "ncacn_ip_tcp"} + + defaultStringBinding *dcerpc.StringBinding + initErr error ) -// *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` +func init() { + if defaultStringBinding, initErr = dcerpc.ParseStringBinding(DefaultEndpoint); initErr != nil { + panic(initErr) + } } // Connect to the target & initialize DCE & TSCH clients -func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target) (err error) { - if mod.dce == nil { - mod.dce = dcerpc2.NewDCEClient(ctx, false, &dcerpc2.SmbConfig{}) - if err = mod.dce.Connect(ctx, creds, target); err != nil { - return fmt.Errorf("DCE connect: %w", err) - } else if mod.tsch, err = itaskschedulerservice.NewTaskSchedulerServiceClient(ctx, mod.dce.DCE(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)); err != nil { - return fmt.Errorf("init MS-TSCH client: %w", err) - } - mod.log.Info().Msg("DCE connection successful") - } - return +func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) { + + //var port uint16 + var endpoint string = DefaultEndpoint + //var stringBinding = defaultStringBinding + var epmOpts []dcerpc.Option + var dceOpts []dcerpc.Option + + log := zerolog.Ctx(ctx).With(). + Str("func", "Connect").Logger() + + if mod.dce == nil { + if ccfg.ConnectionMethod == exec.ConnectionMethodDCE { + if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok { + return fmt.Errorf("invalid configuration for DCE connection method") + } else { + // Connect to ITaskSchedulerService + { + // Parse target & creds + ctx = gssapi.NewSecurityContext(ctx) + ao, err := dcerpcauth.AuthenticationOptions(ctx, creds, target, &dcerpcauth.Options{}) + if err != nil { + log.Error().Err(err).Msg("Failed to parse authentication options") + return fmt.Errorf("parse auth options: %w", err) + } + dceOpts = append(cfg.Options, + dcerpc.WithLogger(log), + dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy), // AuthLevelPktPrivacy is required for TSCH/ATSVC + dcerpc.WithObjectUUID(TschRpcUuid)) + + if cfg.Endpoint != nil { + endpoint = cfg.Endpoint.String() + } + if cfg.NoEpm { + dceOpts = append(dceOpts, dcerpc.WithEndpoint(endpoint)) + } else { + epmOpts = append(epmOpts, dceOpts...) + dceOpts = append(dceOpts, + epm.EndpointMapper(ctx, target.AddressWithoutPort(), append(epmOpts, ao...)...)) + if !cfg.EpmAuto { + dceOpts = append(dceOpts, dcerpc.WithEndpoint(endpoint)) + } + } + log = log.With().Str("endpoint", endpoint).Logger() + log.Info().Msg("Connecting to target") + /* + if !cfg.NoEpm { + mapperOpts := append(dceOpts, ao...) + dceOpts = append(dceOpts, + epm.EndpointMapper(ctx, target.AddressWithoutPort(), mapperOpts...), + dcerpc.WithEndpoint(fmt.Sprintf("%s:", stringBinding.ProtocolSequence.String()))) + + } else { + dceOpts = append(dceOpts, dcerpc.WithEndpoint(stringBinding.String())) + } + */ + // Create DCERPC dialer + mod.dce, err = dcerpc.Dial(ctx, target.AddressWithoutPort(), append(dceOpts, ao...)...) + if err != nil { + log.Error().Err(err).Msg("Failed to create DCERPC dialer") + return fmt.Errorf("create DCERPC dialer: %w", err) + } + + // Create ITaskSchedulerService + 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, creds *adauth.Credential, target *adauth.Target, ccfg *exec.CleanupConfig) (err error) { - mod.log = zerolog.Ctx(ctx).With(). - Str("module", "tsch"). - Str("method", ccfg.CleanupMethod).Logger() - mod.creds = creds - mod.target = target - - if ccfg.CleanupMethod == MethodDelete { - if cfg, ok := ccfg.CleanupMethodConfig.(MethodDeleteConfig); !ok { - return errors.New("invalid configuration") - } else { - if err = mod.Connect(ctx, creds, target); err != nil { - return fmt.Errorf("connect: %w", err) - } else if _, err = mod.tsch.Delete(ctx, &itaskschedulerservice.DeleteRequest{ - Path: cfg.TaskPath, - Flags: 0, - }); err != nil { - mod.log.Error().Err(err).Str("task", cfg.TaskPath).Msg("Failed to delete task") - return fmt.Errorf("delete task: %w", err) - } else { - mod.log.Info().Str("task", cfg.TaskPath).Msg("Task deleted successfully") - } - } - } else { - return fmt.Errorf("method not implemented: %s", ccfg.CleanupMethod) - } - 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, creds *adauth.Credential, target *adauth.Target, ecfg *exec.ExecutionConfig) (err error) { - - mod.log = zerolog.Ctx(ctx).With(). - Str("module", "tsch"). - Str("method", ecfg.ExecutionMethod).Logger() - mod.creds = creds - mod.target = target - - 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) - - task := &task{ - TaskVersion: "1.2", // static - TaskNamespace: "http://schemas.microsoft.com/windows/2004/02/mit/task", // static - TimeTriggers: []taskTimeTrigger{ - { - StartBoundary: startTime.Format(TaskXMLDurationFormat), - Enabled: true, - }, - }, - Principals: defaultPrincipals, - Settings: defaultSettings, - Actions: actions{ - Context: defaultPrincipals.Principals[0].ID, - Exec: []actionExec{ - { - Command: ecfg.ExecutableName, - Arguments: ecfg.ExecutableArgs, - }, - }, - }, - } - if !cfg.NoDelete && !cfg.CallDelete { - if cfg.StopDelay == 0 { - // EndBoundary cannot be >= StartBoundary - cfg.StopDelay = 1 * time.Second - } - task.Settings.DeleteExpiredTaskAfter = xmlDuration(cfg.DeleteDelay) - task.TimeTriggers[0].EndBoundary = stopTime.Format(TaskXMLDurationFormat) - } - - if doc, err := xml.Marshal(task); err != nil { - return fmt.Errorf("marshal task XML: %w", err) - - } else { - mod.log.Debug().Str("task", string(doc)).Msg("Task XML generated") - docStr := TaskXMLHeader + string(doc) - - taskPath := cfg.TaskPath - taskName := cfg.TaskName - - if taskName == "" { - taskName = util.RandomString() - } - if taskPath == "" { - taskPath = `\` + taskName - } - - if err = mod.Connect(ctx, creds, target); err != nil { - return fmt.Errorf("connect: %w", err) - } - defer func() { - if err = mod.dce.Close(ctx); err != nil { - mod.log.Warn().Err(err).Msg("Failed to dispose dce client") - } else { - mod.log.Debug().Msg("Disposed DCE client") - } - }() - var response *itaskschedulerservice.RegisterTaskResponse - if response, err = mod.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ - Path: taskPath, - XML: docStr, - Flags: 0, // TODO - LogonType: 0, // TASK_LOGON_NONE - CredsCount: 0, - Creds: nil, - }); err != nil { - return err - - } else { - mod.log.Info().Str("path", response.ActualPath).Msg("Task registered successfully") - - if !cfg.NoDelete { - if cfg.CallDelete { - defer func() { - if err = mod.Cleanup(ctx, creds, target, &exec.CleanupConfig{ - CleanupMethod: MethodDelete, - CleanupMethodConfig: MethodDeleteConfig{TaskPath: taskPath}, - }); err != nil { - mod.log.Error().Err(err).Msg("Failed to delete task") - } - }() - mod.log.Info().Dur("ms", cfg.StartDelay).Msg("Waiting for task to run") - select { - case <-ctx.Done(): - mod.log.Warn().Msg("Cancelling execution") - return err - case <-time.After(cfg.StartDelay + (time.Second * 2)): // + two seconds - // TODO: check if task is running yet; delete if the wait period is over - break - } - return err - } else { - mod.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 - taskName := cfg.TaskName - - if taskName == "" { - mod.log.Debug().Msg("Task name not defined. Using random string") - taskName = util.RandomString() - } - if taskPath == "" { - taskPath = `\` + taskName - } - if !TaskNameRegex.MatchString(taskName) { - return fmt.Errorf("invalid task name: %s", taskName) - } - if !TaskPathRegex.MatchString(taskPath) { - return fmt.Errorf("invalid task path: %s", taskPath) - } - - mod.log.Debug().Msg("Using demand method") - settings := defaultSettings - settings.AllowStartOnDemand = true - task := &task{ - TaskVersion: "1.2", // static - TaskNamespace: "http://schemas.microsoft.com/windows/2004/02/mit/task", // static - Principals: defaultPrincipals, - Settings: defaultSettings, - Actions: actions{ - Context: defaultPrincipals.Principals[0].ID, - Exec: []actionExec{ - { - Command: ecfg.ExecutableName, - Arguments: ecfg.ExecutableArgs, - }, - }, - }, - } - if doc, err := xml.Marshal(task); err != nil { - return fmt.Errorf("marshal task: %w", err) - } else { - docStr := TaskXMLHeader + string(doc) - - if err = mod.Connect(ctx, creds, target); err != nil { - return fmt.Errorf("connect: %w", err) - } - defer func() { - if err = mod.dce.Close(ctx); err != nil { - mod.log.Warn().Err(err).Msg("Failed to dispose dce client") - } else { - mod.log.Debug().Msg("Disposed DCE client") - } - }() - - var response *itaskschedulerservice.RegisterTaskResponse - if response, err = mod.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ - Path: taskPath, - XML: docStr, - Flags: 0, // TODO - LogonType: 0, // TASK_LOGON_NONE - CredsCount: 0, - Creds: nil, - }); err != nil { - return fmt.Errorf("register task: %w", err) - - } else { - mod.log.Info().Str("task", response.ActualPath).Msg("Task registered successfully") - if !cfg.NoDelete { - defer func() { - if err = mod.Cleanup(ctx, creds, target, &exec.CleanupConfig{ - CleanupMethod: MethodDelete, - CleanupMethodConfig: MethodDeleteConfig{TaskPath: taskPath}, - }); err != nil { - mod.log.Error().Err(err).Msg("Failed to delete task") - } - }() - } - if _, err = mod.tsch.Run(ctx, &itaskschedulerservice.RunRequest{ - Path: response.ActualPath, - Flags: 0, // Maybe we want to use these? - }); err != nil { - return err - } else { - mod.log.Info().Str("task", response.ActualPath).Msg("Started task") - } - } - } - } - } else { - return fmt.Errorf("method not implemented: %s", ecfg.ExecutionMethod) - } - - return nil +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), + //EndBoundary: stopTime.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() + } + tr := taskTimeTrigger{Enabled: true} + st := newSettings(true, true, false) + tk := newTask(st, nil, triggers{TimeTriggers: []taskTimeTrigger{tr}}, 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) + } + _, err := mod.tsch.Run(ctx, &itaskschedulerservice.RunRequest{ + Path: taskPath, + Flags: 0, // Maybe we want to use these? + }) + 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 index dbd9ade..8e31840 100644 --- a/internal/exec/tsch/module.go +++ b/internal/exec/tsch/module.go @@ -1,48 +1,46 @@ package tschexec import ( - "github.com/FalconOpsLLC/goexec/internal/client/dcerpc" - "github.com/RedTeamPentesting/adauth" - "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" - "github.com/rs/zerolog" - "time" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" + "time" ) type Module struct { - creds *adauth.Credential - target *adauth.Target - - log zerolog.Logger - dce *dcerpc.DCEClient - tsch itaskschedulerservice.TaskSchedulerServiceClient + // dce holds the working DCE connection interface + dce dcerpc.Conn + // tsch holds the ITaskSchedulerService client + tsch itaskschedulerservice.TaskSchedulerServiceClient + // createdTasks holds any tasks that are created - for cleanup + createdTasks []string } type MethodRegisterConfig struct { - NoDelete bool - CallDelete bool - TaskName string - TaskPath string - StartDelay time.Duration - StopDelay time.Duration - DeleteDelay time.Duration + NoDelete bool + CallDelete bool + //TaskName string + TaskPath string + StartDelay time.Duration + StopDelay time.Duration + DeleteDelay time.Duration } type MethodDemandConfig struct { - NoDelete bool - CallDelete bool - TaskName string - TaskPath string - StopDelay time.Duration - DeleteDelay time.Duration + NoDelete bool + CallDelete bool + TaskName string + TaskPath string + StopDelay time.Duration + DeleteDelay time.Duration } type MethodDeleteConfig struct { - TaskPath string + TaskPath string } const ( - MethodRegister string = "register" - MethodDemand string = "demand" - MethodDelete string = "delete" - MethodChange string = "update" + 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 new file mode 100644 index 0000000..928d029 --- /dev/null +++ b/internal/exec/tsch/task.go @@ -0,0 +1,85 @@ +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 index bc3ed0b..f2476f1 100644 --- a/internal/exec/tsch/tsch.go +++ b/internal/exec/tsch/tsch.go @@ -1,102 +1,143 @@ package tschexec import ( - "encoding/xml" + "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 = `<?xml version="1.0" encoding="UTF-16"?>` ) // 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"` + 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 { - StopOnIdleEnd bool `xml:"StopOnIdleEnd"` - RestartOnIdle bool `xml:"RestartOnIdle"` + 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"` + 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"` + XMLName xml.Name `xml:"Exec"` + Command string `xml:"Command"` + Arguments string `xml:"Arguments"` } type actions struct { - XMLName xml.Name `xml:"Actions"` - Context string `xml:"Context,attr"` - Exec []actionExec `xml:"Exec,omitempty"` + 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"` + 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"` + 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"` - TimeTriggers []taskTimeTrigger `xml:"Triggers>TimeTrigger,omitempty"` - Actions actions `xml:"Actions"` - Principals principals `xml:"Principals"` - Settings settings `xml:"Settings"` + XMLName xml.Name `xml:"Task"` + TaskVersion string `xml:"version,attr"` + TaskNamespace string `xml:"xmlns,attr"` + //TimeTriggers []taskTimeTrigger `xml:"Triggers>TimeTrigger,omitempty"` // TODO: triggers type + Triggers triggers `xml:"Triggers"` + Actions actions `xml:"Actions"` + Principals principals `xml:"Principals"` + Settings settings `xml:"Settings"` } -var ( - defaultSettings = settings{ - MultipleInstancesPolicy: "IgnoreNew", - DisallowStartIfOnBatteries: false, - StopIfGoingOnBatteries: false, - AllowHardTerminate: true, - RunOnlyIfNetworkAvailable: false, - IdleSettings: idleSettings{ - StopOnIdleEnd: true, - RestartOnIdle: false, - }, - AllowStartOnDemand: true, - Enabled: true, - Hidden: true, - RunOnlyIfIdle: false, - WakeToRun: false, - Priority: 7, // 7 is a pretty standard value for scheduled tasks - StartWhenAvailable: true, - } - defaultPrincipals = principals{ - Principals: []principal{ - { - ID: "SYSTEM", - UserID: "S-1-5-18", - RunLevel: "HighestAvailable", - }, - }, - } -) +// 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 index 962fc60..5f47738 100644 --- a/internal/exec/wmi/exec.go +++ b/internal/exec/wmi/exec.go @@ -34,11 +34,14 @@ var ( 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 } @@ -50,6 +53,7 @@ func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target ctx = gssapi.NewSecurityContext(ctx) log := zerolog.Ctx(ctx).With(). + Str("module", "tsch"). Str("func", "Connect").Logger() // Assemble DCERPC options @@ -81,7 +85,7 @@ func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target // Create DCERPC dialer mod.dce, err = dcerpc.Dial(ctx, target.AddressWithoutPort(), append(baseOpts, append(authOpts, dcerpc.WithEndpoint(rb))...)...) if err != nil { - return fmt.Errorf("DCERPC dial: %w", err) + return fmt.Errorf("create DCERPC dialer: %w", err) } // Create remote activation client ia, err := iactivation.NewActivationClient(ctx, mod.dce, append(baseOpts, dcerpc.WithEndpoint(rb))...) |