diff options
-rw-r--r-- | TODO.md | 22 | ||||
-rw-r--r-- | cmd/root.go | 122 | ||||
-rw-r--r-- | cmd/wmi.go | 142 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | internal/exec/exec.go | 51 | ||||
-rw-r--r-- | internal/exec/wmi/exec.go | 198 | ||||
-rw-r--r-- | internal/exec/wmi/module.go | 33 | ||||
-rw-r--r-- | internal/exec/wmi/wmi.go | 26 | ||||
-rw-r--r-- | internal/util/util.go | 32 |
10 files changed, 525 insertions, 104 deletions
@@ -1,8 +1,22 @@ # TODO -## High Priority -- [ ] Add dcom module +## Resolve Before Release + +### Higher Priority +- [ ] Add WMI module +- [ ] Add psexec module (RemComSvc) +- [ ] Testing on different Windows versions + +### Other +- [ ] Fix SCMR `change` method so that dependencies field isn't permanently overwritten +- [ ] Add `delete` command to all modules that may involve cleanup - use `tsch delete` for reference -## Lower Priority +## Resolve Eventually + +### Higher Priority +- [ ] Add dcom module - [ ] 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 delete command to scmr - should look similar to `tsch delete`
\ No newline at end of file + +### Lower Priority +- [ ] `--ctf` option - allow unsafe OPSEC (i.e. fetching execution output via file write/read) +- [ ] ability to specify multiple targets
\ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 473f1ad..9a84e28 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,79 +1,83 @@ package cmd import ( - "context" - "fmt" - "github.com/RedTeamPentesting/adauth" - "github.com/rs/zerolog" - "github.com/spf13/cobra" - "os" - "regexp" + "context" + "fmt" + "github.com/RedTeamPentesting/adauth" + "github.com/rs/zerolog" + "github.com/spf13/cobra" + "os" + "regexp" ) var ( - //logFile string - log zerolog.Logger - ctx context.Context - authOpts *adauth.Options + //logFile string + log zerolog.Logger + ctx context.Context + authOpts *adauth.Options - debug bool - command string - executable string - executablePath string - executableArgs string + debug bool + command string + executable string + executablePath string + executableArgs string + workingDirectory string - needsTarget = func(cmd *cobra.Command, args []string) (err error) { - if len(args) != 1 { - return fmt.Errorf("command require exactly one positional argument: [target]") - } - if creds, target, err = authOpts.WithTarget(ctx, "cifs", 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") - } - return - } + needsTarget = func(cmd *cobra.Command, args []string) (err error) { + if len(args) != 1 { + return fmt.Errorf("command require exactly one positional argument: [target]") + } + if creds, target, err = authOpts.WithTarget(ctx, "cifs", 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") + } + return + } - 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") - } - log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel).With().Timestamp().Logger() - if debug { - log = log.Level(zerolog.DebugLevel) - } - return - }, - } + 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") + } + log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel).With().Timestamp().Logger() + if debug { + log = log.Level(zerolog.DebugLevel) + } + return + }, + } ) func init() { - ctx = context.Background() + ctx = context.Background() - rootCmd.InitDefaultVersionFlag() - rootCmd.InitDefaultHelpCmd() - rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging") + rootCmd.InitDefaultVersionFlag() + rootCmd.InitDefaultHelpCmd() + rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging") - authOpts = &adauth.Options{Debug: log.Debug().Msgf} - authOpts.RegisterFlags(rootCmd.PersistentFlags()) + authOpts = &adauth.Options{Debug: log.Debug().Msgf} + authOpts.RegisterFlags(rootCmd.PersistentFlags()) - scmrCmdInit() - rootCmd.AddCommand(scmrCmd) + scmrCmdInit() + rootCmd.AddCommand(scmrCmd) - tschCmdInit() - rootCmd.AddCommand(tschCmd) + tschCmdInit() + rootCmd.AddCommand(tschCmd) + + wmiCmdInit() + rootCmd.AddCommand(wmiCmd) } func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } } diff --git a/cmd/wmi.go b/cmd/wmi.go new file mode 100644 index 0000000..1808226 --- /dev/null +++ b/cmd/wmi.go @@ -0,0 +1,142 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/exec" + wmiexec "github.com/FalconOpsLLC/goexec/internal/exec/wmi" + "github.com/spf13/cobra" + "regexp" + "strings" +) + +func wmiCmdInit() { + wmiCustomCmdInit() + wmiCmd.AddCommand(wmiCustomCmd) + wmiProcessCmdInit() + wmiCmd.AddCommand(wmiProcessCmd) +} + +func wmiCustomCmdInit() { + wmiCustomCmd.Flags().StringVarP(&wmiArgMethod, "method", "m", "", `WMI Method to use in the format CLASS.METHOD (i.e. "Win32_Process.Create")`) + wmiCustomCmd.Flags().StringVarP(&wmiArgMethodArgs, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`) + if err := wmiCustomCmd.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) + } +} + +var ( + // for custom method + wmiArgMethod string + wmiArgMethodArgs string + + wmiClass string + wmiMethod string + wmiMethodArgsMap map[string]any + methodRegex = regexp.MustCompile(`^\w+\.\w+$`) + + wmiCmd = &cobra.Command{ + Use: "wmi", + Short: "Establish execution via WMI", + Args: cobra.NoArgs, + } + wmiCustomCmd = &cobra.Command{ + Use: "custom", + Short: "Execute specified WMI method", + Long: `Description: + The custom method creates an instance of the specified WMI class (-c), + then calls the provided method (-m) with the provided arguments (-A). + +References: + https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-classes + `, + Args: func(cmd *cobra.Command, args []string) (err error) { + if err = needsTarget(cmd, args); err == nil { + if wmiArgMethod != "" && !methodRegex.MatchString(wmiArgMethod) { + return fmt.Errorf("invalid CLASS.METHOD syntax: %s", wmiArgMethod) + } + if err = json.Unmarshal([]byte(wmiArgMethodArgs), &wmiMethodArgsMap); err != nil { + err = fmt.Errorf("failed to parse JSON arguments: %w", err) + } + } + return + }, + Run: func(cmd *cobra.Command, args []string) { + module := wmiexec.Module{} + + connCfg := &exec.ConnectionConfig{} + cleanCfg := &exec.CleanupConfig{} + + parts := strings.SplitN(wmiArgMethod, ".", 2) + wmiClass = parts[0] + wmiMethod = parts[1] + + execCfg := &exec.ExecutionConfig{ + ExecutableName: executable, + ExecutableArgs: executableArgs, + ExecutionMethod: wmiexec.MethodCustom, + ExecutionMethodConfig: wmiexec.MethodCustomConfig{ + Class: wmiClass, + Method: wmiMethod, + Arguments: wmiMethodArgsMap, + }, + } + 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") + + } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { + log.Error().Err(err).Msg("Cleanup failed") + } + }, + } + + wmiProcessCmd = &cobra.Command{ + Use: "process", + Short: "Create a Windows process", + Long: `Description: + The process method creates an instance of the Win32_Process WMI class, + then calls the Win32_Process.Create method with the provided command (-c), + and optional working directory (-d). + +References: + https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/create-method-in-class-win32-process +`, + Args: needsTarget, + Run: func(cmd *cobra.Command, args []string) { + module := wmiexec.Module{} + + connCfg := &exec.ConnectionConfig{} + cleanCfg := &exec.CleanupConfig{} + + execCfg := &exec.ExecutionConfig{ + ExecutableName: executable, + ExecutableArgs: executableArgs, + ExecutionMethod: wmiexec.MethodProcess, + ExecutionMethodConfig: wmiexec.MethodProcessConfig{ + Command: command, + WorkingDirectory: workingDirectory, + }, + } + 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") + + } else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil { + log.Error().Err(err).Msg("Cleanup failed") + } + }, + } +) @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877 + github.com/google/uuid v1.6.0 github.com/oiweiwei/go-msrpc v1.2.1 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.9.1 @@ -8,6 +8,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w= github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 16fa543..a89cf7b 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -1,44 +1,45 @@ package exec import ( - "context" - "github.com/RedTeamPentesting/adauth" + "context" + "github.com/RedTeamPentesting/adauth" ) +type ConnectionConfig struct { + ConnectionMethod string + ConnectionMethodConfig interface{} +} + type CleanupConfig struct { - CleanupMethod string - CleanupMethodConfig interface{} + 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{} - ExecutionOutput string // not implemented - ExecutionOutputConfig interface{} // not implemented + 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{} + //ExecutionOutput string // not implemented + //ExecutionOutputConfig interface{} // not implemented } 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` + 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 { - // Exec performs a single execution task without the need to call Init. - Exec(context.Context, *adauth.Credential, *adauth.Target, *ExecutionConfig) error - Cleanup(context.Context, *adauth.Credential, *adauth.Target, *CleanupConfig) error - - // Init assigns the provided TODO - //Init(ctx context.Context, creds *adauth.Credential, target *adauth.Target) - //Shell(ctx context.Context, input chan *ExecutionConfig, output chan []byte) + Connect(context.Context, *adauth.Credential, *adauth.Target, *ConnectionConfig) error + Exec(context.Context, *ExecutionConfig) error + Cleanup(context.Context, *CleanupConfig) error } func (cfg *ExecutionConfig) GetRawCommand() string { - if cfg.ExecutableArgs != "" { - return cfg.ExecutablePath + " " + cfg.ExecutableArgs - } - return cfg.ExecutablePath + if cfg.ExecutableArgs != "" { + return cfg.ExecutablePath + " " + cfg.ExecutableArgs + } + return cfg.ExecutablePath } diff --git a/internal/exec/wmi/exec.go b/internal/exec/wmi/exec.go new file mode 100644 index 0000000..962fc60 --- /dev/null +++ b/internal/exec/wmi/exec.go @@ -0,0 +1,198 @@ +package wmiexec + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/FalconOpsLLC/goexec/internal/exec" + "github.com/RedTeamPentesting/adauth" + "github.com/RedTeamPentesting/adauth/dcerpcauth" + "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/ssp/gssapi" + "github.com/rs/zerolog" +) + +const ( + ProtocolSequenceRPC uint16 = 7 + ProtocolSequenceNP uint16 = 15 +) + +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("func", "Cleanup").Logger() + + if err = mod.dce.Close(ctx); err != nil { + log.Warn().Err(err).Msg("Failed to close DCERPC connection") + } + return +} + +func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, _ *exec.ConnectionConfig) (err error) { + + var baseOpts, authOpts []dcerpc.Option + var ipid *dcom.IPID // This will store the IPID of the remote instance + var bind2Opts []dcerpc.Option + + ctx = gssapi.NewSecurityContext(ctx) + log := zerolog.Ctx(ctx).With(). + Str("func", "Connect").Logger() + + // Assemble DCERPC options + { + baseOpts = []dcerpc.Option{ + dcerpc.WithLogger(log), + dcerpc.WithSign(), // Enforce signing + dcerpc.WithSeal(), // Enforce packet stub encryption + } + // Add target name option if possible + if tn, err := target.Hostname(ctx); err == nil { + baseOpts = append(baseOpts, dcerpc.WithTargetName(tn)) + } else { + log.Debug().Err(err).Msg("Failed to get target hostname") + } + // Parse target and credentials + if authOpts, err = dcerpcauth.AuthenticationOptions(ctx, creds, target, &dcerpcauth.Options{}); err != nil { + return fmt.Errorf("parse authentication options: %w", err) + } + } + + // Establish first connection (REMACT) + { + // Connection options + rp := "ncacn_ip_tcp" // Underlying protocol + ro := 135 // RPC port + rb := fmt.Sprintf("%s:[%d]", rp, ro) // RPC binding + + // Create DCERPC dialer + mod.dce, err = dcerpc.Dial(ctx, target.AddressWithoutPort(), append(baseOpts, append(authOpts, dcerpc.WithEndpoint(rb))...)...) + if err != nil { + return fmt.Errorf("DCERPC dial: %w", err) + } + // Create remote activation client + ia, err := iactivation.NewActivationClient(ctx, mod.dce, append(baseOpts, dcerpc.WithEndpoint(rb))...) + if err != nil { + return fmt.Errorf("create activation client: %w", err) + } + // Send remote activation request + act, err := ia.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ + ORPCThis: ORPCThis, + ClassID: wmi.Level1LoginClassID.GUID(), + IIDs: []*dcom.IID{iwbemlevel1login.Level1LoginIID}, + RequestedProtocolSequences: []uint16{ProtocolSequenceRPC, ProtocolSequenceNP}, // TODO: dynamic + }) + 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 + + // This ensures that the original target address/hostname is used in the string binding + origBind := retBinds[0].String() + if bind, err := dcerpc.ParseStringBinding(origBind); err != nil { + log.Warn().Str("binding", origBind).Err(err).Msg("Failed to parse binding string returned by server") + bind2Opts = act.OXIDBindings.EndpointsByProtocol(rp) // Try using the server supplied string binding + } else { + bind.NetworkAddress = target.AddressWithoutPort() // Replace address/hostname in new string binding + bs := bind.String() + log.Info().Str("binding", bs).Msg("found binding") + bind2Opts = append(bind2Opts, dcerpc.WithEndpoint(bs)) // Use the new string binding + } + } + + // Establish second connection (WMI) + { + bind2Opts = append(bind2Opts, authOpts...) + mod.dce, err = dcerpc.Dial(ctx, target.AddressWithoutPort(), append(baseOpts, bind2Opts...)...) + if err != nil { + return fmt.Errorf("dial WMI: %w", err) + } + // Create login client + loginClient, err := iwbemlevel1login.NewLevel1LoginClient(ctx, mod.dce, append(baseOpts, dcom.WithIPID(ipid))...) + if err != nil { + return fmt.Errorf("initialize wbem login client: %w", err) + } + login, err := loginClient.NTLMLogin(ctx, &iwbemlevel1login.NTLMLoginRequest{ // TODO: Other login opts/methods? + This: ORPCThis, + NetworkResource: "//./root/cimv2", // TODO: make this dynamic + }) + 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("iwbemservices.NewServicesClient: %w", err) + } + } + return nil +} + +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 == MethodCustom { + if cfg, ok := ecfg.ExecutionMethodConfig.(MethodCustomConfig); !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 new file mode 100644 index 0000000..f90af42 --- /dev/null +++ b/internal/exec/wmi/module.go @@ -0,0 +1,33 @@ +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 MethodCustomConfig struct { + Class string + Method string + Arguments map[string]any +} + +type MethodProcessConfig struct { + Command string + WorkingDirectory string +} + +const ( + MethodCustom = "custom" + MethodProcess = "process" +) diff --git a/internal/exec/wmi/wmi.go b/internal/exec/wmi/wmi.go new file mode 100644 index 0000000..dd003a3 --- /dev/null +++ b/internal/exec/wmi/wmi.go @@ -0,0 +1,26 @@ +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 252815e..36d7ea2 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,35 +1,35 @@ package util import ( - "math/rand" // not crypto secure - "regexp" + "math/rand" // not crypto secure + "regexp" ) const randHostnameCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" const randStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var ( - // Up to 15 characters; only letters, digits, and hyphens (with hyphens not at the start or end). - randHostnameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-]{0,14}[a-zA-Z0-9]$`) + // Up to 15 characters; only letters, digits, and hyphens (with hyphens not at the start or end). + randHostnameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-]{0,14}[a-zA-Z0-9]$`) ) func RandomHostname() (hostname string) { - for { - // between 2 and 10 characters - if hostname = RandomStringFromCharset(randHostnameCharset, rand.Intn(8)+2); randHostnameRegex.MatchString(hostname) { - return - } - } + for { + // between 2 and 10 characters + if hostname = RandomStringFromCharset(randHostnameCharset, rand.Intn(8)+2); randHostnameRegex.MatchString(hostname) { + return + } + } } func RandomString() string { - return RandomStringFromCharset(randStringCharset, rand.Intn(10)+6) + return RandomStringFromCharset(randStringCharset, rand.Intn(10)+6) } func RandomStringFromCharset(charset string, length int) string { - b := make([]byte, length) - for i := range length { - b[i] = charset[rand.Intn(len(charset))] - } - return string(b) + b := make([]byte, length) + for i := range length { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) } |