aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorBryan McNulty <bryanmcnulty@protonmail.com>2025-03-08 06:07:29 -0600
committerBryan McNulty <bryanmcnulty@protonmail.com>2025-03-08 06:07:29 -0600
commit7574b7370be083ff563fa8ad6d01d5ac776d7e4d (patch)
tree78aeb5d671e35aee8780fd92b2511cad00efd67d /internal
parentd8eca7209d000609fea08d2973a402a41a4cf921 (diff)
downloadgoexec-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.go21
-rw-r--r--internal/exec/exec.go4
-rw-r--r--internal/exec/tsch/exec.go499
-rw-r--r--internal/exec/tsch/module.go56
-rw-r--r--internal/exec/tsch/task.go85
-rw-r--r--internal/exec/tsch/tsch.go183
-rw-r--r--internal/exec/wmi/exec.go6
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))...)