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 /internal | |
parent | d8eca7209d000609fea08d2973a402a41a4cf921 (diff) | |
download | goexec-7574b7370be083ff563fa8ad6d01d5ac776d7e4d.tar.gz goexec-7574b7370be083ff563fa8ad6d01d5ac776d7e4d.zip |
Add a bunch of DCE related options to TSCH module
Diffstat (limited to 'internal')
-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 |
7 files changed, 483 insertions, 371 deletions
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))...) |