aboutsummaryrefslogtreecommitdiff
path: root/pkg
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 /pkg
parenta827b67d47cba7b02ea9599fe6bb88ffb3a6967d (diff)
downloadgoexec-55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c.tar.gz
goexec-55eb4275fb760ac7a3ce1444f5ae0ded9e2ff91c.zip
rewrote everything lol
Diffstat (limited to 'pkg')
-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.go85
-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
32 files changed, 2322 insertions, 0 deletions
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/pkg/goexec/dcom/util.go b/pkg/goexec/dcom/util.go
new file mode 100644
index 0000000..5ed8a60
--- /dev/null
+++ b/pkg/goexec/dcom/util.go
@@ -0,0 +1,85 @@
+package dcomexec
+
+import (
+ "context"
+ "fmt"
+ "github.com/oiweiwei/go-msrpc/dcerpc"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut"
+ "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0"
+ "strings"
+)
+
+func callComMethod(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{},
+ 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,
+ IID: &dcom.IID{},
+ LocaleID: LcEnglishUs,
+
+ DispatchIDMember: gr.DispatchID[0],
+ }
+
+ 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}
+)