aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryan McNulty <bryanmcnulty@protonmail.com>2025-04-16 12:11:58 -0500
committerBryan McNulty <bryanmcnulty@protonmail.com>2025-04-16 12:11:58 -0500
commit55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c (patch)
treeedf4ec3b814fb10ccdbf759a62819a865d3e8141
parenta827b67d47cba7b02ea9599fe6bb88ffb3a6967d (diff)
downloadgoexec-55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c.tar.gz
goexec-55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c.zip
rewrote everything lol
-rw-r--r--Dockerfile1
-rw-r--r--TODO.md4
-rw-r--r--cmd/args.go126
-rw-r--r--cmd/dcom.go102
-rw-r--r--cmd/root.go172
-rw-r--r--cmd/rpc.go86
-rw-r--r--cmd/scmr.go243
-rw-r--r--cmd/tsch.go303
-rw-r--r--cmd/wmi.go169
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--internal/client/dce/dce.go74
-rw-r--r--internal/exec/dcom/exec.go181
-rw-r--r--internal/exec/dcom/module.go21
-rw-r--r--internal/exec/exec.go54
-rw-r--r--internal/exec/scmr/exec.go345
-rw-r--r--internal/exec/scmr/module.go44
-rw-r--r--internal/exec/scmr/service.go25
-rw-r--r--internal/exec/tsch/exec.go189
-rw-r--r--internal/exec/tsch/module.go44
-rw-r--r--internal/exec/tsch/task.go85
-rw-r--r--internal/exec/tsch/tsch.go141
-rw-r--r--internal/exec/wmi/exec.go166
-rw-r--r--internal/exec/wmi/module.go33
-rw-r--r--internal/exec/wmi/wmi.go26
-rw-r--r--internal/util/util.go5
-rw-r--r--pkg/goexec/auth.go11
-rw-r--r--pkg/goexec/clean.go31
-rw-r--r--pkg/goexec/client.go26
-rw-r--r--pkg/goexec/dce/client.go89
-rw-r--r--pkg/goexec/dce/default.go5
-rw-r--r--pkg/goexec/dce/options.go108
-rw-r--r--pkg/goexec/dcom/dcom.go34
-rw-r--r--pkg/goexec/dcom/mmc.go50
-rw-r--r--pkg/goexec/dcom/module.go111
-rw-r--r--pkg/goexec/dcom/util.go (renamed from internal/exec/dcom/dcom.go)40
-rw-r--r--pkg/goexec/exec.go14
-rw-r--r--pkg/goexec/io.go87
-rw-r--r--pkg/goexec/method.go72
-rw-r--r--pkg/goexec/module.go25
-rw-r--r--pkg/goexec/proxy.go33
-rw-r--r--pkg/goexec/scmr/change.go144
-rw-r--r--pkg/goexec/scmr/create.go125
-rw-r--r--pkg/goexec/scmr/delete.go46
-rw-r--r--pkg/goexec/scmr/module.go131
-rw-r--r--pkg/goexec/scmr/scmr.go19
-rw-r--r--pkg/goexec/smb/client.go111
-rw-r--r--pkg/goexec/smb/default.go10
-rw-r--r--pkg/goexec/smb/options.go98
-rw-r--r--pkg/goexec/smb/output.go63
-rw-r--r--pkg/goexec/tsch/create.go113
-rw-r--r--pkg/goexec/tsch/demand.go91
-rw-r--r--pkg/goexec/tsch/module.go173
-rw-r--r--pkg/goexec/tsch/tsch.go166
-rw-r--r--pkg/goexec/wmi/call.go31
-rw-r--r--pkg/goexec/wmi/module.go140
-rw-r--r--pkg/goexec/wmi/proc.go64
-rw-r--r--pkg/goexec/wmi/wmi.go16
58 files changed, 2890 insertions, 2030 deletions
diff --git a/Dockerfile b/Dockerfile
index 6de9006..c28cd30 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,6 +5,7 @@ WORKDIR /go/src/
COPY cmd/ cmd/
COPY internal/ internal/
+COPY pkg/ pkg/
COPY main.go go.mod go.sum ./
ENV CGO_ENABLED=0
diff --git a/TODO.md b/TODO.md
index 5800c7b..a73357a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -8,13 +8,14 @@
- [X] Session hijacking
- [ ] 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)
- [ ] Add more trigger types
+- [ ] Generate random name/path
### SCMR
- [X] Clean up SCMR module
- [X] add dynamic string binding support
- [X] general clean up. Use TSCH & WMI as reference
-- [ ] Output
+- [X] Output
- [ ] Fix SCMR `change` method so that dependencies field isn't permanently overwritten
### DCOM
@@ -41,6 +42,7 @@
## Resolve Eventually
+- [ ] Packet stub encryption for ncacn_ip_tcp doesn't appear to be working...
- [ ] `--shell` option
- [ ] Add Go tests
- [ ] ability to specify multiple targets
diff --git a/cmd/args.go b/cmd/args.go
new file mode 100644
index 0000000..50e7c74
--- /dev/null
+++ b/cmd/args.go
@@ -0,0 +1,126 @@
+package cmd
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+)
+
+func registerRpcFlags(cmd *cobra.Command) {
+ rpcFlags := pflag.NewFlagSet("RPC", pflag.ExitOnError)
+
+ rpcFlags.BoolVar(&rpcClient.NoEpm, "no-epm", false, "Do not use EPM to automatically detect endpoints")
+ //rpcFlags.BoolVar(&rpcClient.Options.EpmAuto, "epm-auto", false, "Automatically detect endpoints instead of using the module defaults")
+ rpcFlags.BoolVar(&rpcClient.NoSign, "no-sign", false, "Disable signing on DCE messages")
+ rpcFlags.BoolVar(&rpcClient.NoSeal, "no-seal", false, "Disable packet stub encryption on DCE messages")
+ rpcFlags.StringVar(&rpcClient.Filter, "epm-filter", "", "String binding to filter endpoints returned by EPM")
+ rpcFlags.StringVar(&rpcClient.Endpoint, "endpoint", "", "Explicit RPC endpoint definition")
+
+ cmd.PersistentFlags().AddFlagSet(rpcFlags)
+
+ cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter")
+ cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter")
+}
+
+func registerProcessExecutionArgs(cmd *cobra.Command) {
+ group := pflag.NewFlagSet("Execution", pflag.ExitOnError)
+
+ group.StringVarP(&exec.Input.Arguments, "args", "a", "", "Command line arguments")
+ group.StringVarP(&exec.Input.CommandLine, "command", "c", "", "Windows process command line (executable & arguments)")
+ group.StringVarP(&exec.Input.Executable, "executable", "e", "", "Windows executable to invoke")
+
+ cmd.PersistentFlags().AddFlagSet(group)
+
+ cmd.MarkFlagsOneRequired("executable", "command")
+ cmd.MarkFlagsMutuallyExclusive("executable", "command")
+}
+
+func registerExecutionOutputArgs(cmd *cobra.Command) {
+ group := pflag.NewFlagSet("Output", pflag.ExitOnError)
+
+ group.StringVarP(&outputPath, "output", "o", "", `Fetch execution output to file or "-" for standard output`)
+ group.StringVarP(&outputMethod, "output-method", "m", "smb", "Method to fetch execution output")
+ group.StringVar(&exec.Output.RemotePath, "remote-output", "", "Location to temporarily store output on remote filesystem")
+ group.BoolVar(&exec.Output.NoDelete, "no-delete-output", false, "Preserve output file on remote filesystem")
+
+ cmd.PersistentFlags().AddFlagSet(group)
+}
+
+func args(reqs ...func(*cobra.Command, []string) error) (fn func(*cobra.Command, []string) error) {
+
+ return func(cmd *cobra.Command, args []string) (err error) {
+
+ for _, req := range reqs {
+ if err = req(cmd, args); err != nil {
+ return
+ }
+ }
+ return
+ }
+}
+
+func argsTarget(proto string) func(cmd *cobra.Command, args []string) error {
+
+ return func(cmd *cobra.Command, args []string) (err error) {
+
+ if len(args) != 1 {
+ return errors.New("command require exactly one positional argument: [target]")
+ }
+
+ if credential, target, err = authOpts.WithTarget(context.TODO(), proto, args[0]); err != nil {
+ return fmt.Errorf("failed to parse target: %w", err)
+ }
+
+ if credential == nil {
+ return errors.New("no credentials supplied")
+ }
+ if target == nil {
+ return errors.New("no target supplied")
+ }
+ return
+ }
+}
+
+func argsSmbClient() func(cmd *cobra.Command, args []string) error {
+ return args(
+ argsTarget("cifs"),
+
+ func(_ *cobra.Command, _ []string) error {
+
+ smbClient.Credential = credential
+ smbClient.Target = target
+ smbClient.Proxy = proxy
+
+ return smbClient.Parse(context.TODO())
+ },
+ )
+}
+
+func argsRpcClient(proto string) func(cmd *cobra.Command, args []string) error {
+ return args(
+ argsTarget(proto),
+
+ func(cmd *cobra.Command, args []string) (err error) {
+
+ rpcClient.Target = target
+ rpcClient.Credential = credential
+ rpcClient.Proxy = proxy
+
+ return rpcClient.Parse(context.TODO())
+ },
+ )
+}
+
+func argsOutput(methods ...string) func(cmd *cobra.Command, args []string) error {
+
+ var as []func(*cobra.Command, []string) error
+
+ for _, method := range methods {
+ if method == "smb" {
+ as = append(as, argsSmbClient())
+ }
+ }
+ return args(as...)
+}
diff --git a/cmd/dcom.go b/cmd/dcom.go
index d105b0c..5bcec78 100644
--- a/cmd/dcom.go
+++ b/cmd/dcom.go
@@ -1,38 +1,37 @@
package cmd
import (
- "github.com/FalconOpsLLC/goexec/internal/exec"
- dcomexec "github.com/FalconOpsLLC/goexec/internal/exec/dcom"
- "github.com/spf13/cobra"
+ "context"
+ dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom"
+ "github.com/oiweiwei/go-msrpc/ssp/gssapi"
+ "github.com/spf13/cobra"
)
func dcomCmdInit() {
- registerRpcFlags(dcomCmd)
- dcomMmcCmdInit()
- dcomCmd.AddCommand(dcomMmcCmd)
+ registerRpcFlags(dcomCmd)
+ dcomMmcCmdInit()
+ dcomCmd.AddCommand(dcomMmcCmd)
}
func dcomMmcCmdInit() {
- dcomMmcCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke")
- dcomMmcCmd.Flags().StringVarP(&workingDirectory, "directory", "d", `C:\`, "Working directory")
- dcomMmcCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Process command line")
- dcomMmcCmd.Flags().StringVar(&windowState, "window", "Minimized", "Window state")
- dcomMmcCmd.Flags().StringVarP(&command, "command", "c", ``, "Windows executable & arguments to run")
+ dcomMmcCmd.Flags().StringVarP(&dcomMmc.WorkingDirectory, "directory", "d", `C:\`, "Working directory")
+ dcomMmcCmd.Flags().StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state")
- dcomMmcCmd.MarkFlagsOneRequired("executable", "command")
- dcomMmcCmd.MarkFlagsMutuallyExclusive("executable", "command")
+ registerProcessExecutionArgs(dcomMmcCmd)
}
var (
- dcomCmd = &cobra.Command{
- Use: "dcom",
- Short: "Establish execution via DCOM",
- Args: cobra.NoArgs,
- }
- dcomMmcCmd = &cobra.Command{
- Use: "mmc [target]",
- Short: "Establish execution via the DCOM MMC20.Application object",
- Long: `Description:
+ dcomMmc dcomexec.DcomMmc
+
+ dcomCmd = &cobra.Command{
+ Use: "dcom",
+ Short: "Establish execution via DCOM",
+ Args: cobra.NoArgs,
+ }
+ dcomMmcCmd = &cobra.Command{
+ Use: "mmc [target]",
+ Short: "Establish execution via the DCOM MMC20.Application object",
+ Long: `Description:
The mmc method uses the exposed MMC20.Application object to call Document.ActiveView.ShellExec,
and ultimately execute system commands.
@@ -42,34 +41,39 @@ References:
https://github.com/fortra/impacket/blob/master/examples/dcomexec.py
https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mmc/view-executeshellcommand
`,
- Args: needsRpcTarget("host"),
- Run: func(cmd *cobra.Command, args []string) {
+ Args: argsRpcClient("host"),
+ Run: func(cmd *cobra.Command, args []string) {
+ var err error
+
+ ctx := gssapi.NewSecurityContext(context.Background())
+
+ ctx = log.With().
+ Str("module", "dcom").
+ Str("method", "mmc").
+ Logger().
+ WithContext(ctx)
+
+ if err = rpcClient.Connect(ctx); err != nil {
+ log.Fatal().Err(err).Msg("Connection failed")
+ }
- ctx = log.With().
- Str("module", "dcom").
- Str("method", "mmc").
- Logger().WithContext(ctx)
+ defer func() {
+ closeErr := rpcClient.Close(ctx)
+ if closeErr != nil {
+ log.Error().Err(closeErr).Msg("Failed to close connection")
+ }
+ }()
- module := dcomexec.Module{}
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
- }
- execCfg := &exec.ExecutionConfig{
- ExecutableName: executable,
- ExecutableArgs: executableArgs,
- ExecutionMethod: dcomexec.MethodMmc,
+ if err = dcomMmc.Init(ctx, &rpcClient); err != nil {
+ log.Error().Err(err).Msg("Module initialization failed")
+ returnCode = 1
+ return
+ }
- ExecutionMethodConfig: dcomexec.MethodMmcConfig{
- WorkingDirectory: workingDirectory,
- WindowState: windowState,
- },
- }
- if err := module.Connect(ctx, creds, target, connCfg); err != nil {
- log.Fatal().Err(err).Msg("Connection failed")
- } else if err = module.Exec(ctx, execCfg); err != nil {
- log.Fatal().Err(err).Msg("Execution failed")
- }
- },
- }
+ if err = dcomMmc.Execute(ctx, exec.Input); err != nil {
+ log.Error().Err(err).Msg("Execution failed")
+ returnCode = 1
+ }
+ },
+ }
)
diff --git a/cmd/root.go b/cmd/root.go
index cbc38c0..733bb75 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,122 +1,105 @@
package cmd
import (
- "context"
"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/oiweiwei/go-msrpc/ssp"
+ "github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
- "net/url"
"os"
- "regexp"
- "strings"
)
var (
- //logFile string
- log zerolog.Logger
- ctx context.Context
- authOpts *adauth.Options
-
- hostname string
- proxyStr string
- proxyUrl *url.URL
-
- // Root flags
- unsafe bool // not implemented
- debug bool
-
- // Generic flags
- command string
- executable string
- executablePath string
- executableArgs string
- workingDirectory string
- windowState string
+ debug bool
+ logJson bool
+ returnCode int
+ outputMethod string
+ outputPath string
+ proxy string
+
+ log zerolog.Logger
+
+ rpcClient dce.Client
+ smbClient smb.Client
+
+ exec = goexec.ExecutionIO{
+ Input: new(goexec.ExecutionInput),
+ Output: new(goexec.ExecutionOutput),
+ }
+
+ authOpts *adauth.Options
+ credential *adauth.Credential
+ target *adauth.Target
rootCmd = &cobra.Command{
- Use: "goexec",
- PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
- // For modules that require a full executable path
- if executablePath != "" && !regexp.MustCompile(`^([a-zA-Z]:)?\\`).MatchString(executablePath) {
- return fmt.Errorf("executable path (-e) must be an absolute Windows path, i.e. C:\\Windows\\System32\\cmd.exe")
- }
- if command != "" {
- p := strings.SplitN(command, " ", 2)
- executable = p[0]
- if len(p) > 1 {
- executableArgs = p[1]
- }
+ Use: "goexec",
+ Short: `Windows remote execution multitool`,
+ Long: `TODO`,
+
+ PersistentPreRun: func(cmd *cobra.Command, args []string) {
+
+ if logJson {
+ log = zerolog.New(os.Stderr)
+ } else {
+ log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
}
- log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel).With().Timestamp().Logger()
+
+ log = log.Level(zerolog.InfoLevel).With().Timestamp().Logger()
if debug {
log = log.Level(zerolog.DebugLevel)
}
- return
+
+ if outputMethod == "smb" {
+ if exec.Output.RemotePath == "" {
+ exec.Output.RemotePath = util.RandomWindowsTempFile()
+ }
+ exec.Output.Provider = &smb.OutputFileFetcher{
+ Client: &smbClient,
+ Share: `C$`,
+ File: exec.Output.RemotePath,
+ }
+ }
},
}
)
-func needs(reqs ...func(*cobra.Command, []string) error) (fn func(*cobra.Command, []string) error) {
- return func(cmd *cobra.Command, args []string) (err error) {
- for _, req := range reqs {
- if err = req(cmd, args); err != nil {
- return
- }
- }
- return
- }
-}
+func init() {
+ // Cobra init
+ {
+ cobra.EnableCommandSorting = false
-func needsTarget(proto string) func(cmd *cobra.Command, args []string) error {
+ rootCmd.InitDefaultVersionFlag()
+ rootCmd.InitDefaultHelpCmd()
+ rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging")
+ rootCmd.PersistentFlags().BoolVar(&logJson, "log-json", false, "Log in JSON format")
- return func(cmd *cobra.Command, args []string) (err error) {
- if proxyStr != "" {
- if proxyUrl, err = url.Parse(proxyStr); err != nil {
- return fmt.Errorf("failed to parse proxy URL %q: %w", proxyStr, err)
- }
- }
- if len(args) != 1 {
- return fmt.Errorf("command require exactly one positional argument: [target]")
- }
- if creds, target, err = authOpts.WithTarget(ctx, proto, args[0]); err != nil {
- return fmt.Errorf("failed to parse target: %w", err)
- }
- if creds == nil {
- return fmt.Errorf("no credentials supplied")
- }
- if target == nil {
- return fmt.Errorf("no target supplied")
- }
- if hostname, err = target.Hostname(ctx); err != nil {
- log.Debug().Err(err).Msg("Could not get target hostname")
- }
- return
+ dcomCmdInit()
+ rootCmd.AddCommand(dcomCmd)
+
+ wmiCmdInit()
+ rootCmd.AddCommand(wmiCmd)
+
+ scmrCmdInit()
+ rootCmd.AddCommand(scmrCmd)
+
+ tschCmdInit()
+ rootCmd.AddCommand(tschCmd)
}
-}
-func init() {
- ctx = context.Background()
-
- cobra.EnableCommandSorting = false
-
- rootCmd.InitDefaultVersionFlag()
- rootCmd.InitDefaultHelpCmd()
- rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging")
- rootCmd.PersistentFlags().StringVarP(&proxyStr, "proxy", "x", "", "Proxy URL")
- rootCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "[NOT IMPLEMENTED] Don't ask for permission to run unsafe actions")
-
- authOpts = &adauth.Options{Debug: log.Debug().Msgf}
- authOpts.RegisterFlags(rootCmd.PersistentFlags())
-
- scmrCmdInit()
- rootCmd.AddCommand(scmrCmd)
- tschCmdInit()
- rootCmd.AddCommand(tschCmd)
- wmiCmdInit()
- rootCmd.AddCommand(wmiCmd)
- dcomCmdInit()
- rootCmd.AddCommand(dcomCmd)
+ // Auth init
+ {
+ gssapi.AddMechanism(ssp.SPNEGO)
+ gssapi.AddMechanism(ssp.NTLM)
+ gssapi.AddMechanism(ssp.KRB5)
+
+ authOpts = &adauth.Options{Debug: log.Debug().Msgf}
+ authOpts.RegisterFlags(rootCmd.PersistentFlags())
+ }
}
func Execute() {
@@ -124,4 +107,5 @@ func Execute() {
fmt.Println(err)
os.Exit(1)
}
+ os.Exit(returnCode)
}
diff --git a/cmd/rpc.go b/cmd/rpc.go
deleted file mode 100644
index cfc7a9d..0000000
--- a/cmd/rpc.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package cmd
-
-import (
- "fmt"
- "github.com/FalconOpsLLC/goexec/internal/client/dce"
- "github.com/oiweiwei/go-msrpc/dcerpc"
- "github.com/spf13/cobra"
- "github.com/spf13/pflag"
- "golang.org/x/net/proxy"
- "regexp"
-)
-
-func needsRpcTarget(proto string) func(cmd *cobra.Command, args []string) error {
- return func(cmd *cobra.Command, args []string) (err error) {
-
- if err = needsTarget(proto)(cmd, args); err != nil {
- return err
- }
- if proxyUrl != nil {
- if netDialer, err := proxy.FromURL(proxyUrl, nil); err != nil {
- return fmt.Errorf("proxy dialer from URL: %w", err)
- } else if dceDialer, ok := netDialer.(dcerpc.Dialer); !ok {
- return fmt.Errorf("failed to cast %T to dcerpc.Dialer", netDialer)
- } else {
- dceConfig.Options = append(dceConfig.Options, dcerpc.WithDialer(dceDialer))
- }
- }
- if argDceStringBinding != "" {
- dceConfig.Endpoint, err = dcerpc.ParseStringBinding(argDceStringBinding)
- if err != nil {
- return fmt.Errorf("failed to parse RPC endpoint: %w", err)
- }
- dceConfig.NoEpm = true // If an explicit endpoint is set, don't use EPM
-
- } else if argDceEpmFilter != "" {
- // This ensures that filters like "ncacn_ip_tcp" will be made into a valid binding (i.e. "ncacn_ip_tcp:")
- if ok, err := regexp.MatchString(`^\w+$`, argDceEpmFilter); err == nil && ok {
- argDceEpmFilter += ":"
- }
- dceConfig.EpmFilter, err = dcerpc.ParseStringBinding(argDceEpmFilter)
- if err != nil {
- return fmt.Errorf("failed to parse EPM filter: %w", err)
- }
- }
- if hostname != "" {
- dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithTargetName(fmt.Sprintf("%s/%s", proto, hostname)))
- }
- if !argDceNoSign {
- dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithSign())
- dceConfig.EpmOptions = append(dceConfig.EpmOptions, dcerpc.WithSign())
- }
- if argDceNoSeal {
- dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithInsecure())
- } else {
- dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
- dceConfig.EpmOptions = append(dceConfig.EpmOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
- }
- return nil
- }
-}
-
-var (
- // DCE arguments
- argDceStringBinding string
- argDceEpmFilter string
- argDceNoSeal bool
- argDceNoSign bool
-
- // DCE options
- dceStringBinding *dcerpc.StringBinding
- dceConfig dce.ConnectionMethodDCEConfig
-)
-
-func registerRpcFlags(cmd *cobra.Command) {
- rpcFlags := pflag.NewFlagSet("RPC", pflag.ExitOnError)
- rpcFlags.BoolVar(&dceConfig.NoEpm, "no-epm", false, "Do not use EPM to automatically detect endpoints")
- rpcFlags.BoolVar(&dceConfig.EpmAuto, "epm-auto", false, "Automatically detect endpoints instead of using the module defaults")
- rpcFlags.BoolVar(&argDceNoSign, "no-sign", false, "Disable signing on DCE messages")
- rpcFlags.BoolVar(&argDceNoSeal, "no-seal", false, "Disable packet stub encryption on DCE messages")
- rpcFlags.StringVarP(&argDceEpmFilter, "epm-filter", "F", "", "String binding to filter endpoints returned by EPM")
- rpcFlags.StringVar(&argDceStringBinding, "endpoint", "", "Explicit RPC endpoint definition")
- cmd.PersistentFlags().AddFlagSet(rpcFlags)
-
- cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter")
- cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter")
-}
diff --git a/cmd/scmr.go b/cmd/scmr.go
index 11e1379..08e23d7 100644
--- a/cmd/scmr.go
+++ b/cmd/scmr.go
@@ -1,14 +1,12 @@
package cmd
import (
- "github.com/FalconOpsLLC/goexec/internal/exec"
+ "context"
"github.com/FalconOpsLLC/goexec/internal/util"
- "github.com/FalconOpsLLC/goexec/internal/windows"
- "github.com/RedTeamPentesting/adauth"
- "github.com/oiweiwei/go-msrpc/dcerpc"
+ "github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/spf13/cobra"
- scmrexec "github.com/FalconOpsLLC/goexec/internal/exec/scmr"
+ scmrexec "github.com/FalconOpsLLC/goexec/pkg/goexec/scmr"
)
func scmrCmdInit() {
@@ -22,53 +20,55 @@ func scmrCmdInit() {
}
func scmrCreateCmdInit() {
- scmrCreateCmd.Flags().StringVarP(&scmrDisplayName, "display-name", "n", "", "Display name of service to create")
- scmrCreateCmd.Flags().StringVarP(&scmrServiceName, "service-name", "s", "", "Name of service to create")
- scmrCreateCmd.Flags().BoolVar(&scmrNoDelete, "no-delete", false, "Don't delete service after execution")
- scmrCreateCmd.Flags().BoolVar(&scmrNoStart, "no-start", false, "Don't start service")
- scmrCreateCmd.Flags().StringVarP(&executablePath, "executable-path", "f", "", "Full path to a remote Windows executable file")
- scmrCreateCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to the executable")
- scmrCreateCmd.Flags().BoolVarP(&scmrOutput, "output", "O", false, "Fetch program output")
+ scmrCreateCmd.Flags().StringVarP(&scmrCreate.DisplayName, "display-name", "n", "", "Display name of service to create")
+ scmrCreateCmd.Flags().StringVarP(&scmrCreate.ServiceName, "service-name", "s", "", "Name of service to create")
+ scmrCreateCmd.Flags().BoolVar(&scmrCreate.NoDelete, "no-delete", false, "Don't delete service after execution")
+ scmrCreateCmd.Flags().BoolVar(&scmrCreate.NoStart, "no-start", false, "Don't start service")
+
+ scmrCreateCmd.Flags().StringVarP(&exec.Input.ExecutablePath, "executable-path", "f", "", "Full path to a remote Windows executable")
+ scmrCreateCmd.Flags().StringVarP(&exec.Input.Arguments, "args", "a", "", "Arguments to pass to the executable")
+
+ scmrCreateCmd.MarkFlagsMutuallyExclusive("no-delete", "no-start")
+
if err := scmrCreateCmd.MarkFlagRequired("executable-path"); err != nil {
panic(err)
}
}
func scmrChangeCmdInit() {
- scmrChangeCmd.Flags().StringVarP(&scmrDisplayName, "display-name", "n", "", "Display name of service to create")
- scmrChangeCmd.Flags().BoolVar(&scmrNoStart, "no-start", false, "Don't start service")
- scmrChangeCmd.Flags().StringVarP(&scmrServiceName, "service-name", "s", "", "Name of service to modify")
- scmrChangeCmd.Flags().StringVarP(&executablePath, "executable-path", "f", "", "Full path to remote Windows executable")
- scmrChangeCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable")
+ scmrChangeCmd.Flags().BoolVar(&scmrChange.NoStart, "no-start", false, "Don't start service")
+ scmrChangeCmd.Flags().StringVarP(&scmrChange.ServiceName, "service-name", "s", "", "Name of service to modify")
+
+ scmrChangeCmd.Flags().StringVarP(&exec.Input.ExecutablePath, "executable-path", "f", "", "Full path to remote Windows executable")
+ scmrChangeCmd.Flags().StringVarP(&exec.Input.Arguments, "args", "a", "", "Arguments to pass to executable")
+
if err := scmrChangeCmd.MarkFlagRequired("service-name"); err != nil {
panic(err)
}
+ if err := scmrCreateCmd.MarkFlagRequired("executable-path"); err != nil {
+ panic(err)
+ }
}
func scmrDeleteCmdInit() {
- scmrDeleteCmd.Flags().StringArrayVarP(&scmrServiceNames, "service-name", "s", scmrServiceNames, "Name of service(s) to delete")
+ scmrDeleteCmd.Flags().StringVarP(&scmrDelete.ServiceName, "service-name", "s", scmrDelete.ServiceName, "Name of service to delete")
+
if err := scmrDeleteCmd.MarkFlagRequired("service-name"); err != nil {
panic(err)
}
}
var (
- // scmr arguments
- scmrServiceName string
- scmrServiceNames []string
- scmrDisplayName string
- scmrNoDelete bool
- scmrNoStart bool
- scmrOutput bool
-
- creds *adauth.Credential
- target *adauth.Target
+ scmrCreate scmrexec.ScmrCreate
+ scmrChange scmrexec.ScmrChange
+ scmrDelete scmrexec.ScmrDelete
scmrCmd = &cobra.Command{
Use: "scmr",
Short: "Establish execution via SCMR",
Args: cobra.NoArgs,
}
+
scmrCreateCmd = &cobra.Command{
Use: "create [target]",
Short: "Create & run a new Windows service to gain execution",
@@ -79,138 +79,145 @@ var (
References:
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/6a8ca926-9477-4dd4-b766-692fab07227e
`,
- Args: needs(needsTarget("host"), needsRpcTarget("host")),
+ Args: argsRpcClient("cifs"),
+
Run: func(cmd *cobra.Command, args []string) {
+ var err error
+
+ ctx := gssapi.NewSecurityContext(context.Background())
- if scmrServiceName == "" {
- log.Warn().Msg("No service name was specified, using random string")
- scmrServiceName = util.RandomString()
+ ctx = log.With().
+ Str("module", "scmr").
+ Str("method", "create").
+ Logger().
+ WithContext(ctx)
+
+ if scmrCreate.ServiceName == "" {
+ log.Warn().Msg("No service name was provided. Using a random string")
+ scmrCreate.ServiceName = util.RandomString()
}
- if scmrNoDelete {
+
+ if scmrCreate.NoDelete {
log.Warn().Msg("Service will not be deleted after execution")
}
- if scmrDisplayName == "" {
+
+ if scmrCreate.DisplayName == "" {
log.Debug().Msg("No display name specified, using service name as display name")
- scmrDisplayName = scmrServiceName
+ scmrCreate.DisplayName = scmrCreate.ServiceName
}
- executor := scmrexec.Module{}
- cleanCfg := &exec.CleanupConfig{
- CleanupMethod: scmrexec.CleanupMethodDelete,
- CleanupMethodConfig: scmrexec.CleanupMethodDeleteConfig{},
- }
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
- }
- execCfg := &exec.ExecutionConfig{
- ExecutablePath: executablePath,
- ExecutableArgs: executableArgs,
- ReturnOutput: scmrOutput,
- ExecutionMethod: scmrexec.MethodCreate,
-
- ExecutionMethodConfig: scmrexec.MethodCreateConfig{
- NoDelete: scmrNoDelete,
- ServiceName: util.RandomStringIfBlank(scmrServiceName),
- DisplayName: scmrDisplayName,
- ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
- StartType: windows.SERVICE_DEMAND_START,
- },
- }
- ctx = log.With().
- Str("module", "scmr").
- Str("method", "create").
- Logger().WithContext(ctx)
-
- if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
+ if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
- if !scmrNoDelete {
- defer func() {
- if err := executor.Cleanup(ctx, cleanCfg); err != nil {
- log.Error().Err(err).Msg("Cleanup failed")
- }
- }()
+
+ defer func() {
+ closeErr := rpcClient.Close(ctx)
+ if closeErr != nil {
+ log.Error().Err(closeErr).Msg("Failed to close connection")
+ }
+ }()
+
+ defer func() {
+ cleanErr := scmrCreate.Clean(ctx)
+ if cleanErr != nil {
+ log.Warn().Err(cleanErr).Msg("Clean operation failed")
+ }
+ }()
+
+ if err = scmrCreate.Init(ctx, &rpcClient); err != nil {
+ log.Error().Err(err).Msg("Module initialization failed")
+ returnCode = 2
+ return
}
- if err := executor.Exec(ctx, execCfg); err != nil {
+
+ if err = scmrCreate.Execute(ctx, exec.Input); err != nil {
log.Error().Err(err).Msg("Execution failed")
+ returnCode = 4
}
},
}
+
scmrChangeCmd = &cobra.Command{
Use: "change [target]",
Short: "Change an existing Windows service to gain execution",
- Args: needs(needsTarget("host"), needsRpcTarget("host")),
+ Args: argsRpcClient("cifs"),
Run: func(cmd *cobra.Command, args []string) {
+ var err error
+
+ ctx := gssapi.NewSecurityContext(context.Background())
- executor := scmrexec.Module{}
- cleanCfg := &exec.CleanupConfig{
- CleanupMethod: scmrexec.CleanupMethodRevert,
- CleanupMethodConfig: scmrexec.CleanupMethodRevertConfig{},
- }
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
- }
- execCfg := &exec.ExecutionConfig{
- ExecutablePath: executablePath,
- ExecutableArgs: executableArgs,
- ExecutionMethod: scmrexec.MethodChange,
-
- ExecutionMethodConfig: scmrexec.MethodChangeConfig{
- NoStart: scmrNoStart,
- ServiceName: scmrServiceName,
- },
- }
ctx = log.With().
Str("module", "scmr").
Str("method", "change").
- Logger().WithContext(ctx)
+ Logger().
+ WithContext(ctx)
- if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
+ if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
- if !scmrNoDelete {
- defer func() {
- if err := executor.Cleanup(ctx, cleanCfg); err != nil {
- log.Error().Err(err).Msg("Cleanup failed")
- }
- }()
+
+ defer func() {
+ closeErr := rpcClient.Close(ctx)
+ if closeErr != nil {
+ log.Error().Err(closeErr).Msg("Failed to close connection")
+ }
+ }()
+
+ defer func() {
+ cleanErr := scmrChange.Clean(ctx)
+ if cleanErr != nil {
+ log.Warn().Err(cleanErr).Msg("Clean operation failed")
+ }
+ }()
+
+ if err = scmrChange.Init(ctx, &rpcClient); err != nil {
+ log.Error().Err(err).Msg("Module initialization failed")
+ returnCode = 2
+ return
}
- if err := executor.Exec(ctx, execCfg); err != nil {
+
+ if err = scmrChange.Execute(ctx, exec.Input); err != nil {
log.Error().Err(err).Msg("Execution failed")
+ returnCode = 4
}
},
}
scmrDeleteCmd = &cobra.Command{
Use: "delete [target]",
Short: "Delete an existing Windows service",
- Long: `Description:
-TODO
-`,
- Args: needs(needsTarget("host"), needsRpcTarget("host")),
+ Long: `TODO`,
+
+ Args: argsRpcClient("cifs"),
Run: func(cmd *cobra.Command, args []string) {
- dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithInsecure())
+ var err error
+
+ ctx := gssapi.NewSecurityContext(context.Background())
- executor := scmrexec.Module{}
- cleanCfg := &exec.CleanupConfig{
- CleanupMethod: scmrexec.CleanupMethodDelete,
- CleanupMethodConfig: scmrexec.CleanupMethodDeleteConfig{ServiceNames: scmrServiceNames},
- }
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
- }
ctx = log.With().
Str("module", "scmr").
Str("method", "delete").
- Logger().WithContext(ctx)
+ Logger().
+ WithContext(ctx)
- if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
+ if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
+ }
+
+ defer func() {
+ closeErr := rpcClient.Close(ctx)
+ if closeErr != nil {
+ log.Error().Err(closeErr).Msg("Failed to close connection")
+ }
+ }()
+
+ if err = scmrDelete.Init(ctx, &rpcClient); err != nil {
+ log.Error().Err(err).Msg("Module initialization failed")
+ returnCode = 2
+ }
- } else if err = executor.Cleanup(ctx, cleanCfg); err != nil {
- log.Fatal().Err(err).Msg("Delete failed")
+ if err = scmrDelete.Clean(ctx); err != nil {
+ log.Warn().Err(err).Msg("Clean failed")
+ returnCode = 4
}
},
}
diff --git a/cmd/tsch.go b/cmd/tsch.go
index f377d56..7c0fdde 100644
--- a/cmd/tsch.go
+++ b/cmd/tsch.go
@@ -1,154 +1,89 @@
package cmd
import (
- "fmt"
- "github.com/FalconOpsLLC/goexec/internal/exec"
- "github.com/FalconOpsLLC/goexec/internal/exec/tsch"
+ "context"
+ "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"
- "regexp"
+ "io"
+ "os"
"time"
)
func tschCmdInit() {
registerRpcFlags(tschCmd)
- tschDeleteCmdInit()
- tschCmd.AddCommand(tschDeleteCmd)
- tschRegisterCmdInit()
- tschCmd.AddCommand(tschRegisterCmd)
tschDemandCmdInit()
tschCmd.AddCommand(tschDemandCmd)
+
+ tschCreateCmdInit()
+ tschCmd.AddCommand(tschCreateCmd)
}
-func tschDeleteCmdInit() {
- tschDeleteCmd.Flags().StringVarP(&tschTaskPath, "path", "t", "", "Scheduled task path")
- if err := tschDeleteCmd.MarkFlagRequired("path"); err != nil {
- panic(err)
+func argsTschTaskIdentifiers(name, path string) error {
+ switch {
+ case path != "":
+ return tschexec.ValidateTaskPath(path)
+ case name != "":
+ return tschexec.ValidateTaskName(name)
+ default:
}
+ return nil
}
-func tschDemandCmdInit() {
- tschDemandCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke")
- tschDemandCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable")
- tschDemandCmd.Flags().StringVarP(&tschTaskName, "name", "n", "", "Target task name")
- tschDemandCmd.Flags().BoolVar(&tschNoDelete, "no-delete", false, "Don't delete task after execution")
- tschDemandCmd.Flags().Uint32Var(&tschSessionId, "session-id", 0, "Hijack existing session")
- if err := tschDemandCmd.MarkFlagRequired("executable"); err != nil {
- panic(err)
- }
+func argsTschDemand(_ *cobra.Command, _ []string) error {
+ return argsTschTaskIdentifiers(tschDemand.TaskName, tschDemand.TaskPath)
}
-func tschRegisterCmdInit() {
- tschRegisterCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke")
- tschRegisterCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable")
- tschRegisterCmd.Flags().StringVarP(&tschTaskName, "name", "n", "", "Target task name")
- tschRegisterCmd.Flags().DurationVar(&tschStopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This will not stop the process spawned by the task")
- tschRegisterCmd.Flags().DurationVarP(&tschDelay, "delay-start", "d", 5*time.Second, "Delay between task registration and execution")
- tschRegisterCmd.Flags().DurationVarP(&tschDeleteDelay, "delay-delete", "D", 0*time.Second, "Delay between task termination and deletion")
- tschRegisterCmd.Flags().BoolVar(&tschNoDelete, "no-delete", false, "Don't delete task after execution")
- tschRegisterCmd.Flags().BoolVar(&tschCallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task")
-
- tschRegisterCmd.MarkFlagsMutuallyExclusive("no-delete", "delay-delete")
- tschRegisterCmd.MarkFlagsMutuallyExclusive("no-delete", "call-delete")
- tschRegisterCmd.MarkFlagsMutuallyExclusive("delay-delete", "call-delete")
-
- if err := tschRegisterCmd.MarkFlagRequired("executable"); err != nil {
- panic(err)
- }
+func argsTschCreate(_ *cobra.Command, _ []string) error {
+ return argsTschTaskIdentifiers(tschCreate.TaskName, tschCreate.TaskPath)
}
-func tschArgs(principal string) func(cmd *cobra.Command, args []string) error {
- return func(cmd *cobra.Command, args []string) error {
- if tschTaskPath != "" && !tschTaskPathRegex.MatchString(tschTaskPath) {
- return fmt.Errorf("invalid task path: %s", tschTaskPath)
- }
- if tschTaskName != "" {
- if !tschTaskNameRegex.MatchString(tschTaskName) {
- return fmt.Errorf("invalid task name: %s", tschTaskName)
-
- } else if tschTaskPath == "" {
- tschTaskPath = `\` + tschTaskName
- }
- }
- return needsRpcTarget(principal)(cmd, args)
- }
+func tschDemandCmdInit() {
+ tschDemandCmd.Flags().StringVarP(&tschDemand.TaskName, "name", "t", "", "Name of task to register")
+ tschDemandCmd.Flags().StringVarP(&tschDemand.TaskPath, "path", "P", "", "Path of task to register")
+ tschDemandCmd.Flags().Uint32Var(&tschDemand.SessionId, "session", 0, "Hijack existing session given the session ID")
+ tschDemandCmd.Flags().BoolVar(&tschDemand.NoDelete, "no-delete", false, "Don't delete task after execution")
+ tschDemandCmd.Flags().StringVar(&tschDemand.UserSid, "sid", "S-1-5-18", "User SID to impersonate")
+
+ registerProcessExecutionArgs(tschDemandCmd)
+ registerExecutionOutputArgs(tschDemandCmd)
+
+ tschDemandCmd.MarkFlagsMutuallyExclusive("name", "path")
+}
+
+func tschCreateCmdInit() {
+ tschCreateCmd.Flags().StringVarP(&tschCreate.TaskName, "name", "t", "", "Name of task to register")
+ tschCreateCmd.Flags().StringVarP(&tschCreate.TaskPath, "path", "P", "", "Path of task to register")
+ tschCreateCmd.Flags().DurationVar(&tschCreate.StopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This will not stop the process spawned by the task")
+ tschCreateCmd.Flags().DurationVar(&tschCreate.StartDelay, "start-delay", 5*time.Second, "Delay between task registration and execution")
+ tschCreateCmd.Flags().DurationVar(&tschCreate.DeleteDelay, "delete-delay", 0*time.Second, "Delay between task termination and deletion")
+ tschCreateCmd.Flags().BoolVar(&tschCreate.NoDelete, "no-delete", false, "Don't delete task after execution")
+ tschCreateCmd.Flags().BoolVar(&tschCreate.CallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task")
+ tschCreateCmd.Flags().StringVar(&tschCreate.UserSid, "sid", "S-1-5-18", "User SID to impersonate")
+
+ registerProcessExecutionArgs(tschCreateCmd)
+
+ tschCreateCmd.MarkFlagsMutuallyExclusive("name", "path")
}
var (
- tschSessionId uint32
- tschNoDelete bool
- tschCallDelete bool
- tschDeleteDelay time.Duration
- tschStopDelay time.Duration
- tschDelay time.Duration
- tschTaskName string
- tschTaskPath string
-
- tschTaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`)
- tschTaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`)
+ tschDemand tschexec.TschDemand
+ tschCreate tschexec.TschCreate
tschCmd = &cobra.Command{
Use: "tsch",
- Short: "Establish execution via TSCH",
+ Short: "Establish execution via Windows Task Scheduler (MS-TSCH)",
Args: cobra.NoArgs,
}
- tschRegisterCmd = &cobra.Command{
- Use: "register [target]",
- Short: "Register a remote scheduled task with an automatic start time",
- Long: `Description:
- The register method calls SchRpcRegisterTask to register a scheduled task
- with an automatic start time.This method avoids directly calling SchRpcRun,
- and can even avoid calling SchRpcDelete by populating the DeleteExpiredTaskAfter
- Setting.
-References:
- SchRpcRegisterTask - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167
- SchRpcRun - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/77f2250d-500a-40ee-be18-c82f7079c4f0
- SchRpcDelete - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/360bb9b1-dd2a-4b36-83ee-21f12cb97cff
- DeleteExpiredTaskAfter - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/6bfde6fe-440e-4ddd-b4d6-c8fc0bc06fae
-`,
- Args: tschArgs("cifs"),
- Run: func(cmd *cobra.Command, args []string) {
-
- log = log.With().
- Str("module", "tsch").
- Str("method", "register").
- Logger()
- if tschNoDelete {
- log.Warn().Msg("Task will not be deleted after execution")
- }
-
- module := tschexec.Module{}
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
- }
- execCfg := &exec.ExecutionConfig{
- ExecutableName: executable,
- ExecutableArgs: executableArgs,
- ExecutionMethod: tschexec.MethodRegister,
-
- ExecutionMethodConfig: tschexec.MethodRegisterConfig{
- NoDelete: tschNoDelete,
- CallDelete: tschCallDelete,
- StartDelay: tschDelay,
- StopDelay: tschStopDelay,
- DeleteDelay: tschDeleteDelay,
- TaskPath: tschTaskPath,
- },
- }
- if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil {
- log.Fatal().Err(err).Msg("Connection failed")
- } else if err = module.Exec(log.WithContext(ctx), execCfg); err != nil {
- log.Fatal().Err(err).Msg("Execution failed")
- }
- },
- }
tschDemandCmd = &cobra.Command{
Use: "demand [target]",
Short: "Register a remote scheduled task and demand immediate start",
Long: `Description:
- Similar to the register method, the demand method will call SchRpcRegisterTask,
+ 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.
@@ -156,68 +91,110 @@ References:
SchRpcRegisterTask - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167
SchRpcRun - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/77f2250d-500a-40ee-be18-c82f7079c4f0
`,
- Args: tschArgs("cifs"),
+ Args: args(
+ argsRpcClient("cifs"),
+ argsSmbClient(),
+ argsTschDemand,
+ ),
+
Run: func(cmd *cobra.Command, args []string) {
+ var err error
+
+ tschDemand.Client = &rpcClient
+ tschDemand.IO = exec
- log = log.With().
- Str("module", "tsch").
- Str("method", "register").
- Logger()
- if tschNoDelete {
- log.Warn().Msg("Task will not be deleted after execution")
+ if tschDemand.TaskName == "" && tschDemand.TaskPath == "" {
+ tschDemand.TaskPath = `\` + util.RandomString()
}
- module := tschexec.Module{}
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
+
+ ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO()))
+
+ var writer io.WriteCloser
+
+ if outputPath == "-" {
+ writer = os.Stdout
+
+ } else if outputPath != "" {
+
+ if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
+ log.Fatal().Err(err).Msg("Failed to open output file")
+ }
+ defer writer.Close()
}
- execCfg := &exec.ExecutionConfig{
- ExecutableName: executable,
- ExecutableArgs: executableArgs,
- ExecutionMethod: tschexec.MethodDemand,
-
- ExecutionMethodConfig: tschexec.MethodDemandConfig{
- NoDelete: tschNoDelete,
- TaskPath: tschTaskPath,
- SessionId: tschSessionId,
- },
+
+ if err = goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil {
+ log.Fatal().Err(err).Msg("Operation failed")
}
- if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil {
- log.Fatal().Err(err).Msg("Connection failed")
- } else if err = module.Exec(log.WithContext(ctx), execCfg); err != nil {
- log.Fatal().Err(err).Msg("Execution failed")
+
+ if outputPath != "" {
+ if reader, err := tschDemand.GetOutput(ctx); err == nil {
+ _, err = io.Copy(writer, reader)
+
+ } else {
+ log.Error().Err(err).Msg("Failed to get process execution output")
+ returnCode = 2
+ }
}
},
}
- tschDeleteCmd = &cobra.Command{
- Use: "delete [target]",
- Short: "Manually delete a scheduled task",
+ tschCreateCmd = &cobra.Command{
+ Use: "create [target]",
+ Short: "Create a remote scheduled task with an automatic start time",
Long: `Description:
- The delete method manually deletes a scheduled task by calling SchRpcDelete
+ 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.
References:
+ SchRpcRegisterTask - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167
+ SchRpcRun - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/77f2250d-500a-40ee-be18-c82f7079c4f0
SchRpcDelete - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/360bb9b1-dd2a-4b36-83ee-21f12cb97cff
+ DeleteExpiredTaskAfter - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/6bfde6fe-440e-4ddd-b4d6-c8fc0bc06fae
`,
- Args: tschArgs("cifs"),
+ Args: args(
+ argsRpcClient("cifs"),
+ argsSmbClient(),
+ argsTschCreate,
+ ),
+
Run: func(cmd *cobra.Command, args []string) {
- log = log.With().
- Str("module", "tsch").
- Str("method", "delete").
- Logger()
-
- module := tschexec.Module{}
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
+ var err error
+
+ tschCreate.Tsch.Client = &rpcClient
+ tschCreate.IO = exec
+
+ if tschCreate.TaskName == "" && tschDemand.TaskPath == "" {
+ tschCreate.TaskPath = `\` + util.RandomString()
}
- cleanCfg := &exec.CleanupConfig{
- CleanupMethod: tschexec.MethodDelete,
- CleanupMethodConfig: tschexec.MethodDeleteConfig{TaskPath: tschTaskPath},
+
+ ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO()))
+
+ var writer io.WriteCloser
+
+ if outputPath == "-" {
+ writer = os.Stdout
+
+ } else if outputPath != "" {
+
+ if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
+ log.Fatal().Err(err).Msg("Failed to open output file")
+ }
+ defer writer.Close()
}
- if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil {
- log.Fatal().Err(err).Msg("Connection failed")
- } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil {
- log.Fatal().Err(err).Msg("Cleanup failed")
+
+ if err = goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil {
+ log.Fatal().Err(err).Msg("Operation failed")
+ }
+
+ if outputPath != "" {
+ if reader, err := tschDemand.GetOutput(ctx); err == nil {
+ _, err = io.Copy(writer, reader)
+
+ } else {
+ log.Error().Err(err).Msg("Failed to get process execution output")
+ returnCode = 2
+ }
}
},
}
diff --git a/cmd/wmi.go b/cmd/wmi.go
index 59096ec..196ff82 100644
--- a/cmd/wmi.go
+++ b/cmd/wmi.go
@@ -1,52 +1,65 @@
package cmd
import (
+ "context"
"encoding/json"
"fmt"
- "github.com/FalconOpsLLC/goexec/internal/exec"
- wmiexec "github.com/FalconOpsLLC/goexec/internal/exec/wmi"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ wmiexec "github.com/FalconOpsLLC/goexec/pkg/goexec/wmi"
+ "github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/spf13/cobra"
+ "io"
+ "os"
)
func wmiCmdInit() {
registerRpcFlags(wmiCmd)
+
wmiCallCmdInit()
wmiCmd.AddCommand(wmiCallCmd)
- wmiProcessCmdInit()
- wmiCmd.AddCommand(wmiProcessCmd)
+
+ wmiProcCmdInit()
+ wmiCmd.AddCommand(wmiProcCmd)
+}
+
+func wmiCallArgs(_ *cobra.Command, _ []string) error {
+ return json.Unmarshal([]byte(wmiArguments), &wmiCall.Args)
}
func wmiCallCmdInit() {
- wmiCallCmd.Flags().StringVarP(&dceConfig.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace")
- wmiCallCmd.Flags().StringVarP(&wmi.Class, "class", "C", "", `WMI class to instantiate (i.e. "Win32_Process")`)
- wmiCallCmd.Flags().StringVarP(&wmi.Method, "method", "m", "", `WMI Method to call (i.e. "Create")`)
- wmiCallCmd.Flags().StringVarP(&wmi.Args, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`)
+ wmiCallCmd.Flags().StringVarP(&wmiCall.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace")
+ wmiCallCmd.Flags().StringVarP(&wmiCall.Class, "class", "C", "", `WMI class to instantiate (i.e. "Win32_Process")`)
+ wmiCallCmd.Flags().StringVarP(&wmiCall.Method, "method", "m", "", `WMI Method to call (i.e. "Create")`)
+ wmiCallCmd.Flags().StringVarP(&wmiArguments, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`)
+
+ if err := wmiCallCmd.MarkFlagRequired("class"); err != nil {
+ panic(err)
+ }
if err := wmiCallCmd.MarkFlagRequired("method"); err != nil {
panic(err)
}
}
-func wmiProcessCmdInit() {
- wmiProcessCmd.Flags().StringVarP(&command, "command", "c", "", "Process command line")
- wmiProcessCmd.Flags().StringVarP(&workingDirectory, "directory", "d", `C:\`, "Working directory")
- if err := wmiProcessCmd.MarkFlagRequired("command"); err != nil {
- panic(err)
- }
+func wmiProcCmdInit() {
+ wmiProcCmd.Flags().StringVarP(&wmiProc.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace")
+ wmiProcCmd.Flags().StringVarP(&wmiProc.WorkingDirectory, "directory", "d", `C:\`, "Working directory")
+
+ registerProcessExecutionArgs(wmiProcCmd)
+ registerExecutionOutputArgs(wmiProcCmd)
}
var (
- wmi struct {
- Class string
- Method string
- Args string
- }
- wmiMethodArgsMap map[string]any
+ wmiCall = wmiexec.WmiCall{}
+ wmiProc = wmiexec.WmiProc{}
+
+ wmiArguments string
wmiCmd = &cobra.Command{
Use: "wmi",
- Short: "Establish execution via WMI",
+ Short: "Establish execution via wmi",
Args: cobra.NoArgs,
}
+
wmiCallCmd = &cobra.Command{
Use: "call",
Short: "Execute specified WMI method",
@@ -56,52 +69,48 @@ var (
References:
https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-classes
- `,
- Args: needs(needsTarget("cifs"), needsRpcTarget("cifs"), func(cmd *cobra.Command, args []string) (err error) {
- if err = json.Unmarshal([]byte(wmi.Args), &wmiMethodArgsMap); err != nil {
- err = fmt.Errorf("parse JSON arguments: %w", err)
- }
- return
- }),
+`,
+ Args: args(argsRpcClient("host"), wmiCallArgs),
+
Run: func(cmd *cobra.Command, args []string) {
- executor := wmiexec.Module{}
- cleanCfg := &exec.CleanupConfig{} // TODO
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
- }
+ var err error
- execCfg := &exec.ExecutionConfig{
- ExecutableName: executable,
- ExecutableArgs: executableArgs,
- ExecutionMethod: wmiexec.MethodCall,
- ExecutionMethodConfig: wmiexec.MethodCallConfig{
- Class: wmi.Class,
- Method: wmi.Method,
- Arguments: wmiMethodArgsMap,
- },
- }
+ ctx := gssapi.NewSecurityContext(context.Background())
ctx = log.With().
Str("module", "wmi").
- Str("method", "proc").
- Logger().WithContext(ctx)
+ Str("method", "call").
+ Logger().
+ WithContext(ctx)
- if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
+ if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
+
defer func() {
- if err := executor.Cleanup(ctx, cleanCfg); err != nil {
- log.Error().Err(err).Msg("Cleanup failed")
+ closeErr := rpcClient.Close(ctx)
+ if closeErr != nil {
+ log.Error().Err(closeErr).Msg("Failed to close connection")
}
}()
- if err := executor.Exec(ctx, execCfg); err != nil {
- log.Error().Err(err).Msg("Execution failed")
+
+ if err = wmiCall.Init(ctx); err != nil {
+ log.Error().Err(err).Msg("Module initialization failed")
+ returnCode = 2
+ return
}
+
+ out, err := wmiCall.Call(ctx)
+ if err != nil {
+ log.Error().Err(err).Msg("Call failed")
+ returnCode = 4
+ return
+ }
+ fmt.Println(string(out))
},
}
- wmiProcessCmd = &cobra.Command{
+ wmiProcCmd = &cobra.Command{
Use: "proc",
Short: "Start a Windows process",
Long: `Description:
@@ -112,41 +121,41 @@ References:
References:
https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/create-method-in-class-win32-process
`,
- Args: needs(needsTarget("cifs"), needsRpcTarget("cifs")),
+ Args: args(argsOutput("smb"), argsRpcClient("host")),
+
Run: func(cmd *cobra.Command, args []string) {
+ var err error
+
+ wmiProc.Client = &rpcClient
+ wmiProc.IO = exec
+
+ ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO()))
+
+ var writer io.WriteCloser
+
+ if outputPath == "-" {
+ writer = os.Stdout
- executor := wmiexec.Module{}
- cleanCfg := &exec.CleanupConfig{} // TODO
- connCfg := &exec.ConnectionConfig{
- ConnectionMethod: exec.ConnectionMethodDCE,
- ConnectionMethodConfig: dceConfig,
+ } else if outputPath != "" {
+
+ if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
+ log.Fatal().Err(err).Msg("Failed to open output file")
+ }
+ defer writer.Close()
}
- execCfg := &exec.ExecutionConfig{
- ExecutableName: executable,
- ExecutableArgs: executableArgs,
- ExecutionMethod: wmiexec.MethodProcess,
-
- ExecutionMethodConfig: wmiexec.MethodProcessConfig{
- Command: command,
- WorkingDirectory: workingDirectory,
- },
+
+ if err = goexec.ExecuteCleanMethod(ctx, &wmiProc, &exec); err != nil {
+ log.Fatal().Err(err).Msg("Operation failed")
}
- ctx = log.With().
- Str("module", "wmi").
- Str("method", "proc").
- Logger().WithContext(ctx)
+ if outputPath != "" {
+ if reader, err := wmiProc.GetOutput(ctx); err == nil {
+ _, err = io.Copy(writer, reader)
- if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
- log.Fatal().Err(err).Msg("Connection failed")
- }
- defer func() {
- if err := executor.Cleanup(ctx, cleanCfg); err != nil {
- log.Error().Err(err).Msg("Cleanup failed")
+ } else {
+ log.Error().Err(err).Msg("Failed to get process execution output")
+ returnCode = 2
}
- }()
- if err := executor.Exec(ctx, execCfg); err != nil {
- log.Error().Err(err).Msg("Execution failed")
}
},
}
diff --git a/go.mod b/go.mod
index 793956b..7224fc0 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/FalconOpsLLC/goexec
go 1.24.1
require (
- github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877
+ github.com/RedTeamPentesting/adauth v0.2.0
github.com/google/uuid v1.6.0
github.com/oiweiwei/go-msrpc v1.2.5
github.com/rs/zerolog v1.34.0
diff --git a/go.sum b/go.sum
index b2629fd..d611052 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877 h1:n5V0EER+EvvmUZitR5zGFUMyoIHnI12SWFMciI7kh70=
github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877/go.mod h1:iHf/fY7CueB7qLHZ5YgTZXvrVCSLJy4+tAifOSNLAFQ=
+github.com/RedTeamPentesting/adauth v0.2.0 h1:pNb4xcEd/oG/JY8aXkBm8Y0veVaUA/CsKFhgEgNCPik=
+github.com/RedTeamPentesting/adauth v0.2.0/go.mod h1:AhWZEgl98YkKEta6kXTOSocQh2SbJJZg4nKqF+uY4bc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/internal/client/dce/dce.go b/internal/client/dce/dce.go
deleted file mode 100644
index 0230c2a..0000000
--- a/internal/client/dce/dce.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package dce
-
-import (
- "context"
- "errors"
- "fmt"
- "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/ssp/gssapi"
- "github.com/rs/zerolog"
-)
-
-type ConnectionMethodDCEConfig struct {
- NoEpm bool // NoEpm disables EPM
- EpmAuto bool // EpmAuto will find any suitable endpoint, without any filter
- Endpoint *dcerpc.StringBinding // Endpoint is the explicit endpoint passed to dcerpc.WithEndpoint for use without EPM
- EpmFilter *dcerpc.StringBinding // EpmFilter is the rough filter used to pick an EPM endpoint
- Options []dcerpc.Option // Options stores the options that will be passed to all dialers
- DceOptions []dcerpc.Option // DceOptions are the options passed to dcerpc.Dial
- EpmOptions []dcerpc.Option // EpmOptions are the options passed to epm.EndpointMapper
- Resource string // Resource stores the target network resource (usually for DCOM)
-}
-
-func (cfg *ConnectionMethodDCEConfig) GetDce(ctx context.Context, cred *adauth.Credential, target *adauth.Target, endpoint, object string, arbOpts ...dcerpc.Option) (cc dcerpc.Conn, err error) {
-
- log := zerolog.Ctx(ctx).With().
- Str("client", "DCERPC").Logger()
-
- // Mandatory logging
- dceOpts := append(append(cfg.Options, arbOpts...), append(cfg.DceOptions, dcerpc.WithLogger(log))...)
- epmOpts := append(cfg.Options, append(cfg.EpmOptions, dcerpc.WithLogger(log))...)
-
- ctx = gssapi.NewSecurityContext(ctx)
- auth, err := dcerpcauth.AuthenticationOptions(ctx, cred, target, &dcerpcauth.Options{})
- if err != nil {
- log.Error().Err(err).Msg("Failed to parse authentication options")
- return nil, fmt.Errorf("parse auth options: %w", err)
- }
- addr := target.AddressWithoutPort()
- log = log.With().Str("address", addr).Logger()
-
- if object != "" {
- if id, err := uuid.Parse(object); err != nil {
- log.Error().Err(err).Msg("Failed to parse input object UUID")
- } else {
- dceOpts = append(dceOpts, dcerpc.WithObjectUUID(id))
- }
- }
- if cfg.Endpoint != nil {
- dceOpts = append(dceOpts, dcerpc.WithEndpoint(cfg.Endpoint.String()))
- log.Debug().Str("binding", cfg.Endpoint.String()).Msg("Using endpoint")
-
- } else if !cfg.NoEpm {
- dceOpts = append(dceOpts, epm.EndpointMapper(ctx, addr, append(epmOpts, auth...)...))
- log.Debug().Msg("Using endpoint mapper")
-
- if cfg.EpmFilter != nil {
- dceOpts = append(dceOpts, dcerpc.WithEndpoint(cfg.EpmFilter.String()))
- log.Debug().Str("filter", cfg.EpmFilter.String()).Msg("Using endpoint filter")
- }
- } else if endpoint != "" {
- dceOpts = append(dceOpts, dcerpc.WithEndpoint(endpoint))
- log.Debug().Str("endpoint", endpoint).Msg("Using default endpoint")
-
- } else {
- log.Err(err).Msg("Invalid DCE connection options")
- return nil, errors.New("get DCE: invalid connection options")
- }
-
- return dcerpc.Dial(ctx, target.AddressWithoutPort(), append(dceOpts, auth...)...)
-}
diff --git a/internal/exec/dcom/exec.go b/internal/exec/dcom/exec.go
deleted file mode 100644
index a34baf4..0000000
--- a/internal/exec/dcom/exec.go
+++ /dev/null
@@ -1,181 +0,0 @@
-package dcomexec
-
-import (
- "context"
- "errors"
- "fmt"
- "github.com/FalconOpsLLC/goexec/internal/client/dce"
- "github.com/FalconOpsLLC/goexec/internal/exec"
- "github.com/RedTeamPentesting/adauth"
- guuid "github.com/google/uuid"
- "github.com/oiweiwei/go-msrpc/dcerpc"
- "github.com/oiweiwei/go-msrpc/midl/uuid"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0"
- "github.com/oiweiwei/go-msrpc/msrpc/dtyp"
- "github.com/rs/zerolog"
-)
-
-const (
- DefaultDcomEndpoint = "ncacn_ip_tcp:[135]"
-)
-
-var (
- MmcUuid = uuid.MustParse("49B2791A-B1AE-4C90-9B8E-E860BA07F889")
- ShellWindowsUuid = uuid.MustParse("9BA05972-F6A8-11CF-A442-00A0C90A8F39")
- RandCid = dcom.CID(*dtyp.GUIDFromUUID(uuid.MustParse(guuid.NewString())))
- IDispatchIID = &dcom.IID{
- Data1: 0x20400,
- Data2: 0x0,
- Data3: 0x0,
- Data4: []byte{0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46},
- }
- ComVersion = &dcom.COMVersion{
- MajorVersion: 5,
- MinorVersion: 7,
- }
- MmcClsid = dcom.ClassID(*dtyp.GUIDFromUUID(MmcUuid))
- ORPCThis = &dcom.ORPCThis{
- Version: ComVersion,
- CID: &RandCid,
- }
-)
-
-func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) {
-
- log := zerolog.Ctx(ctx).With().
- Str("method", ccfg.ConnectionMethod).
- Str("func", "Exec").Logger()
-
- if ccfg.ConnectionMethod == exec.ConnectionMethodDCE {
- if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok {
- return errors.New("invalid configuration for DCE connection method")
- } else {
- opts := []dcerpc.Option{dcerpc.WithSign(), dcerpc.WithSecurityLevel(0)}
-
- // Create DCE connection
- if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultDcomEndpoint, "", opts...); err != nil {
- log.Error().Err(err).Msg("Failed to initialize DCE dialer")
- return fmt.Errorf("create DCE dialer: %w", err)
- }
-
- inst := &dcom.InstantiationInfoData{
- ClassID: &MmcClsid,
- IID: []*dcom.IID{IDispatchIID},
- ClientCOMVersion: ComVersion,
- }
- scm := &dcom.SCMRequestInfoData{
- RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{
- RequestedProtocolSequences: []uint16{7},
- },
- }
- loc := &dcom.LocationInfoData{}
- ac := &dcom.ActivationContextInfoData{}
- ap := &dcom.ActivationProperties{
- DestinationContext: 2,
- Properties: []dcom.ActivationProperty{inst, ac, loc, scm},
- }
- apin, err := ap.ActivationPropertiesIn()
- if err != nil {
- return err
- }
- act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, mod.dce)
- if err != nil {
- return err
- }
- cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{
- ORPCThis: &dcom.ORPCThis{
- Version: ComVersion,
- Flags: 1,
- CID: &RandCid,
- },
- ActPropertiesIn: apin,
- })
- if err != nil {
- return err
- }
- log.Info().Msg("RemoteCreateInstance succeeded")
-
- apout := &dcom.ActivationProperties{}
- if err = apout.Parse(cr.ActPropertiesOut); err != nil {
- return err
- }
- si := apout.SCMReplyInfoData()
- pi := apout.PropertiesOutInfo()
-
- if si == nil {
- return fmt.Errorf("remote create instance response: SCMReplyInfoData is nil")
- }
- if pi == nil {
- return fmt.Errorf("remote create instance response: PropertiesOutInfo is nil")
- }
- oIPID := pi.InterfaceData[0].IPID()
-
- opts = append(opts, si.RemoteReply.OXIDBindings.EndpointsByProtocol("ncacn_ip_tcp")...) // TODO
- mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultDcomEndpoint, "", opts...)
- if err != nil {
- return err
- }
- log.Info().Msg("created new DCERPC dialer")
-
- mod.dc, err = idispatch.NewDispatchClient(ctx, mod.dce, dcom.WithIPID(oIPID))
- if err != nil {
- return err
- }
- log.Info().Msg("created IDispatch client")
- }
- }
- return
-}
-
-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 == MethodMmc {
- if cfg, ok := ecfg.ExecutionMethodConfig.(MethodMmcConfig); !ok {
- return errors.New("invalid configuration")
-
- } else {
- // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mmc/view-executeshellcommand
- method := "Document.ActiveView.ExecuteShellCommand"
- log = log.With().Str("classMethod", method).Logger()
-
- log.Info().
- Str("executable", ecfg.ExecutableName).
- Str("arguments", ecfg.ExecutableArgs).Msg("Attempting execution")
-
- command := &oaut.Variant{
- Size: 5,
- VT: 8,
- VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: ecfg.ExecutableName}}},
- }
- directory := &oaut.Variant{
- Size: 5,
- VT: 8,
- VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: cfg.WorkingDirectory}}},
- }
- parameters := &oaut.Variant{
- Size: 5,
- VT: 8,
- VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: ecfg.ExecutableArgs}}},
- }
- windowState := &oaut.Variant{
- Size: 5,
- VT: 8,
- VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: cfg.WindowState}}},
- }
- // Arguments must be passed in reverse order
- if _, err := callMethod(ctx, mod.dc, method, windowState, parameters, directory, command); err != nil {
- log.Error().Err(err).Msg("Failed to call method")
- return fmt.Errorf("call %q: %w", method, err)
- }
- log.Info().Msg("Method call successful")
- }
- }
- return nil
-}
diff --git a/internal/exec/dcom/module.go b/internal/exec/dcom/module.go
deleted file mode 100644
index bbd50b5..0000000
--- a/internal/exec/dcom/module.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package dcomexec
-
-import (
- "github.com/oiweiwei/go-msrpc/dcerpc"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0"
-)
-
-type Module struct {
- dce dcerpc.Conn
- dc idispatch.DispatchClient
- hostname string
-}
-
-type MethodMmcConfig struct {
- WorkingDirectory string
- WindowState string
-}
-
-const (
- MethodMmc string = "mmc"
-)
diff --git a/internal/exec/exec.go b/internal/exec/exec.go
deleted file mode 100644
index db83d91..0000000
--- a/internal/exec/exec.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package exec
-
-import (
- "context"
- "fmt"
- "github.com/RedTeamPentesting/adauth"
- "strings"
-)
-
-const (
- ConnectionMethodDCE = "dcerpc"
-)
-
-type ConnectionConfig struct {
- ConnectionMethod string
- ConnectionMethodConfig interface{}
-}
-
-type CleanupConfig struct {
- CleanupMethod string
- CleanupMethodConfig interface{}
-}
-
-type ExecutionConfig struct {
- ExecutableName string // ExecutableName represents the name of the executable; i.e. "notepad.exe", "calc"
- ExecutablePath string // ExecutablePath represents the full path to the executable; i.e. `C:\Windows\explorer.exe`
- ExecutableArgs string // ExecutableArgs represents the arguments to be passed to the executable during execution; i.e. "/C whoami"
-
- ExecutionMethod string // ExecutionMethod represents the specific execution strategy used by the module.
- ExecutionMethodConfig interface{}
- ReturnOutput bool
-}
-
-type ShellConfig struct {
- ShellName string // ShellName specifies the name of the shell executable; i.e. "cmd.exe", "powershell"
- ShellPath string // ShellPath is the full Windows path to the shell executable; i.e. `C:\Windows\System32\cmd.exe`
-}
-
-type Module interface {
- Connect(context.Context, *adauth.Credential, *adauth.Target, *ConnectionConfig) error
- Exec(context.Context, *ExecutionConfig) error
- Cleanup(context.Context, *CleanupConfig) error
-}
-
-func (cfg *ExecutionConfig) GetRawCommand() string {
- executable := cfg.ExecutablePath
- if strings.Contains(executable, " ") {
- executable = fmt.Sprintf("%q", executable)
- }
- if cfg.ExecutableArgs != "" {
- return executable + " " + cfg.ExecutableArgs
- }
- return executable
-}
diff --git a/internal/exec/scmr/exec.go b/internal/exec/scmr/exec.go
deleted file mode 100644
index 9d15dd5..0000000
--- a/internal/exec/scmr/exec.go
+++ /dev/null
@@ -1,345 +0,0 @@
-package scmrexec
-
-import (
- "context"
- "errors"
- "fmt"
- "github.com/FalconOpsLLC/goexec/internal/client/dce"
- "github.com/FalconOpsLLC/goexec/internal/exec"
- "github.com/FalconOpsLLC/goexec/internal/util"
- "github.com/FalconOpsLLC/goexec/internal/windows"
- "github.com/RedTeamPentesting/adauth"
- "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
- "github.com/rs/zerolog"
-)
-
-const (
- ScmrDefaultEndpoint = "ncacn_np:[svcctl]"
- ScmrDefaultObject = "367ABB81-9844-35F1-AD32-98F038001003"
-)
-
-func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) {
-
- log := zerolog.Ctx(ctx).With().
- Str("func", "Connect").Logger()
-
- if ccfg.ConnectionMethod == exec.ConnectionMethodDCE {
- if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok {
- return fmt.Errorf("invalid configuration for DCE connection method")
- } else {
- // Fetch target hostname - for opening SCM handle
- if mod.hostname, err = target.Hostname(ctx); err != nil {
- log.Debug().Err(err).Msg("Failed to get target hostname")
- mod.hostname = util.RandomHostname()
- err = nil
- }
- connect := func(ctx context.Context) error {
- // Create DCE connection
- if mod.dce, err = cfg.GetDce(ctx, creds, target, ScmrDefaultEndpoint, ScmrDefaultObject); err != nil {
- log.Error().Err(err).Msg("Failed to initialize DCE dialer")
- return fmt.Errorf("create DCE dialer: %w", err)
- }
- log.Info().Msg("DCE dialer initialized")
-
- // Create SVCCTL client
- mod.ctl, err = svcctl.NewSvcctlClient(ctx, mod.dce)
- if err != nil {
- log.Error().Err(err).Msg("Failed to initialize SCMR client")
- return fmt.Errorf("init SCMR client: %w", err)
- }
- log.Info().Msg("DCE connection successful")
- return nil
- }
- mod.reconnect = func(c context.Context) error {
- mod.dce = nil
- mod.ctl = nil
- return connect(c)
- }
- return connect(ctx)
- }
- } else {
- return errors.New("unsupported connection method")
- }
-}
-
-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 len(mod.services) == 0 {
- if cfg, ok := ccfg.CleanupMethodConfig.(CleanupMethodDeleteConfig); ok && len(cfg.ServiceNames) > 0 {
- for _, svcName := range cfg.ServiceNames {
- if svcName != "" {
- mod.services = append(mod.services, remoteService{name: svcName})
- }
- }
- }
- }
- if mod.dce == nil || mod.ctl == nil {
- // Try to reconnect
- if err := mod.reconnect(ctx); err != nil {
- log.Error().Err(err).Msg("Reconnect failed")
- return err
- }
- log.Info().Msg("Reconnect successful")
- }
- if mod.scm == nil {
- // Open a handle to SCM (again)
- if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{
- MachineName: util.CheckNullString(mod.hostname),
- DatabaseName: "ServicesActive\x00",
- DesiredAccess: ServiceAllAccess, // TODO: Replace
- }); err != nil {
- log.Error().Err(err).Msg("Failed to open an SCM handle")
- return err
- } else {
- mod.scm = resp.SCM
- log.Info().Msg("Opened an SCM handle")
- }
- }
-
- for _, rsvc := range mod.services {
- log = log.With().Str("service", rsvc.name).Logger()
-
- if rsvc.handle == nil {
- // Open a handle to the service in question
- if or, err := mod.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{
- ServiceManager: mod.scm,
- ServiceName: rsvc.name,
- DesiredAccess: windows.SERVICE_DELETE | windows.SERVICE_CHANGE_CONFIG,
- }); err != nil {
- log.Error().Err(err).Msg("Failed to open a service handle")
- continue
- } else {
- rsvc.handle = or.Service
- }
- log.Info().Msg("Service handle opened")
- }
- if ccfg.CleanupMethod == CleanupMethodDelete {
- // Delete the service
- if _, err = mod.ctl.DeleteService(ctx, &svcctl.DeleteServiceRequest{Service: rsvc.handle}); err != nil {
- log.Error().Err(err).Msg("Failed to delete service")
- continue
- }
- log.Info().Msg("Service deleted successfully")
-
- } else if ccfg.CleanupMethod == CleanupMethodRevert {
- // Revert the service configuration & state
- log.Info().Msg("Attempting to revert service configuration")
- if _, err = mod.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{
- Service: rsvc.handle,
- //Dependencies: []byte(rsvc.originalConfig.Dependencies), // TODO: ensure this works
- ServiceType: rsvc.originalConfig.ServiceType,
- StartType: rsvc.originalConfig.StartType,
- ErrorControl: rsvc.originalConfig.ErrorControl,
- BinaryPathName: rsvc.originalConfig.BinaryPathName,
- LoadOrderGroup: rsvc.originalConfig.LoadOrderGroup,
- ServiceStartName: rsvc.originalConfig.ServiceStartName,
- DisplayName: rsvc.originalConfig.DisplayName,
- TagID: rsvc.originalConfig.TagID,
- }); err != nil {
- log.Error().Err(err).Msg("Failed to revert service configuration")
- continue
- }
- log.Info().Msg("Service configuration reverted")
- }
- if _, err = mod.ctl.CloseService(ctx, &svcctl.CloseServiceRequest{ServiceObject: rsvc.handle}); err != nil {
- log.Warn().Err(err).Msg("Failed to close service handle")
- return nil
- }
- log.Info().Msg("Closed service handle")
- }
- return
-}
-
-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 == MethodCreate {
- if cfg, ok := ecfg.ExecutionMethodConfig.(MethodCreateConfig); !ok {
- return errors.New("invalid configuration")
-
- } else {
- svc := remoteService{
- name: cfg.ServiceName,
- }
- // Open a handle to SCM
- if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{
- MachineName: util.CheckNullString(mod.hostname),
- DatabaseName: "ServicesActive\x00",
- DesiredAccess: ServiceAllAccess, // TODO: Replace
- }); err != nil {
- log.Debug().Err(err).Msg("Failed to open SCM handle")
- return fmt.Errorf("open SCM handle: %w", err)
- } else {
- mod.scm = resp.SCM
- log.Info().Msg("Opened SCM handle")
- }
- // Create service
- serviceName := util.RandomStringIfBlank(svc.name)
- resp, err := mod.ctl.CreateServiceW(ctx, &svcctl.CreateServiceWRequest{
- ServiceManager: mod.scm,
- ServiceName: serviceName,
- DisplayName: util.RandomStringIfBlank(cfg.DisplayName),
- BinaryPathName: ecfg.GetRawCommand(),
- ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
- StartType: windows.SERVICE_DEMAND_START,
- DesiredAccess: ServiceAllAccess, // TODO: Replace
- })
- if err != nil || resp == nil || resp.Return != 0 {
- log.Error().Err(err).Msg("Failed to create service")
- return fmt.Errorf("create service: %w", err)
- }
- defer func() {
- mod.services = append(mod.services, svc)
- }()
- svc.handle = resp.Service
-
- log = log.With().
- Str("service", serviceName).Logger()
- log.Info().Msg("Service created")
-
- // Start the service
- sr, err := mod.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle})
- if err != nil {
-
- if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case)
- log.Warn().Err(err).Msg("Service execution deadline exceeded")
- // Connection closes, so we nullify the client variables and handles
- mod.dce = nil
- mod.ctl = nil
- mod.scm = nil
- svc.handle = nil
-
- } else if sr != nil && sr.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT { // Check for request timeout
- log.Info().Msg("Received request timeout. Execution was likely successful")
-
- } else {
- log.Error().Err(err).Msg("Failed to start service")
- return fmt.Errorf("start service: %w", err)
- }
- // Inform the caller that execution was likely successful despite error
- err = nil
- } else {
- log.Info().Msg("Started service")
- }
- }
- } else if ecfg.ExecutionMethod == MethodChange {
- if cfg, ok := ecfg.ExecutionMethodConfig.(MethodChangeConfig); !ok {
- return errors.New("invalid configuration")
-
- } else {
- svc := remoteService{
- name: cfg.ServiceName,
- }
-
- // Open a handle to SCM
- if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{
- MachineName: util.CheckNullString(mod.hostname),
- DatabaseName: "ServicesActive\x00",
- DesiredAccess: ServiceAllAccess, // TODO: Replace
- }); err != nil {
- log.Debug().Err(err).Msg("Failed to open SCM handle")
- return fmt.Errorf("open SCM handle: %w", err)
- } else {
- mod.scm = resp.SCM
- log.Info().Msg("Opened SCM handle")
- }
-
- // Open a handle to the desired service
- if resp, err := mod.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{
- ServiceManager: mod.scm,
- ServiceName: svc.name,
- DesiredAccess: ServiceAllAccess, // TODO: Replace
- }); err != nil {
- log.Error().Err(err).Msg("Failed to open service handle")
- return fmt.Errorf("open service: %w", err)
- } else {
- svc.handle = resp.Service
- }
-
- // Note original service status
- if resp, err := mod.ctl.QueryServiceStatus(ctx, &svcctl.QueryServiceStatusRequest{
- Service: svc.handle,
- }); err != nil {
- log.Warn().Err(err).Msg("Failed to get service status")
- } else {
- svc.originalState = resp.ServiceStatus
- }
-
- // Note original service configuration
- if resp, err := mod.ctl.QueryServiceConfigW(ctx, &svcctl.QueryServiceConfigWRequest{
- Service: svc.handle,
- BufferLength: 8 * 1024,
- }); err != nil {
- log.Error().Err(err).Msg("Failed to fetch service configuration")
- return fmt.Errorf("get service config: %w", err)
- } else {
- log.Info().Str("binaryPath", resp.ServiceConfig.BinaryPathName).Msg("Fetched original service configuration")
- svc.originalConfig = resp.ServiceConfig
- }
-
- // Stop service if its running
- if svc.originalState == nil || svc.originalState.CurrentState != windows.SERVICE_STOPPED {
- if resp, err := mod.ctl.ControlService(ctx, &svcctl.ControlServiceRequest{
- Service: svc.handle,
- Control: windows.SERVICE_STOPPED,
- }); err != nil {
- if resp != nil && resp.Return == windows.ERROR_SERVICE_NOT_ACTIVE {
- log.Info().Msg("Service is already stopped")
- } else {
- log.Error().Err(err).Msg("Failed to stop service")
- }
- } else {
- log.Info().Msg("Service stopped")
- }
- }
- defer func() {
- mod.services = append(mod.services, svc)
- }()
- // Change service configuration
- if _, err = mod.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{
- Service: svc.handle,
- BinaryPathName: ecfg.GetRawCommand(),
- //Dependencies: []byte(svc.originalConfig.Dependencies), // TODO: ensure this works
- ServiceType: svc.originalConfig.ServiceType,
- StartType: windows.SERVICE_DEMAND_START,
- ErrorControl: svc.originalConfig.ErrorControl,
- LoadOrderGroup: svc.originalConfig.LoadOrderGroup,
- ServiceStartName: svc.originalConfig.ServiceStartName,
- DisplayName: svc.originalConfig.DisplayName,
- TagID: svc.originalConfig.TagID,
- }); err != nil {
- log.Error().Err(err).Msg("Failed to change service configuration")
- return fmt.Errorf("change service configuration: %w", err)
- }
- log.Info().Msg("Successfully altered service configuration")
-
- if !cfg.NoStart {
- if resp, err := mod.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle}); err != nil {
-
- if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case)
- log.Warn().Err(err).Msg("Service execution deadline exceeded")
- // Connection closes, so we nullify the client variables and handles
- mod.dce = nil
- mod.ctl = nil
- mod.scm = nil
- svc.handle = nil
-
- } else if resp != nil && resp.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT { // Check for request timeout
- log.Info().Err(err).Msg("Received request timeout. Execution was likely successful")
- } else {
- log.Error().Err(err).Msg("Failed to start service")
- return fmt.Errorf("start service: %w", err)
- }
- }
- }
- }
- }
- return
-}
diff --git a/internal/exec/scmr/module.go b/internal/exec/scmr/module.go
deleted file mode 100644
index 0372668..0000000
--- a/internal/exec/scmr/module.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package scmrexec
-
-import (
- "context"
- "github.com/oiweiwei/go-msrpc/dcerpc"
- "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
-)
-
-const (
- MethodCreate = "create"
- MethodChange = "change"
-
- CleanupMethodDelete = "delete"
- CleanupMethodRevert = "revert"
-)
-
-type Module struct {
- hostname string // The target hostname
- dce dcerpc.Conn
- reconnect func(context.Context) error
-
- ctl svcctl.SvcctlClient
- scm *svcctl.Handle
- services []remoteService
-}
-
-type MethodCreateConfig struct {
- NoDelete bool
- ServiceName string
- DisplayName string
- ServiceType uint32
- StartType uint32
-}
-
-type MethodChangeConfig struct {
- NoStart bool
- ServiceName string
-}
-
-type CleanupMethodDeleteConfig struct {
- ServiceNames []string
-}
-
-type CleanupMethodRevertConfig struct{}
diff --git a/internal/exec/scmr/service.go b/internal/exec/scmr/service.go
deleted file mode 100644
index 49c7506..0000000
--- a/internal/exec/scmr/service.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package scmrexec
-
-import (
- "context"
- "github.com/FalconOpsLLC/goexec/internal/windows"
- "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
-)
-
-const (
- ServiceDeleteAccess uint32 = windows.SERVICE_DELETE
- ServiceModifyAccess uint32 = windows.SERVICE_QUERY_CONFIG | windows.SERVICE_CHANGE_CONFIG | windows.SERVICE_STOP | windows.SERVICE_START | windows.SERVICE_DELETE
- ServiceCreateAccess uint32 = windows.SC_MANAGER_CREATE_SERVICE | windows.SERVICE_START | windows.SERVICE_STOP | windows.SERVICE_DELETE
- ServiceAllAccess uint32 = ServiceCreateAccess | ServiceModifyAccess
-)
-
-type remoteService struct {
- name string
- handle *svcctl.Handle
- originalConfig *svcctl.QueryServiceConfigW
- originalState *svcctl.ServiceStatus
-}
-
-func (mod *Module) parseServiceDependencies(ctx context.Context) (err error) {
- return nil // TODO
-}
diff --git a/internal/exec/tsch/exec.go b/internal/exec/tsch/exec.go
deleted file mode 100644
index 49a2dc2..0000000
--- a/internal/exec/tsch/exec.go
+++ /dev/null
@@ -1,189 +0,0 @@
-package tschexec
-
-import (
- "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/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
- "github.com/rs/zerolog"
- "time"
-)
-
-const (
- TschDefaultEndpoint = "ncacn_np:[atsvc]"
- TschDefaultObject = "86D35949-83C9-4044-B424-DB363231FD0C"
-)
-
-// Connect to the target & initialize DCE & TSCH clients
-func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) {
-
- log := zerolog.Ctx(ctx).With().
- Str("func", "Connect").Logger()
-
- if ccfg.ConnectionMethod == exec.ConnectionMethodDCE {
- if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok {
- return fmt.Errorf("invalid configuration for DCE connection method")
- } else {
- // Create DCERPC dialer
- mod.dce, err = cfg.GetDce(ctx, creds, target, TschDefaultEndpoint, TschDefaultObject)
- if err != nil {
- log.Error().Err(err).Msg("Failed to create DCERPC dialer")
- return fmt.Errorf("create DCERPC dialer: %w", err)
- }
- // Create ITaskSchedulerService client
- 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, 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, 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),
- 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()
- }
-
- st := newSettings(true, true, false)
- tk := newTask(st, nil, triggers{}, 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)
- }
-
- var flags uint32
-
- if cfg.SessionId != 0 {
- flags |= 4
- }
- _, err := mod.tsch.Run(ctx, &itaskschedulerservice.RunRequest{
- Path: taskPath,
- Flags: flags,
- SessionID: cfg.SessionId,
- })
- 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
deleted file mode 100644
index 33c0723..0000000
--- a/internal/exec/tsch/module.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package tschexec
-
-import (
- "github.com/oiweiwei/go-msrpc/dcerpc"
- "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
- "time"
-)
-
-type Module struct {
- // dce holds the working DCE connection interface
- dce dcerpc.Conn
- // tsch holds the ITaskSchedulerService client
- tsch itaskschedulerservice.TaskSchedulerServiceClient
-}
-
-type MethodRegisterConfig struct {
- NoDelete bool
- CallDelete bool
- TaskPath string
- StartDelay time.Duration
- StopDelay time.Duration
- DeleteDelay time.Duration
-}
-
-type MethodDemandConfig struct {
- NoDelete bool
- CallDelete bool
- SessionId uint32
- 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/task.go b/internal/exec/tsch/task.go
deleted file mode 100644
index 928d029..0000000
--- a/internal/exec/tsch/task.go
+++ /dev/null
@@ -1,85 +0,0 @@
-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
deleted file mode 100644
index d47e513..0000000
--- a/internal/exec/tsch/tsch.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package tschexec
-
-import (
- "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"`
-}
-
-type idleSettings struct {
- 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"`
-}
-
-type actionExec struct {
- XMLName xml.Name `xml:"Exec"`
- Command string `xml:"Command"`
- Arguments string `xml:"Arguments,omitempty"`
-}
-
-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,omitempty"`
-}
-
-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"`
- Triggers triggers `xml:"Triggers"`
- Actions actions `xml:"Actions"`
- Principals principals `xml:"Principals"`
- Settings settings `xml:"Settings"`
-}
-
-// 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
deleted file mode 100644
index 7ae33ba..0000000
--- a/internal/exec/wmi/exec.go
+++ /dev/null
@@ -1,166 +0,0 @@
-package wmiexec
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "github.com/FalconOpsLLC/goexec/internal/client/dce"
- "github.com/FalconOpsLLC/goexec/internal/exec"
- "github.com/RedTeamPentesting/adauth"
- "github.com/oiweiwei/go-msrpc/dcerpc"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemlevel1login/v0"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0"
- "github.com/rs/zerolog"
-)
-
-const (
- ProtocolSequenceRPC uint16 = 7
- ProtocolSequenceNP uint16 = 15
- DefaultWmiEndpoint string = "ncacn_ip_tcp:[135]"
-)
-
-var (
- ComVersion = &dcom.COMVersion{
- MajorVersion: 5,
- MinorVersion: 7,
- }
- ORPCThis = &dcom.ORPCThis{Version: ComVersion}
-)
-
-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
-}
-
-func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) {
-
- log := zerolog.Ctx(ctx).With().
- Str("method", ccfg.ConnectionMethod).
- Str("func", "Connect").Logger()
-
- if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok {
- return errors.New("invalid configuration for DCE connection method")
- } else {
- var dceOpts []dcerpc.Option
-
- // Create DCE connection
- if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultWmiEndpoint, "", dceOpts...); err != nil {
- log.Error().Err(err).Msg("Failed to initialize DCE dialer")
- return fmt.Errorf("create DCE dialer: %w", err)
- }
- ia, err := iactivation.NewActivationClient(ctx, mod.dce)
- if err != nil {
- log.Error().Err(err).Msg("Failed to create activation client")
- return fmt.Errorf("create activation client: %w", err)
- }
- act, err := ia.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{
- ORPCThis: ORPCThis,
- ClassID: wmi.Level1LoginClassID.GUID(),
- IIDs: []*dcom.IID{iwbemlevel1login.Level1LoginIID},
- RequestedProtocolSequences: []uint16{ProtocolSequenceRPC}, // TODO: Named pipe support
- })
- if err != nil {
- return fmt.Errorf("request remote activation: %w", err)
- }
- if act.HResult != 0 {
- return fmt.Errorf("remote activation failed with code %d", act.HResult)
- }
- retBinds := act.OXIDBindings.GetStringBindings()
- if len(act.InterfaceData) < 1 || len(retBinds) < 1 {
- return errors.New("remote activation failed")
- }
- ipid := act.InterfaceData[0].GetStandardObjectReference().Std.IPID
-
- for _, b := range retBinds {
- sb, err := dcerpc.ParseStringBinding("ncacn_ip_tcp:" + b.NetworkAddr)
- if err != nil {
- log.Debug().Err(err).Msg("Failed to parse string binding")
- }
- sb.NetworkAddress = target.AddressWithoutPort()
- dceOpts = append(dceOpts, dcerpc.WithEndpoint(sb.String()))
- }
-
- if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultWmiEndpoint, "", dceOpts...); err != nil {
- log.Error().Err(err).Msg("Failed to initialize secondary DCE dialer")
- }
- loginClient, err := iwbemlevel1login.NewLevel1LoginClient(ctx, mod.dce, dcom.WithIPID(ipid))
- if err != nil {
- return fmt.Errorf("initialize wbem login client: %w", err)
- }
- login, err := loginClient.NTLMLogin(ctx, &iwbemlevel1login.NTLMLoginRequest{
- This: ORPCThis,
- NetworkResource: cfg.Resource,
- })
- if err != nil {
- return fmt.Errorf("ntlm login: %w", err)
- }
-
- mod.sc, err = iwbemservices.NewServicesClient(ctx, mod.dce, dcom.WithIPID(login.Namespace.InterfacePointer().IPID()))
- if err != nil {
- return fmt.Errorf("create services client: %w", err)
- }
- }
- return
-}
-
-func (mod *Module) Exec(ctx context.Context, ecfg *exec.ExecutionConfig) (err error) {
- log := zerolog.Ctx(ctx).With().
- Str("module", "tsch").
- Str("method", ecfg.ExecutionMethod).Logger()
-
- if ecfg.ExecutionMethod == MethodCall {
- if cfg, ok := ecfg.ExecutionMethodConfig.(MethodCallConfig); !ok {
- return errors.New("invalid execution configuration")
-
- } else {
- out, err := mod.query(ctx, cfg.Class, cfg.Method, cfg.Arguments)
- if err != nil {
- return fmt.Errorf("query: %w", err)
- }
- if outJson, err := json.Marshal(out); err != nil {
- log.Error().Err(err).Msg("failed to marshal call output")
- } else {
- fmt.Println(string(outJson))
- }
- }
- } else if ecfg.ExecutionMethod == MethodProcess {
- if cfg, ok := ecfg.ExecutionMethodConfig.(MethodProcessConfig); !ok {
- return errors.New("invalid execution configuration")
- } else {
- out, err := mod.query(ctx, "Win32_Process", "Create", map[string]any{
- "CommandLine": cfg.Command,
- "WorkingDir": cfg.WorkingDirectory,
- })
- if err != nil {
- return fmt.Errorf("query: %w", err)
- }
- if pid, ok := out["ProcessId"]; ok && pid != nil {
- log.Info().
- Any("PID", pid).
- Any("return", out["ReturnValue"]).
- Msg("Process created")
- } else {
- log.Error().
- Any("return", out["ReturnValue"]).
- Msg("Process creation failed")
- return errors.New("failed to create process")
- }
- }
- } else {
- return errors.New("unsupported execution method")
- }
- return nil
-}
diff --git a/internal/exec/wmi/module.go b/internal/exec/wmi/module.go
deleted file mode 100644
index 0e83aa8..0000000
--- a/internal/exec/wmi/module.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package wmiexec
-
-import (
- "github.com/RedTeamPentesting/adauth"
- "github.com/oiweiwei/go-msrpc/dcerpc"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0"
- "github.com/rs/zerolog"
-)
-
-type Module struct {
- creds *adauth.Credential
- target *adauth.Target
-
- log zerolog.Logger
- dce dcerpc.Conn
- sc iwbemservices.ServicesClient
-}
-
-type MethodCallConfig struct {
- Class string
- Method string
- Arguments map[string]any
-}
-
-type MethodProcessConfig struct {
- Command string
- WorkingDirectory string
-}
-
-const (
- MethodCall = "call"
- MethodProcess = "process"
-)
diff --git a/internal/exec/wmi/wmi.go b/internal/exec/wmi/wmi.go
deleted file mode 100644
index dd003a3..0000000
--- a/internal/exec/wmi/wmi.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package wmiexec
-
-import (
- "context"
- "errors"
- "fmt"
- "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmio/query"
-)
-
-func (mod *Module) query(ctx context.Context, class, method string, values map[string]any) (outValues map[string]any, err error) {
- outValues = make(map[string]any)
- if mod.sc == nil {
- err = errors.New("module has not been initialized")
- return
- }
- if out, err := query.NewBuilder(ctx, mod.sc, ComVersion).
- Spawn(class). // The class to instantiate (i.e. Win32_Process)
- Method(method). // The method to call (i.e. Create)
- Values(values). // The values to pass to method
- Exec().
- Object(); err == nil {
- return out.Values(), err
- }
- err = fmt.Errorf("(*query.Builder).Spawn: %w", err)
- return
-}
diff --git a/internal/util/util.go b/internal/util/util.go
index f8f12ba..555297a 100644
--- a/internal/util/util.go
+++ b/internal/util/util.go
@@ -1,6 +1,7 @@
package util
import (
+ "github.com/google/uuid"
"math/rand" // not crypto secure
"regexp"
"strings"
@@ -23,6 +24,10 @@ func RandomHostname() (hostname string) {
}
}
+func RandomWindowsTempFile() string {
+ return `\Windows\Temp\` + strings.ToUpper(uuid.New().String())
+}
+
func RandomString() string {
return RandomStringFromCharset(randStringCharset, rand.Intn(10)+6)
}
diff --git a/pkg/goexec/auth.go b/pkg/goexec/auth.go
new file mode 100644
index 0000000..e2e9c3e
--- /dev/null
+++ b/pkg/goexec/auth.go
@@ -0,0 +1,11 @@
+package goexec
+
+import (
+ "github.com/RedTeamPentesting/adauth"
+)
+
+// AuthOptions holds Windows / Active Directory authentication parameters
+type AuthOptions struct {
+ Target *adauth.Target
+ Credential *adauth.Credential
+}
diff --git a/pkg/goexec/clean.go b/pkg/goexec/clean.go
new file mode 100644
index 0000000..a853dc4
--- /dev/null
+++ b/pkg/goexec/clean.go
@@ -0,0 +1,31 @@
+package goexec
+
+import (
+ "context"
+ "github.com/rs/zerolog"
+)
+
+type CleanProvider interface {
+ Clean(ctx context.Context) (err error)
+}
+
+type Cleaner struct {
+ workers []func(ctx context.Context) error
+}
+
+func (c *Cleaner) AddCleaner(worker func(ctx context.Context) error) {
+ c.workers = append(c.workers, worker)
+}
+
+func (c *Cleaner) Clean(ctx context.Context) (err error) {
+ log := zerolog.Ctx(ctx).With().
+ Str("component", "cleaner").Logger()
+
+ for _, worker := range c.workers {
+ if err = worker(log.WithContext(ctx)); err != nil {
+
+ log.Warn().Err(err).Msg("Clean worker failed")
+ }
+ }
+ return
+}
diff --git a/pkg/goexec/client.go b/pkg/goexec/client.go
new file mode 100644
index 0000000..bee0399
--- /dev/null
+++ b/pkg/goexec/client.go
@@ -0,0 +1,26 @@
+package goexec
+
+import "context"
+
+// Client represents an application layer network client
+type Client interface {
+
+ // Connect establishes a connection to the remote server
+ Connect(ctx context.Context) error
+
+ // Close terminates the active connection and frees allocated resources
+ Close(ctx context.Context) error
+}
+
+// ClientOptions represents configuration options for a Client
+type ClientOptions struct {
+
+ // Proxy specifies the URI of the proxy server to route client requests through
+ Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"`
+
+ // Host specifies the hostname or IP address that the client should connect to
+ Host string `json:"host" yaml:"host"`
+
+ // Port specifies the network port on Host that the client will connect to
+ Port uint16 `json:"port" yaml:"port"`
+}
diff --git a/pkg/goexec/dce/client.go b/pkg/goexec/dce/client.go
new file mode 100644
index 0000000..e8dadae
--- /dev/null
+++ b/pkg/goexec/dce/client.go
@@ -0,0 +1,89 @@
+package dce
+
+import (
+ "context"
+ "fmt"
+ "github.com/oiweiwei/go-msrpc/dcerpc"
+ "github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3"
+ "github.com/rs/zerolog"
+)
+
+type Client struct {
+ Options
+
+ conn dcerpc.Conn
+ hostname string
+}
+
+func NewClient() *Client {
+ return new(Client)
+}
+
+func (c *Client) String() string {
+ return ClientName
+}
+
+func (c *Client) Reconnect(ctx context.Context, opts ...dcerpc.Option) (err error) {
+ c.DcerpcOptions = append(c.DcerpcOptions, opts...)
+
+ return c.Connect(ctx)
+}
+
+func (c *Client) Dce() (dce dcerpc.Conn) {
+ return c.conn
+}
+
+func (c *Client) Logger(ctx context.Context) (log zerolog.Logger) {
+ return zerolog.Ctx(ctx).With().
+ Str("client", c.String()).Logger()
+}
+
+func (c *Client) Connect(ctx context.Context) (err error) {
+
+ log := c.Logger(ctx)
+ ctx = log.WithContext(ctx)
+
+ var do, eo []dcerpc.Option
+
+ do = append(do, c.DcerpcOptions...)
+ do = append(do, c.authOptions...)
+
+ if !c.NoSign {
+ do = append(do, dcerpc.WithSign())
+ eo = append(eo, dcerpc.WithSign())
+ }
+ if !c.NoSeal {
+ do = append(do, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
+ eo = append(eo, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
+ }
+
+ if !c.NoLog {
+ do = append(do, dcerpc.WithLogger(log))
+ eo = append(eo, dcerpc.WithLogger(log))
+ }
+
+ if !c.NoEpm {
+ log.Debug().Msg("Using endpoint mapper")
+
+ eo = append(eo, c.epmOptions...)
+ eo = append(eo, c.authOptions...)
+
+ do = append(do, epm.EndpointMapper(ctx, c.Host, eo...))
+ }
+
+ for _, e := range c.stringBindings {
+ do = append(do, dcerpc.WithEndpoint(e.String()))
+ }
+
+ if c.conn, err = dcerpc.Dial(ctx, c.Host, do...); err != nil {
+
+ log.Error().Err(err).Msgf("Failed to connect to %s endpoint", c.String())
+ return fmt.Errorf("dial %s: %w", c.String(), err)
+ }
+
+ return
+}
+
+func (c *Client) Close(ctx context.Context) (err error) {
+ return c.conn.Close(ctx)
+}
diff --git a/pkg/goexec/dce/default.go b/pkg/goexec/dce/default.go
new file mode 100644
index 0000000..d3a912f
--- /dev/null
+++ b/pkg/goexec/dce/default.go
@@ -0,0 +1,5 @@
+package dce
+
+const (
+ ClientName = "DCE"
+)
diff --git a/pkg/goexec/dce/options.go b/pkg/goexec/dce/options.go
new file mode 100644
index 0000000..e13ef43
--- /dev/null
+++ b/pkg/goexec/dce/options.go
@@ -0,0 +1,108 @@
+package dce
+
+import (
+ "context"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/RedTeamPentesting/adauth/dcerpcauth"
+ "github.com/oiweiwei/go-msrpc/dcerpc"
+ "net"
+)
+
+type Options struct {
+ goexec.ClientOptions
+ goexec.AuthOptions
+
+ // NoSign disables packet signing by omitting dcerpc.WithSign()
+ NoSign bool `json:"no_sign" yaml:"no_sign"`
+
+ // NoSeal disables packet stub encryption by omitting dcerpc.WithSeal()
+ NoSeal bool `json:"no_seal" yaml:"no_seal"`
+
+ // NoLog disables logging by omitting dcerpc.WithLogger(...)
+ NoLog bool `json:"no_log" yaml:"no_log"`
+
+ // NoEpm disables DCE endpoint mapper communications
+ NoEpm bool `json:"no_epm" yaml:"no_epm"`
+
+ // Endpoint stores the explicit DCE string binding to use
+ Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
+
+ // Filter stores the filter for returned endpoints from an endpoint mapper
+ Filter string `json:"filter,omitempty" yaml:"filter,omitempty"`
+
+ netDialer goexec.Dialer
+ dialer dcerpc.Dialer
+ authOptions []dcerpc.Option
+ DcerpcOptions []dcerpc.Option
+ epmOptions []dcerpc.Option
+ stringBindings []*dcerpc.StringBinding
+}
+
+func (c *Client) Parse(ctx context.Context) (err error) {
+
+ // Reset internals
+ {
+ c.netDialer = nil
+ c.dialer = nil
+ c.stringBindings = []*dcerpc.StringBinding{}
+ c.authOptions = []dcerpc.Option{}
+ c.DcerpcOptions = []dcerpc.Option{}
+ c.epmOptions = []dcerpc.Option{
+ dcerpc.WithSign(), // Require signing for EPM
+ }
+ }
+
+ if !c.NoSeal {
+ // Enable encryption
+ c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
+ c.epmOptions = append(c.epmOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
+ }
+ if !c.NoSign {
+ // Enable signing
+ c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithSign())
+ //c.epmOptions = append(c.epmOptions, dcerpc.WithSign())
+ }
+
+ // Parse DCERPC endpoint
+ if c.Endpoint != "" {
+ sb, err := dcerpc.ParseStringBinding(c.Endpoint)
+ if err != nil {
+ return err
+ }
+ c.stringBindings = append(c.stringBindings, sb)
+ }
+
+ // Parse EPM filter
+ if c.Filter != "" {
+ sb, err := dcerpc.ParseStringBinding(c.Filter)
+ if err != nil {
+ return err
+ }
+ c.stringBindings = append(c.stringBindings, sb)
+ }
+
+ if c.Proxy == "" {
+ c.netDialer = &net.Dialer{} // FUTURE: additional dial c
+
+ } else {
+ // Parse proxy URL
+ d, err := goexec.ParseProxyURI(c.Proxy)
+ if err != nil {
+ return err
+ }
+ var ok bool
+ if c.dialer, ok = d.(dcerpc.Dialer); !ok {
+ return fmt.Errorf("cannot cast %T to dcerpc.Dialer", d)
+ }
+ }
+
+ // Parse authentication parameters
+ if c.authOptions, err = dcerpcauth.AuthenticationOptions(ctx, c.Credential, c.Target, &dcerpcauth.Options{}); err != nil {
+ return fmt.Errorf("parse auth c: %w", err)
+ }
+
+ c.Host = c.Target.AddressWithoutPort()
+
+ return
+}
diff --git a/pkg/goexec/dcom/dcom.go b/pkg/goexec/dcom/dcom.go
new file mode 100644
index 0000000..e66a382
--- /dev/null
+++ b/pkg/goexec/dcom/dcom.go
@@ -0,0 +1,34 @@
+package dcomexec
+
+import (
+ guuid "github.com/google/uuid"
+ "github.com/oiweiwei/go-msrpc/midl/uuid"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom"
+ "github.com/oiweiwei/go-msrpc/msrpc/dtyp"
+)
+
+const (
+ LcEnglishUs uint32 = 0x409
+)
+
+var (
+ ShellWindowsUuid = uuid.MustParse("9BA05972-F6A8-11CF-A442-00A0C90A8F39")
+ Mmc20Uuid = uuid.MustParse("49B2791A-B1AE-4C90-9B8E-E860BA07F889")
+
+ RandCid = dcom.CID(*dtyp.GUIDFromUUID(uuid.MustParse(guuid.NewString())))
+ IDispatchIID = &dcom.IID{
+ Data1: 0x20400,
+ Data2: 0x0,
+ Data3: 0x0,
+ Data4: []byte{0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46},
+ }
+ ComVersion = &dcom.COMVersion{
+ MajorVersion: 5,
+ MinorVersion: 7,
+ }
+ MmcClsid = dcom.ClassID(*dtyp.GUIDFromUUID(Mmc20Uuid))
+ ORPCThis = &dcom.ORPCThis{
+ Version: ComVersion,
+ CID: &RandCid,
+ }
+)
diff --git a/pkg/goexec/dcom/mmc.go b/pkg/goexec/dcom/mmc.go
new file mode 100644
index 0000000..9c92af3
--- /dev/null
+++ b/pkg/goexec/dcom/mmc.go
@@ -0,0 +1,50 @@
+package dcomexec
+
+import (
+ "context"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/rs/zerolog"
+)
+
+const (
+ MethodMmc = "MMC" // MMC20.Application::Document.ActiveView.ExecuteShellCommand
+)
+
+type DcomMmc struct {
+ DcomExec
+
+ WorkingDirectory string
+ WindowState string
+}
+
+// Execute will perform command execution via the MMC20.Application DCOM object.
+func (m *DcomMmc) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).
+ Str("method", MethodMmc).
+ Logger()
+
+ method := "Document.ActiveView.ExecuteShellCommand"
+
+ var args = in.Arguments
+ if args == "" {
+ args = " " // the process arguments can't be a blank string
+ }
+
+ // Arguments must be passed in reverse order
+ if _, err := callComMethod(ctx,
+ m.dispatchClient,
+ method,
+ stringToVariant(m.WindowState),
+ stringToVariant(in.Arguments),
+ stringToVariant(m.WorkingDirectory),
+ stringToVariant(in.Executable)); err != nil {
+
+ log.Error().Err(err).Msg("Failed to call method")
+ return fmt.Errorf("call %q: %w", method, err)
+ }
+ log.Info().Msg("Method call successful")
+ return
+}
diff --git a/pkg/goexec/dcom/module.go b/pkg/goexec/dcom/module.go
new file mode 100644
index 0000000..47dc7ca
--- /dev/null
+++ b/pkg/goexec/dcom/module.go
@@ -0,0 +1,111 @@
+package dcomexec
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
+ "github.com/oiweiwei/go-msrpc/dcerpc"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0"
+ "github.com/rs/zerolog"
+)
+
+const (
+ ModuleName = "DCOM"
+)
+
+type DcomExec struct {
+ client *dce.Client
+ dispatchClient idispatch.DispatchClient
+}
+
+func (m *DcomExec) Init(ctx context.Context, c *dce.Client) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).Logger()
+
+ m.client = c
+
+ if m.client.Dce() == nil {
+ return errors.New("DCE connection not initialized")
+ }
+
+ opts := []dcerpc.Option{
+ dcerpc.WithSign(),
+ }
+
+ inst := &dcom.InstantiationInfoData{
+ ClassID: &MmcClsid,
+ IID: []*dcom.IID{IDispatchIID},
+ ClientCOMVersion: ComVersion,
+ }
+ ac := &dcom.ActivationContextInfoData{}
+ loc := &dcom.LocationInfoData{}
+ scm := &dcom.SCMRequestInfoData{
+ RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{
+ RequestedProtocolSequences: []uint16{7},
+ },
+ }
+
+ ap := &dcom.ActivationProperties{
+ DestinationContext: 2,
+ Properties: []dcom.ActivationProperty{inst, ac, loc, scm},
+ }
+
+ apin, err := ap.ActivationPropertiesIn()
+ if err != nil {
+ return err
+ }
+
+ act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, m.client.Dce())
+ if err != nil {
+ return err
+ }
+
+ cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{
+ ORPCThis: &dcom.ORPCThis{
+ Version: ComVersion,
+ Flags: 1,
+ CID: &RandCid,
+ },
+ ActPropertiesIn: apin,
+ })
+ if err != nil {
+ return err
+ }
+ log.Info().Msg("RemoteCreateInstance succeeded")
+
+ apout := new(dcom.ActivationProperties)
+ if err = apout.Parse(cr.ActPropertiesOut); err != nil {
+ return err
+ }
+ si := apout.SCMReplyInfoData()
+ pi := apout.PropertiesOutInfo()
+
+ if si == nil {
+ return fmt.Errorf("remote create instance response: SCMReplyInfoData is nil")
+ }
+
+ if pi == nil {
+ return fmt.Errorf("remote create instance response: PropertiesOutInfo is nil")
+ }
+
+ oIPID := pi.InterfaceData[0].IPID()
+ opts = append(opts, si.RemoteReply.OXIDBindings.EndpointsByProtocol("ncacn_ip_tcp")...) // TODO
+
+ err = c.Reconnect(ctx, opts...)
+ if err != nil {
+ return err
+ }
+ log.Info().Msg("created new DCERPC dialer")
+
+ m.dispatchClient, err = idispatch.NewDispatchClient(ctx, c.Dce(), dcom.WithIPID(oIPID))
+ if err != nil {
+ return err
+ }
+ log.Info().Msg("created IDispatch client")
+
+ return
+}
diff --git a/internal/exec/dcom/dcom.go b/pkg/goexec/dcom/util.go
index b96bbc8..5ed8a60 100644
--- a/internal/exec/dcom/dcom.go
+++ b/pkg/goexec/dcom/util.go
@@ -10,56 +10,76 @@ import (
"strings"
)
-const (
- LC_ENGLISH_US uint32 = 0x409
-)
+func callComMethod(ctx context.Context, dc idispatch.DispatchClient, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) {
-func callMethod(ctx context.Context, dc idispatch.DispatchClient, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) {
parts := strings.Split(method, ".")
var id *dcom.IPID
var gr *idispatch.GetIDsOfNamesResponse
for i, obj := range parts {
+
var opts []dcerpc.CallOption
+
if id != nil {
opts = append(opts, dcom.WithIPID(id))
}
+
gr, err = dc.GetIDsOfNames(ctx, &idispatch.GetIDsOfNamesRequest{
This: ORPCThis,
IID: &dcom.IID{},
- Names: []string{obj + "\x00"},
- LocaleID: LC_ENGLISH_US,
+ LocaleID: LcEnglishUs,
+
+ Names: []string{obj + "\x00"},
}, opts...)
if err != nil {
return nil, fmt.Errorf("get dispatch ID of name %q: %w", obj, err)
}
+
if len(gr.DispatchID) < 1 {
return nil, fmt.Errorf("dispatch ID of name %q not found", obj)
}
+
irq := &idispatch.InvokeRequest{
- This: ORPCThis,
+ This: ORPCThis,
+ IID: &dcom.IID{},
+ LocaleID: LcEnglishUs,
+
DispatchIDMember: gr.DispatchID[0],
- IID: &dcom.IID{},
- LocaleID: LC_ENGLISH_US,
}
+
if i >= len(parts)-1 {
irq.Flags = 1
irq.DispatchParams = &oaut.DispatchParams{ArgsCount: uint32(len(args)), Args: args}
return dc.Invoke(ctx, irq, opts...)
}
irq.Flags = 2
+
ir, err = dc.Invoke(ctx, irq, opts...)
if err != nil {
return nil, fmt.Errorf("get properties of object %q: %w", obj, err)
}
+
di, ok := ir.VarResult.VarUnion.GetValue().(*oaut.Dispatch)
if !ok {
return nil, fmt.Errorf("invalid dispatch object for %q", obj)
}
id = di.InterfacePointer().GetStandardObjectReference().Std.IPID
}
-
return
}
+
+func stringToVariant(s string) *oaut.Variant {
+ return &oaut.Variant{
+ Size: 5,
+ VT: 8,
+ VarUnion: &oaut.Variant_VarUnion{
+ Value: &oaut.Variant_VarUnion_BSTR{
+ BSTR: &oaut.String{
+ Data: s,
+ },
+ },
+ },
+ }
+}
diff --git a/pkg/goexec/exec.go b/pkg/goexec/exec.go
new file mode 100644
index 0000000..cb44f19
--- /dev/null
+++ b/pkg/goexec/exec.go
@@ -0,0 +1,14 @@
+package goexec
+
+import "context"
+
+type ExecutionProvider interface {
+ Execute(ctx context.Context, in *ExecutionInput) (err error)
+}
+
+type Executor struct{}
+
+type CleanExecutionProvider interface {
+ ExecutionProvider
+ CleanProvider
+}
diff --git a/pkg/goexec/io.go b/pkg/goexec/io.go
new file mode 100644
index 0000000..846a1fd
--- /dev/null
+++ b/pkg/goexec/io.go
@@ -0,0 +1,87 @@
+package goexec
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+)
+
+type OutputProvider interface {
+ GetOutput(ctx context.Context) (out io.ReadCloser, err error)
+}
+
+type ExecutionIO struct {
+ Cleaner
+
+ Input *ExecutionInput
+ Output *ExecutionOutput
+}
+
+type ExecutionOutput struct {
+ NoDelete bool
+ RemotePath string
+ Provider OutputProvider
+}
+
+type ExecutionInput struct {
+ FilePath string
+ Executable string
+ ExecutablePath string
+ Arguments string
+ CommandLine string
+}
+
+func (execIO *ExecutionIO) GetOutput(ctx context.Context) (out io.ReadCloser, err error) {
+
+ if execIO.Output.Provider != nil {
+ return execIO.Output.Provider.GetOutput(ctx)
+ }
+ return nil, errors.New("output provider not set")
+}
+
+func (execIO *ExecutionIO) CommandLine() string {
+ return execIO.Input.Command()
+}
+
+func (execIO *ExecutionIO) String() (cmd string) {
+
+ cmd = execIO.Input.Command()
+
+ if execIO.Output.Provider != nil && execIO.Output.RemotePath != "" {
+ return fmt.Sprintf(`C:\Windows\System32\cmd.exe /C %s > %s`, cmd, execIO.Output.RemotePath)
+ }
+ return
+}
+
+func (i *ExecutionInput) Command() string {
+
+ if i.CommandLine == "" {
+
+ if i.ExecutablePath != "" {
+ i.CommandLine = i.ExecutablePath
+
+ } else if i.Executable != "" {
+ i.CommandLine = i.Executable
+ }
+
+ if i.Arguments != "" {
+ i.CommandLine += " " + i.Arguments
+ }
+ }
+ return i.CommandLine
+}
+
+func (i *ExecutionInput) String() string {
+ return i.Command()
+}
+
+func (i *ExecutionInput) UploadReader(_ context.Context) (reader io.Reader, err error) {
+
+ if i.FilePath != "" {
+ return os.OpenFile(i.FilePath, os.O_RDONLY, 0)
+ }
+ return bytes.NewBufferString(i.Command()), nil
+}
diff --git a/pkg/goexec/method.go b/pkg/goexec/method.go
new file mode 100644
index 0000000..7ea0ddc
--- /dev/null
+++ b/pkg/goexec/method.go
@@ -0,0 +1,72 @@
+package goexec
+
+import (
+ "context"
+ "fmt"
+ "github.com/rs/zerolog"
+)
+
+type Method interface{}
+
+type RemoteMethod interface {
+ Connect(ctx context.Context) error
+ Init(ctx context.Context) error
+}
+
+type RemoteExecuteMethod interface {
+ RemoteMethod
+ Execute(ctx context.Context, io *ExecutionIO) error
+}
+
+type RemoteCleanMethod interface {
+ RemoteMethod
+ Clean(ctx context.Context) error
+}
+
+type RemoteExecuteCleanMethod interface {
+ RemoteExecuteMethod
+ Clean(ctx context.Context) error
+}
+
+type RemoteExecuteCleanMethodWithOutput interface {
+ RemoteExecuteCleanMethod
+ OutputProvider
+}
+
+func ExecuteMethod(ctx context.Context, module RemoteExecuteMethod, execIO *ExecutionIO) (err error) {
+
+ log := zerolog.Ctx(ctx)
+
+ if err = module.Connect(ctx); err != nil {
+ log.Error().Err(err).Msg("Connection failed")
+ return fmt.Errorf("connect: %w", err)
+ }
+
+ if err = module.Init(ctx); err != nil {
+ log.Error().Err(err).Msg("Module initialization failed")
+ return fmt.Errorf("init module: %w", err)
+ }
+
+ if err = module.Execute(ctx, execIO); err != nil {
+ log.Error().Err(err).Msg("Execution failed")
+ return fmt.Errorf("execute: %w", err)
+ }
+ return
+}
+
+func ExecuteCleanMethod(ctx context.Context, module RemoteExecuteCleanMethod, 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")
+ }
+ }()
+
+ return ExecuteMethod(ctx, module, execIO)
+}
+
+func ExecuteCleanMethodWithOutput(ctx context.Context, module RemoteExecuteCleanMethodWithOutput, execIO *ExecutionIO) (err error) {
+ return ExecuteCleanMethod(ctx, module, execIO)
+}
diff --git a/pkg/goexec/module.go b/pkg/goexec/module.go
new file mode 100644
index 0000000..6544b78
--- /dev/null
+++ b/pkg/goexec/module.go
@@ -0,0 +1,25 @@
+package goexec
+
+import "context"
+
+type Module interface {
+ Init(ctx context.Context) error
+}
+
+type ExecutionModule interface {
+ Module
+ ExecutionProvider
+}
+
+type CleanExecutionModule interface {
+ Module
+ ExecutionProvider
+ CleanProvider
+}
+
+type CleanExecutionOutputModule interface {
+ Module
+ ExecutionProvider
+ CleanProvider
+ OutputProvider
+}
diff --git a/pkg/goexec/proxy.go b/pkg/goexec/proxy.go
new file mode 100644
index 0000000..8f20758
--- /dev/null
+++ b/pkg/goexec/proxy.go
@@ -0,0 +1,33 @@
+package goexec
+
+import (
+ "fmt"
+ "golang.org/x/net/proxy"
+ "net"
+ "net/url"
+)
+
+// Dialer outlines a basic implementation for establishing network connections
+type Dialer interface {
+
+ // Dial establishes a network connection (net.Conn) using the provided parameters
+ Dial(network string, address string) (connection net.Conn, err error)
+}
+
+// ParseProxyURI parses the provided proxy URI spec to a Dialer
+func ParseProxyURI(uri string) (dialer Dialer, err error) {
+
+ // Parse proxy spec as URL
+ u, err := url.Parse(uri)
+ if err != nil {
+ return nil, fmt.Errorf("parse proxy URI: %w", err)
+ }
+
+ // Create dialer from URL
+ dialer, err = proxy.FromURL(u, nil)
+ if err != nil {
+ return nil, fmt.Errorf("init proxy: %w", err)
+ }
+
+ return
+}
diff --git a/pkg/goexec/scmr/change.go b/pkg/goexec/scmr/change.go
new file mode 100644
index 0000000..c8321f7
--- /dev/null
+++ b/pkg/goexec/scmr/change.go
@@ -0,0 +1,144 @@
+package scmrexec
+
+import (
+ "context"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/internal/windows"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
+ "github.com/rs/zerolog"
+)
+
+const (
+ MethodChange = "Change"
+)
+
+type ScmrChange struct {
+ Scmr
+ goexec.Cleaner
+ goexec.Executor
+
+ NoStart bool
+ ServiceName string
+}
+
+func (m *ScmrChange) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).
+ Str("method", MethodChange).
+ Str("service", m.ServiceName).
+ Logger()
+
+ svc := &service{name: m.ServiceName}
+
+ openResponse, err := m.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{
+ ServiceManager: m.scm,
+ ServiceName: svc.name,
+ DesiredAccess: ServiceAllAccess,
+ })
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to open service handle")
+ return fmt.Errorf("open service request: %w", err)
+ }
+ if openResponse.Return != 0 {
+ log.Error().Err(err).Msg("Failed to open service handle")
+ return fmt.Errorf("create service: %w", err)
+ }
+
+ svc.handle = openResponse.Service
+ log.Info().Msg("Opened service handle")
+
+ defer m.AddCleaner(func(ctxInner context.Context) error {
+
+ r, errInner := m.ctl.CloseService(ctxInner, &svcctl.CloseServiceRequest{
+ ServiceObject: svc.handle,
+ })
+ if errInner != nil {
+ return fmt.Errorf("close service: %w", errInner)
+ }
+ if r.Return != 0 {
+ return fmt.Errorf("close service returned non-zero exit code: %02x", r.Return)
+ }
+ log.Info().Msg("Closed service handle")
+
+ return nil
+ })
+
+ // Note original service configuration
+ queryResponse, err := m.ctl.QueryServiceConfigW(ctx, &svcctl.QueryServiceConfigWRequest{
+ Service: svc.handle,
+ BufferLength: 8 * 1024,
+ })
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to fetch service configuration")
+ return fmt.Errorf("get service config: %w", err)
+ }
+ if queryResponse.Return != 0 {
+ log.Error().Err(err).Msg("Failed to query service configuration")
+ return fmt.Errorf("query service config: %w", err)
+ }
+
+ log.Info().Str("binaryPath", queryResponse.ServiceConfig.BinaryPathName).Msg("Fetched original service configuration")
+ svc.originalConfig = queryResponse.ServiceConfig
+
+ stopResponse, err := m.ctl.ControlService(ctx, &svcctl.ControlServiceRequest{
+ Service: svc.handle,
+ Control: windows.SERVICE_CONTROL_STOP,
+ })
+
+ if err != nil {
+ if stopResponse == nil || stopResponse.Return != windows.ERROR_SERVICE_NOT_ACTIVE {
+
+ log.Error().Err(err).Msg("Failed to stop existing service")
+ return fmt.Errorf("stop service: %w", err)
+ }
+
+ log.Debug().Msg("Service is not running")
+
+ // TODO: restore state
+ /*
+ defer m.AddCleaner(func(ctxInner context.Context) error {
+ // ...
+ return nil
+ })
+ */
+
+ } else {
+ log.Info().Msg("Stopped existing service")
+ }
+
+ changeResponse, err := m.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{
+ Service: svc.handle,
+ BinaryPathName: in.String(),
+ DisplayName: svc.originalConfig.DisplayName,
+ ServiceType: svc.originalConfig.ServiceType,
+ StartType: windows.SERVICE_DEMAND_START,
+ ErrorControl: svc.originalConfig.ErrorControl,
+ LoadOrderGroup: svc.originalConfig.LoadOrderGroup,
+ ServiceStartName: svc.originalConfig.ServiceStartName,
+ TagID: svc.originalConfig.TagID,
+ //Dependencies: []byte(svc.originalConfig.Dependencies), // TODO
+ })
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to request service configuration change")
+ return fmt.Errorf("change service config request: %w", err)
+ }
+ if changeResponse.Return != 0 {
+ log.Error().Err(err).Msg("Failed to change service configuration")
+ return fmt.Errorf("change service config: %w", err)
+ }
+
+ if !m.NoStart {
+
+ err = m.startService(ctx, svc)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to start service")
+ }
+ }
+
+ return
+}
diff --git a/pkg/goexec/scmr/create.go b/pkg/goexec/scmr/create.go
new file mode 100644
index 0000000..6de50f8
--- /dev/null
+++ b/pkg/goexec/scmr/create.go
@@ -0,0 +1,125 @@
+package scmrexec
+
+import (
+ "context"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/internal/util"
+ "github.com/FalconOpsLLC/goexec/internal/windows"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
+ "github.com/rs/zerolog"
+)
+
+const (
+ MethodCreate = "Create"
+)
+
+type ScmrCreate struct {
+ Scmr
+ goexec.Cleaner
+ goexec.Executor
+
+ NoDelete bool
+ NoStart bool
+ ServiceName string
+ DisplayName string
+}
+
+func (m *ScmrCreate) ensure() {
+ if m.ServiceName == "" {
+ m.ServiceName = util.RandomString()
+ }
+ if m.DisplayName == "" {
+ m.DisplayName = m.ServiceName
+ }
+}
+
+func (m *ScmrCreate) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) {
+ m.ensure()
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).
+ Str("method", MethodCreate).
+ Str("service", m.ServiceName).
+ Logger()
+
+ svc := &service{name: m.ServiceName}
+
+ resp, err := m.ctl.CreateServiceW(ctx, &svcctl.CreateServiceWRequest{
+ ServiceManager: m.scm,
+ ServiceName: m.ServiceName,
+ DisplayName: m.DisplayName,
+ BinaryPathName: in.String(),
+ ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
+ StartType: windows.SERVICE_DEMAND_START,
+ DesiredAccess: ServiceAllAccess, // TODO: Replace
+ })
+
+ if err != nil {
+ log.Error().Err(err).Msg("Create service request failed")
+ return fmt.Errorf("create service request: %w", err)
+ }
+
+ if resp.Return != 0 {
+ log.Error().Err(err).Msg("Failed to create service")
+ return fmt.Errorf("create service returned non-zero exit code: %02x", resp.Return)
+ }
+
+ if !m.NoDelete {
+ m.AddCleaner(func(ctxInner context.Context) error {
+
+ r, errInner := m.ctl.DeleteService(ctxInner, &svcctl.DeleteServiceRequest{
+ Service: svc.handle,
+ })
+ if errInner != nil {
+ return fmt.Errorf("delete service: %w", errInner)
+ }
+ if r.Return != 0 {
+ return fmt.Errorf("delete service returned non-zero exit code: %02x", r.Return)
+ }
+ log.Info().Msg("Deleted service")
+
+ return nil
+ })
+ }
+
+ m.AddCleaner(func(ctxInner context.Context) error {
+
+ r, errInner := m.ctl.CloseService(ctxInner, &svcctl.CloseServiceRequest{
+ ServiceObject: svc.handle,
+ })
+ if errInner != nil {
+ return fmt.Errorf("close service: %w", errInner)
+ }
+ if r.Return != 0 {
+ return fmt.Errorf("close service returned non-zero exit code: %02x", r.Return)
+ }
+ log.Info().Msg("Closed service handle")
+
+ return nil
+ })
+
+ log.Info().Msg("Created service")
+ svc.handle = resp.Service
+
+ if !m.NoStart {
+
+ err = m.startService(ctx, svc)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to start service")
+ }
+ }
+ if svc.handle == nil {
+
+ if err = m.Reconnect(ctx); err != nil {
+ return err
+ }
+
+ svc, err = m.openService(ctx, svc.name)
+ if err != nil {
+ return fmt.Errorf("reopen service: %w", err)
+ }
+ }
+
+ return
+}
diff --git a/pkg/goexec/scmr/delete.go b/pkg/goexec/scmr/delete.go
new file mode 100644
index 0000000..d327a61
--- /dev/null
+++ b/pkg/goexec/scmr/delete.go
@@ -0,0 +1,46 @@
+package scmrexec
+
+import (
+ "context"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
+ "github.com/rs/zerolog"
+)
+
+const (
+ MethodDelete = "Delete"
+)
+
+type ScmrDelete struct {
+ Scmr
+ goexec.Cleaner
+
+ ServiceName string
+}
+
+func (m *ScmrDelete) Clean(ctx context.Context) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).
+ Str("method", MethodDelete).
+ Str("service", m.ServiceName).
+ Logger()
+
+ svc, err := m.openService(ctx, m.ServiceName)
+ if err != nil {
+ return err
+ }
+ deleteResponse, err := m.ctl.DeleteService(ctx, &svcctl.DeleteServiceRequest{
+ Service: svc.handle,
+ })
+ if err != nil {
+ return fmt.Errorf("delete service: %w", err)
+ }
+ if deleteResponse.Return != 0 {
+ return fmt.Errorf("delete service returned non-zero exit code: %02x", deleteResponse.Return)
+ }
+
+ log.Info().Msg("Deleted service")
+ return
+}
diff --git a/pkg/goexec/scmr/module.go b/pkg/goexec/scmr/module.go
new file mode 100644
index 0000000..efa3d50
--- /dev/null
+++ b/pkg/goexec/scmr/module.go
@@ -0,0 +1,131 @@
+package scmrexec
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/internal/util"
+ "github.com/FalconOpsLLC/goexec/internal/windows"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
+ "github.com/oiweiwei/go-msrpc/dcerpc"
+ "github.com/oiweiwei/go-msrpc/midl/uuid"
+ "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
+ "github.com/rs/zerolog"
+)
+
+const (
+ ModuleName = "SCMR"
+
+ DefaultEndpoint = "ncacn_np:[svcctl]"
+ ScmrUuid = "367ABB81-9844-35F1-AD32-98F038001003"
+)
+
+type Scmr struct {
+ client *dce.Client
+ ctl svcctl.SvcctlClient
+ scm *svcctl.Handle
+
+ hostname string
+}
+
+func (m *Scmr) Init(ctx context.Context, c *dce.Client) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).Logger()
+
+ m.client = c
+
+ if m.client.Dce() == nil {
+ return errors.New("DCE connection not initialized")
+ }
+
+ m.hostname, err = c.Target.Hostname(ctx)
+ if err != nil {
+ log.Debug().Err(err).Msg("Failed to determine target hostname")
+ }
+ if m.hostname == "" {
+ m.hostname = util.RandomHostname()
+ }
+
+ m.ctl, err = svcctl.NewSvcctlClient(ctx, m.client.Dce(), dcerpc.WithObjectUUID(uuid.MustParse(ScmrUuid)))
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to initialize SVCCTL client")
+ return fmt.Errorf("create SVCCTL client: %w", err)
+ }
+ log.Info().Msg("Created SVCCTL client")
+
+ resp, err := m.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{
+ MachineName: m.hostname,
+ DatabaseName: "ServicesActive\x00",
+ DesiredAccess: ServiceAllAccess, // TODO: Replace
+ })
+ if err != nil {
+ log.Debug().Err(err).Msg("Failed to open SCM handle")
+ return fmt.Errorf("open SCM handle: %w", err)
+ }
+ log.Info().Msg("Opened SCM handle")
+
+ m.scm = resp.SCM
+
+ return
+}
+
+func (m *Scmr) Reconnect(ctx context.Context) (err error) {
+
+ if err = m.client.Reconnect(ctx); err != nil {
+ return fmt.Errorf("reconnect: %w", err)
+ }
+ if err = m.Init(ctx, m.client); err != nil {
+ return fmt.Errorf("reconnect SCMR: %w", err)
+ }
+ return
+}
+
+// openService will a handle to the desired service
+func (m *Scmr) openService(ctx context.Context, name string) (svc *service, err error) {
+
+ log := zerolog.Ctx(ctx)
+
+ resp, err := m.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{
+ ServiceManager: m.scm,
+ ServiceName: name,
+ DesiredAccess: ServiceAllAccess, // TODO: dynamic
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to open service handle")
+ return nil, fmt.Errorf("open service: %w", err)
+ }
+
+ log.Info().Msg("Opened service handle")
+
+ svc = new(service)
+ svc.name = name
+ svc.handle = resp.Service
+
+ return
+}
+
+func (m *Scmr) startService(ctx context.Context, svc *service) error {
+
+ log := zerolog.Ctx(ctx)
+
+ sr, err := m.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle})
+
+ if err != nil {
+
+ if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case)
+ log.Warn().Msg("Service execution deadline exceeded")
+ svc.handle = nil
+ return nil
+
+ } else if sr.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT {
+ log.Info().Msg("Received request timeout. Execution was likely successful")
+ return nil
+ }
+
+ log.Error().Err(err).Msg("Failed to start service")
+ return fmt.Errorf("start service: %w", err)
+ }
+ log.Info().Msg("Service started successfully")
+ return nil
+}
diff --git a/pkg/goexec/scmr/scmr.go b/pkg/goexec/scmr/scmr.go
new file mode 100644
index 0000000..3ee8f9c
--- /dev/null
+++ b/pkg/goexec/scmr/scmr.go
@@ -0,0 +1,19 @@
+package scmrexec
+
+import (
+ "github.com/FalconOpsLLC/goexec/internal/windows"
+ "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
+)
+
+const (
+ ServiceDeleteAccess uint32 = windows.SERVICE_DELETE
+ ServiceModifyAccess uint32 = windows.SERVICE_QUERY_CONFIG | windows.SERVICE_CHANGE_CONFIG | windows.SERVICE_STOP | windows.SERVICE_START | windows.SERVICE_DELETE
+ ServiceCreateAccess uint32 = windows.SC_MANAGER_CREATE_SERVICE | windows.SERVICE_START | windows.SERVICE_STOP | windows.SERVICE_DELETE
+ ServiceAllAccess uint32 = ServiceCreateAccess | ServiceModifyAccess
+)
+
+type service struct {
+ name string
+ handle *svcctl.Handle
+ originalConfig *svcctl.QueryServiceConfigW
+}
diff --git a/pkg/goexec/smb/client.go b/pkg/goexec/smb/client.go
new file mode 100644
index 0000000..d9e8772
--- /dev/null
+++ b/pkg/goexec/smb/client.go
@@ -0,0 +1,111 @@
+package smb
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/oiweiwei/go-smb2.fork"
+ "github.com/rs/zerolog"
+ "net"
+)
+
+type Client struct {
+ ClientOptions
+
+ conn net.Conn
+ sess *smb2.Session
+ mount *smb2.Share
+}
+
+func (c *Client) Session() (sess *smb2.Session) {
+ return c.sess
+}
+
+func (c *Client) String() string {
+ return ClientName
+}
+
+func (c *Client) Logger(ctx context.Context) zerolog.Logger {
+ return zerolog.Ctx(ctx).With().Str("client", c.String()).Logger()
+}
+
+func (c *Client) Mount(_ context.Context, share string) (err error) {
+
+ if c.sess == nil {
+ return errors.New("SMB session not initialized")
+ }
+
+ c.mount, err = c.sess.Mount(share)
+ 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()))
+ }
+ }
+
+ // Establish TCP connection
+ c.conn, err = c.netDialer.Dial("tcp", net.JoinHostPort(c.Host, fmt.Sprintf("%d", c.Port)))
+
+ if err != nil {
+ return err
+ }
+
+ 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)
+
+ 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())
+
+ return
+}
+
+func (c *Client) Close(ctx context.Context) (err error) {
+
+ log := c.Logger(ctx)
+
+ // Close TCP connection
+ if c.conn != nil {
+ defer func() {
+ if err = c.conn.Close(); err != nil {
+ log.Error().Err(err).Msgf("Failed to close %s connection", c.String())
+ }
+ log.Debug().Msgf("Closed %s connection", c.String())
+ }()
+ }
+
+ // Close SMB session
+ if c.sess != nil {
+ defer func() {
+ if err = c.sess.Logoff(); err != nil {
+ log.Error().Err(err).Msgf("Failed to discard %s session", c.String())
+ }
+ log.Debug().Msgf("Discarded %s session", c.String())
+ }()
+ }
+
+ // Unmount SMB share
+ if c.mount != nil {
+ defer func() {
+ if err = c.mount.Umount(); err != nil {
+ log.Error().Err(err).Msg("Failed to unmount share")
+ }
+ log.Debug().Msg("Unmounted file share")
+ }()
+ }
+ return
+}
diff --git a/pkg/goexec/smb/default.go b/pkg/goexec/smb/default.go
new file mode 100644
index 0000000..94f50de
--- /dev/null
+++ b/pkg/goexec/smb/default.go
@@ -0,0 +1,10 @@
+package smb
+
+import "github.com/oiweiwei/go-msrpc/smb2"
+
+const (
+ ClientName = "SMB"
+
+ DefaultPort = 445
+ DefaultDialect = smb2.SMB311
+)
diff --git a/pkg/goexec/smb/options.go b/pkg/goexec/smb/options.go
new file mode 100644
index 0000000..0c2ffb6
--- /dev/null
+++ b/pkg/goexec/smb/options.go
@@ -0,0 +1,98 @@
+package smb
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/RedTeamPentesting/adauth/smbauth"
+ msrpcSMB2 "github.com/oiweiwei/go-msrpc/smb2"
+ "github.com/oiweiwei/go-smb2.fork"
+ "net"
+)
+
+var supportedDialects = map[msrpcSMB2.Dialect]msrpcSMB2.Dialect{
+ 2_0_2: msrpcSMB2.SMB202,
+ 2_1_0: msrpcSMB2.SMB210,
+ 3_0_0: msrpcSMB2.SMB300,
+ 3_0_2: msrpcSMB2.SMB302,
+ 3_1_1: msrpcSMB2.SMB311,
+
+ 0x202: msrpcSMB2.SMB202,
+ 0x210: msrpcSMB2.SMB210,
+ 0x300: msrpcSMB2.SMB300,
+ 0x302: msrpcSMB2.SMB302,
+ 0x311: msrpcSMB2.SMB311,
+}
+
+// ClientOptions holds configuration settings for an SMB client
+type ClientOptions struct {
+ goexec.ClientOptions
+ goexec.AuthOptions
+
+ // NoSign disables packet signing
+ NoSign bool `json:"no_sign" yaml:"no_sign"`
+
+ // NoSeal disables packet encryption
+ NoSeal bool `json:"no_seal" yaml:"no_seal"`
+
+ // Dialect sets the SMB dialect to be passed to smb2.WithDialect()
+ Dialect msrpcSMB2.Dialect `json:"dialect" yaml:"dialect"`
+
+ netDialer goexec.Dialer
+ dialer *smb2.Dialer
+}
+
+func (c *Client) Parse(ctx context.Context) (err error) {
+
+ var do []msrpcSMB2.DialerOption
+
+ if c.Port == 0 {
+ c.Port = DefaultPort
+ }
+ if c.Dialect == 0 {
+ c.Dialect = DefaultDialect
+ }
+
+ // Validate SMB dialect/version
+ if d, ok := supportedDialects[c.Dialect]; ok {
+ do = append(do, msrpcSMB2.WithDialect(d))
+
+ } else {
+ return errors.New("unsupported SMB version")
+ }
+
+ if c.Proxy == "" {
+ c.netDialer = &net.Dialer{} // FUTURE: additional dial c
+
+ } else {
+ // Parse proxy URL
+ c.netDialer, err = goexec.ParseProxyURI(c.Proxy)
+ if err != nil {
+ return err
+ }
+ }
+
+ if !c.NoSeal {
+ // Enable encryption
+ do = append(do, msrpcSMB2.WithSeal())
+ }
+ if !c.NoSign {
+ // Enable signing
+ do = append(do, msrpcSMB2.WithSign())
+ }
+
+ // Validate authentication parameters
+ c.dialer, err = smbauth.Dialer(ctx, c.Credential, c.Target,
+ &smbauth.Options{
+ SMBOptions: do,
+ })
+
+ if err != nil {
+ return fmt.Errorf("set %s auth: %w", ClientName, err)
+ }
+
+ c.Host = c.Target.AddressWithoutPort()
+
+ return nil
+}
diff --git a/pkg/goexec/smb/output.go b/pkg/goexec/smb/output.go
new file mode 100644
index 0000000..35c40c8
--- /dev/null
+++ b/pkg/goexec/smb/output.go
@@ -0,0 +1,63 @@
+package smb
+
+import (
+ "context"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "io"
+ "os"
+ "regexp"
+ "time"
+)
+
+var (
+ DefaultOutputPollInterval = 1 * time.Second
+ DefaultOutputPollTimeout = 60 * time.Second
+ pathPrefix = regexp.MustCompile(`^([a-zA-Z]:)?\\*`)
+)
+
+type OutputFileFetcher struct {
+ goexec.Cleaner
+
+ Client *Client
+ Share string
+ File string
+ PollInterval time.Duration
+ PollTimeout time.Duration
+
+ relativePath string
+}
+
+func (o *OutputFileFetcher) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) {
+
+ if o.PollInterval == 0 {
+ o.PollInterval = DefaultOutputPollInterval
+ }
+ if o.PollTimeout == 0 {
+ o.PollTimeout = DefaultOutputPollTimeout
+ }
+
+ o.relativePath = pathPrefix.ReplaceAllString(o.File, "")
+
+ err = o.Client.Connect(ctx)
+ if err != nil {
+ return
+ }
+
+ err = o.Client.Mount(ctx, o.Share)
+ if err != nil {
+ return
+ }
+
+ stopAt := time.Now().Add(o.PollTimeout)
+
+ for {
+ if time.Now().After(stopAt) {
+ return
+ }
+ if reader, err = o.Client.mount.OpenFile(o.relativePath, os.O_RDONLY, 0); err == nil {
+ return
+ }
+ time.Sleep(o.PollInterval)
+ }
+ return
+}
diff --git a/pkg/goexec/tsch/create.go b/pkg/goexec/tsch/create.go
new file mode 100644
index 0000000..c0f24d1
--- /dev/null
+++ b/pkg/goexec/tsch/create.go
@@ -0,0 +1,113 @@
+package tschexec
+
+import (
+ "context"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec/smb"
+ "github.com/rs/zerolog"
+ "time"
+)
+
+const (
+ MethodCreate = "Create"
+)
+
+type TschCreate struct {
+ Tsch
+ goexec.Executor
+ goexec.Cleaner
+ smb.OutputFileFetcher
+
+ IO goexec.ExecutionIO
+
+ NoDelete bool
+ CallDelete bool
+ StartDelay time.Duration
+ StopDelay time.Duration
+ DeleteDelay time.Duration
+ TimeOffset time.Duration
+ // FEATURE: more opts
+}
+
+func (m *TschCreate) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).
+ Str("method", MethodCreate).
+ Str("task", m.TaskName).
+ Logger()
+
+ startTime := time.Now().UTC().Add(m.StartDelay)
+ stopTime := startTime.Add(m.StopDelay)
+
+ trigger := taskTimeTrigger{
+ StartBoundary: startTime.Format(TaskXmlDurationFormat),
+ Enabled: true,
+ }
+
+ var deleteAfter string
+
+ if !m.NoDelete && !m.CallDelete {
+
+ if m.StopDelay == 0 {
+ m.StopDelay = time.Second // value is required, 1 second by default
+ }
+ trigger.EndBoundary = stopTime.Format(TaskXmlDurationFormat)
+ deleteAfter = xmlDuration(m.DeleteDelay)
+ }
+
+ path, err := m.registerTask(ctx,
+ &registerOptions{
+ AllowStartOnDemand: true,
+ AllowHardTerminate: true,
+ Hidden: !m.NotHidden,
+ triggers: taskTriggers{
+ TimeTriggers: []taskTimeTrigger{trigger},
+ },
+ DeleteAfter: deleteAfter,
+ },
+ execIO,
+ )
+ if err != nil {
+ return err
+ }
+
+ if !m.NoDelete {
+ if m.CallDelete {
+
+ m.AddCleaner(func(ctxInner context.Context) error {
+
+ log.Info().Msg("Waiting for task to start...")
+
+ select {
+ case <-ctxInner.Done():
+ log.Warn().Msg("Task deletion cancelled")
+
+ case <-time.After(m.StartDelay + (5 * time.Second)): // 5 second buffer
+ /*
+ for {
+ stat, err := m.tsch.GetLastRunInfo(ctx, &itaskschedulerservice.GetLastRunInfoRequest{
+ Path: path,
+ })
+ if err != nil {
+ log.Warn().Err(err).Msg("Failed to get last run info. Assuming task was executed")
+
+ } else if stat.LastRuntime.AsTime().IsZero() {
+ log.Warn().Msg("Task was not yet executed. Waiting 5 additional seconds")
+
+ time.Sleep(5 * time.Second)
+ continue
+ }
+ break
+ }
+ */
+ }
+ return m.deleteTask(ctxInner, path)
+ })
+
+ } else {
+ log.Info().Time("when", stopTime).Msg("Task is scheduled to delete")
+ }
+ }
+ return
+}
diff --git a/pkg/goexec/tsch/demand.go b/pkg/goexec/tsch/demand.go
new file mode 100644
index 0000000..cd2ccd5
--- /dev/null
+++ b/pkg/goexec/tsch/demand.go
@@ -0,0 +1,91 @@
+package tschexec
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
+ "github.com/rs/zerolog"
+ "io"
+)
+
+const (
+ MethodDemand = "Demand"
+)
+
+type TschDemand struct {
+ Tsch
+ goexec.Executor
+ goexec.Cleaner
+
+ IO goexec.ExecutionIO
+
+ NoDelete bool
+ NoStart bool
+ SessionId uint32
+}
+
+func (m *TschDemand) Execute(ctx context.Context, in *goexec.ExecutionIO) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).
+ Str("method", MethodDemand).
+ Str("task", m.TaskName).
+ Logger()
+
+ path, err := m.registerTask(ctx,
+ &registerOptions{
+ AllowStartOnDemand: true,
+ AllowHardTerminate: true,
+ Hidden: !m.NotHidden,
+ triggers: taskTriggers{},
+ },
+ in,
+ )
+ if err != nil {
+ return err
+ }
+
+ log.Info().Msg("Task registered")
+
+ if !m.NoDelete {
+ m.AddCleaner(func(ctxInner context.Context) error {
+ return m.deleteTask(ctxInner, path)
+ })
+ }
+
+ if !m.NoStart {
+
+ var flags uint32
+ if m.SessionId != 0 {
+ flags |= 4
+ }
+
+ runResponse, err := m.tsch.Run(ctx, &itaskschedulerservice.RunRequest{
+ Path: path,
+ Flags: flags,
+ SessionID: m.SessionId,
+ })
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to run task")
+ return fmt.Errorf("run task: %w", err)
+ }
+ if ret := uint32(runResponse.Return); ret != 0 {
+ log.Error().Str("code", fmt.Sprintf("0x%08x", ret)).Msg("Task returned non-zero exit code")
+ return fmt.Errorf("task returned non-zero exit code: 0x%08x", ret)
+ }
+
+ log.Info().Msg("Task started successfully")
+ }
+ return
+}
+
+func (m *TschDemand) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) {
+
+ if m.IO.Output != nil {
+ return m.IO.GetOutput(ctx)
+ }
+ return nil, errors.New("no available output provider")
+}
diff --git a/pkg/goexec/tsch/module.go b/pkg/goexec/tsch/module.go
new file mode 100644
index 0000000..13d7b24
--- /dev/null
+++ b/pkg/goexec/tsch/module.go
@@ -0,0 +1,173 @@
+package tschexec
+
+import (
+ "context"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
+ "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
+ "github.com/rs/zerolog"
+ "strings"
+)
+
+const (
+ ModuleName = "TSCH"
+)
+
+type Tsch struct {
+ goexec.Cleaner
+
+ Client *dce.Client
+ tsch itaskschedulerservice.TaskSchedulerServiceClient
+
+ TaskName string
+ TaskPath string
+ UserSid string
+ NotHidden bool
+}
+
+type registerOptions struct {
+ AllowStartOnDemand bool
+ AllowHardTerminate bool
+ StartWhenAvailable bool
+ Hidden bool
+ DeleteAfter string
+
+ triggers taskTriggers
+}
+
+func (m *Tsch) Connect(ctx context.Context) (err error) {
+
+ if err = m.Client.Connect(ctx); err == nil {
+ m.AddCleaner(m.Client.Close)
+ }
+ return
+}
+
+func (m *Tsch) Init(ctx context.Context) (err error) {
+
+ if m.Client.Dce() == nil {
+ return errors.New("DCE connection not initialized")
+ }
+
+ // Create ITaskSchedulerService Client
+ m.tsch, err = itaskschedulerservice.NewTaskSchedulerServiceClient(ctx, m.Client.Dce())
+ return
+}
+
+func (m *Tsch) taskPath() string {
+ if m.TaskPath == "" {
+ m.TaskPath = `\` + m.TaskName
+ }
+ return m.TaskPath
+}
+
+func (m *Tsch) registerTask(ctx context.Context, opts *registerOptions, in *goexec.ExecutionIO) (path string, err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("task", m.TaskName).
+ Logger()
+
+ ctx = log.WithContext(ctx)
+
+ principalId := "1"
+
+ settings := taskSettings{
+ MultipleInstancesPolicy: "IgnoreNew",
+ IdleSettings: taskIdleSettings{
+ StopOnIdleEnd: true,
+ RestartOnIdle: false,
+ },
+ Enabled: true,
+ Priority: 7, // a pretty standard value for scheduled tasks
+
+ AllowHardTerminate: opts.AllowHardTerminate,
+ AllowStartOnDemand: opts.AllowStartOnDemand,
+ Hidden: opts.Hidden,
+ StartWhenAvailable: opts.StartWhenAvailable,
+ DeleteExpiredTaskAfter: opts.DeleteAfter,
+ }
+
+ principals := taskPrincipals{
+ Principals: []taskPrincipal{
+ {
+ ID: principalId, // TODO: dynamic
+ UserID: m.UserSid,
+ RunLevel: "HighestAvailable",
+ },
+ }}
+
+ e := taskActionExec{}
+
+ if ea := strings.SplitN(in.String(), " ", 2); len(ea) == 1 {
+ e.Command = ea[0]
+ } else {
+ e.Command = ea[0]
+ e.Arguments = ea[1]
+ }
+
+ actions := taskActions{
+ Context: principalId,
+ Exec: []taskActionExec{e},
+ }
+
+ def := task{
+ TaskVersion: TaskXmlVersion,
+ TaskNamespace: TaskXmlNamespace,
+ Triggers: opts.triggers,
+ Actions: actions,
+ Principals: principals,
+ Settings: settings,
+ }
+
+ // 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(def)
+
+ 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")
+
+ registerResponse, err := m.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{
+ Path: m.taskPath(),
+ XML: taskXml,
+ Flags: 0, // FEATURE: dynamic
+ SDDL: "",
+ LogonType: 0, // FEATURE: dynamic
+ CredsCount: 0,
+ Creds: nil,
+ })
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to register task")
+ return "", fmt.Errorf("register task: %w", err)
+ }
+
+ return registerResponse.ActualPath, nil
+}
+
+func (m *Tsch) deleteTask(ctx context.Context, taskPath string) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("path", taskPath).Logger()
+
+ _, err = m.tsch.Delete(ctx, &itaskschedulerservice.DeleteRequest{
+ Path: taskPath,
+ })
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to delete task")
+ return fmt.Errorf("delete task: %w", err)
+ }
+
+ log.Info().Msg("Task deleted")
+
+ return
+}
diff --git a/pkg/goexec/tsch/tsch.go b/pkg/goexec/tsch/tsch.go
new file mode 100644
index 0000000..e51433d
--- /dev/null
+++ b/pkg/goexec/tsch/tsch.go
@@ -0,0 +1,166 @@
+package tschexec
+
+import (
+ "encoding/xml"
+ "fmt"
+ "regexp"
+ "time"
+)
+
+const (
+ TaskXmlHeader = `<?xml version="1.0" encoding="UTF-16"?>`
+ TaskXmlNamespace = "http://schemas.microsoft.com/windows/2004/02/mit/task"
+ TaskXmlVersion = "1.2"
+ TaskXmlDurationFormat = "2006-01-02T15:04:05.9999999Z"
+)
+
+var (
+ TaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`)
+ TaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`)
+)
+
+// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f
+
+type taskTriggers 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"`
+}
+
+type taskIdleSettings struct {
+ XMLName xml.Name `xml:"IdleSettings"`
+ StopOnIdleEnd bool `xml:"StopOnIdleEnd"`
+ RestartOnIdle bool `xml:"RestartOnIdle"`
+}
+
+type taskSettings 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 taskIdleSettings `xml:"IdleSettings,omitempty"`
+}
+
+type taskActionExec struct {
+ XMLName xml.Name `xml:"Exec"`
+ Command string `xml:"Command"`
+ Arguments string `xml:"Arguments,omitempty"`
+}
+
+type taskActions struct {
+ XMLName xml.Name `xml:"Actions"`
+ Context string `xml:"Context,attr"`
+ Exec []taskActionExec `xml:"Exec,omitempty"`
+}
+
+type taskPrincipals struct {
+ XMLName xml.Name `xml:"Principals"`
+ Principals []taskPrincipal `xml:"Principal,omitempty"`
+}
+
+type taskPrincipal 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"`
+ Triggers taskTriggers `xml:"Triggers"`
+ Actions taskActions `xml:"Actions"`
+ Principals taskPrincipals `xml:"Principals"`
+ Settings taskSettings `xml:"Settings"`
+}
+
+// newSettings just creates a taskSettings instance with the necessary values + a few dynamic ones
+func newSettings(terminate, onDemand, startWhenAvailable bool) *taskSettings {
+ return &taskSettings{
+ MultipleInstancesPolicy: "IgnoreNew",
+ AllowHardTerminate: terminate,
+ IdleSettings: taskIdleSettings{
+ 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 *taskSettings, pr []taskPrincipal, tr taskTriggers, cmd, args string) *task {
+ if se == nil {
+ se = newSettings(true, true, false)
+ }
+ if pr == nil || len(pr) == 0 {
+ pr = []taskPrincipal{
+ {
+ 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: taskPrincipals{Principals: pr},
+ Settings: *se,
+ Actions: taskActions{
+ Context: pr[0].ID,
+ Exec: []taskActionExec{
+ {
+ 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/pkg/goexec/wmi/call.go b/pkg/goexec/wmi/call.go
new file mode 100644
index 0000000..e4386b5
--- /dev/null
+++ b/pkg/goexec/wmi/call.go
@@ -0,0 +1,31 @@
+package wmiexec
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/rs/zerolog"
+)
+
+const (
+ MethodCall = "Call"
+)
+
+type WmiCall struct {
+ Wmi
+
+ Class string
+ Method string
+ Args map[string]any
+}
+
+func (m *WmiCall) Call(ctx context.Context) (out []byte, err error) {
+ var outMap map[string]any
+
+ if outMap, err = m.query(ctx, m.Class, m.Method, m.Args); err != nil {
+ return
+ }
+ zerolog.Ctx(ctx).Info().Msg("Call succeeded")
+
+ out, err = json.Marshal(outMap)
+ return
+}
diff --git a/pkg/goexec/wmi/module.go b/pkg/goexec/wmi/module.go
new file mode 100644
index 0000000..20d687a
--- /dev/null
+++ b/pkg/goexec/wmi/module.go
@@ -0,0 +1,140 @@
+package wmiexec
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
+ "github.com/oiweiwei/go-msrpc/dcerpc"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemlevel1login/v0"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmio/query"
+ "github.com/rs/zerolog"
+)
+
+const (
+ ModuleName = "WMI"
+ DefaultEndpoint = "ncacn_ip_tcp:[135]"
+)
+
+type Wmi struct {
+ goexec.Cleaner
+ Client *dce.Client
+
+ Resource string
+
+ servicesClient iwbemservices.ServicesClient
+}
+
+func (m *Wmi) Connect(ctx context.Context) (err error) {
+
+ if err = m.Client.Connect(ctx); err == nil {
+ m.AddCleaner(m.Client.Close)
+ }
+ return
+}
+
+func (m *Wmi) Init(ctx context.Context) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).Logger()
+
+ if m.Client.Dce() == nil {
+ return errors.New("DCE connection not initialized")
+ }
+
+ actClient, err := iactivation.NewActivationClient(ctx, m.Client.Dce())
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to initialize IActivation client")
+ return fmt.Errorf("create IActivation client: %w", err)
+ }
+
+ actResponse, err := actClient.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{
+ ORPCThis: ORPCThis,
+ ClassID: wmi.Level1LoginClassID.GUID(),
+ IIDs: []*dcom.IID{iwbemlevel1login.Level1LoginIID},
+ RequestedProtocolSequences: []uint16{ProtocolSequenceRPC}, // FEATURE: Named pipe support?
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to activate remote object")
+ return fmt.Errorf("request remote activation: %w", err)
+ }
+ if actResponse.HResult != 0 {
+ return fmt.Errorf("remote activation failed with code %d", actResponse.HResult)
+ }
+
+ log.Info().Msg("Remote activation succeeded")
+
+ var newOpts []dcerpc.Option
+
+ for _, bind := range actResponse.OXIDBindings.GetStringBindings() {
+ stringBinding, err := dcerpc.ParseStringBinding("ncacn_ip_tcp:" + bind.NetworkAddr) // TODO: try bind.String()
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to parse string binding")
+ continue
+ }
+ stringBinding.NetworkAddress = m.Client.Target.AddressWithoutPort()
+ newOpts = append(newOpts, dcerpc.WithEndpoint(stringBinding.String()))
+ }
+
+ if err = m.Client.Reconnect(ctx, newOpts...); err != nil {
+ log.Error().Err(err).Msg("Failed to connect to remote instance")
+ return fmt.Errorf("connect remote instance: %w", err)
+ }
+
+ log.Info().Msg("Connected to remote instance")
+
+ ipid := actResponse.InterfaceData[0].GetStandardObjectReference().Std.IPID
+ loginClient, err := iwbemlevel1login.NewLevel1LoginClient(ctx, m.Client.Dce(), dcom.WithIPID(ipid))
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to create IWbemLevel1Login client")
+ return fmt.Errorf("create IWbemLevel1Login client: %w", err)
+ }
+
+ login, err := loginClient.NTLMLogin(ctx, &iwbemlevel1login.NTLMLoginRequest{
+ This: ORPCThis,
+ NetworkResource: m.Resource,
+ })
+
+ log.Info().Msg("Completed NTLMLogin operation")
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to login on remote instance")
+ return fmt.Errorf("login: IWbemLevel1Login::NTLMLogin: %w", err)
+ }
+
+ ipid = login.Namespace.InterfacePointer().IPID()
+ m.servicesClient, err = iwbemservices.NewServicesClient(ctx, m.Client.Dce(), dcom.WithIPID(ipid))
+
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to create services client")
+ return fmt.Errorf("create IWbemServices client: %w", err)
+ }
+
+ log.Info().Msg("Initialized services client")
+
+ return
+}
+
+func (m *Wmi) query(ctx context.Context, class, method string, values map[string]any) (outValues map[string]any, err error) {
+ outValues = make(map[string]any)
+
+ if m.servicesClient == nil {
+ return nil, errors.New("module has not been initialized")
+ }
+ if out, err := query.NewBuilder(ctx, m.servicesClient, ComVersion).
+ Spawn(class). // The class to instantiate (i.e. Win32_Process)
+ Method(method). // The method to call (i.e. Create)
+ Values(values). // The values to pass to method
+ Exec().
+ Object(); err == nil {
+ return out.Values(), err
+ }
+ return nil, fmt.Errorf("spawn WMI query: %w", err)
+}
diff --git a/pkg/goexec/wmi/proc.go b/pkg/goexec/wmi/proc.go
new file mode 100644
index 0000000..25e515c
--- /dev/null
+++ b/pkg/goexec/wmi/proc.go
@@ -0,0 +1,64 @@
+package wmiexec
+
+import (
+ "context"
+ "errors"
+ "github.com/FalconOpsLLC/goexec/pkg/goexec"
+ "github.com/rs/zerolog"
+ "io"
+)
+
+const (
+ MethodProc = "Proc"
+)
+
+type WmiProc struct {
+ Wmi
+ IO goexec.ExecutionIO
+ WorkingDirectory string
+}
+
+func (m *WmiProc) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) {
+
+ log := zerolog.Ctx(ctx).With().
+ Str("module", ModuleName).
+ Str("method", MethodProc).
+ Logger()
+ ctx = log.WithContext(ctx)
+
+ if execIO == nil {
+ return errors.New("execution IO is nil")
+ }
+
+ out, err := m.query(ctx,
+ "Win32_Process",
+ "Create",
+
+ map[string]any{
+ "CommandLine": execIO.String(),
+ "WorkingDir": m.WorkingDirectory,
+ },
+ )
+ if err != nil {
+ return
+ }
+
+ if pid := out["ProcessId"].(uint32); pid != 0 {
+ log = log.With().Uint32("pid", pid).Logger()
+ }
+ log.Info().Err(err).Msg("Process created")
+
+ if ret := out["ReturnValue"].(uint32); ret != 0 {
+ log.Error().Err(err).Uint32("return", ret).Msg("Process returned non-zero exit code")
+ }
+ return
+}
+
+func (m *WmiProc) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) {
+
+ if m.IO.Output != nil {
+
+ return m.IO.GetOutput(ctx)
+ }
+ return nil, errors.New("no available output provider")
+}
diff --git a/pkg/goexec/wmi/wmi.go b/pkg/goexec/wmi/wmi.go
new file mode 100644
index 0000000..ad7d453
--- /dev/null
+++ b/pkg/goexec/wmi/wmi.go
@@ -0,0 +1,16 @@
+package wmiexec
+
+import "github.com/oiweiwei/go-msrpc/msrpc/dcom"
+
+const (
+ ProtocolSequenceRPC uint16 = 7
+ ProtocolSequenceNP uint16 = 15
+)
+
+var (
+ ComVersion = &dcom.COMVersion{
+ MajorVersion: 5,
+ MinorVersion: 7,
+ }
+ ORPCThis = &dcom.ORPCThis{Version: ComVersion}
+)