diff options
Diffstat (limited to 'internal/exec/tsch')
-rw-r--r-- | internal/exec/tsch/exec.go | 289 | ||||
-rw-r--r-- | internal/exec/tsch/module.go | 48 | ||||
-rw-r--r-- | internal/exec/tsch/tsch.go | 102 |
3 files changed, 439 insertions, 0 deletions
diff --git a/internal/exec/tsch/exec.go b/internal/exec/tsch/exec.go new file mode 100644 index 0000000..51f157c --- /dev/null +++ b/internal/exec/tsch/exec.go @@ -0,0 +1,289 @@ +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" +) + +const ( + TaskXMLDurationFormat = "2006-01-02T15:04:05.9999999Z" + TaskXMLHeader = `<?xml version="1.0" encoding="UTF-16"?>` +) + +var ( + TaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`) // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1 + TaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`) +) + +// *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` +} + +// 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) 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) 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 +} diff --git a/internal/exec/tsch/module.go b/internal/exec/tsch/module.go new file mode 100644 index 0000000..dbd9ade --- /dev/null +++ b/internal/exec/tsch/module.go @@ -0,0 +1,48 @@ +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" +) + +type Module struct { + creds *adauth.Credential + target *adauth.Target + + log zerolog.Logger + dce *dcerpc.DCEClient + tsch itaskschedulerservice.TaskSchedulerServiceClient +} + +type MethodRegisterConfig struct { + 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 +} + +type MethodDeleteConfig struct { + TaskPath string +} + +const ( + MethodRegister string = "register" + MethodDemand string = "demand" + MethodDelete string = "delete" + MethodChange string = "update" +) diff --git a/internal/exec/tsch/tsch.go b/internal/exec/tsch/tsch.go new file mode 100644 index 0000000..bc3ed0b --- /dev/null +++ b/internal/exec/tsch/tsch.go @@ -0,0 +1,102 @@ +package tschexec + +import ( + "encoding/xml" +) + +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f + +type taskTimeTrigger struct { + XMLName xml.Name `xml:"TimeTrigger"` + StartBoundary string `xml:"StartBoundary,omitempty"` // Derived from time.Time + EndBoundary string `xml:"EndBoundary,omitempty"` // Derived from time.Time; must be > StartBoundary + Enabled bool `xml:"Enabled"` +} + +type idleSettings struct { + StopOnIdleEnd bool `xml:"StopOnIdleEnd"` + RestartOnIdle bool `xml:"RestartOnIdle"` +} + +type settings struct { + XMLName xml.Name `xml:"Settings"` + Enabled bool `xml:"Enabled"` + Hidden bool `xml:"Hidden"` + DisallowStartIfOnBatteries bool `xml:"DisallowStartIfOnBatteries"` + StopIfGoingOnBatteries bool `xml:"StopIfGoingOnBatteries"` + AllowHardTerminate bool `xml:"AllowHardTerminate"` + RunOnlyIfNetworkAvailable bool `xml:"RunOnlyIfNetworkAvailable"` + AllowStartOnDemand bool `xml:"AllowStartOnDemand"` + WakeToRun bool `xml:"WakeToRun"` + RunOnlyIfIdle bool `xml:"RunOnlyIfIdle"` + StartWhenAvailable bool `xml:"StartWhenAvailable"` + Priority int `xml:"Priority,omitempty"` // 1 to 10 inclusive + MultipleInstancesPolicy string `xml:"MultipleInstancesPolicy,omitempty"` + ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` + DeleteExpiredTaskAfter string `xml:"DeleteExpiredTaskAfter,omitempty"` // Derived from time.Duration + IdleSettings idleSettings `xml:"IdleSettings,omitempty"` +} + +type actionExec struct { + XMLName xml.Name `xml:"Exec"` + Command string `xml:"Command"` + Arguments string `xml:"Arguments"` +} + +type actions struct { + XMLName xml.Name `xml:"Actions"` + Context string `xml:"Context,attr"` + Exec []actionExec `xml:"Exec,omitempty"` +} + +type principals struct { + XMLName xml.Name `xml:"Principals"` + Principals []principal `xml:"Principal"` +} + +type principal struct { + XMLName xml.Name `xml:"Principal"` + ID string `xml:"id,attr"` + UserID string `xml:"UserId"` + RunLevel string `xml:"RunLevel"` +} + +type task struct { + XMLName xml.Name `xml:"Task"` + TaskVersion string `xml:"version,attr"` + TaskNamespace string `xml:"xmlns,attr"` + TimeTriggers []taskTimeTrigger `xml:"Triggers>TimeTrigger,omitempty"` + 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", + }, + }, + } +) |