aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryan McNulty <bryanmcnulty@protonmail.com>2025-03-07 08:52:48 -0600
committerBryan McNulty <bryanmcnulty@protonmail.com>2025-03-07 08:52:48 -0600
commite87dd341dde93c289b6774f636e6767476b84a79 (patch)
tree3181b18f79b587bd04d98ed886f3505f37faeb2d
parenta5c860b8ab24c198b7390fbde90044754e35c1c5 (diff)
downloadgoexec-e87dd341dde93c289b6774f636e6767476b84a79.tar.gz
goexec-e87dd341dde93c289b6774f636e6767476b84a79.zip
Added wmiexec module + updated TODO
-rw-r--r--TODO.md22
-rw-r--r--cmd/root.go122
-rw-r--r--cmd/wmi.go142
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--internal/exec/exec.go51
-rw-r--r--internal/exec/wmi/exec.go198
-rw-r--r--internal/exec/wmi/module.go33
-rw-r--r--internal/exec/wmi/wmi.go26
-rw-r--r--internal/util/util.go32
10 files changed, 525 insertions, 104 deletions
diff --git a/TODO.md b/TODO.md
index e20be1b..c70c113 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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")
+ }
+ },
+ }
+)
diff --git a/go.mod b/go.mod
index 2b64e5b..a921866 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index a46de81..790a126 100644
--- a/go.sum
+++ b/go.sum
@@ -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)
}