aboutsummaryrefslogtreecommitdiff
path: root/internal
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 /internal
parenta5c860b8ab24c198b7390fbde90044754e35c1c5 (diff)
downloadgoexec-e87dd341dde93c289b6774f636e6767476b84a79.tar.gz
goexec-e87dd341dde93c289b6774f636e6767476b84a79.zip
Added wmiexec module + updated TODO
Diffstat (limited to 'internal')
-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
5 files changed, 299 insertions, 41 deletions
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)
}