diff options
author | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-04-20 18:23:36 -0500 |
---|---|---|
committer | Bryan McNulty <bryanmcnulty@protonmail.com> | 2025-04-20 18:23:36 -0500 |
commit | 1168c8657117cb72426e9e2bfc68bf8ae9575bb1 (patch) | |
tree | b6735b553e80719ccf453bde8db694e192bac8ee | |
parent | 6ade3ddd945e50d7a145294ac4681489be5d22f8 (diff) | |
download | goexec-1168c8657117cb72426e9e2bfc68bf8ae9575bb1.tar.gz goexec-1168c8657117cb72426e9e2bfc68bf8ae9575bb1.zip |
Improve smb.OutputFileFetcher; introduce stage input
-rw-r--r-- | TODO.md | 4 | ||||
-rw-r--r-- | cmd/args.go | 7 | ||||
-rw-r--r-- | cmd/root.go | 34 | ||||
-rw-r--r-- | cmd/scmr.go | 3 | ||||
-rw-r--r-- | cmd/tsch.go | 388 | ||||
-rw-r--r-- | pkg/goexec/io.go | 9 | ||||
-rw-r--r-- | pkg/goexec/method.go | 12 | ||||
-rw-r--r-- | pkg/goexec/smb/client.go | 159 | ||||
-rw-r--r-- | pkg/goexec/smb/input.go | 66 | ||||
-rw-r--r-- | pkg/goexec/smb/output.go | 34 |
10 files changed, 416 insertions, 300 deletions
@@ -8,14 +8,14 @@ - [X] Session hijacking - [X] Generate random name/path - [X] Output -- [ ] 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) +- [X] 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) ### SCMR - [X] Clean up SCMR module - [X] add dynamic string binding support - [X] general clean up. Use TSCH & WMI as reference -- [X] Output +- [ ] Output - [ ] Fix SCMR `change` method so that dependencies field isn't permanently overwritten ### DCOM diff --git a/cmd/args.go b/cmd/args.go index 8af8598..109a528 100644 --- a/cmd/args.go +++ b/cmd/args.go @@ -29,8 +29,13 @@ func registerNetworkFlags(fs *pflag.FlagSet) { //cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter") } +func registerStageFlags(fs *pflag.FlagSet) { + fs.StringVarP(&stageFilePath, "stage", "E", "", "File to stage and execute") + //fs.StringVarP(&stageArgs ...) +} + func registerExecutionFlags(fs *pflag.FlagSet) { - fs.StringVarP(&exec.Input.Executable, "executable", "e", "", "Windows executable to invoke") + fs.StringVarP(&exec.Input.Executable, "exec", "e", "", "Remote Windows executable to invoke") fs.StringVarP(&exec.Input.Arguments, "args", "a", "", "Process command line arguments") fs.StringVarP(&exec.Input.Command, "command", "c", "", "Windows process command line (executable & arguments)") diff --git a/cmd/root.go b/cmd/root.go index b1feaf2..a648b32 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,11 +2,11 @@ package cmd import ( "fmt" - "github.com/FalconOpsLLC/goexec/internal/util" "github.com/FalconOpsLLC/goexec/pkg/goexec" "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" "github.com/FalconOpsLLC/goexec/pkg/goexec/smb" "github.com/RedTeamPentesting/adauth" + "github.com/google/uuid" "github.com/oiweiwei/go-msrpc/ssp" "github.com/oiweiwei/go-msrpc/ssp/gssapi" "github.com/rs/zerolog" @@ -55,10 +55,13 @@ var ( defaultAuthFlags, defaultLogFlags, defaultNetRpcFlags *flagSet - returnCode int - outputMethod string - outputPath string - proxy string + returnCode int + + // === IO === + stageFilePath string + outputMethod string + outputPath string + // ========== // === Logging === logJson bool // Log output in JSON lines @@ -70,8 +73,11 @@ var ( log zerolog.Logger // =============== + // === Network === + proxy string rpcClient dce.Client smbClient smb.Client + // =============== exec = goexec.ExecutionIO{ Input: new(goexec.ExecutionInput), @@ -86,15 +92,15 @@ var ( Use: "goexec", Short: `goexec - Windows remote execution multitool`, Long: ` - ___ ___ ___ _ _ ___ ___ -| . | . | -_|_'_| -_| _| -|_ |___|___|_,_|___|___| -|___| + ___ ___ ___ _ _ ___ ___ + | . | . | -_|_'_| -_| _| + |_ |___|___|_,_|___|___| + |___| Authors: FalconOps LLC (@FalconOpsLLC), Bryan McNulty (@bryanmcnulty) -> Goexec is designed to facilitate remote execution on Windows systems, +> Goexec is designed to achieve remote execution on Windows systems, while providing an extremely flexible CLI and a strong focus on OPSEC. `, @@ -130,11 +136,12 @@ Authors: FalconOps LLC (@FalconOpsLLC), if outputPath != "" { if outputMethod == "smb" { if exec.Output.RemotePath == "" { - exec.Output.RemotePath = util.RandomWindowsTempFile() + exec.Output.RemotePath = `C:\Windows\Temp\` + uuid.NewString() } exec.Output.Provider = &smb.OutputFileFetcher{ Client: &smbClient, - Share: `C$`, + Share: `ADMIN$`, // TODO: dynamic + SharePath: `C:\Windows`, File: exec.Output.RemotePath, DeleteOutputFile: !exec.Output.NoDelete, } @@ -147,6 +154,9 @@ Authors: FalconOps LLC (@FalconOpsLLC), if err := logFile.Close(); err != nil { // ... } + if err := exec.Input.StageFile.Close(); err != nil { + // ... + } }, } ) diff --git a/cmd/scmr.go b/cmd/scmr.go index 5ad8f21..4edcf22 100644 --- a/cmd/scmr.go +++ b/cmd/scmr.go @@ -75,6 +75,7 @@ func scmrChangeCmdInit() { // TODO: SCMR output //registerExecutionOutputFlags(scmrChangeExecFlags.Flags) + //registerStageFlags(scmrChangeExecFlags.Flags) cmdFlags[scmrChangeCmd] = []*flagSet{ scmrChangeFlags, @@ -186,7 +187,7 @@ References: ctx := log.With(). Str("module", "scmr"). - Str("method", "create"). + Str("method", "change"). Logger().WithContext(gssapi.NewSecurityContext(context.Background())) if err := goexec.ExecuteCleanMethod(ctx, &scmrChange, &exec); err != nil { diff --git a/cmd/tsch.go b/cmd/tsch.go index f52a064..d9eee9a 100644 --- a/cmd/tsch.go +++ b/cmd/tsch.go @@ -1,232 +1,232 @@ package cmd import ( - "context" - "fmt" - "github.com/FalconOpsLLC/goexec/internal/util" - "github.com/FalconOpsLLC/goexec/pkg/goexec" - tschexec "github.com/FalconOpsLLC/goexec/pkg/goexec/tsch" - "github.com/oiweiwei/go-msrpc/ssp/gssapi" - "github.com/spf13/cobra" - "time" + "context" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/util" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + tschexec "github.com/FalconOpsLLC/goexec/pkg/goexec/tsch" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" + "github.com/spf13/cobra" + "time" ) func tschCmdInit() { - cmdFlags[tschCmd] = []*flagSet{ - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - tschDemandCmdInit() - tschCreateCmdInit() - tschChangeCmdInit() - - tschCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) - tschCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) - tschCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) - tschCmd.AddCommand(tschDemandCmd, tschCreateCmd, tschChangeCmd) + cmdFlags[tschCmd] = []*flagSet{ + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + tschDemandCmdInit() + tschCreateCmdInit() + tschChangeCmdInit() + + tschCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) + tschCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) + tschCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) + tschCmd.AddCommand(tschDemandCmd, tschCreateCmd, tschChangeCmd) } func tschDemandCmdInit() { - tschDemandFlags := newFlagSet("Task Scheduler") + tschDemandFlags := newFlagSet("Task Scheduler") - tschDemandFlags.Flags.StringVarP(&tschTask, "task", "t", "", "Name or path of the new task") - tschDemandFlags.Flags.Uint32Var(&tschDemand.SessionId, "session", 0, "Hijack existing session given the session ID") - tschDemandFlags.Flags.StringVar(&tschDemand.UserSid, "sid", "S-1-5-18", "User SID to impersonate") - tschDemandFlags.Flags.BoolVar(&tschDemand.NoDelete, "no-delete", false, "Don't delete task after execution") + tschDemandFlags.Flags.StringVarP(&tschTask, "task", "t", "", "Name or path of the new task") + tschDemandFlags.Flags.Uint32Var(&tschDemand.SessionId, "session", 0, "Hijack existing session given the session ID") + tschDemandFlags.Flags.StringVar(&tschDemand.UserSid, "sid", "S-1-5-18", "User SID to impersonate") + tschDemandFlags.Flags.BoolVar(&tschDemand.NoDelete, "no-delete", false, "Don't delete task after execution") - tschDemandExecFlags := newFlagSet("Execution") + tschDemandExecFlags := newFlagSet("Execution") - registerExecutionFlags(tschDemandExecFlags.Flags) - registerExecutionOutputFlags(tschDemandExecFlags.Flags) + registerExecutionFlags(tschDemandExecFlags.Flags) + registerExecutionOutputFlags(tschDemandExecFlags.Flags) - cmdFlags[tschDemandCmd] = []*flagSet{ - tschDemandFlags, - tschDemandExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } + cmdFlags[tschDemandCmd] = []*flagSet{ + tschDemandFlags, + tschDemandExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } - tschDemandCmd.Flags().AddFlagSet(tschDemandFlags.Flags) - tschDemandCmd.Flags().AddFlagSet(tschDemandExecFlags.Flags) - tschDemandCmd.MarkFlagsOneRequired("executable", "command") + tschDemandCmd.Flags().AddFlagSet(tschDemandFlags.Flags) + tschDemandCmd.Flags().AddFlagSet(tschDemandExecFlags.Flags) + tschDemandCmd.MarkFlagsOneRequired("exec", "command") } func tschCreateCmdInit() { - tschCreateFlags := newFlagSet("Task Scheduler") - - tschCreateFlags.Flags.StringVarP(&tschTask, "task", "t", "", "Name or path of the new task") - tschCreateFlags.Flags.DurationVar(&tschCreate.StopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This won't stop the spawned process") - tschCreateFlags.Flags.DurationVar(&tschCreate.StartDelay, "start-delay", 5*time.Second, "Delay between task registration and execution") - //tschCreateFlags.Flags.DurationVar(&tschCreate.DeleteDelay, "delete-delay", 0*time.Second, "Delay between task termination and deletion") - tschCreateFlags.Flags.BoolVar(&tschCreate.NoDelete, "no-delete", false, "Don't delete task after execution") - tschCreateFlags.Flags.BoolVar(&tschCreate.CallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task") - tschCreateFlags.Flags.StringVar(&tschCreate.UserSid, "sid", "S-1-5-18", "User `SID` to impersonate") - - tschCreateExecFlags := newFlagSet("Execution") - - registerExecutionFlags(tschCreateExecFlags.Flags) - registerExecutionOutputFlags(tschCreateExecFlags.Flags) - - cmdFlags[tschCreateCmd] = []*flagSet{ - tschCreateFlags, - tschCreateExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - - tschCreateCmd.Flags().AddFlagSet(tschCreateFlags.Flags) - tschCreateCmd.Flags().AddFlagSet(tschCreateExecFlags.Flags) - tschCreateCmd.MarkFlagsOneRequired("executable", "command") + tschCreateFlags := newFlagSet("Task Scheduler") + + tschCreateFlags.Flags.StringVarP(&tschTask, "task", "t", "", "Name or path of the new task") + tschCreateFlags.Flags.DurationVar(&tschCreate.StopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This won't stop the spawned process") + tschCreateFlags.Flags.DurationVar(&tschCreate.StartDelay, "start-delay", 5*time.Second, "Delay between task registration and execution") + //tschCreateFlags.Flags.DurationVar(&tschCreate.DeleteDelay, "delete-delay", 0*time.Second, "Delay between task termination and deletion") + tschCreateFlags.Flags.BoolVar(&tschCreate.NoDelete, "no-delete", false, "Don't delete task after execution") + tschCreateFlags.Flags.BoolVar(&tschCreate.CallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task") + tschCreateFlags.Flags.StringVar(&tschCreate.UserSid, "sid", "S-1-5-18", "User `SID` to impersonate") + + tschCreateExecFlags := newFlagSet("Execution") + + registerExecutionFlags(tschCreateExecFlags.Flags) + registerExecutionOutputFlags(tschCreateExecFlags.Flags) + + cmdFlags[tschCreateCmd] = []*flagSet{ + tschCreateFlags, + tschCreateExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + + tschCreateCmd.Flags().AddFlagSet(tschCreateFlags.Flags) + tschCreateCmd.Flags().AddFlagSet(tschCreateExecFlags.Flags) + tschCreateCmd.MarkFlagsOneRequired("exec", "command") } func tschChangeCmdInit() { - tschChangeFlags := newFlagSet("Task Scheduler") - - tschChangeFlags.Flags.StringVarP(&tschChange.TaskPath, "task", "t", "", "Path to existing task") - tschChangeFlags.Flags.BoolVar(&tschChange.NoStart, "no-start", false, "Don't start the task") - tschChangeFlags.Flags.BoolVar(&tschChange.NoRevert, "no-revert", false, "Don't restore the original task definition") - - tschChangeExecFlags := newFlagSet("Execution") - - registerExecutionFlags(tschChangeExecFlags.Flags) - registerExecutionOutputFlags(tschChangeExecFlags.Flags) - - cmdFlags[tschChangeCmd] = []*flagSet{ - tschChangeFlags, - tschChangeExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - - tschChangeCmd.Flags().AddFlagSet(tschChangeFlags.Flags) - tschChangeCmd.Flags().AddFlagSet(tschChangeExecFlags.Flags) - - // Constraints - { - if err := tschChangeCmd.MarkFlagRequired("task"); err != nil { - panic(err) - } - tschChangeCmd.MarkFlagsOneRequired("executable", "command") - } + tschChangeFlags := newFlagSet("Task Scheduler") + + tschChangeFlags.Flags.StringVarP(&tschChange.TaskPath, "task", "t", "", "Path to existing task") + tschChangeFlags.Flags.BoolVar(&tschChange.NoStart, "no-start", false, "Don't start the task") + tschChangeFlags.Flags.BoolVar(&tschChange.NoRevert, "no-revert", false, "Don't restore the original task definition") + + tschChangeExecFlags := newFlagSet("Execution") + + registerExecutionFlags(tschChangeExecFlags.Flags) + registerExecutionOutputFlags(tschChangeExecFlags.Flags) + + cmdFlags[tschChangeCmd] = []*flagSet{ + tschChangeFlags, + tschChangeExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + + tschChangeCmd.Flags().AddFlagSet(tschChangeFlags.Flags) + tschChangeCmd.Flags().AddFlagSet(tschChangeExecFlags.Flags) + + // Constraints + { + if err := tschChangeCmd.MarkFlagRequired("task"); err != nil { + panic(err) + } + tschChangeCmd.MarkFlagsOneRequired("exec", "command") + } } func argsTask(*cobra.Command, []string) error { - switch { - case tschTask == "": - tschTask = `\` + util.RandomString() - case tschexec.ValidateTaskPath(tschTask) == nil: - case tschexec.ValidateTaskName(tschTask) == nil: - tschTask = `\` + tschTask - default: - return fmt.Errorf("invalid task Label or path: %q", tschTask) - } - return nil + switch { + case tschTask == "": + tschTask = `\` + util.RandomString() + case tschexec.ValidateTaskPath(tschTask) == nil: + case tschexec.ValidateTaskName(tschTask) == nil: + tschTask = `\` + tschTask + default: + return fmt.Errorf("invalid task Label or path: %q", tschTask) + } + return nil } var ( - tschDemand tschexec.TschDemand - tschCreate tschexec.TschCreate - tschChange tschexec.TschChange - - tschTask string - - tschCmd = &cobra.Command{ - Use: "tsch", - Short: "Execute with Windows Task Scheduler (MS-TSCH)", - GroupID: "module", - Args: cobra.NoArgs, - } - - tschDemandCmd = &cobra.Command{ - Use: "demand [target]", - Short: "Register a remote scheduled task and demand immediate start", - Long: `Description: + tschDemand tschexec.TschDemand + tschCreate tschexec.TschCreate + tschChange tschexec.TschChange + + tschTask string + + tschCmd = &cobra.Command{ + Use: "tsch", + Short: "Execute with Windows Task Scheduler (MS-TSCH)", + GroupID: "module", + Args: cobra.NoArgs, + } + + tschDemandCmd = &cobra.Command{ + Use: "demand [target]", + Short: "Register a remote scheduled task and demand immediate start", + Long: `Description: Similar to the create method, the demand method will call SchRpcRegisterTask, But rather than setting a defined time when the task will start, it will additionally call SchRpcRun to forcefully start the task. `, - Args: args( - argsRpcClient("cifs"), - argsOutput("smb"), - argsTask, - ), - - Run: func(*cobra.Command, []string) { - tschDemand.IO = exec - tschDemand.Client = &rpcClient - tschDemand.TaskPath = tschTask - - ctx := log.With(). - Str("module", "tsch"). - Str("method", "demand"). - Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) - - if err := goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - tschCreateCmd = &cobra.Command{ - Use: "create [target]", - Short: "Create a remote scheduled task with an automatic start time", - Long: `Description: + Args: args( + argsRpcClient("cifs"), + argsOutput("smb"), + argsTask, + ), + + Run: func(*cobra.Command, []string) { + tschDemand.IO = exec + tschDemand.Client = &rpcClient + tschDemand.TaskPath = tschTask + + ctx := log.With(). + Str("module", "tsch"). + Str("method", "demand"). + Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) + + if err := goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + tschCreateCmd = &cobra.Command{ + Use: "create [target]", + Short: "Create a remote scheduled task with an automatic start time", + Long: `Description: The create method calls SchRpcRegisterTask to register a scheduled task with an automatic start time.This method avoids directly calling SchRpcRun, and can even avoid calling SchRpcDelete by populating the DeleteExpiredTaskAfter Setting. `, - Args: args( - argsRpcClient("cifs"), - argsOutput("smb"), - argsTask, - ), - - Run: func(*cobra.Command, []string) { - tschCreate.Tsch.Client = &rpcClient - tschCreate.IO = exec - tschCreate.TaskPath = tschTask - - ctx := log.With(). - Str("module", "tsch"). - Str("method", "create"). - Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) - - if err := goexec.ExecuteCleanMethod(ctx, &tschCreate, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - tschChangeCmd = &cobra.Command{ - Use: "change [target]", - Short: "Modify an existing task to spawn an arbitrary process", - Long: `Description: + Args: args( + argsRpcClient("cifs"), + argsOutput("smb"), + argsTask, + ), + + Run: func(*cobra.Command, []string) { + tschCreate.Tsch.Client = &rpcClient + tschCreate.IO = exec + tschCreate.TaskPath = tschTask + + ctx := log.With(). + Str("module", "tsch"). + Str("method", "create"). + Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) + + if err := goexec.ExecuteCleanMethod(ctx, &tschCreate, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + tschChangeCmd = &cobra.Command{ + Use: "change [target]", + Short: "Modify an existing task to spawn an arbitrary process", + Long: `Description: The change method calls SchRpcRetrieveTask to fetch the definition of an existing task (-t), then modifies the task definition to spawn a process`, - Args: args( - argsRpcClient("cifs"), - argsOutput("smb"), - - func(*cobra.Command, []string) error { - return tschexec.ValidateTaskPath(tschChange.TaskPath) - }, - ), - - Run: func(*cobra.Command, []string) { - tschChange.Tsch.Client = &rpcClient - tschChange.IO = exec - - ctx := log.With(). - Str("module", "tsch"). - Str("method", "change"). - Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) - - if err := goexec.ExecuteCleanMethod(ctx, &tschChange, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } + Args: args( + argsRpcClient("cifs"), + argsOutput("smb"), + + func(*cobra.Command, []string) error { + return tschexec.ValidateTaskPath(tschChange.TaskPath) + }, + ), + + Run: func(*cobra.Command, []string) { + tschChange.Tsch.Client = &rpcClient + tschChange.IO = exec + + ctx := log.With(). + Str("module", "tsch"). + Str("method", "change"). + Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) + + if err := goexec.ExecuteCleanMethod(ctx, &tschChange, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } ) diff --git a/pkg/goexec/io.go b/pkg/goexec/io.go index ab2b704..1d4358f 100644 --- a/pkg/goexec/io.go +++ b/pkg/goexec/io.go @@ -27,7 +27,7 @@ type ExecutionOutput struct { } type ExecutionInput struct { - FilePath string + StageFile io.ReadCloser Executable string ExecutablePath string Arguments string @@ -89,3 +89,10 @@ func (i *ExecutionInput) CommandLine() (cmd []string) { func (i *ExecutionInput) String() string { return strings.Join(i.CommandLine(), " ") } + +func (i *ExecutionInput) Reader() (reader io.Reader) { + if i.StageFile != nil { + return i.StageFile + } + return strings.NewReader(i.String()) +} diff --git a/pkg/goexec/method.go b/pkg/goexec/method.go index e57442f..88898f2 100644 --- a/pkg/goexec/method.go +++ b/pkg/goexec/method.go @@ -102,17 +102,15 @@ func ExecuteCleanAuxiliaryMethod(ctx context.Context, module CleanAuxiliaryMetho func ExecuteCleanMethod(ctx context.Context, module CleanExecutionMethod, execIO *ExecutionIO) (err error) { log := zerolog.Ctx(ctx) - defer func() { - if err = module.Clean(ctx); err != nil { - log.Error().Err(err).Msg("Module cleanup failed") - err = nil - } - }() - if err = ExecuteMethod(ctx, module, execIO); err != nil { return } + if err = module.Clean(ctx); err != nil { + log.Error().Err(err).Msg("Module cleanup failed") + err = nil + } + if execIO.Output != nil && execIO.Output.Provider != nil { log.Info().Msg("Collecting output") diff --git a/pkg/goexec/smb/client.go b/pkg/goexec/smb/client.go index 3b41e39..d95481c 100644 --- a/pkg/goexec/smb/client.go +++ b/pkg/goexec/smb/client.go @@ -1,112 +1,123 @@ package smb import ( - "context" - "errors" - "fmt" - "github.com/oiweiwei/go-smb2.fork" - "github.com/rs/zerolog" - "net" + "context" + "errors" + "fmt" + "github.com/oiweiwei/go-smb2.fork" + "github.com/rs/zerolog" + "net" ) type Client struct { - ClientOptions + ClientOptions - conn net.Conn - sess *smb2.Session - mount *smb2.Share + conn net.Conn + sess *smb2.Session + mount *smb2.Share + + connected bool + share string } func (c *Client) Session() (sess *smb2.Session) { - return c.sess + return c.sess } func (c *Client) String() string { - return ClientName + return ClientName } func (c *Client) Logger(ctx context.Context) zerolog.Logger { - return zerolog.Ctx(ctx).With().Str("client", c.String()).Logger() + return zerolog.Ctx(ctx).With().Str("client", c.String()).Logger() } func (c *Client) Mount(ctx context.Context, share string) (err error) { - if c.sess == nil { - return errors.New("SMB session not initialized") - } + if c.sess == nil { + return errors.New("SMB session not initialized") + } - c.mount, err = c.sess.Mount(share) - zerolog.Ctx(ctx).Debug().Str("share", share).Msg("Mounted SMB share") + c.mount, err = c.sess.Mount(share) + zerolog.Ctx(ctx).Debug().Str("share", share).Msg("Mounted SMB share") + c.share = share - return + return } func (c *Client) Connect(ctx context.Context) (err error) { - log := c.Logger(ctx) - { - if c.netDialer == nil { - panic(fmt.Errorf("TCP dialer not initialized")) - } - if c.dialer == nil { - panic(fmt.Errorf("%s dialer not initialized", c.String())) - } - } + log := c.Logger(ctx) + { + if c.netDialer == nil { + panic(fmt.Errorf("TCP dialer not initialized")) + } + if c.dialer == nil { + panic(fmt.Errorf("%s dialer not initialized", c.String())) + } + } - // Establish TCP connection - c.conn, err = c.netDialer.Dial("tcp", net.JoinHostPort(c.Host, fmt.Sprintf("%d", c.Port))) + // Establish TCP connection + c.conn, err = c.netDialer.Dial("tcp", net.JoinHostPort(c.Host, fmt.Sprintf("%d", c.Port))) - if err != nil { - return err - } + if err != nil { + return err + } - log = log.With().Str("address", c.conn.RemoteAddr().String()).Logger() - log.Debug().Msgf("Connected to %s server", c.String()) + log = log.With().Str("address", c.conn.RemoteAddr().String()).Logger() + log.Debug().Msgf("Connected to %s server", c.String()) - // Open SMB session - c.sess, err = c.dialer.DialContext(ctx, c.conn) + // Open SMB session + c.sess, err = c.dialer.DialContext(ctx, c.conn) - if err != nil { - log.Error().Err(err).Msgf("Failed to open %s session", c.String()) - return fmt.Errorf("dial %s: %w", c.String(), err) - } + if err != nil { + log.Error().Err(err).Msgf("Failed to open %s session", c.String()) + return fmt.Errorf("dial %s: %w", c.String(), err) + } + log.Debug().Msgf("Opened %s session", c.String()) - log.Debug().Msgf("Opened %s session", c.String()) + c.connected = true - return + return } func (c *Client) Close(ctx context.Context) (err error) { - log := c.Logger(ctx) - - // Close SMB session - if c.sess != nil { - defer func() { - if err = c.sess.Logoff(); err != nil { - log.Debug().Err(err).Msgf("Failed to discard SMB session") - } - log.Debug().Msgf("Discarded SMB session") - }() - - } else if c.conn != nil { - - defer func() { - if err = c.conn.Close(); err != nil { - log.Debug().Err(err).Msgf("Failed to disconnect SMB client") - } - log.Debug().Msgf("Disconnected SMB session") - }() - } - - // Unmount SMB share - if c.mount != nil { - defer func() { - if err = c.mount.Umount(); err != nil { - log.Debug().Err(err).Msg("Failed to unmount share") - } - log.Debug().Msg("Unmounted file share") - }() - } - return + log := c.Logger(ctx) + + c.connected = false + + // Close SMB session + if c.sess != nil { + defer func() { + if err = c.sess.Logoff(); err != nil { + log.Debug().Err(err).Msgf("Failed to discard SMB session") + } else { + log.Debug().Msg("Discarded SMB session") + } + }() + + } else if c.conn != nil { + + defer func() { + if err = c.conn.Close(); err != nil { + log.Debug().Err(err).Msgf("Failed to disconnect SMB client") + } else { + log.Debug().Msg("Disconnected SMB client") + } + }() + } + + // Unmount SMB share + if c.mount != nil { + defer func() { + if err = c.mount.Umount(); err != nil { + log.Debug().Err(err).Msg("Failed to unmount share") + } else { + log.Debug().Msg("Unmounted file share") + } + c.share = "" + }() + } + return } diff --git a/pkg/goexec/smb/input.go b/pkg/goexec/smb/input.go new file mode 100644 index 0000000..b9cb3bc --- /dev/null +++ b/pkg/goexec/smb/input.go @@ -0,0 +1,66 @@ +package smb + +import ( + "context" + "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "io" + "os" + "path" + "strings" +) + +type FileStager struct { + goexec.Cleaner + + Client *Client + + Share string + SharePath string + File string + relativePath string + ForceReconnect bool + DeleteStage bool +} + +func (o *FileStager) Stage(ctx context.Context, reader io.Reader) (err error) { + + o.relativePath = path.Join( + strings.ReplaceAll(pathPrefix.ReplaceAllString(o.SharePath, ""), `\`, "/"), + strings.ReplaceAll(pathPrefix.ReplaceAllString(o.File, ""), `\`, "/"), + ) + + if o.ForceReconnect || !o.Client.connected { + err = o.Client.Connect(ctx) + if err != nil { + return + } + defer o.AddCleaners(o.Client.Close) + } + + if o.ForceReconnect || o.Client.share != o.Share { + err = o.Client.Mount(ctx, o.Share) + if err != nil { + return + } + } + + writer, err := o.Client.mount.OpenFile(o.relativePath, os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("open remote file for writing: %w", err) + } + + if _, err = io.Copy(writer, reader); err != nil { + return + } + + o.AddCleaners(func(_ context.Context) error { return writer.Close() }) + + if o.DeleteStage { + o.AddCleaners(func(_ context.Context) error { + return o.Client.mount.Remove(o.relativePath) + }) + } + + return +} diff --git a/pkg/goexec/smb/output.go b/pkg/goexec/smb/output.go index f0a656d..d75cddb 100644 --- a/pkg/goexec/smb/output.go +++ b/pkg/goexec/smb/output.go @@ -4,9 +4,12 @@ import ( "context" "errors" "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/rs/zerolog" "io" "os" + "path/filepath" "regexp" + "strings" "time" ) @@ -22,8 +25,10 @@ type OutputFileFetcher struct { Client *Client Share string + SharePath string File string DeleteOutputFile bool + ForceReconnect bool PollInterval time.Duration PollTimeout time.Duration @@ -32,6 +37,8 @@ type OutputFileFetcher struct { func (o *OutputFileFetcher) GetOutput(ctx context.Context, writer io.Writer) (err error) { + log := zerolog.Ctx(ctx) + if o.PollInterval == 0 { o.PollInterval = DefaultOutputPollInterval } @@ -39,17 +46,28 @@ func (o *OutputFileFetcher) GetOutput(ctx context.Context, writer io.Writer) (er o.PollTimeout = DefaultOutputPollTimeout } - o.relativePath = pathPrefix.ReplaceAllString(o.File, "") + shp := pathPrefix.ReplaceAllString(strings.ToLower(strings.ReplaceAll(o.SharePath, `\`, "/")), "") + fp := pathPrefix.ReplaceAllString(strings.ToLower(strings.ReplaceAll(o.File, `\`, "/")), "") - err = o.Client.Connect(ctx) - if err != nil { + if o.relativePath, err = filepath.Rel(shp, fp); err != nil { return } - defer o.AddCleaners(o.Client.Close) - err = o.Client.Mount(ctx, o.Share) - if err != nil { - return + log.Info().Str("path", o.relativePath).Msg("Fetching output file") + + if o.ForceReconnect || !o.Client.connected { + err = o.Client.Connect(ctx) + if err != nil { + return + } + defer o.AddCleaners(o.Client.Close) + } + + if o.ForceReconnect || o.Client.share != o.Share { + err = o.Client.Mount(ctx, o.Share) + if err != nil { + return + } } stopAt := time.Now().Add(o.PollTimeout) @@ -57,7 +75,7 @@ func (o *OutputFileFetcher) GetOutput(ctx context.Context, writer io.Writer) (er for { if time.Now().After(stopAt) { - return errors.New("output timeout") + return errors.New("execution output timeout") } if reader, err = o.Client.mount.OpenFile(o.relativePath, os.O_RDONLY, 0); err == nil { break |