├── .github └── workflows │ └── go.yml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── TODO.md ├── cmd ├── args.go ├── dcom.go ├── root.go ├── scmr.go ├── tsch.go └── wmi.go ├── go.mod ├── go.sum ├── internal └── util │ └── util.go ├── main.go └── pkg └── goexec ├── auth.go ├── clean.go ├── client.go ├── dce ├── client.go ├── default.go └── options.go ├── dcom ├── dcom.go ├── mmc.go ├── module.go ├── shellbrowserwindow.go ├── shellwindows.go └── util.go ├── exec.go ├── io.go ├── method.go ├── proxy.go ├── scmr ├── change.go ├── create.go ├── delete.go ├── module.go └── scmr.go ├── smb ├── client.go ├── default.go ├── input.go ├── options.go └── output.go ├── tsch ├── change.go ├── create.go ├── demand.go ├── module.go ├── task │ ├── action.go │ ├── misc.go │ ├── settings.go │ ├── task.go │ └── trigger.go └── tsch.go └── wmi ├── call.go ├── module.go ├── proc.go └── wmi.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | # Adapted from https://github.com/RedTeamPentesting/adauth/blob/main/.github/workflows/check.yml :) 4 | 5 | name: Go 6 | 7 | on: [push] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | go-version: [ '1.23', '1.24' ] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup Go ${{ matrix.go-version }} 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | 24 | - name: Lint 25 | uses: golangci/golangci-lint-action@v3 26 | with: 27 | version: v1.64 28 | args: --verbose --timeout 5m 29 | 30 | - name: Check go.mod 31 | run: | 32 | echo "check if go.mod is up to date" 33 | go mod tidy 34 | git diff --exit-code go.mod 35 | 36 | - name: Build 37 | run: go build -v ./... 38 | 39 | - name: Test 40 | run: go test -v ./... 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .logs 3 | .local 4 | /dist 5 | /goexec 6 | /goexec.* 7 | /patch 8 | /go.work 9 | /go.work.sum 10 | *.pcap 11 | *.pcapng 12 | *.log 13 | *.json 14 | *.exe 15 | *.prof 16 | *.cprof 17 | *.mprof 18 | /tests 19 | *._go 20 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: goexec 3 | 4 | before: 5 | hooks: 6 | - go mod tidy 7 | - go generate ./... 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | ldflags: -s -w 12 | goos: 13 | - darwin 14 | - windows 15 | - linux 16 | goarch: 17 | - amd64 18 | - arm64 19 | 20 | ignore: 21 | - goos: windows 22 | goarch: arm 23 | 24 | #upx: 25 | # - enabled: true 26 | # goos: [ linux ] 27 | # compress: best 28 | # lzma: true 29 | 30 | archives: 31 | - name_template: "{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}" 32 | files: 33 | - README.md 34 | - LICENSE 35 | format_overrides: 36 | - goos: windows 37 | format: zip 38 | 39 | checksum: 40 | name_template: "checksums.txt" 41 | snapshot: 42 | name_template: "{{ incpatch .Version }}" 43 | changelog: 44 | sort: asc 45 | filters: 46 | exclude: 47 | - "^docs:" 48 | - "^doc:" 49 | - "^ci:" 50 | - "^Merge pull request" 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS goexec-builder 2 | LABEL builder="true" 3 | 4 | WORKDIR /go/src/ 5 | 6 | COPY cmd/ cmd/ 7 | COPY internal/ internal/ 8 | COPY pkg/ pkg/ 9 | COPY main.go go.mod go.sum ./ 10 | 11 | ENV CGO_ENABLED=0 12 | 13 | RUN go mod download 14 | RUN go build -ldflags="-s -w" -o /go/bin/goexec 15 | 16 | # [For debugging] 17 | #FROM alpine:3 AS goexec 18 | 19 | FROM scratch AS goexec 20 | COPY --from="goexec-builder" /go/bin/goexec /usr/local/bin/goexec 21 | 22 | WORKDIR /io 23 | ENTRYPOINT ["/usr/local/bin/goexec"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 FalconOps LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoExec - Remote Execution Multitool 2 | 3 | ![goexec](https://github.com/user-attachments/assets/16782082-5a42-477c-95e2-46295bbe3c34) 4 | 5 | GoExec is a new take on some of the methods used to gain remote execution on Windows devices. GoExec implements a number of largely unrealized execution methods and provides significant OPSEC improvements overall. 6 | 7 | The original post about GoExec v0.1.0 can be found [here](https://www.falconops.com/blog/introducing-goexec) 8 | 9 | ## Installation 10 | 11 | ### Build & Install with Go 12 | 13 | To build this project from source, you will need Go version 1.23.* or greater and a 64-bit target architecture. More information on managing Go installations can be found [here](https://go.dev/doc/manage-install) 14 | 15 | ```shell 16 | # Install goexec 17 | go install -ldflags="-s -w" github.com/FalconOpsLLC/goexec@latest 18 | ``` 19 | 20 | #### Manual Installation 21 | 22 | For pre-release features, fetch the latest commit and build manually. 23 | 24 | ```shell 25 | # (Linux) Install GoExec manually from source 26 | # Fetch source 27 | git clone https://github.com/FalconOpsLLC/goexec 28 | cd goexec 29 | 30 | # Build goexec (Go >= 1.23) 31 | CGO_ENABLED=0 go build -ldflags="-s -w" 32 | 33 | # (Optional) Install goexec to /usr/local/bin/goexec 34 | sudo install ./goexec /usr/local/bin 35 | ``` 36 | 37 | ### Install with Docker 38 | 39 | We've provided a Dockerfile to build and run GoExec within Docker containers. 40 | 41 | ```shell 42 | # (Linux) Install GoExec Docker image 43 | # Fetch source 44 | git clone https://github.com/FalconOpsLLC/goexec 45 | cd goexec 46 | 47 | # Build goexec image 48 | docker build . --tag goexec --network host 49 | 50 | # Run goexec via Docker container 51 | alias goexec='docker run -it --rm --name goexec goexec' 52 | goexec -h # display help menu 53 | ``` 54 | 55 | ### Install from Release 56 | 57 | You may also download [the latest release](https://github.com/FalconOpsLLC/goexec/releases/latest) for 64-bit Windows, macOS, or Linux. 58 | 59 | ## Usage 60 | 61 | GoExec is made up of modules for each remote service used (i.e. `wmi`, `scmr`, etc.), and specific methods within each module (i.e. `wmi proc`, `scmr change`, etc.) 62 | 63 | ```text 64 | Usage: 65 | goexec [command] [flags] 66 | 67 | Execution Commands: 68 | dcom Execute with Distributed Component Object Model (MS-DCOM) 69 | wmi Execute with Windows Management Instrumentation (MS-WMI) 70 | scmr Execute with Service Control Manager Remote (MS-SCMR) 71 | tsch Execute with Windows Task Scheduler (MS-TSCH) 72 | 73 | Additional Commands: 74 | help Help about any command 75 | completion Generate the autocompletion script for the specified shell 76 | 77 | Logging: 78 | -D, --debug Enable debug logging 79 | -O, --log-file file Write JSON logging output to file 80 | -j, --json Write logging output in JSON lines 81 | -q, --quiet Disable info logging 82 | 83 | Authentication: 84 | -u, --user user@domain Username ('user@domain', 'domain\user', 'domain/user' or 'user') 85 | -p, --password string Password 86 | -H, --nt-hash hash NT hash ('NT', ':NT' or 'LM:NT') 87 | --aes-key hex key Kerberos AES hex key 88 | --pfx file Client certificate and private key as PFX file 89 | --pfx-password string Password for PFX file 90 | --ccache file Kerberos CCache file name (defaults to $KRB5CCNAME, currently unset) 91 | --dc string Domain controller 92 | -k, --kerberos Use Kerberos authentication 93 | 94 | Use "goexec [command] --help" for more information about a command. 95 | ``` 96 | 97 | ### Fetching Remote Process Output 98 | 99 | 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` 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. 100 | 101 | ### WMI Module (`wmi`) 102 | 103 | The `wmi` module uses remote Windows Management Instrumentation (WMI) to spawn processes (`wmi proc`), or manually call a method (`wmi call`). 104 | 105 | ```text 106 | Usage: 107 | goexec wmi [command] [flags] 108 | 109 | Available Commands: 110 | proc Start a Windows process 111 | call Execute specified WMI method 112 | 113 | ... [inherited flags] ... 114 | 115 | Network: 116 | -x, --proxy URI Proxy URI 117 | -F, --epm-filter string String binding to filter endpoints returned 118 | by the RPC endpoint mapper (EPM) 119 | --endpoint string Explicit RPC endpoint definition 120 | --no-epm Do not use EPM to automatically detect RPC 121 | endpoints 122 | --no-sign Disable signing on DCERPC messages 123 | --no-seal Disable packet stub encryption on DCERPC messages 124 | ``` 125 | 126 | #### Process Creation Method (`wmi proc`) 127 | 128 | The `proc` method creates an instance of the `Win32_Process` WMI class, then calls the `Create` method to spawn a process with the provided arguments. 129 | 130 | ```text 131 | Usage: 132 | goexec wmi proc [target] [flags] 133 | 134 | Execution: 135 | -e, --exec string Remote Windows executable to invoke 136 | -a, --args string Process command line arguments 137 | -c, --command string Windows process command line (executable & 138 | arguments) 139 | -o, --out string Fetch execution output to file or "-" for 140 | standard output 141 | -m, --out-method string Method to fetch execution output (default "smb") 142 | --no-delete-out Preserve output file on remote filesystem 143 | -d, --directory string Working directory (default "C:\\") 144 | 145 | ... [inherited flags] ... 146 | ``` 147 | 148 | ##### Examples 149 | 150 | ```shell 151 | # Run an executable without arguments 152 | ./goexec wmi proc "$target" \ 153 | -u "$auth_user" \ 154 | -p "$auth_pass" \ 155 | -e 'C:\Windows\Temp\Beacon.exe' \ 156 | 157 | # Authenticate with NT hash, fetch output from `cmd.exe /c whoami /all` 158 | ./goexec wmi proc "$target" \ 159 | -u "$auth_user" \ 160 | -H "$auth_nt" \ 161 | -e 'cmd.exe' \ 162 | -a '/C whoami /all' \ 163 | -o- # Fetch output to STDOUT 164 | ``` 165 | 166 | #### (Auxiliary) Call Method (`wmi call`) 167 | 168 | The `call` method gives the operator full control over a WMI method call. You can list available classes and methods on Windows with PowerShell's [`Get-CimClass`](https://learn.microsoft.com/en-us/powershell/module/cimcmdlets/get-cimclass?view=powershell-7.5). 169 | 170 | ```text 171 | Usage: 172 | goexec wmi call [target] [flags] 173 | 174 | WMI: 175 | -n, --namespace string WMI namespace (default "//./root/cimv2") 176 | -C, --class string WMI class to instantiate (i.e. "Win32_Process") 177 | -m, --method string WMI Method to call (i.e. "Create") 178 | -A, --args string WMI Method argument(s) in JSON dictionary format (i.e. {"Command":"calc.exe"}) (default "{}") 179 | 180 | ... [inherited flags] ... 181 | ``` 182 | 183 | ##### Examples 184 | 185 | ```shell 186 | # Call StdRegProv.EnumKey - enumerate registry subkeys of HKLM\SYSTEM 187 | ./goexec wmi call "$target" \ 188 | -u "$auth_user" \ 189 | -p "$auth_pass" \ 190 | -C 'StdRegProv' \ 191 | -m 'EnumKey' \ 192 | -A '{"sSubKeyName":"SYSTEM"}' 193 | ``` 194 | 195 | ### DCOM Module (`dcom`) 196 | 197 | The `dcom` module uses exposed Distributed Component Object Model (DCOM) objects to spawn processes. 198 | 199 | > [!WARNING] 200 | > 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. 201 | 202 | ```text 203 | Usage: 204 | goexec dcom [command] [flags] 205 | 206 | Available Commands: 207 | mmc Execute with the MMC20.Application DCOM object 208 | shellwindows Execute with the ShellWindows DCOM object 209 | shellbrowserwindow Execute with the ShellBrowserWindow DCOM object 210 | 211 | ... [inherited flags] ... 212 | 213 | Network: 214 | -x, --proxy URI Proxy URI 215 | -F, --epm-filter string String binding to filter endpoints returned 216 | by the RPC endpoint mapper (EPM) 217 | --endpoint string Explicit RPC endpoint definition 218 | --no-epm Do not use EPM to automatically detect RPC 219 | endpoints 220 | --no-sign Disable signing on DCERPC messages 221 | --no-seal Disable packet stub encryption on DCERPC messages 222 | ``` 223 | 224 | #### `MMC20.Application` Method (`dcom mmc`) 225 | 226 | The `mmc` method uses the exposed `MMC20.Application` object to call `Document.ActiveView.ShellExec`, and ultimately spawn a process on the remote host. 227 | 228 | ```text 229 | Usage: 230 | goexec dcom mmc [target] [flags] 231 | 232 | Execution: 233 | -e, --exec string Remote Windows executable to invoke 234 | -a, --args string Process command line arguments 235 | -c, --command string Windows process command line (executable & 236 | arguments) 237 | -o, --out string Fetch execution output to file or "-" for 238 | standard output 239 | -m, --out-method string Method to fetch execution output (default "smb") 240 | --no-delete-out Preserve output file on remote filesystem 241 | --directory directory Working directory (default "C:\\") 242 | --window string Window state (default "Minimized") 243 | 244 | ... [inherited flags] ... 245 | ``` 246 | 247 | ##### Examples 248 | 249 | ```shell 250 | # Authenticate with NT hash, fetch output from `cmd.exe /c whoami /priv` to file 251 | ./goexec dcom mmc "$target" \ 252 | -u "$auth_user" \ 253 | -H "$auth_nt" \ 254 | -e 'cmd.exe' \ 255 | -a '/c whoami /priv' \ 256 | -o ./privs.bin # Save output to ./privs.bin 257 | ``` 258 | 259 | #### `ShellWindows` Method (`dcom shellwindows`) 260 | 261 | The `shellwindows` method uses the [ShellWindows](https://learn.microsoft.com/en-us/windows/win32/shell/shellwindows) DCOM object to call `Item().Document.Application.ShellExecute` and spawn a remote process. This execution method isn't nearly as stable as the `dcom mmc` method for a few reasons: 262 | 263 | - This method may not work on the latest Windows versions 264 | - It may require that there is an active desktop session on the target machine. 265 | - Successful execution may be on behalf of the desktop user, not necessarily an administrator. 266 | 267 | ```text 268 | Usage: 269 | goexec dcom shellwindows [target] [flags] 270 | 271 | Execution: 272 | -e, --exec string Remote Windows executable to invoke 273 | -a, --args string Process command line arguments 274 | -c, --command string Windows process command line (executable & arguments) 275 | -o, --out string Fetch execution output to file or "-" for standard output 276 | -m, --out-method string Method to fetch execution output (default "smb") 277 | --no-delete-out Preserve output file on remote filesystem 278 | --directory directory Working directory (default "C:\\") 279 | --app-window ID Application window state ID (default "0") 280 | ... [inherited flags] ... 281 | ``` 282 | 283 | The app window argument (`--app-window`) must be one of the values described [here (`vShow` parameter)](https://learn.microsoft.com/en-us/windows/win32/shell/shell-shellexecute). 284 | 285 | ##### Examples 286 | 287 | ```shell 288 | # Authenticate with local admin NT hash, execute `netstat.exe -anop tcp` w/ output 289 | ./goexec dcom shellwindows "$target" \ 290 | -u "$auth_user" \ 291 | -H "$auth_nt" \ 292 | -e 'netstat.exe' \ 293 | -a '-anop tcp' \ 294 | -o- # write to standard output 295 | 296 | # Authenticate with local admin password, open maximized notepad window on desktop 297 | ./goexec dcom shellwindows "$target" \ 298 | -u "$auth_user" \ 299 | -p "$auth_pass" \ 300 | -e 'notepad.exe' \ 301 | --directory 'C:\Windows' \ 302 | --app-window 3 # Maximized 303 | ``` 304 | 305 | #### `ShellBrowserWindow` Method (`dcom shellbrowserwindow`) 306 | 307 | The `shellbrowserwindow` method uses the exposed [ShellBrowserWindow](https://strontic.github.io/xcyclopedia/library/clsid_c08afd90-f2a1-11d1-8455-00a0c91f3880.html) DCOM object to call `Document.Application.ShellExecute` and spawn the provided process. The potential constraints of this method are similar to the [ShellWindows method](#shellwindows-method-dcom-shellwindows). 308 | 309 | ```text 310 | Usage: 311 | goexec dcom shellbrowserwindow [target] [flags] 312 | 313 | Execution: 314 | -e, --exec string Remote Windows executable to invoke 315 | -a, --args string Process command line arguments 316 | -c, --command string Windows process command line (executable & arguments) 317 | -o, --out string Fetch execution output to file or "-" for standard output 318 | -m, --out-method string Method to fetch execution output (default "smb") 319 | --no-delete-out Preserve output file on remote filesystem 320 | --directory directory Working directory (default "C:\\") 321 | --app-window ID Application window state ID (default "0") 322 | 323 | ... [inherited flags] ... 324 | ``` 325 | 326 | ##### Examples 327 | 328 | ```shell 329 | # Authenticate with NT hash, open explorer.exe maximized 330 | ./goexec dcom shellbrowserwindow "$target" \ 331 | -u "$auth_user@$domain" \ 332 | -H "$auth_nt" \ 333 | -e 'explorer.exe' \ 334 | --app-window 3 335 | ``` 336 | 337 | ### Task Scheduler Module (`tsch`) 338 | 339 | 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. 340 | 341 | ```text 342 | Usage: 343 | goexec tsch [command] [flags] 344 | 345 | Available Commands: 346 | demand Register a remote scheduled task and demand immediate start 347 | create Create a remote scheduled task with an automatic start time 348 | change Modify an existing task to spawn an arbitrary process 349 | 350 | ... [inherited flags] ... 351 | 352 | Network: 353 | -x, --proxy URI Proxy URI 354 | -F, --epm-filter string String binding to filter endpoints returned by the RPC endpoint mapper (EPM) 355 | --endpoint string Explicit RPC endpoint definition 356 | --no-epm Do not use EPM to automatically detect RPC endpoints 357 | --no-sign Disable signing on DCERPC messages 358 | --no-seal Disable packet stub encryption on DCERPC messages 359 | ``` 360 | 361 | #### Create Scheduled Task (`tsch create`) 362 | 363 | 364 | The `create` method registers a scheduled task using [SchRpcRegisterTask](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167) with an automatic start time via [TimeTrigger](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/385126bf-ed3a-4131-8d51-d88b9c00cfe9), and optional automatic deletion with the [DeleteExpiredTaskAfter](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/6bfde6fe-440e-4ddd-b4d6-c8fc0bc06fae) setting. 365 | 366 | ```text 367 | Usage: 368 | goexec tsch create [target] [flags] 369 | 370 | Task Scheduler: 371 | -t, --task string Name or path of the new task 372 | --delay-stop duration Delay between task execution and termination. This won't stop the spawned process (default 5s) 373 | --start-delay duration Delay between task registration and execution (default 5s) 374 | --no-delete Don't delete task after execution 375 | --call-delete Directly call SchRpcDelete to delete task 376 | --sid SID User SID to impersonate (default "S-1-5-18") 377 | 378 | Execution: 379 | -e, --exec string Remote Windows executable to invoke 380 | -a, --args string Process command line arguments 381 | -c, --command string Windows process command line (executable & arguments) 382 | -o, --out string Fetch execution output to file or "-" for standard output 383 | -m, --out-method string Method to fetch execution output (default "smb") 384 | --no-delete-out Preserve output file on remote filesystem 385 | 386 | ... [inherited flags] ... 387 | ``` 388 | 389 | ##### Examples 390 | 391 | ```shell 392 | # Authenticate with NT hash via Kerberos, register task at \Microsoft\Windows\GoExec, execute `C:\Windows\Temp\Beacon.exe` 393 | ./goexec tsch create "$target" \ 394 | --user "${auth_user}@${domain}" \ 395 | --nt-hash "$auth_nt" \ 396 | --dc "$dc_ip" \ 397 | --kerberos \ 398 | --task '\Microsoft\Windows\GoExec' \ 399 | --exec 'C:\Windows\Temp\Beacon.exe' 400 | ``` 401 | 402 | #### Create Scheduled Task & Demand Start (`tsch demand`) 403 | 404 | Similar to the `create` method, the `demand` method will call `SchRpcRegisterTask`, but rather than setting a defined time when the task will start, it will additionally call `SchRpcRun` to forcefully start the task. This method can additionally hijack desktop sessions when provided the session ID with `--session`. 405 | 406 | ```text 407 | Usage: 408 | goexec tsch demand [target] [flags] 409 | 410 | Task Scheduler: 411 | -t, --task string Name or path of the new task 412 | --session uint32 Hijack existing session given the session ID 413 | --sid string User SID to impersonate (default "S-1-5-18") 414 | --no-delete Don't delete task after execution 415 | 416 | Execution: 417 | -e, --exec string Remote Windows executable to invoke 418 | -a, --args string Process command line arguments 419 | -c, --command string Windows process command line (executable & arguments) 420 | -o, --out string Fetch execution output to file or "-" for standard output 421 | -m, --out-method string Method to fetch execution output (default "smb") 422 | --no-delete-out Preserve output file on remote filesystem 423 | 424 | ... [inherited flags] ... 425 | ``` 426 | 427 | ##### Examples 428 | 429 | ```shell 430 | # Use random task name, execute `notepad.exe` on desktop session 1 431 | ./goexec tsch demand "$target" \ 432 | --user "$auth_user" \ 433 | --password "$auth_pass" \ 434 | --exec 'notepad.exe' \ 435 | --session 1 436 | 437 | # Authenticate with NT hash via Kerberos, 438 | # register task at \Microsoft\Windows\GoExec (will be deleted), 439 | # execute `C:\Windows\System32\cmd.exe /c set` with output 440 | ./goexec tsch demand "$target" \ 441 | --user "${auth_user}@${domain}" \ 442 | --nt-hash "$auth_nt" \ 443 | --dc "$dc_ip" \ 444 | --kerberos \ 445 | --task '\Microsoft\Windows\GoExec' \ 446 | --exec 'C:\Windows\System32\cmd.exe' \ 447 | --args '/c set' \ 448 | --out - 449 | ``` 450 | 451 | #### Modify Scheduled Task Definition (`tsch change`) 452 | 453 | The `change` method calls `SchRpcRetrieveTask` to fetch the definition of an existing 454 | task (`-t`/`--task`), then modifies the task definition to spawn a process before restoring the original. 455 | 456 | ```text 457 | Usage: 458 | goexec tsch change [target] [flags] 459 | 460 | Task Scheduler: 461 | -t, --task string Path to existing task 462 | --no-start Don't start the task 463 | --no-revert Don't restore the original task definition 464 | 465 | Execution: 466 | -e, --exec string Remote Windows executable to invoke 467 | -a, --args string Process command line arguments 468 | -c, --command string Windows process command line (executable & arguments) 469 | -o, --out string Fetch execution output to file or "-" for standard output 470 | -m, --out-method string Method to fetch execution output (default "smb") 471 | --no-delete-out Preserve output file on remote filesystem 472 | 473 | ... [inherited flags] ... 474 | ``` 475 | 476 | ##### Examples 477 | 478 | ```shell 479 | # Enable debug logging, Modify "\Microsoft\Windows\UPnP\UPnPHostConfig" to run `cmd.exe /c whoami /all` with output 480 | ./goexec tsch change $target --debug \ 481 | -u "${auth_user}" \ 482 | -p "${auth_pass}" \ 483 | -t '\Microsoft\Windows\UPnP\UPnPHostConfig' \ 484 | -e 'cmd.exe' \ 485 | -a '/C whoami /all' \ 486 | -o >(tr -d '\r') # Send output to another program (zsh/bash) 487 | ``` 488 | 489 | ### SCMR Module (`scmr`) 490 | 491 | The SCMR module works a lot like [`smbexec.py`](https://github.com/fortra/impacket/blob/master/examples/smbexec.py), but it provides additional RPC transports to evade network monitoring or firewall rules, and some minor OPSEC improvements overall. 492 | 493 | > [!WARNING] 494 | > The `scmr` module cannot fetch process output at the moment. This will be added in a future release. 495 | 496 | ```text 497 | Usage: 498 | goexec scmr [command] [flags] 499 | 500 | Available Commands: 501 | create Spawn a remote process by creating & running a Windows service 502 | change Change an existing Windows service to spawn an arbitrary process 503 | delete Delete an existing Windows service 504 | 505 | ... [inherited flags] ... 506 | 507 | Network: 508 | -x, --proxy URI Proxy URI 509 | -F, --epm-filter string String binding to filter endpoints returned by the RPC endpoint mapper (EPM) 510 | --endpoint string Explicit RPC endpoint definition 511 | --no-epm Do not use EPM to automatically detect RPC endpoints 512 | --no-sign Disable signing on DCERPC messages 513 | --no-seal Disable packet stub encryption on DCERPC messages 514 | ``` 515 | 516 | #### Create Service (`scmr create`) 517 | 518 | The `create` method is used to spawn a process by creating a Windows service. This method requires the full path to a remote executable (i.e. `C:\Windows\System32\calc.exe`) 519 | 520 | ```text 521 | Usage: 522 | goexec scmr create [target] [flags] 523 | 524 | Execution: 525 | -f, --executable-path string Full path to a remote Windows executable 526 | -a, --args string Arguments to pass to the executable 527 | 528 | Service: 529 | -n, --display-name string Display name of service to create 530 | -s, --service string Name of service to create 531 | --no-delete Don't delete service after execution 532 | --no-start Don't start service 533 | ``` 534 | 535 | ##### Examples 536 | 537 | ```shell 538 | # Use MSRPC instead of SMB, use custom service name, execute `cmd.exe` 539 | ./goexec scmr create "$target" \ 540 | -u "${auth_user}@${domain}" \ 541 | -p "$auth_pass" \ 542 | -f 'C:\Windows\System32\cmd.exe' \ 543 | -F 'ncacn_ip_tcp:' 544 | 545 | # Directly dial svcctl named pipe ("ncacn_np:[svcctl]"), 546 | # use random service name, 547 | # execute `C:\Windows\System32\calc.exe` 548 | ./goexec scmr create "$target" \ 549 | -u "${auth_user}@${domain}" \ 550 | -p "$auth_pass" \ 551 | -f 'C:\Windows\System32\calc.exe' \ 552 | --endpoint 'ncacn_np:[svcctl]' --no-epm 553 | ``` 554 | 555 | #### Modify Service (`scmr change`) 556 | 557 | The SCMR module's `change` method executes programs by modifying existing Windows services using the RChangeServiceConfigW method rather than calling RCreateServiceW like `scmr create`. The modified service is restored to its original state after execution 558 | 559 | > [!WARNING] 560 | > Using this module on important Windows services may brick the OS. Try using a less important service like `PlugPlay`. 561 | 562 | ```text 563 | Usage: 564 | goexec scmr change [target] [flags] 565 | 566 | Service Control: 567 | -s, --service-name string Name of service to modify 568 | --no-start Don't start service 569 | 570 | Execution: 571 | -f, --executable-path string Full path to remote Windows executable 572 | -a, --args string Arguments to pass to executable 573 | ``` 574 | 575 | ##### Examples 576 | 577 | ```shell 578 | # Used named pipe transport, Modify the PlugPlay service to execute `C:\Windows\System32\cmd.exe /c C:\Windows\Temp\stage.bat` 579 | ./goexec scmr change $target \ 580 | -u "$auth_user" \ 581 | -p "$auth_pass" \ 582 | -F "ncacn_np:" \ 583 | -s PlugPlay \ 584 | -f 'C:\Windows\System32\cmd.exe' \ 585 | -a '/c C:\Windows\Temp\stage.bat' 586 | ``` 587 | 588 | #### (Auxiliary) Delete Service 589 | 590 | The SCMR module's auxiliary `delete` method will simply delete the provided service. 591 | 592 | ```text 593 | Usage: 594 | goexec scmr delete [target] [flags] 595 | 596 | Service Control: 597 | -s, --service-name string Name of service to delete 598 | ``` 599 | 600 | ## Acknowledgements 601 | 602 | - [@oiweiwei](https://github.com/oiweiwei) for the wonderful [go-msrpc](https://github.com/oiweiwei/go-msrpc) module 603 | - [@RedTeamPentesting](https://github.com/RedTeamPentesting) and [Erik Geiser](https://github.com/rtpt-erikgeiser) for the [adauth](https://github.com/RedTeamPentesting/adauth) module 604 | - The developers and contributors of [Impacket](https://github.com/fortra/impacket) for the inspiration and technical reference 605 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | We wanted to make development of this project as transparent as possible, so we've included our development TODOs in this file. 4 | 5 | ## TSCH 6 | 7 | - [X] Clean up TSCH module 8 | - [X] Session hijacking 9 | - [X] Generate random name/path 10 | - [X] Output 11 | - [X] Add `tsch change` 12 | - [ ] Serialize XML with default indent level? 13 | 14 | ### `tsch change` 15 | 16 | - [ ] Add `--session` (like `tsch demand`) 17 | - [ ] Add the option to avoid direct task start using TimeTrigger (similar to `tsch create`) 18 | 19 | ## SCMR 20 | 21 | - [X] Clean up SCMR module 22 | - [X] add dynamic string binding support 23 | - [X] general cleanup. Use TSCH & WMI as reference 24 | - [ ] Output 25 | 26 | ## DCOM 27 | 28 | - [X] Add DCOM module 29 | - [X] MMC20.Application method 30 | - [X] Output 31 | 32 | ## WMI 33 | 34 | - [X] Add WMI module 35 | - [X] Clean up WMI module 36 | - [X] Output 37 | - [ ] WMI `reg` subcommand - read & edit the registry 38 | - [ ] File transfer functionality 39 | 40 | ## Other 41 | 42 | - [X] Add proxy support - see https://github.com/oiweiwei/go-msrpc/issues/21 43 | - [X] README 44 | - [X] pprof integration (hidden flag(s)) 45 | - [X] Descriptions for all modules and methods 46 | - [ ] Add SMB file transfer interface 47 | 48 | ## Bugs 49 | 50 | - [X] (Fixed) SMB transport for SCMR module - `rpc_s_cannot_support: The requested operation is not supported.` 51 | - [X] (Fixed) Proxy - EPM doesn't use the proxy dialer 52 | - [X] (Fixed) Kerberos requests don't dial through proxy 53 | - [X] (Fixed) Panic when closing nil log file 54 | - [X] `scmr change` doesn't revert service cmdline 55 | - [X] Fix SCMR `change` method so that dependencies field isn't permanently overwritten 56 | 57 | ## Lower Priority 58 | 59 | - [ ] `--shell` option 60 | - [ ] Add Go tests 61 | - [ ] ability to specify multiple targets 62 | 63 | ### TSCH 64 | 65 | - [ ] Add more trigger types 66 | 67 | ### SCMR 68 | 69 | - [ ] `psexec` with PsExeSVC.exe AND NOT Impacket's RemCom build - https://sensepost.com/blog/2025/psexecing-the-right-way-and-why-zero-trust-is-mandatory/ 70 | 71 | ### DCOM 72 | 73 | - [X] ShellWindows 74 | - [ ] ShellBrowserWindow 75 | 76 | ### WinRM 77 | 78 | - [ ] Add basic WinRM module - https://github.com/bryanmcnulty/winrm 79 | - [ ] File transfer functionality 80 | - [ ] Shell functionality -------------------------------------------------------------------------------- /cmd/args.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/pflag" 10 | "os" 11 | ) 12 | 13 | func registerLoggingFlags(fs *pflag.FlagSet) { 14 | fs.SortFlags = false 15 | fs.BoolVarP(&logDebug, "debug", "D", false, "Enable debug logging") 16 | fs.StringVarP(&logOutput, "log-file", "O", "", "Write JSON logging output to `file`") 17 | fs.BoolVarP(&logJson, "json", "j", false, "Write logging output in JSON lines") 18 | fs.BoolVarP(&logQuiet, "quiet", "q", false, "Disable info logging") 19 | } 20 | 21 | func registerNetworkFlags(fs *pflag.FlagSet) { 22 | fs.StringVarP(&proxy, "proxy", "x", "", "Proxy `URI`") 23 | fs.StringVarP(&rpcClient.Filter, "epm-filter", "F", "", "String binding to filter endpoints returned by the RPC endpoint mapper (EPM)") 24 | fs.StringVar(&rpcClient.Endpoint, "endpoint", "", "Explicit RPC endpoint definition") 25 | fs.BoolVar(&rpcClient.NoEpm, "no-epm", false, "Do not use EPM to automatically detect RPC endpoints") 26 | fs.BoolVar(&rpcClient.NoSign, "no-sign", false, "Disable signing on DCERPC messages") 27 | fs.BoolVar(&rpcClient.NoSeal, "no-seal", false, "Disable packet stub encryption on DCERPC messages") 28 | 29 | //cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter") 30 | //cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter") 31 | } 32 | 33 | // FUTURE: automatically stage & execute file 34 | /* 35 | func registerStageFlags(fs *pflag.FlagSet) { 36 | fs.StringVarP(&stageFilePath, "stage", "E", "", "File to stage and execute") 37 | //fs.StringVarP(&stageArgs ...) 38 | } 39 | */ 40 | 41 | func registerExecutionFlags(fs *pflag.FlagSet) { 42 | fs.StringVarP(&exec.Input.Executable, "exec", "e", "", "Remote Windows executable to invoke") 43 | fs.StringVarP(&exec.Input.Arguments, "args", "a", "", "Process command line arguments") 44 | fs.StringVarP(&exec.Input.Command, "command", "c", "", "Windows process command line (executable & arguments)") 45 | 46 | //cmd.MarkFlagsOneRequired("executable", "command") 47 | //cmd.MarkFlagsMutuallyExclusive("executable", "command") 48 | } 49 | 50 | func registerExecutionOutputFlags(fs *pflag.FlagSet) { 51 | fs.StringVarP(&outputPath, "out", "o", "", `Fetch execution output to file or "-" for standard output`) 52 | fs.StringVarP(&outputMethod, "out-method", "m", "smb", "Method to fetch execution output") 53 | //fs.StringVar(&exec.Output.RemotePath, "out-remote", "", "Location to temporarily store output on remote filesystem") 54 | fs.BoolVar(&exec.Output.NoDelete, "no-delete-out", false, "Preserve output file on remote filesystem") 55 | } 56 | 57 | func args(reqs ...func(*cobra.Command, []string) error) (fn func(*cobra.Command, []string) error) { 58 | return func(cmd *cobra.Command, args []string) (err error) { 59 | 60 | for _, req := range reqs { 61 | if err = req(cmd, args); err != nil { 62 | return 63 | } 64 | } 65 | return 66 | } 67 | } 68 | 69 | func argsAcceptValues(name string, in *string, valid ...string) func(*cobra.Command, []string) error { 70 | return func(*cobra.Command, []string) error { 71 | for _, v := range valid { 72 | if *in == v { 73 | return nil 74 | } 75 | } 76 | if j, err := json.Marshal(valid); err == nil { 77 | return fmt.Errorf("parse %s: %q doesn't match any accepted values: %s", name, *in, string(j)) 78 | } else { 79 | return err 80 | } 81 | } 82 | } 83 | 84 | func argsTarget(proto string) func(cmd *cobra.Command, args []string) error { 85 | 86 | return func(cmd *cobra.Command, args []string) (err error) { 87 | 88 | if len(args) != 1 { 89 | return errors.New("command require exactly one positional argument: [target]") 90 | } 91 | 92 | if credential, target, err = adAuthOpts.WithTarget(context.TODO(), proto, args[0]); err != nil { 93 | return fmt.Errorf("failed to parse target: %w", err) 94 | } 95 | 96 | if credential == nil { 97 | return errors.New("no credentials supplied") 98 | } 99 | if target == nil { 100 | return errors.New("no target supplied") 101 | } 102 | return 103 | } 104 | } 105 | 106 | func argsSmbClient() func(cmd *cobra.Command, args []string) error { 107 | return args( 108 | argsTarget("cifs"), 109 | 110 | func(_ *cobra.Command, _ []string) error { 111 | 112 | smbClient.Credential = credential 113 | smbClient.Target = target 114 | smbClient.Proxy = proxy 115 | 116 | return smbClient.Parse(context.TODO()) 117 | }, 118 | ) 119 | } 120 | 121 | func argsRpcClient(proto string) func(cmd *cobra.Command, args []string) error { 122 | return args( 123 | argsTarget(proto), 124 | 125 | func(cmd *cobra.Command, args []string) (err error) { 126 | 127 | rpcClient.Target = target 128 | rpcClient.Credential = credential 129 | rpcClient.Proxy = proxy 130 | 131 | return rpcClient.Parse(context.TODO()) 132 | }, 133 | ) 134 | } 135 | 136 | func argsOutput(methods ...string) func(cmd *cobra.Command, args []string) error { 137 | 138 | var as []func(*cobra.Command, []string) error 139 | 140 | for _, method := range methods { 141 | if method == "smb" { 142 | as = append(as, argsSmbClient()) 143 | } 144 | } 145 | 146 | return args(append(as, func(*cobra.Command, []string) (err error) { 147 | 148 | if outputPath != "" { 149 | if outputPath == "-" { 150 | exec.Output.Writer = os.Stdout 151 | 152 | } else if exec.Output.Writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { 153 | log.Fatal().Err(err).Msg("Failed to open output file") 154 | } 155 | } 156 | return 157 | })...) 158 | } 159 | -------------------------------------------------------------------------------- /cmd/dcom.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 6 | dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom" 7 | "github.com/oiweiwei/go-msrpc/ssp/gssapi" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func dcomCmdInit() { 12 | cmdFlags[dcomCmd] = []*flagSet{ 13 | defaultAuthFlags, 14 | defaultLogFlags, 15 | defaultNetRpcFlags, 16 | } 17 | dcomMmcCmdInit() 18 | dcomShellWindowsCmdInit() 19 | dcomShellBrowserWindowCmdInit() 20 | 21 | dcomCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) 22 | dcomCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) 23 | dcomCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) 24 | dcomCmd.AddCommand(dcomMmcCmd, dcomShellWindowsCmd, dcomShellBrowserWindowCmd) 25 | } 26 | 27 | func dcomMmcCmdInit() { 28 | dcomMmcExecFlags := newFlagSet("Execution") 29 | 30 | registerExecutionFlags(dcomMmcExecFlags.Flags) 31 | registerExecutionOutputFlags(dcomMmcExecFlags.Flags) 32 | 33 | dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WorkingDirectory, "directory", `C:\`, "Working `directory`") 34 | dcomMmcExecFlags.Flags.StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state") 35 | 36 | cmdFlags[dcomMmcCmd] = []*flagSet{ 37 | dcomMmcExecFlags, 38 | defaultAuthFlags, 39 | defaultLogFlags, 40 | defaultNetRpcFlags, 41 | } 42 | dcomMmcCmd.Flags().AddFlagSet(dcomMmcExecFlags.Flags) 43 | 44 | // Constraints 45 | dcomMmcCmd.MarkFlagsOneRequired("command", "exec") 46 | } 47 | 48 | func dcomShellWindowsCmdInit() { 49 | dcomShellWindowsExecFlags := newFlagSet("Execution") 50 | 51 | registerExecutionFlags(dcomShellWindowsExecFlags.Flags) 52 | registerExecutionOutputFlags(dcomShellWindowsExecFlags.Flags) 53 | 54 | dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WorkingDirectory, "directory", `C:\`, "Working `directory`") 55 | dcomShellWindowsExecFlags.Flags.StringVar(&dcomShellWindows.WindowState, "app-window", "0", "Application window state `ID`") 56 | 57 | cmdFlags[dcomShellWindowsCmd] = []*flagSet{ 58 | dcomShellWindowsExecFlags, 59 | defaultAuthFlags, 60 | defaultLogFlags, 61 | defaultNetRpcFlags, 62 | } 63 | dcomShellWindowsCmd.Flags().AddFlagSet(dcomShellWindowsExecFlags.Flags) 64 | 65 | // Constraints 66 | dcomShellWindowsCmd.MarkFlagsOneRequired("command", "exec") 67 | } 68 | 69 | func dcomShellBrowserWindowCmdInit() { 70 | dcomShellBrowserWindowExecFlags := newFlagSet("Execution") 71 | 72 | registerExecutionFlags(dcomShellBrowserWindowExecFlags.Flags) 73 | registerExecutionOutputFlags(dcomShellBrowserWindowExecFlags.Flags) 74 | 75 | dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WorkingDirectory, "directory", `C:\`, "Working `directory`") 76 | dcomShellBrowserWindowExecFlags.Flags.StringVar(&dcomShellBrowserWindow.WindowState, "app-window", "0", "Application window state `ID`") 77 | 78 | cmdFlags[dcomShellBrowserWindowCmd] = []*flagSet{ 79 | dcomShellBrowserWindowExecFlags, 80 | defaultAuthFlags, 81 | defaultLogFlags, 82 | defaultNetRpcFlags, 83 | } 84 | dcomShellBrowserWindowCmd.Flags().AddFlagSet(dcomShellBrowserWindowExecFlags.Flags) 85 | 86 | // Constraints 87 | dcomShellBrowserWindowCmd.MarkFlagsOneRequired("command", "exec") 88 | } 89 | 90 | var ( 91 | dcomMmc dcomexec.DcomMmc 92 | dcomShellWindows dcomexec.DcomShellWindows 93 | dcomShellBrowserWindow dcomexec.DcomShellBrowserWindow 94 | 95 | dcomCmd = &cobra.Command{ 96 | Use: "dcom", 97 | Short: "Execute with Distributed Component Object Model (MS-DCOM)", 98 | Long: `Description: 99 | The dcom module uses exposed Distributed Component Object Model (DCOM) objects to spawn processes.`, 100 | GroupID: "module", 101 | Args: cobra.NoArgs, 102 | } 103 | 104 | dcomMmcCmd = &cobra.Command{ 105 | Use: "mmc [target]", 106 | Short: "Execute with the MMC20.Application DCOM object", 107 | Long: `Description: 108 | The mmc method uses the exposed MMC20.Application object to call Document.ActiveView.ShellExec, 109 | and ultimately spawn a process on the remote host.`, 110 | Args: args( 111 | argsRpcClient("host"), 112 | argsOutput("smb"), 113 | argsAcceptValues("window", &dcomMmc.WindowState, "Minimized", "Maximized", "Restored"), 114 | ), 115 | Run: func(cmd *cobra.Command, args []string) { 116 | dcomMmc.Client = &rpcClient 117 | dcomMmc.IO = exec 118 | dcomMmc.ClassID = dcomexec.Mmc20Uuid 119 | 120 | ctx := log.With(). 121 | Str("module", dcomexec.ModuleName). 122 | Str("method", dcomexec.MethodMmc). 123 | Logger().WithContext(gssapi.NewSecurityContext(context.Background())) 124 | 125 | if err := goexec.ExecuteCleanMethod(ctx, &dcomMmc, &exec); err != nil { 126 | log.Fatal().Err(err).Msg("Operation failed") 127 | } 128 | }, 129 | } 130 | 131 | dcomShellWindowsCmd = &cobra.Command{ 132 | Use: "shellwindows [target]", 133 | Short: "Execute with the ShellWindows DCOM object", 134 | Long: `Description: 135 | The shellwindows method uses the exposed ShellWindows DCOM object on older Windows installations 136 | to call Item().Document.Application.ShellExecute, and spawn the provided process.`, 137 | Args: args( 138 | argsRpcClient("host"), 139 | argsOutput("smb"), 140 | argsAcceptValues("app-window", &dcomShellWindows.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), 141 | ), 142 | Run: func(cmd *cobra.Command, args []string) { 143 | dcomShellWindows.Client = &rpcClient 144 | dcomShellWindows.IO = exec 145 | dcomShellWindows.ClassID = dcomexec.ShellWindowsUuid 146 | 147 | ctx := log.With(). 148 | Str("module", dcomexec.ModuleName). 149 | Str("method", dcomexec.MethodShellWindows). 150 | Logger().WithContext(gssapi.NewSecurityContext(context.Background())) 151 | 152 | if err := goexec.ExecuteCleanMethod(ctx, &dcomShellWindows, &exec); err != nil { 153 | log.Fatal().Err(err).Msg("Operation failed") 154 | } 155 | }, 156 | } 157 | 158 | dcomShellBrowserWindowCmd = &cobra.Command{ 159 | Use: "shellbrowserwindow [target]", 160 | Short: "Execute with the ShellBrowserWindow DCOM object", 161 | Long: `Description: 162 | The shellbrowserwindow method uses the exposed ShellBrowserWindow DCOM object on older Windows installations 163 | to call Document.Application.ShellExecute, and spawn the provided process.`, 164 | Args: args( 165 | argsRpcClient("host"), 166 | argsOutput("smb"), 167 | argsAcceptValues("app-window", &dcomShellBrowserWindow.WindowState, "0", "1", "2", "3", "4", "5", "7", "10"), 168 | ), 169 | Run: func(cmd *cobra.Command, args []string) { 170 | dcomShellBrowserWindow.Client = &rpcClient 171 | dcomShellBrowserWindow.IO = exec 172 | dcomShellBrowserWindow.ClassID = dcomexec.ShellBrowserWindowUuid 173 | 174 | ctx := log.With(). 175 | Str("module", dcomexec.ModuleName). 176 | Str("method", dcomexec.MethodShellBrowserWindow). 177 | Logger().WithContext(gssapi.NewSecurityContext(context.Background())) 178 | 179 | if err := goexec.ExecuteCleanMethod(ctx, &dcomShellBrowserWindow, &exec); err != nil { 180 | log.Fatal().Err(err).Msg("Operation failed") 181 | } 182 | }, 183 | } 184 | ) 185 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" 7 | "github.com/FalconOpsLLC/goexec/pkg/goexec/smb" 8 | "github.com/RedTeamPentesting/adauth" 9 | "github.com/google/uuid" 10 | "github.com/oiweiwei/go-msrpc/ssp" 11 | "github.com/oiweiwei/go-msrpc/ssp/gssapi" 12 | "github.com/rs/zerolog" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/pflag" 15 | "golang.org/x/term" 16 | "io" 17 | "os" 18 | "runtime/pprof" 19 | ) 20 | 21 | type flagSet struct { 22 | Label string 23 | Flags *pflag.FlagSet 24 | } 25 | 26 | const helpTemplate = `Usage:{{if .Runnable}} 27 | {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} 28 | {{.CommandPath}} [command] [flags]{{end}}{{if gt (len .Aliases) 0}} 29 | 30 | Aliases: 31 | {{.NameAndAliases}}{{end}}{{if .HasExample}} 32 | 33 | Examples: 34 | {{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} 35 | 36 | Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} 37 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} 38 | 39 | {{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} 40 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} 41 | 42 | Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} 43 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if (ne .Name "completion")}}{{range $_, $v := cmdFlags .}} 44 | 45 | {{$v.Label|trimTrailingWhitespaces}}: 46 | {{flags $v.Flags|trimTrailingWhitespaces}}{{end}}{{end}}{{if .HasHelpSubCommands}} 47 | 48 | Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} 49 | {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} 50 | 51 | Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} 52 | ` 53 | 54 | var ( 55 | cmdFlags = make(map[*cobra.Command][]*flagSet) 56 | 57 | defaultAuthFlags, defaultLogFlags, defaultNetRpcFlags *flagSet 58 | 59 | returnCode int 60 | toClose []io.Closer 61 | 62 | // === IO === 63 | //stageFilePath string // FUTURE 64 | outputMethod string 65 | outputPath string 66 | // ========== 67 | 68 | // === Logging === 69 | logJson bool // Log output in JSON lines 70 | logDebug bool // Output debug log messages 71 | logQuiet bool // Suppress logging output 72 | logOutput string // Log output file 73 | logLevel = zerolog.InfoLevel 74 | logFile io.WriteCloser = os.Stderr 75 | log zerolog.Logger 76 | // =============== 77 | 78 | // === Network === 79 | proxy string 80 | rpcClient dce.Client 81 | smbClient smb.Client 82 | // =============== 83 | 84 | // === Resource profiling === 85 | cpuProfile string 86 | memProfile string 87 | cpuProfileFile io.WriteCloser 88 | memProfileFile io.WriteCloser 89 | // ========================== 90 | 91 | exec = goexec.ExecutionIO{ 92 | Input: new(goexec.ExecutionInput), 93 | Output: new(goexec.ExecutionOutput), 94 | } 95 | 96 | adAuthOpts *adauth.Options 97 | credential *adauth.Credential 98 | target *adauth.Target 99 | 100 | rootCmd = &cobra.Command{ 101 | Use: "goexec", 102 | Short: `goexec - Windows remote execution multitool`, 103 | Long: ` 104 | ___ ___ ___ _ _ ___ ___ 105 | | . | . | -_|_'_| -_| _| 106 | |_ |___|___|_,_|___|___| 107 | |___| 108 | 109 | Authors: FalconOps LLC (@FalconOpsLLC), 110 | Bryan McNulty (@bryanmcnulty) 111 | 112 | > Goexec is designed to achieve remote execution on Windows systems, 113 | while providing an extremely flexible CLI and a strong focus on OPSEC. 114 | `, 115 | 116 | PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { 117 | 118 | // Parse logging options 119 | { 120 | if logOutput != "" { 121 | logFile, err = os.OpenFile(logOutput, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 122 | if err != nil { 123 | return 124 | } 125 | toClose = append(toClose, logFile) 126 | logJson = true 127 | } 128 | if logQuiet { 129 | logLevel = zerolog.ErrorLevel 130 | } else if logDebug { 131 | logLevel = zerolog.DebugLevel 132 | } 133 | if logJson { 134 | log = zerolog.New(logFile).With().Timestamp().Logger() 135 | } else { 136 | log = zerolog.New(zerolog.ConsoleWriter{Out: logFile}).With().Timestamp().Logger() 137 | } 138 | log = log.Level(logLevel) 139 | } 140 | 141 | // CPU / memory profiling 142 | { 143 | if cpuProfile != "" { 144 | if cpuProfileFile, err = os.OpenFile(cpuProfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { 145 | log.Error().Err(err).Msg("Failed to open CPU profile for writing") 146 | return 147 | } 148 | toClose = append(toClose, cpuProfileFile) 149 | 150 | if err = pprof.StartCPUProfile(cpuProfileFile); err != nil { 151 | log.Error().Err(err).Msg("Failed to start CPU profile") 152 | return 153 | } 154 | } 155 | if memProfile != "" { 156 | if memProfileFile, err = os.OpenFile(memProfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { 157 | log.Error().Err(err).Msg("Failed to open memory profile for writing") 158 | return 159 | } 160 | toClose = append(toClose, memProfileFile) 161 | } 162 | } 163 | 164 | if proxy != "" { 165 | rpcClient.Proxy = proxy 166 | smbClient.Proxy = proxy 167 | } 168 | 169 | if outputPath != "" { 170 | if outputMethod == "smb" { 171 | if exec.Output.RemotePath == "" { 172 | exec.Output.RemotePath = `C:\Windows\Temp\` + uuid.NewString() 173 | } 174 | exec.Output.Provider = &smb.OutputFileFetcher{ 175 | Client: &smbClient, 176 | Share: `ADMIN$`, // TODO: dynamic 177 | SharePath: `C:\Windows`, 178 | File: exec.Output.RemotePath, 179 | DeleteOutputFile: !exec.Output.NoDelete, 180 | } 181 | } 182 | } 183 | return 184 | }, 185 | 186 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 187 | 188 | if memProfileFile != nil { 189 | if err := pprof.WriteHeapProfile(memProfileFile); err != nil { 190 | log.Error().Err(err).Msg("Failed to write memory profile") 191 | return 192 | } 193 | } 194 | 195 | if cpuProfileFile != nil { 196 | pprof.StopCPUProfile() 197 | } 198 | 199 | if exec.Input != nil && exec.Input.StageFile != nil { 200 | if err := exec.Input.StageFile.Close(); err != nil { 201 | log.Warn().Err(err).Msg("Failed to close stage file") 202 | } 203 | } 204 | 205 | for _, c := range toClose { 206 | if c != nil { 207 | if err := c.Close(); err != nil { 208 | log.Warn().Err(err).Msg("Failed to close stream") 209 | } 210 | } 211 | } 212 | }, 213 | } 214 | ) 215 | 216 | func newFlagSet(name string) *flagSet { 217 | flags := pflag.NewFlagSet(name, pflag.ExitOnError) 218 | flags.SortFlags = false 219 | return &flagSet{ 220 | Label: name, 221 | Flags: flags, 222 | } 223 | } 224 | 225 | func init() { 226 | // Auth init 227 | { 228 | gssapi.AddMechanism(ssp.SPNEGO) 229 | gssapi.AddMechanism(ssp.NTLM) 230 | gssapi.AddMechanism(ssp.KRB5) 231 | } 232 | 233 | // CPU / Memory profiling 234 | { 235 | rootCmd.PersistentFlags().StringVar(&cpuProfile, "cpu-profile", "", "Write CPU profile to `file`") 236 | rootCmd.PersistentFlags().StringVar(&memProfile, "mem-profile", "", "Write memory profile to `file`") 237 | 238 | if err := rootCmd.PersistentFlags().MarkHidden("cpu-profile"); err != nil { 239 | panic(err) 240 | } 241 | if err := rootCmd.PersistentFlags().MarkHidden("mem-profile"); err != nil { 242 | panic(err) 243 | } 244 | } 245 | 246 | // Cobra init 247 | { 248 | cobra.EnableCommandSorting = false 249 | { 250 | defaultNetRpcFlags = newFlagSet("Network") 251 | registerNetworkFlags(defaultNetRpcFlags.Flags) 252 | } 253 | { 254 | defaultLogFlags = newFlagSet("Logging") 255 | registerLoggingFlags(defaultLogFlags.Flags) 256 | } 257 | { 258 | defaultAuthFlags = newFlagSet("Authentication") 259 | adAuthOpts = &adauth.Options{ 260 | Debug: log.Debug().Msgf, 261 | } 262 | adAuthOpts.RegisterFlags(defaultAuthFlags.Flags) 263 | } 264 | 265 | modules := &cobra.Group{ 266 | ID: "module", 267 | Title: "Execution Commands:", 268 | } 269 | rootCmd.AddGroup(modules) 270 | 271 | cmdFlags[rootCmd] = []*flagSet{ 272 | defaultLogFlags, 273 | defaultAuthFlags, 274 | } 275 | 276 | cobra.AddTemplateFunc("flags", func(fs *pflag.FlagSet) string { 277 | if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil { 278 | return fs.FlagUsagesWrapped(width - 1) 279 | } 280 | return fs.FlagUsagesWrapped(80 - 1) 281 | }) 282 | 283 | cobra.AddTemplateFunc("cmdFlags", func(cmd *cobra.Command) []*flagSet { 284 | return cmdFlags[cmd] 285 | }) 286 | 287 | rootCmd.InitDefaultVersionFlag() 288 | rootCmd.InitDefaultHelpCmd() 289 | rootCmd.SetHelpTemplate("{{if (ne .Long \"\")}}{{.Long}}\n\n{{end}}" + helpTemplate) 290 | rootCmd.SetUsageTemplate(helpTemplate) 291 | 292 | // Modules init 293 | { 294 | dcomCmdInit() 295 | rootCmd.AddCommand(dcomCmd) 296 | wmiCmdInit() 297 | rootCmd.AddCommand(wmiCmd) 298 | scmrCmdInit() 299 | rootCmd.AddCommand(scmrCmd) 300 | tschCmdInit() 301 | rootCmd.AddCommand(tschCmd) 302 | } 303 | } 304 | } 305 | 306 | func Execute() { 307 | if err := rootCmd.Execute(); err != nil { 308 | fmt.Println(err) 309 | os.Exit(1) 310 | } 311 | os.Exit(returnCode) 312 | } 313 | -------------------------------------------------------------------------------- /cmd/scmr.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "github.com/FalconOpsLLC/goexec/internal/util" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "github.com/oiweiwei/go-msrpc/ssp/gssapi" 8 | "github.com/spf13/cobra" 9 | 10 | scmrexec "github.com/FalconOpsLLC/goexec/pkg/goexec/scmr" 11 | ) 12 | 13 | func scmrCmdInit() { 14 | cmdFlags[scmrCmd] = []*flagSet{ 15 | defaultAuthFlags, 16 | defaultLogFlags, 17 | defaultNetRpcFlags, 18 | } 19 | scmrCreateCmdInit() 20 | scmrChangeCmdInit() 21 | scmrDeleteCmdInit() 22 | 23 | scmrCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) 24 | scmrCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) 25 | scmrCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) 26 | scmrCmd.AddCommand(scmrCreateCmd, scmrChangeCmd, scmrDeleteCmd) 27 | } 28 | 29 | func scmrCreateCmdInit() { 30 | scmrCreateFlags := newFlagSet("Service") 31 | 32 | scmrCreateFlags.Flags.StringVarP(&scmrCreate.DisplayName, "display-name", "n", "", "Display name of service to create") 33 | scmrCreateFlags.Flags.StringVarP(&scmrCreate.ServiceName, "service", "s", "", "Name of service to create") 34 | scmrCreateFlags.Flags.BoolVar(&scmrCreate.NoDelete, "no-delete", false, "Don't delete service after execution") 35 | scmrCreateFlags.Flags.BoolVar(&scmrCreate.NoStart, "no-start", false, "Don't start service") 36 | 37 | scmrCreateExecFlags := newFlagSet("Execution") 38 | 39 | // TODO: SCMR output 40 | //registerExecutionOutputFlags(scmrCreateExecFlags.Flags) 41 | 42 | scmrCreateExecFlags.Flags.StringVarP(&exec.Input.ExecutablePath, "executable-path", "f", "", "Full path to a remote Windows executable") 43 | scmrCreateExecFlags.Flags.StringVarP(&exec.Input.Arguments, "args", "a", "", "Arguments to pass to the executable") 44 | 45 | scmrCreateCmd.Flags().AddFlagSet(scmrCreateFlags.Flags) 46 | scmrCreateCmd.Flags().AddFlagSet(scmrCreateExecFlags.Flags) 47 | 48 | cmdFlags[scmrCreateCmd] = []*flagSet{ 49 | scmrCreateExecFlags, 50 | scmrCreateFlags, 51 | defaultAuthFlags, 52 | defaultLogFlags, 53 | defaultNetRpcFlags, 54 | } 55 | 56 | // Constraints 57 | { 58 | //scmrCreateCmd.MarkFlagsMutuallyExclusive("no-delete", "no-start") 59 | if err := scmrCreateCmd.MarkFlagRequired("executable-path"); err != nil { 60 | panic(err) 61 | } 62 | } 63 | } 64 | 65 | func scmrChangeCmdInit() { 66 | scmrChangeFlags := newFlagSet("Service Control") 67 | 68 | scmrChangeFlags.Flags.StringVarP(&scmrChange.ServiceName, "service-name", "s", "", "Name of service to modify") 69 | scmrChangeFlags.Flags.BoolVar(&scmrChange.NoStart, "no-start", false, "Don't start service") 70 | 71 | scmrChangeExecFlags := newFlagSet("Execution") 72 | 73 | scmrChangeExecFlags.Flags.StringVarP(&exec.Input.ExecutablePath, "executable-path", "f", "", "Full path to remote Windows executable") 74 | scmrChangeExecFlags.Flags.StringVarP(&exec.Input.Arguments, "args", "a", "", "Arguments to pass to executable") 75 | 76 | // TODO: SCMR output 77 | //registerExecutionOutputFlags(scmrChangeExecFlags.Flags) 78 | //registerStageFlags(scmrChangeExecFlags.Flags) 79 | 80 | cmdFlags[scmrChangeCmd] = []*flagSet{ 81 | scmrChangeFlags, 82 | scmrChangeExecFlags, 83 | defaultAuthFlags, 84 | defaultLogFlags, 85 | defaultNetRpcFlags, 86 | } 87 | 88 | scmrChangeCmd.Flags().AddFlagSet(scmrChangeFlags.Flags) 89 | scmrChangeCmd.Flags().AddFlagSet(scmrChangeExecFlags.Flags) 90 | 91 | // Constraints 92 | { 93 | if err := scmrChangeCmd.MarkFlagRequired("service-name"); err != nil { 94 | panic(err) 95 | } 96 | if err := scmrCreateCmd.MarkFlagRequired("executable-path"); err != nil { 97 | panic(err) 98 | } 99 | } 100 | } 101 | 102 | func scmrDeleteCmdInit() { 103 | scmrDeleteFlags := newFlagSet("Service Control") 104 | scmrDeleteFlags.Flags.StringVarP(&scmrDelete.ServiceName, "service-name", "s", scmrDelete.ServiceName, "Name of service to delete") 105 | 106 | cmdFlags[scmrDeleteCmd] = []*flagSet{ 107 | scmrDeleteFlags, 108 | defaultAuthFlags, 109 | defaultLogFlags, 110 | defaultNetRpcFlags, 111 | } 112 | 113 | scmrDeleteCmd.Flags().AddFlagSet(scmrDeleteFlags.Flags) 114 | 115 | if err := scmrDeleteCmd.MarkFlagRequired("service-name"); err != nil { 116 | panic(err) 117 | } 118 | } 119 | 120 | var ( 121 | scmrCreate = scmrexec.ScmrCreate{} 122 | scmrChange = scmrexec.ScmrChange{} 123 | scmrDelete = scmrexec.ScmrDelete{} 124 | 125 | scmrCmd = &cobra.Command{ 126 | Use: "scmr", 127 | Short: "Execute with Service Control Manager Remote (MS-SCMR)", 128 | Long: `Description: 129 | The SCMR module works a lot like Impacket's smbexec.py, but it provides additional RPC transports 130 | to evade network monitoring or firewall rules, and some minor OPSEC improvements overall.`, 131 | GroupID: "module", 132 | Args: cobra.NoArgs, 133 | } 134 | 135 | scmrCreateCmd = &cobra.Command{ 136 | Use: "create [target]", 137 | Short: "Spawn a remote process by creating & running a Windows service", 138 | Long: `Description: 139 | The create method calls RCreateServiceW to create a new Windows service on the 140 | remote target with the provided executable & arguments as the lpBinaryPathName`, 141 | Args: args( 142 | argsRpcClient("cifs"), 143 | argsSmbClient(), 144 | ), 145 | 146 | Run: func(cmd *cobra.Command, args []string) { 147 | scmrCreate.Client = &rpcClient 148 | scmrCreate.IO = exec 149 | 150 | log = log.With(). 151 | Str("module", "scmr"). 152 | Str("method", "create"). 153 | Logger() 154 | 155 | // Warnings 156 | { 157 | if scmrCreate.ServiceName == "" { 158 | log.Warn().Msg("No service name was provided. Using a random string") 159 | scmrCreate.ServiceName = util.RandomString() 160 | } 161 | if scmrCreate.NoDelete { 162 | log.Warn().Msg("Service will not be deleted after execution") 163 | } 164 | if scmrCreate.DisplayName == "" { 165 | log.Debug().Msg("No display name specified, using service name as display name") 166 | scmrCreate.DisplayName = scmrCreate.ServiceName 167 | } 168 | } 169 | 170 | ctx := log.WithContext(gssapi.NewSecurityContext(context.Background())) 171 | 172 | if err := goexec.ExecuteCleanMethod(ctx, &scmrCreate, &exec); err != nil { 173 | log.Fatal().Err(err).Msg("Operation failed") 174 | } 175 | }, 176 | } 177 | 178 | scmrChangeCmd = &cobra.Command{ 179 | Use: "change [target]", 180 | Short: "Change an existing Windows service to spawn an arbitrary process", 181 | Long: `Description: 182 | The change method executes programs by modifying existing Windows services 183 | using the RChangeServiceConfigW method rather than calling RCreateServiceW 184 | like scmr create. The modified service is restored to its original state 185 | after execution`, 186 | Args: argsRpcClient("cifs"), 187 | 188 | Run: func(cmd *cobra.Command, args []string) { 189 | scmrChange.Client = &rpcClient 190 | scmrChange.IO = exec 191 | 192 | ctx := log.With(). 193 | Str("module", "scmr"). 194 | Str("method", "change"). 195 | Logger().WithContext(gssapi.NewSecurityContext(context.Background())) 196 | 197 | if err := goexec.ExecuteCleanMethod(ctx, &scmrChange, &exec); err != nil { 198 | log.Fatal().Err(err).Msg("Operation failed") 199 | } 200 | }, 201 | } 202 | scmrDeleteCmd = &cobra.Command{ 203 | Use: "delete [target]", 204 | Short: "Delete an existing Windows service", 205 | Long: `Description: 206 | The delete method will simply delete the provided service.`, 207 | 208 | Args: argsRpcClient("cifs"), 209 | Run: func(cmd *cobra.Command, args []string) { 210 | scmrDelete.Client = &rpcClient 211 | 212 | ctx := log.With(). 213 | Str("module", "scmr"). 214 | Str("method", "delete"). 215 | Logger().WithContext(gssapi.NewSecurityContext(context.Background())) 216 | 217 | if err := goexec.ExecuteCleanAuxiliaryMethod(ctx, &scmrDelete); err != nil { 218 | log.Fatal().Err(err).Msg("Operation failed") 219 | } 220 | }, 221 | } 222 | ) 223 | -------------------------------------------------------------------------------- /cmd/tsch.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/FalconOpsLLC/goexec/internal/util" 7 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 8 | tschexec "github.com/FalconOpsLLC/goexec/pkg/goexec/tsch" 9 | "github.com/oiweiwei/go-msrpc/ssp/gssapi" 10 | "github.com/spf13/cobra" 11 | "time" 12 | ) 13 | 14 | func tschCmdInit() { 15 | cmdFlags[tschCmd] = []*flagSet{ 16 | defaultAuthFlags, 17 | defaultLogFlags, 18 | defaultNetRpcFlags, 19 | } 20 | tschDemandCmdInit() 21 | tschCreateCmdInit() 22 | tschChangeCmdInit() 23 | 24 | tschCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) 25 | tschCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) 26 | tschCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) 27 | tschCmd.AddCommand(tschDemandCmd, tschCreateCmd, tschChangeCmd) 28 | } 29 | 30 | func tschDemandCmdInit() { 31 | tschDemandFlags := newFlagSet("Task Scheduler") 32 | 33 | tschDemandFlags.Flags.StringVarP(&tschTask, "task", "t", "", "Name or path of the new task") 34 | tschDemandFlags.Flags.Uint32Var(&tschDemand.SessionId, "session", 0, "Hijack existing session given the session `ID`") 35 | tschDemandFlags.Flags.StringVar(&tschDemand.UserSid, "sid", "S-1-5-18", "User `SID` to impersonate") 36 | tschDemandFlags.Flags.BoolVar(&tschDemand.NoDelete, "no-delete", false, "Don't delete task after execution") 37 | 38 | tschDemandExecFlags := newFlagSet("Execution") 39 | 40 | registerExecutionFlags(tschDemandExecFlags.Flags) 41 | registerExecutionOutputFlags(tschDemandExecFlags.Flags) 42 | 43 | cmdFlags[tschDemandCmd] = []*flagSet{ 44 | tschDemandFlags, 45 | tschDemandExecFlags, 46 | defaultAuthFlags, 47 | defaultLogFlags, 48 | defaultNetRpcFlags, 49 | } 50 | 51 | tschDemandCmd.Flags().AddFlagSet(tschDemandFlags.Flags) 52 | tschDemandCmd.Flags().AddFlagSet(tschDemandExecFlags.Flags) 53 | tschDemandCmd.MarkFlagsOneRequired("exec", "command") 54 | } 55 | 56 | func tschCreateCmdInit() { 57 | tschCreateFlags := newFlagSet("Task Scheduler") 58 | 59 | tschCreateFlags.Flags.StringVarP(&tschTask, "task", "t", "", "Name or path of the new task") 60 | tschCreateFlags.Flags.DurationVar(&tschCreate.StopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This won't stop the spawned process") 61 | tschCreateFlags.Flags.DurationVar(&tschCreate.StartDelay, "start-delay", 5*time.Second, "Delay between task registration and execution") 62 | //tschCreateFlags.Flags.DurationVar(&tschCreate.DeleteDelay, "delete-delay", 0*time.Second, "Delay between task termination and deletion") 63 | tschCreateFlags.Flags.BoolVar(&tschCreate.NoDelete, "no-delete", false, "Don't delete task after execution") 64 | tschCreateFlags.Flags.BoolVar(&tschCreate.CallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task") 65 | tschCreateFlags.Flags.StringVar(&tschCreate.UserSid, "sid", "S-1-5-18", "User `SID` to impersonate") 66 | 67 | tschCreateExecFlags := newFlagSet("Execution") 68 | 69 | registerExecutionFlags(tschCreateExecFlags.Flags) 70 | registerExecutionOutputFlags(tschCreateExecFlags.Flags) 71 | 72 | cmdFlags[tschCreateCmd] = []*flagSet{ 73 | tschCreateFlags, 74 | tschCreateExecFlags, 75 | defaultAuthFlags, 76 | defaultLogFlags, 77 | defaultNetRpcFlags, 78 | } 79 | 80 | tschCreateCmd.Flags().AddFlagSet(tschCreateFlags.Flags) 81 | tschCreateCmd.Flags().AddFlagSet(tschCreateExecFlags.Flags) 82 | tschCreateCmd.MarkFlagsOneRequired("exec", "command") 83 | } 84 | 85 | func tschChangeCmdInit() { 86 | tschChangeFlags := newFlagSet("Task Scheduler") 87 | 88 | tschChangeFlags.Flags.StringVarP(&tschChange.TaskPath, "task", "t", "", "Path to existing task") 89 | tschChangeFlags.Flags.BoolVar(&tschChange.NoStart, "no-start", false, "Don't start the task") 90 | tschChangeFlags.Flags.BoolVar(&tschChange.NoRevert, "no-revert", false, "Don't restore the original task definition") 91 | 92 | tschChangeExecFlags := newFlagSet("Execution") 93 | 94 | registerExecutionFlags(tschChangeExecFlags.Flags) 95 | registerExecutionOutputFlags(tschChangeExecFlags.Flags) 96 | 97 | cmdFlags[tschChangeCmd] = []*flagSet{ 98 | tschChangeFlags, 99 | tschChangeExecFlags, 100 | defaultAuthFlags, 101 | defaultLogFlags, 102 | defaultNetRpcFlags, 103 | } 104 | 105 | tschChangeCmd.Flags().AddFlagSet(tschChangeFlags.Flags) 106 | tschChangeCmd.Flags().AddFlagSet(tschChangeExecFlags.Flags) 107 | 108 | // Constraints 109 | { 110 | if err := tschChangeCmd.MarkFlagRequired("task"); err != nil { 111 | panic(err) 112 | } 113 | tschChangeCmd.MarkFlagsOneRequired("exec", "command") 114 | } 115 | } 116 | 117 | func argsTask(*cobra.Command, []string) error { 118 | switch { 119 | case tschTask == "": 120 | tschTask = `\` + util.RandomString() 121 | case tschexec.ValidateTaskPath(tschTask) == nil: 122 | case tschexec.ValidateTaskName(tschTask) == nil: 123 | tschTask = `\` + tschTask 124 | default: 125 | return fmt.Errorf("invalid task Label or path: %q", tschTask) 126 | } 127 | return nil 128 | } 129 | 130 | var ( 131 | tschDemand tschexec.TschDemand 132 | tschCreate tschexec.TschCreate 133 | tschChange tschexec.TschChange 134 | 135 | tschTask string 136 | 137 | tschCmd = &cobra.Command{ 138 | Use: "tsch", 139 | Short: "Execute with Windows Task Scheduler (MS-TSCH)", 140 | Long: `Description: 141 | The tsch module makes use of the Windows Task Scheduler service (MS-TSCH) to 142 | spawn processes on the remote target.`, 143 | GroupID: "module", 144 | Args: cobra.NoArgs, 145 | } 146 | 147 | tschDemandCmd = &cobra.Command{ 148 | Use: "demand [target]", 149 | Short: "Register a remote scheduled task and demand immediate start", 150 | Long: `Description: 151 | Similar to the create method, the demand method will call SchRpcRegisterTask, 152 | But rather than setting a defined time when the task will start, it will 153 | additionally call SchRpcRun to forcefully start the task.`, 154 | Args: args( 155 | argsRpcClient("cifs"), 156 | argsOutput("smb"), 157 | argsTask, 158 | ), 159 | 160 | Run: func(*cobra.Command, []string) { 161 | tschDemand.IO = exec 162 | tschDemand.Client = &rpcClient 163 | tschDemand.TaskPath = tschTask 164 | 165 | ctx := log.With(). 166 | Str("module", "tsch"). 167 | Str("method", "demand"). 168 | Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) 169 | 170 | if err := goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil { 171 | log.Fatal().Err(err).Msg("Operation failed") 172 | } 173 | }, 174 | } 175 | tschCreateCmd = &cobra.Command{ 176 | Use: "create [target]", 177 | Short: "Create a remote scheduled task with an automatic start time", 178 | Long: `Description: 179 | The create method calls SchRpcRegisterTask to register a scheduled task 180 | with an automatic start time.This method avoids directly calling SchRpcRun, 181 | and can even avoid calling SchRpcDelete by populating the DeleteExpiredTaskAfter 182 | Setting.`, 183 | Args: args( 184 | argsRpcClient("cifs"), 185 | argsOutput("smb"), 186 | argsTask, 187 | ), 188 | 189 | Run: func(*cobra.Command, []string) { 190 | tschCreate.Client = &rpcClient 191 | tschCreate.IO = exec 192 | tschCreate.TaskPath = tschTask 193 | 194 | ctx := log.With(). 195 | Str("module", "tsch"). 196 | Str("method", "create"). 197 | Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) 198 | 199 | if err := goexec.ExecuteCleanMethod(ctx, &tschCreate, &exec); err != nil { 200 | log.Fatal().Err(err).Msg("Operation failed") 201 | } 202 | }, 203 | } 204 | tschChangeCmd = &cobra.Command{ 205 | Use: "change [target]", 206 | Short: "Modify an existing task to spawn an arbitrary process", 207 | Long: `Description: 208 | The change method calls SchRpcRetrieveTask to fetch the definition of an existing 209 | task (-t), then modifies the task definition to spawn a process`, 210 | Args: args( 211 | argsRpcClient("cifs"), 212 | argsOutput("smb"), 213 | 214 | func(*cobra.Command, []string) error { 215 | return tschexec.ValidateTaskPath(tschChange.TaskPath) 216 | }, 217 | ), 218 | 219 | Run: func(*cobra.Command, []string) { 220 | tschChange.Client = &rpcClient 221 | tschChange.IO = exec 222 | 223 | ctx := log.With(). 224 | Str("module", "tsch"). 225 | Str("method", "change"). 226 | Logger().WithContext(gssapi.NewSecurityContext(context.TODO())) 227 | 228 | if err := goexec.ExecuteCleanMethod(ctx, &tschChange, &exec); err != nil { 229 | log.Fatal().Err(err).Msg("Operation failed") 230 | } 231 | }, 232 | } 233 | ) 234 | -------------------------------------------------------------------------------- /cmd/wmi.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | wmiexec "github.com/FalconOpsLLC/goexec/pkg/goexec/wmi" 8 | "github.com/oiweiwei/go-msrpc/ssp/gssapi" 9 | "github.com/spf13/cobra" 10 | "os" 11 | ) 12 | 13 | func wmiCmdInit() { 14 | cmdFlags[wmiCmd] = []*flagSet{ 15 | defaultAuthFlags, 16 | defaultLogFlags, 17 | defaultNetRpcFlags, 18 | } 19 | wmiCallCmdInit() 20 | wmiProcCmdInit() 21 | 22 | wmiCmd.PersistentFlags().AddFlagSet(defaultAuthFlags.Flags) 23 | wmiCmd.PersistentFlags().AddFlagSet(defaultLogFlags.Flags) 24 | wmiCmd.PersistentFlags().AddFlagSet(defaultNetRpcFlags.Flags) 25 | wmiCmd.AddCommand(wmiProcCmd, wmiCallCmd) 26 | } 27 | 28 | func wmiCallCmdInit() { 29 | wmiCallFlags := newFlagSet("WMI") 30 | 31 | wmiCallFlags.Flags.StringVarP(&wmiCall.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace") 32 | wmiCallFlags.Flags.StringVarP(&wmiCall.Class, "class", "C", "", `WMI class to instantiate (i.e. "Win32_Process")`) 33 | wmiCallFlags.Flags.StringVarP(&wmiCall.Method, "method", "m", "", `WMI Method to call (i.e. "Create")`) 34 | wmiCallFlags.Flags.StringVarP(&wmiArguments, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"Command":"calc.exe"})`) 35 | 36 | wmiCallCmd.Flags().AddFlagSet(wmiCallFlags.Flags) 37 | 38 | cmdFlags[wmiCallCmd] = []*flagSet{ 39 | wmiCallFlags, 40 | defaultAuthFlags, 41 | defaultLogFlags, 42 | defaultNetRpcFlags, 43 | } 44 | if err := wmiCallCmd.MarkFlagRequired("class"); err != nil { 45 | panic(err) 46 | } 47 | if err := wmiCallCmd.MarkFlagRequired("method"); err != nil { 48 | panic(err) 49 | } 50 | } 51 | 52 | func wmiProcCmdInit() { 53 | wmiProcExecFlags := newFlagSet("Execution") 54 | 55 | registerExecutionFlags(wmiProcExecFlags.Flags) 56 | registerExecutionOutputFlags(wmiProcExecFlags.Flags) 57 | 58 | wmiProcExecFlags.Flags.StringVarP(&wmiProc.WorkingDirectory, "directory", "d", `C:\`, "Working directory") 59 | 60 | cmdFlags[wmiProcCmd] = []*flagSet{ 61 | wmiProcExecFlags, 62 | defaultAuthFlags, 63 | defaultLogFlags, 64 | defaultNetRpcFlags, 65 | } 66 | 67 | wmiProcCmd.Flags().AddFlagSet(wmiProcExecFlags.Flags) 68 | } 69 | 70 | var ( 71 | wmiCall = wmiexec.WmiCall{} 72 | wmiProc = wmiexec.WmiProc{} 73 | 74 | wmiArguments string 75 | 76 | wmiCmd = &cobra.Command{ 77 | Use: "wmi", 78 | Short: "Execute with Windows Management Instrumentation (MS-WMI)", 79 | Long: `Description: 80 | The wmi module uses remote Windows Management Instrumentation (WMI) to 81 | perform various operations including process creation.`, 82 | GroupID: "module", 83 | Args: cobra.NoArgs, 84 | } 85 | 86 | wmiCallCmd = &cobra.Command{ 87 | Use: "call [target]", 88 | Short: "Execute specified WMI method", 89 | Long: `Description: 90 | The call method creates an instance of the specified WMI class (-c), 91 | then calls the provided method (-m) with the provided arguments (-A).`, 92 | Args: args( 93 | argsRpcClient("cifs"), 94 | func(cmd *cobra.Command, args []string) error { 95 | return json.Unmarshal([]byte(wmiArguments), &wmiCall.Args) 96 | }), 97 | 98 | Run: func(cmd *cobra.Command, args []string) { 99 | wmiCall.Client = &rpcClient 100 | wmiCall.Out = os.Stdout 101 | 102 | ctx := log.With(). 103 | Str("module", "wmi"). 104 | Str("method", "call"). 105 | Logger().WithContext(gssapi.NewSecurityContext(context.Background())) 106 | 107 | if err := goexec.ExecuteCleanAuxiliaryMethod(ctx, &wmiCall); err != nil { 108 | log.Fatal().Err(err).Msg("Operation failed") 109 | } 110 | }, 111 | } 112 | 113 | wmiProcCmd = &cobra.Command{ 114 | Use: "proc [target]", 115 | Short: "Start a Windows process", 116 | Long: `Description: 117 | The proc method creates an instance of the Win32_Process WMI class, then 118 | calls the Win32_Process.Create method with the provided command (-c), 119 | and optional working directory (-d).`, 120 | Args: args( 121 | argsRpcClient("cifs"), 122 | argsOutput("smb"), 123 | ), 124 | 125 | Run: func(cmd *cobra.Command, args []string) { 126 | wmiProc.Client = &rpcClient 127 | wmiProc.IO = exec 128 | wmiProc.Resource = "//./root/cimv2" 129 | 130 | ctx := log.With(). 131 | Str("module", "wmi"). 132 | Str("method", "proc"). 133 | Logger().WithContext(gssapi.NewSecurityContext(context.Background())) 134 | 135 | if err := goexec.ExecuteCleanMethod(ctx, &wmiProc, &exec); err != nil { 136 | log.Fatal().Err(err).Msg("Operation failed") 137 | } 138 | }, 139 | } 140 | ) 141 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/FalconOpsLLC/goexec 2 | 3 | go 1.23.3 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/RedTeamPentesting/adauth v0.3.0 9 | github.com/google/uuid v1.6.0 10 | github.com/oiweiwei/go-msrpc v1.2.5 11 | github.com/oiweiwei/go-smb2.fork v1.0.0 12 | github.com/rs/zerolog v1.34.0 13 | github.com/spf13/cobra v1.9.1 14 | github.com/spf13/pflag v1.0.6 15 | golang.org/x/net v0.40.0 16 | golang.org/x/term v0.32.0 17 | golang.org/x/text v0.25.0 18 | ) 19 | 20 | require ( 21 | github.com/geoffgarside/ber v1.1.0 // indirect 22 | github.com/hashicorp/go-uuid v1.0.3 // indirect 23 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 24 | github.com/indece-official/go-ebcdic v1.2.0 // indirect 25 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 26 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 27 | github.com/jcmturner/gofork v1.7.6 // indirect 28 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 29 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 30 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 31 | github.com/mattn/go-colorable v0.1.14 // indirect 32 | github.com/mattn/go-isatty v0.0.20 // indirect 33 | github.com/oiweiwei/gokrb5.fork/v9 v9.0.2 // indirect 34 | golang.org/x/crypto v0.38.0 // indirect 35 | golang.org/x/sys v0.33.0 // indirect 36 | software.sslmate.com/src/go-pkcs12 v0.5.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/RedTeamPentesting/adauth v0.3.0 h1:SvzOdHvD+N9tzjrSfTgd+HzKQV4H4TFUqumlbh/H6E0= 2 | github.com/RedTeamPentesting/adauth v0.3.0/go.mod h1:xoCQ4Z6ong9GKeQ24Br5K/CWOTargv9OGkMuXSY0EaQ= 3 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w= 9 | github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= 10 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 14 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 15 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 16 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 17 | github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 18 | github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 19 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 20 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 21 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 22 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 23 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 24 | github.com/indece-official/go-ebcdic v1.2.0 h1:nKCubkNoXrGvBp3MSYuplOQnhANCDEY512Ry5Mwr4a0= 25 | github.com/indece-official/go-ebcdic v1.2.0/go.mod h1:RBddVJt0Ks0eDLRG5dhPwBDRiTNA7n+yv0dVFpSs46Q= 26 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 27 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 28 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 29 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 30 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 31 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 32 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 33 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 34 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 35 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 36 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 37 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 38 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 39 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 40 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 41 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 42 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 43 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 44 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 45 | github.com/oiweiwei/go-msrpc v1.2.5 h1:nIWoU7MWLk5l8vb0pgQ+D67GjDRPC4ybiR+OJtgDWdk= 46 | github.com/oiweiwei/go-msrpc v1.2.5/go.mod h1:WoWRPfm90vRNZDJCwOiUXy39vjyQMAFrFj0zkWTThwY= 47 | github.com/oiweiwei/go-smb2.fork v1.0.0 h1:xHq/eYPM8hQEO/nwCez8YwHWHC8mlcsgw/Neu52fPN4= 48 | github.com/oiweiwei/go-smb2.fork v1.0.0/go.mod h1:h0CzLVvGAmq39izdYVHKyI5cLv6aHdbQAMKEe4dz4N8= 49 | github.com/oiweiwei/gokrb5.fork/v9 v9.0.2 h1:JNkvXMuOEWNXJKzLiyROGfdK31/1RQWA9e5gJxAsl50= 50 | github.com/oiweiwei/gokrb5.fork/v9 v9.0.2/go.mod h1:KEnkAYUYqZ5VwzxLFbv3JHlRhCvdFahjrdjjssMJJkI= 51 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 55 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 56 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 57 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 58 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 59 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 60 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 61 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 63 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 64 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 65 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 66 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 67 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 68 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 69 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 70 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 71 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 72 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 73 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 74 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 75 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 76 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 77 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 78 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 79 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 80 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 81 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 82 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 83 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 84 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 85 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 86 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 88 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 96 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 98 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 99 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 100 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 101 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 102 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 103 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 104 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 105 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 106 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 107 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 108 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 109 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 110 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 112 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 113 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 114 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 115 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 116 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 117 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 118 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 119 | software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= 120 | software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= 121 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "math/rand" // not crypto secure 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | const randHostnameCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-" 11 | const randStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 12 | 13 | var ( 14 | // Up to 15 characters; only letters, digits, and hyphens (with hyphens not at the start or end). 15 | randHostnameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-]{0,14}[a-zA-Z0-9]$`) 16 | ) 17 | 18 | func RandomHostname() (hostname string) { 19 | for { 20 | // between 2 and 10 characters 21 | if hostname = RandomStringFromCharset(randHostnameCharset, rand.Intn(8)+2); randHostnameRegex.MatchString(hostname) { 22 | return 23 | } 24 | } 25 | } 26 | 27 | func RandomWindowsTempFile() string { 28 | return `\Windows\Temp\` + strings.ToUpper(uuid.New().String()) 29 | } 30 | 31 | func RandomString() string { 32 | return RandomStringFromCharset(randStringCharset, rand.Intn(10)+6) 33 | } 34 | 35 | func RandomStringFromCharset(charset string, length int) string { 36 | b := make([]byte, length) 37 | for i := range length { 38 | b[i] = charset[rand.Intn(len(charset))] 39 | } 40 | return string(b) 41 | } 42 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/FalconOpsLLC/goexec/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /pkg/goexec/auth.go: -------------------------------------------------------------------------------- 1 | package goexec 2 | 3 | import ( 4 | "github.com/RedTeamPentesting/adauth" 5 | ) 6 | 7 | // AuthOptions holds Windows / Active Directory authentication parameters 8 | type AuthOptions struct { 9 | Target *adauth.Target 10 | Credential *adauth.Credential 11 | } 12 | -------------------------------------------------------------------------------- /pkg/goexec/clean.go: -------------------------------------------------------------------------------- 1 | package goexec 2 | 3 | import ( 4 | "context" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | type Clean interface { 9 | Clean(ctx context.Context) error 10 | } 11 | 12 | type Cleaner struct { 13 | workers []func(ctx context.Context) error 14 | } 15 | 16 | func (c *Cleaner) AddCleaners(workers ...func(ctx context.Context) error) { 17 | c.workers = append(c.workers, workers...) 18 | } 19 | 20 | func (c *Cleaner) Clean(ctx context.Context) (err error) { 21 | log := zerolog.Ctx(ctx).With(). 22 | Str("component", "cleaner").Logger() 23 | 24 | for _, worker := range c.workers { 25 | if err = worker(log.WithContext(ctx)); err != nil { 26 | 27 | log.Warn().Err(err).Msg("Clean worker failed") 28 | } 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /pkg/goexec/client.go: -------------------------------------------------------------------------------- 1 | package goexec 2 | 3 | import "context" 4 | 5 | // Client represents an application layer network client 6 | type Client interface { 7 | 8 | // Connect establishes a connection to the remote server 9 | Connect(ctx context.Context) error 10 | 11 | // Close terminates the active connection and frees allocated resources 12 | Close(ctx context.Context) error 13 | } 14 | 15 | // ClientOptions represents configuration options for a Client 16 | type ClientOptions struct { 17 | 18 | // Proxy specifies the URI of the proxy server to route client requests through 19 | Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"` 20 | 21 | // Host specifies the hostname or IP address that the client should connect to 22 | Host string `json:"host" yaml:"host"` 23 | 24 | // Port specifies the network port on Host that the client will connect to 25 | Port uint16 `json:"port" yaml:"port"` 26 | } 27 | -------------------------------------------------------------------------------- /pkg/goexec/dce/client.go: -------------------------------------------------------------------------------- 1 | package dce 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/RedTeamPentesting/adauth/smbauth" 7 | "github.com/oiweiwei/go-msrpc/dcerpc" 8 | "github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3" 9 | msrpcSMB2 "github.com/oiweiwei/go-msrpc/smb2" 10 | "github.com/rs/zerolog" 11 | ) 12 | 13 | type Client struct { 14 | Options 15 | 16 | conn dcerpc.Conn 17 | } 18 | 19 | func (c *Client) String() string { 20 | return ClientName 21 | } 22 | 23 | func (c *Client) Reconnect(ctx context.Context, opts ...dcerpc.Option) (err error) { 24 | c.DcerpcOptions = append(c.DcerpcOptions, opts...) 25 | 26 | return c.Connect(ctx) 27 | } 28 | 29 | func (c *Client) Dce() (dce dcerpc.Conn) { 30 | return c.conn 31 | } 32 | 33 | func (c *Client) Logger(ctx context.Context) (log zerolog.Logger) { 34 | return zerolog.Ctx(ctx).With(). 35 | Str("client", c.String()).Logger() 36 | } 37 | 38 | func (c *Client) Connect(ctx context.Context) (err error) { 39 | 40 | log := c.Logger(ctx) 41 | ctx = log.WithContext(ctx) 42 | 43 | var do, eo []dcerpc.Option 44 | 45 | do = append(do, c.DcerpcOptions...) 46 | do = append(do, c.authOptions...) 47 | 48 | do = append(do, dcerpc.WithSeal()) 49 | 50 | if c.Smb { 51 | var so []msrpcSMB2.DialerOption 52 | 53 | if !c.NoSign { 54 | so = append(so, msrpcSMB2.WithSign()) 55 | eo = append(eo, dcerpc.WithSign()) 56 | } 57 | if !c.NoSeal { 58 | so = append(so, msrpcSMB2.WithSeal()) 59 | eo = append(eo, dcerpc.WithSeal()) 60 | } 61 | 62 | if smbDialer, err := smbauth.Dialer(ctx, c.Credential, c.Target, &smbauth.Options{SMBOptions: so}); err != nil { 63 | return fmt.Errorf("parse smb auth: %w", err) 64 | 65 | } else { 66 | do = append(do, dcerpc.WithSMBDialer(smbDialer)) 67 | } 68 | } else { 69 | 70 | if !c.NoSign { 71 | do = append(do, dcerpc.WithSign()) 72 | eo = append(eo, dcerpc.WithSign()) 73 | } 74 | if !c.NoSeal { 75 | do = append(do, dcerpc.WithSeal()) 76 | eo = append(eo, dcerpc.WithSeal()) 77 | } 78 | } 79 | 80 | if !c.NoLog { 81 | do = append(do, dcerpc.WithLogger(log)) 82 | eo = append(eo, dcerpc.WithLogger(log)) 83 | } 84 | 85 | if !c.NoEpm { 86 | log.Debug().Msg("Using endpoint mapper") 87 | 88 | eo = append(eo, c.EpmOptions...) 89 | eo = append(eo, c.authOptions...) 90 | 91 | do = append(do, epm.EndpointMapper(ctx, c.Host, eo...)) 92 | } 93 | 94 | for _, e := range c.stringBindings { 95 | do = append(do, dcerpc.WithEndpoint(e.String())) 96 | } 97 | 98 | if c.conn, err = dcerpc.Dial(ctx, c.Host, do...); err != nil { 99 | 100 | log.Error().Err(err).Msgf("Failed to connect to %s endpoint", c.String()) 101 | return fmt.Errorf("dial %s: %w", c.String(), err) 102 | } 103 | 104 | return 105 | } 106 | 107 | func (c *Client) Close(ctx context.Context) (err error) { 108 | return c.conn.Close(ctx) 109 | } 110 | -------------------------------------------------------------------------------- /pkg/goexec/dce/default.go: -------------------------------------------------------------------------------- 1 | package dce 2 | 3 | const ( 4 | ClientName = "DCE" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/goexec/dce/options.go: -------------------------------------------------------------------------------- 1 | package dce 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "github.com/RedTeamPentesting/adauth/dcerpcauth" 8 | "github.com/oiweiwei/go-msrpc/dcerpc" 9 | "net" 10 | ) 11 | 12 | type Options struct { 13 | goexec.ClientOptions 14 | goexec.AuthOptions 15 | 16 | // NoSign disables packet signing by omitting dcerpc.WithSign() 17 | NoSign bool `json:"no_sign" yaml:"no_sign"` 18 | 19 | // NoSeal disables packet stub encryption by omitting dcerpc.WithSeal() 20 | NoSeal bool `json:"no_seal" yaml:"no_seal"` 21 | 22 | // NoLog disables logging by omitting dcerpc.WithLogger(...) 23 | NoLog bool `json:"no_log" yaml:"no_log"` 24 | 25 | // NoEpm disables DCE endpoint mapper communications 26 | NoEpm bool `json:"no_epm" yaml:"no_epm"` 27 | 28 | // Endpoint stores the explicit DCE string binding to use 29 | Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"` 30 | 31 | // Filter stores the filter for returned endpoints from an endpoint mapper 32 | Filter string `json:"filter,omitempty" yaml:"filter,omitempty"` 33 | 34 | // Smb enables SMB transport for DCE/RPC 35 | Smb bool `json:"use_smb" yaml:"use_smb"` 36 | 37 | stringBindings []*dcerpc.StringBinding 38 | dialer goexec.Dialer 39 | authOptions []dcerpc.Option 40 | DcerpcOptions []dcerpc.Option 41 | EpmOptions []dcerpc.Option 42 | } 43 | 44 | func (c *Client) Parse(ctx context.Context) (err error) { 45 | 46 | // Reset internals 47 | { 48 | c.dialer = nil 49 | c.stringBindings = []*dcerpc.StringBinding{} 50 | c.authOptions = []dcerpc.Option{} 51 | c.DcerpcOptions = []dcerpc.Option{} 52 | c.EpmOptions = []dcerpc.Option{ 53 | dcerpc.WithSign(), // Require signing for EPM 54 | } 55 | } 56 | 57 | if !c.NoSeal { 58 | // Enable encryption 59 | c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) 60 | c.EpmOptions = append(c.EpmOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy)) 61 | } 62 | if !c.NoSign { 63 | // Enable signing 64 | c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithSign()) 65 | //c.EpmOptions = append(c.EpmOptions, dcerpc.WithSign()) 66 | } 67 | 68 | // Parse DCERPC endpoint 69 | if c.Endpoint != "" { 70 | sb, err := dcerpc.ParseStringBinding(c.Endpoint) 71 | if err != nil { 72 | return err 73 | } 74 | if sb.ProtocolSequence == dcerpc.ProtocolSequenceNamedPipe { 75 | c.Smb = true 76 | } 77 | c.stringBindings = append(c.stringBindings, sb) 78 | } 79 | 80 | // Parse EPM filter 81 | if c.Filter != "" { 82 | sb, err := dcerpc.ParseStringBinding(c.Filter) 83 | if err != nil { 84 | return err 85 | } 86 | if sb.ProtocolSequence == dcerpc.ProtocolSequenceNamedPipe { 87 | c.Smb = true 88 | } 89 | c.stringBindings = append(c.stringBindings, sb) 90 | } 91 | 92 | if c.Proxy != "" { 93 | // Parse proxy URL 94 | c.dialer, err = goexec.ParseProxyURI(c.Proxy) 95 | if err != nil { 96 | return err 97 | } 98 | if d, ok := c.dialer.(dcerpc.Dialer); !ok { 99 | return fmt.Errorf("cannot cast %T to dcerpc.Dialer", d) 100 | 101 | } else { 102 | c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithDialer(d)) 103 | c.EpmOptions = append(c.EpmOptions, dcerpc.WithDialer(d)) 104 | } 105 | 106 | } else { 107 | c.dialer = &net.Dialer{} 108 | } 109 | 110 | // Parse authentication parameters 111 | if c.authOptions, err = dcerpcauth.AuthenticationOptions(ctx, c.Credential, c.Target, &dcerpcauth.Options{ 112 | KerberosDialer: c.dialer, // Use the same net dialer as dcerpc 113 | }); err != nil { 114 | return fmt.Errorf("parse auth c: %w", err) 115 | } 116 | 117 | c.Host = c.Target.AddressWithoutPort() 118 | 119 | return 120 | } 121 | -------------------------------------------------------------------------------- /pkg/goexec/dcom/dcom.go: -------------------------------------------------------------------------------- 1 | package dcomexec 2 | 3 | import ( 4 | googleUUID "github.com/google/uuid" 5 | "github.com/oiweiwei/go-msrpc/midl/uuid" 6 | "github.com/oiweiwei/go-msrpc/msrpc/dcom" 7 | "github.com/oiweiwei/go-msrpc/msrpc/dtyp" 8 | ) 9 | 10 | const ( 11 | LcEnglishUs uint32 = 0x409 12 | ) 13 | 14 | var ( 15 | ShellBrowserWindowUuid = uuid.MustParse("C08AFD90-F2A1-11D1-8455-00A0C91F3880") 16 | ShellWindowsUuid = uuid.MustParse("9BA05972-F6A8-11CF-A442-00A0C90A8F39") 17 | Mmc20Uuid = uuid.MustParse("49B2791A-B1AE-4C90-9B8E-E860BA07F889") 18 | 19 | RandCid = dcom.CID(*dtyp.GUIDFromUUID(uuid.MustParse(googleUUID.NewString()))) 20 | IDispatchIID = &dcom.IID{ 21 | Data1: 0x20400, 22 | Data2: 0x0, 23 | Data3: 0x0, 24 | Data4: []byte{0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46}, 25 | } 26 | ComVersion = &dcom.COMVersion{ 27 | MajorVersion: 5, 28 | MinorVersion: 7, 29 | } 30 | ORPCThis = &dcom.ORPCThis{ 31 | Version: ComVersion, 32 | CID: &RandCid, 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/goexec/dcom/mmc.go: -------------------------------------------------------------------------------- 1 | package dcomexec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | const ( 11 | MethodMmc = "MMC" // MMC20.Application::Document.ActiveView.ExecuteShellCommand 12 | ) 13 | 14 | type DcomMmc struct { 15 | Dcom 16 | 17 | IO goexec.ExecutionIO 18 | 19 | WorkingDirectory string 20 | WindowState string 21 | } 22 | 23 | // Execute will perform command execution via the MMC20.Application DCOM object. 24 | func (m *DcomMmc) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { 25 | 26 | log := zerolog.Ctx(ctx).With(). 27 | Str("module", ModuleName). 28 | Str("method", MethodMmc). 29 | Logger() 30 | 31 | method := "Document.ActiveView.ExecuteShellCommand" 32 | 33 | cmdline := execIO.CommandLine() 34 | proc := cmdline[0] 35 | args := cmdline[1] 36 | 37 | // Arguments must be passed in reverse order 38 | if _, err := callComMethod(ctx, 39 | m.dispatchClient, 40 | nil, 41 | method, 42 | stringToVariant(m.WindowState), 43 | stringToVariant(args), 44 | stringToVariant(m.WorkingDirectory), 45 | stringToVariant(proc)); err != nil { 46 | 47 | log.Error().Err(err).Msg("Failed to call method") 48 | return fmt.Errorf("call %q: %w", method, err) 49 | } 50 | log.Info().Msg("Method call successful") 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /pkg/goexec/dcom/module.go: -------------------------------------------------------------------------------- 1 | package dcomexec 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 8 | "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" 9 | "github.com/oiweiwei/go-msrpc/dcerpc" 10 | "github.com/oiweiwei/go-msrpc/midl/uuid" 11 | "github.com/oiweiwei/go-msrpc/msrpc/dcom" 12 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0" 13 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" 14 | "github.com/oiweiwei/go-msrpc/msrpc/dtyp" 15 | "github.com/rs/zerolog" 16 | 17 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 18 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 19 | ) 20 | 21 | const ( 22 | ModuleName = "DCOM" 23 | ) 24 | 25 | type Dcom struct { 26 | goexec.Cleaner 27 | goexec.Executor 28 | 29 | Client *dce.Client 30 | ClassID *uuid.UUID 31 | 32 | dispatchClient idispatch.DispatchClient 33 | } 34 | 35 | func (m *Dcom) Connect(ctx context.Context) (err error) { 36 | 37 | if err = m.Client.Connect(ctx); err == nil { 38 | m.AddCleaners(m.Client.Close) 39 | } 40 | return 41 | } 42 | 43 | func (m *Dcom) Init(ctx context.Context) (err error) { 44 | 45 | log := zerolog.Ctx(ctx).With(). 46 | Str("module", ModuleName).Logger() 47 | 48 | if m.Client == nil || m.Client.Dce() == nil { 49 | return errors.New("DCE connection not initialized") 50 | } 51 | 52 | if m.ClassID == nil { 53 | return errors.New("CLSID not specified") 54 | } 55 | 56 | class := dcom.ClassID(*dtyp.GUIDFromUUID(m.ClassID)) 57 | 58 | if class.GUID() == nil { 59 | return fmt.Errorf("invalid class ID: %s", m.ClassID) 60 | } 61 | 62 | opts := []dcerpc.Option{ 63 | dcerpc.WithSign(), 64 | } 65 | 66 | inst := &dcom.InstantiationInfoData{ 67 | ClassID: &class, 68 | IID: []*dcom.IID{IDispatchIID}, 69 | ClientCOMVersion: ComVersion, 70 | } 71 | ac := &dcom.ActivationContextInfoData{} 72 | loc := &dcom.LocationInfoData{} 73 | scm := &dcom.SCMRequestInfoData{ 74 | RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{ 75 | RequestedProtocolSequences: []uint16{7}, 76 | }, 77 | } 78 | 79 | ap := &dcom.ActivationProperties{ 80 | DestinationContext: 2, 81 | Properties: []dcom.ActivationProperty{inst, ac, loc, scm}, 82 | } 83 | 84 | apin, err := ap.ActivationPropertiesIn() 85 | if err != nil { 86 | return err 87 | } 88 | 89 | act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, m.Client.Dce()) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{ 95 | ORPCThis: &dcom.ORPCThis{ 96 | Version: ComVersion, 97 | Flags: 1, 98 | CID: &RandCid, 99 | }, 100 | ActPropertiesIn: apin, 101 | }) 102 | if err != nil { 103 | return err 104 | } 105 | log.Info().Msg("RemoteCreateInstance succeeded") 106 | 107 | apout := new(dcom.ActivationProperties) 108 | if err = apout.Parse(cr.ActPropertiesOut); err != nil { 109 | return err 110 | } 111 | si := apout.SCMReplyInfoData() 112 | pi := apout.PropertiesOutInfo() 113 | 114 | if si == nil { 115 | return fmt.Errorf("remote create instance response: SCMReplyInfoData is nil") 116 | } 117 | 118 | if pi == nil { 119 | return fmt.Errorf("remote create instance response: PropertiesOutInfo is nil") 120 | } 121 | 122 | // Ensure that the string bindings don't contain the target hostname 123 | for _, bind := range si.RemoteReply.OXIDBindings.GetStringBindings() { 124 | stringBinding, err := dcerpc.ParseStringBinding("ncacn_ip_tcp:" + bind.NetworkAddr) // TODO: try bind.String() 125 | 126 | if err != nil { 127 | log.Debug().Err(err).Msg("Failed to parse string binding") 128 | continue 129 | } 130 | stringBinding.NetworkAddress = "" 131 | opts = append(opts, dcerpc.WithEndpoint(stringBinding.String())) 132 | } 133 | 134 | err = m.Client.Reconnect(ctx, opts...) 135 | if err != nil { 136 | return err 137 | } 138 | log.Info().Msg("created new DCERPC dialer") 139 | 140 | m.dispatchClient, err = idispatch.NewDispatchClient(ctx, m.Client.Dce(), dcom.WithIPID(pi.InterfaceData[0].IPID())) 141 | if err != nil { 142 | return err 143 | } 144 | log.Info().Msg("created IDispatch Client") 145 | 146 | return 147 | } 148 | -------------------------------------------------------------------------------- /pkg/goexec/dcom/shellbrowserwindow.go: -------------------------------------------------------------------------------- 1 | package dcomexec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | const ( 11 | MethodShellBrowserWindow = "ShellBrowserWindow" // ShellBrowserWindow::Document.Application.ShellExecute 12 | ) 13 | 14 | type DcomShellBrowserWindow struct { 15 | Dcom 16 | 17 | IO goexec.ExecutionIO 18 | 19 | WorkingDirectory string 20 | WindowState string 21 | } 22 | 23 | // Execute will perform command execution via the ShellBrowserWindow object. See https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/ 24 | func (m *DcomShellBrowserWindow) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { 25 | 26 | log := zerolog.Ctx(ctx).With(). 27 | Str("module", ModuleName). 28 | Str("method", MethodShellBrowserWindow). 29 | Logger() 30 | 31 | method := "Document.Application.ShellExecute" 32 | 33 | cmdline := execIO.CommandLine() 34 | proc := cmdline[0] 35 | args := cmdline[1] 36 | 37 | // Arguments must be passed in reverse order 38 | if _, err := callComMethod(ctx, m.dispatchClient, 39 | nil, 40 | method, 41 | stringToVariant(m.WindowState), 42 | stringToVariant(""), // FUTURE? 43 | stringToVariant(m.WorkingDirectory), 44 | stringToVariant(args), 45 | stringToVariant(proc)); err != nil { 46 | 47 | log.Error().Err(err).Msg("Failed to call method") 48 | return fmt.Errorf("call %q: %w", method, err) 49 | } 50 | log.Info().Msg("Method call successful") 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /pkg/goexec/dcom/shellwindows.go: -------------------------------------------------------------------------------- 1 | package dcomexec 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 8 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | const ( 13 | MethodShellWindows = "ShellWindows" // ShellWindows::Item().Document.Application.ShellExecute 14 | ) 15 | 16 | type DcomShellWindows struct { 17 | Dcom 18 | 19 | IO goexec.ExecutionIO 20 | 21 | WorkingDirectory string 22 | WindowState string 23 | } 24 | 25 | // Execute will perform command execution via the ShellWindows object. See https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/ 26 | func (m *DcomShellWindows) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { 27 | 28 | log := zerolog.Ctx(ctx).With(). 29 | Str("module", ModuleName). 30 | Str("method", MethodShellWindows). 31 | Logger() 32 | 33 | method := "Item" 34 | 35 | cmdline := execIO.CommandLine() 36 | proc := cmdline[0] 37 | args := cmdline[1] 38 | 39 | iv, err := callComMethod(ctx, 40 | m.dispatchClient, 41 | nil, 42 | "Item") 43 | 44 | if err != nil { 45 | log.Error().Err(err).Msg("Failed to call method") 46 | return fmt.Errorf("call method %q: %w", method, err) 47 | } 48 | 49 | item, ok := iv.VarResult.VarUnion.GetValue().(*oaut.Dispatch) 50 | if !ok { 51 | return errors.New("failed to get dispatch from ShellWindows::Item()") 52 | } 53 | 54 | method = "Document.Application.ShellExecute" 55 | 56 | // Arguments must be passed in reverse order 57 | if _, err := callComMethod(ctx, m.dispatchClient, 58 | item.InterfacePointer(). 59 | GetStandardObjectReference(). 60 | Std.IPID, 61 | method, 62 | stringToVariant(m.WindowState), 63 | stringToVariant(""), // FUTURE? 64 | stringToVariant(m.WorkingDirectory), 65 | stringToVariant(args), 66 | stringToVariant(proc)); err != nil { 67 | 68 | log.Error().Err(err).Msg("Failed to call method") 69 | return fmt.Errorf("call %q: %w", method, err) 70 | } 71 | log.Info().Msg("Method call successful") 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /pkg/goexec/dcom/util.go: -------------------------------------------------------------------------------- 1 | package dcomexec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/oiweiwei/go-msrpc/dcerpc" 7 | "github.com/oiweiwei/go-msrpc/msrpc/dcom" 8 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut" 9 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0" 10 | "strings" 11 | 12 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 13 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 14 | ) 15 | 16 | func callComMethod(ctx context.Context, dc idispatch.DispatchClient, id *dcom.IPID, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) { 17 | 18 | parts := strings.Split(method, ".") 19 | 20 | for i, obj := range parts { 21 | 22 | var opts []dcerpc.CallOption 23 | 24 | if id != nil { 25 | opts = append(opts, dcom.WithIPID(id)) 26 | } 27 | 28 | gr, err := dc.GetIDsOfNames(ctx, &idispatch.GetIDsOfNamesRequest{ 29 | This: ORPCThis, 30 | IID: &dcom.IID{}, 31 | LocaleID: LcEnglishUs, 32 | 33 | Names: []string{obj + "\x00"}, 34 | }, opts...) 35 | 36 | if err != nil { 37 | return nil, fmt.Errorf("get dispatch ID of name %q: %w", obj, err) 38 | } 39 | 40 | if len(gr.DispatchID) < 1 { 41 | return nil, fmt.Errorf("dispatch ID of name %q not found", obj) 42 | } 43 | 44 | irq := &idispatch.InvokeRequest{ 45 | This: ORPCThis, 46 | IID: &dcom.IID{}, 47 | LocaleID: LcEnglishUs, 48 | 49 | DispatchIDMember: gr.DispatchID[0], 50 | } 51 | 52 | if i >= len(parts)-1 { 53 | irq.Flags = 1 54 | irq.DispatchParams = &oaut.DispatchParams{ArgsCount: uint32(len(args)), Args: args} 55 | return dc.Invoke(ctx, irq, opts...) 56 | } 57 | irq.Flags = 2 58 | 59 | ir, err = dc.Invoke(ctx, irq, opts...) 60 | if err != nil { 61 | return nil, fmt.Errorf("get properties of object %q: %w", obj, err) 62 | } 63 | 64 | di, ok := ir.VarResult.VarUnion.GetValue().(*oaut.Dispatch) 65 | if !ok { 66 | return nil, fmt.Errorf("invalid dispatch object for %q", obj) 67 | } 68 | id = di.InterfacePointer().GetStandardObjectReference().Std.IPID 69 | } 70 | return 71 | } 72 | 73 | func stringToVariant(s string) *oaut.Variant { 74 | return &oaut.Variant{ 75 | Size: 5, 76 | VT: 8, 77 | VarUnion: &oaut.Variant_VarUnion{ 78 | Value: &oaut.Variant_VarUnion_BSTR{ 79 | BSTR: &oaut.String{ 80 | Data: s, 81 | }, 82 | }, 83 | }, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/goexec/exec.go: -------------------------------------------------------------------------------- 1 | package goexec 2 | 3 | // Executor is a structure shared by all execution methods 4 | type Executor struct { 5 | } 6 | -------------------------------------------------------------------------------- /pkg/goexec/io.go: -------------------------------------------------------------------------------- 1 | package goexec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | type OutputProvider interface { 11 | GetOutput(ctx context.Context, writer io.Writer) (err error) 12 | Clean(ctx context.Context) (err error) 13 | } 14 | 15 | type ExecutionIO struct { 16 | Cleaner 17 | 18 | Input *ExecutionInput 19 | Output *ExecutionOutput 20 | } 21 | 22 | type ExecutionOutput struct { 23 | NoDelete bool 24 | RemotePath string 25 | Provider OutputProvider 26 | Writer io.WriteCloser 27 | } 28 | 29 | type ExecutionInput struct { 30 | StageFile io.ReadCloser 31 | Executable string 32 | ExecutablePath string 33 | Arguments string 34 | Command string 35 | } 36 | 37 | func (execIO *ExecutionIO) GetOutput(ctx context.Context) (err error) { 38 | if execIO.Output.Provider != nil { 39 | return execIO.Output.Provider.GetOutput(ctx, execIO.Output.Writer) 40 | } 41 | return nil 42 | } 43 | 44 | func (execIO *ExecutionIO) Clean(ctx context.Context) (err error) { 45 | if execIO.Output.Provider != nil { 46 | return execIO.Output.Provider.Clean(ctx) 47 | } 48 | return nil 49 | } 50 | 51 | func (execIO *ExecutionIO) CommandLine() (cmd []string) { 52 | if execIO.Output.Provider != nil && execIO.Output.RemotePath != "" { 53 | return []string{ 54 | `C:\Windows\System32\cmd.exe`, 55 | fmt.Sprintf(`/C %s > %s 2>&1`, execIO.Input.String(), execIO.Output.RemotePath), 56 | } 57 | } 58 | return execIO.Input.CommandLine() 59 | } 60 | 61 | func (execIO *ExecutionIO) String() (str string) { 62 | cmd := execIO.CommandLine() 63 | // Ensure that executable paths are quoted 64 | if strings.Contains(cmd[0], " ") { 65 | str = fmt.Sprintf(`%q %s`, cmd[0], strings.Join(cmd[1:], " ")) 66 | } else { 67 | str = strings.Join(cmd, " ") 68 | } 69 | return strings.Trim(str, " \t\n\r") // trim whitespace 70 | } 71 | 72 | func (i *ExecutionInput) CommandLine() (cmd []string) { 73 | cmd = make([]string, 2) 74 | cmd[1] = i.Arguments 75 | 76 | switch { 77 | case i.Command != "": 78 | return strings.SplitN(i.Command, " ", 2) 79 | 80 | case i.ExecutablePath != "": 81 | cmd[0] = i.ExecutablePath 82 | 83 | case i.Executable != "": 84 | cmd[0] = i.Executable 85 | } 86 | 87 | return cmd 88 | } 89 | 90 | func (i *ExecutionInput) String() string { 91 | return strings.Join(i.CommandLine(), " ") 92 | } 93 | 94 | func (i *ExecutionInput) Reader() (reader io.Reader) { 95 | if i.StageFile != nil { 96 | return i.StageFile 97 | } 98 | return strings.NewReader(i.String()) 99 | } 100 | -------------------------------------------------------------------------------- /pkg/goexec/method.go: -------------------------------------------------------------------------------- 1 | package goexec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | type Method interface { 10 | Connect(ctx context.Context) error 11 | Init(ctx context.Context) error 12 | } 13 | 14 | type CleanMethod interface { 15 | Method 16 | Clean 17 | } 18 | 19 | type ExecutionMethod interface { 20 | Method 21 | Execute(ctx context.Context, io *ExecutionIO) error 22 | } 23 | 24 | type CleanExecutionMethod interface { 25 | ExecutionMethod 26 | Clean 27 | } 28 | 29 | type AuxiliaryMethod interface { 30 | Method 31 | Call(ctx context.Context) error 32 | } 33 | 34 | type CleanAuxiliaryMethod interface { 35 | AuxiliaryMethod 36 | Clean 37 | } 38 | 39 | func ExecuteMethod(ctx context.Context, module ExecutionMethod, execIO *ExecutionIO) (err error) { 40 | log := zerolog.Ctx(ctx) 41 | 42 | if err = module.Connect(ctx); err != nil { 43 | log.Error().Err(err).Msg("Connection failed") 44 | return fmt.Errorf("connect: %w", err) 45 | } 46 | log.Debug().Msg("Module connected") 47 | 48 | if err = module.Init(ctx); err != nil { 49 | log.Error().Err(err).Msg("Module initialization failed") 50 | return fmt.Errorf("init module: %w", err) 51 | } 52 | log.Debug().Msg("Module initialized") 53 | 54 | if err = module.Execute(ctx, execIO); err != nil { 55 | log.Error().Err(err).Msg("Execution failed") 56 | return fmt.Errorf("execute: %w", err) 57 | } 58 | 59 | return 60 | } 61 | 62 | func ExecuteAuxiliaryMethod(ctx context.Context, module AuxiliaryMethod) (err error) { 63 | log := zerolog.Ctx(ctx) 64 | 65 | if err = module.Connect(ctx); err != nil { 66 | log.Error().Err(err).Msg("Connection failed") 67 | return fmt.Errorf("connect: %w", err) 68 | } 69 | log.Debug().Msg("Auxiliary module connected") 70 | 71 | if err = module.Init(ctx); err != nil { 72 | log.Error().Err(err).Msg("Module initialization failed") 73 | return fmt.Errorf("init module: %w", err) 74 | } 75 | log.Debug().Msg("Auxiliary module initialized") 76 | 77 | if err = module.Call(ctx); err != nil { 78 | log.Error().Err(err).Msg("Auxiliary method failed") 79 | return fmt.Errorf("call: %w", err) 80 | } 81 | log.Debug().Msg("Auxiliary method succeeded") 82 | 83 | return nil 84 | } 85 | 86 | func ExecuteCleanAuxiliaryMethod(ctx context.Context, module CleanAuxiliaryMethod) (err error) { 87 | log := zerolog.Ctx(ctx) 88 | 89 | defer func() { 90 | if err = module.Clean(ctx); err != nil { 91 | log.Error().Err(err).Msg("Module cleanup failed") 92 | err = nil 93 | } 94 | }() 95 | 96 | if err = ExecuteAuxiliaryMethod(ctx, module); err != nil { 97 | return fmt.Errorf("execute auxiliary method: %w", err) 98 | } 99 | return 100 | } 101 | 102 | func ExecuteCleanMethod(ctx context.Context, module CleanExecutionMethod, execIO *ExecutionIO) (err error) { 103 | log := zerolog.Ctx(ctx) 104 | 105 | if err = ExecuteMethod(ctx, module, execIO); err != nil { 106 | return 107 | } 108 | 109 | if err = module.Clean(ctx); err != nil { 110 | log.Error().Err(err).Msg("Module cleanup failed") 111 | err = nil 112 | } 113 | 114 | if execIO.Output != nil && execIO.Output.Provider != nil { 115 | log.Info().Msg("Collecting output") 116 | 117 | defer func() { 118 | if cleanErr := execIO.Output.Provider.Clean(ctx); cleanErr != nil { 119 | log.Debug().Err(cleanErr).Msg("Output provider cleanup failed") 120 | } 121 | }() 122 | 123 | if err := execIO.Output.Provider.GetOutput(ctx, execIO.Output.Writer); err != nil { 124 | log.Error().Err(err).Msg("Output collection failed") 125 | return fmt.Errorf("get output: %w", err) 126 | } 127 | log.Debug().Msg("Output collection succeeded") 128 | } 129 | return 130 | } 131 | -------------------------------------------------------------------------------- /pkg/goexec/proxy.go: -------------------------------------------------------------------------------- 1 | package goexec 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/net/proxy" 6 | "net" 7 | "net/url" 8 | ) 9 | 10 | // Dialer outlines a basic implementation for establishing network connections 11 | type Dialer interface { 12 | 13 | // Dial establishes a network connection (net.Conn) using the provided parameters 14 | Dial(network string, address string) (connection net.Conn, err error) 15 | } 16 | 17 | // ParseProxyURI parses the provided proxy URI spec to a Dialer 18 | func ParseProxyURI(uri string) (dialer Dialer, err error) { 19 | 20 | // Parse proxy spec as URL 21 | u, err := url.Parse(uri) 22 | if err != nil { 23 | return nil, fmt.Errorf("parse proxy URI: %w", err) 24 | } 25 | 26 | // Create dialer from URL 27 | dialer, err = proxy.FromURL(u, nil) 28 | if err != nil { 29 | return nil, fmt.Errorf("init proxy: %w", err) 30 | } 31 | 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /pkg/goexec/scmr/change.go: -------------------------------------------------------------------------------- 1 | package scmrexec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" 8 | "github.com/rs/zerolog" 9 | 10 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 11 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 12 | ) 13 | 14 | const ( 15 | MethodChange = "Change" 16 | ) 17 | 18 | type ScmrChange struct { 19 | Scmr 20 | goexec.Cleaner 21 | goexec.Executor 22 | 23 | IO goexec.ExecutionIO 24 | 25 | NoStart bool 26 | NoRevert bool 27 | ServiceName string 28 | } 29 | 30 | func (m *ScmrChange) Execute(ctx context.Context, in *goexec.ExecutionIO) (err error) { 31 | 32 | log := zerolog.Ctx(ctx).With(). 33 | Str("service", m.ServiceName). 34 | Logger() 35 | 36 | svc := &service{name: m.ServiceName} 37 | 38 | openResponse, err := m.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{ 39 | ServiceManager: m.scm, 40 | ServiceName: svc.name, 41 | DesiredAccess: ServiceAllAccess, 42 | }) 43 | 44 | if err != nil { 45 | log.Error().Err(err).Msg("Failed to open service handle") 46 | return fmt.Errorf("open service request: %w", err) 47 | } 48 | if openResponse.Return != 0 { 49 | log.Error().Err(err).Msg("Failed to open service handle") 50 | return fmt.Errorf("create service: %w", err) 51 | } 52 | 53 | svc.handle = openResponse.Service 54 | log.Info().Msg("Opened service handle") 55 | 56 | defer m.AddCleaners(func(ctxInner context.Context) error { 57 | return m.closeService(ctxInner, svc) 58 | }) 59 | 60 | // Note the original service configuration 61 | queryResponse, err := m.ctl.QueryServiceConfigW(ctx, &svcctl.QueryServiceConfigWRequest{ 62 | Service: svc.handle, 63 | BufferLength: 8 * 1024, 64 | }) 65 | 66 | if err != nil { 67 | log.Error().Err(err).Msg("Failed to fetch service configuration") 68 | return fmt.Errorf("get service config: %w", err) 69 | } 70 | 71 | log.Info().Str("binaryPath", queryResponse.ServiceConfig.BinaryPathName).Msg("Fetched original service configuration") 72 | svc.originalConfig = queryResponse.ServiceConfig 73 | 74 | stopResponse, err := m.ctl.ControlService(ctx, &svcctl.ControlServiceRequest{ 75 | Service: svc.handle, 76 | Control: ServiceControlStop, 77 | }) 78 | 79 | if err != nil { 80 | if stopResponse == nil || stopResponse.Return != ErrorServiceNotActive { 81 | 82 | log.Error().Err(err).Msg("Failed to stop existing service") 83 | return fmt.Errorf("stop service: %w", err) 84 | } 85 | 86 | log.Debug().Msg("Service is not running") 87 | 88 | // FEATURE: restore state 89 | /* 90 | defer m.AddCleaners(func(ctxInner context.Context) error { 91 | // ... 92 | return nil 93 | }) 94 | */ 95 | 96 | } else { 97 | log.Info().Msg("Stopped existing service") 98 | } 99 | 100 | req := &svcctl.ChangeServiceConfigWRequest{ 101 | Service: svc.handle, 102 | BinaryPathName: in.String(), 103 | DisplayName: svc.originalConfig.DisplayName, 104 | ServiceType: svc.originalConfig.ServiceType, 105 | StartType: ServiceDemandStart, 106 | ErrorControl: svc.originalConfig.ErrorControl, 107 | LoadOrderGroup: svc.originalConfig.LoadOrderGroup, 108 | ServiceStartName: svc.originalConfig.ServiceStartName, 109 | TagID: svc.originalConfig.TagID, 110 | Dependencies: parseDependencies(svc.originalConfig.Dependencies), 111 | } 112 | 113 | bpn := svc.originalConfig.BinaryPathName 114 | 115 | _, err = m.ctl.ChangeServiceConfigW(ctx, req) 116 | 117 | if err != nil { 118 | log.Error().Err(err).Msg("Failed to request service configuration change") 119 | return fmt.Errorf("change service config request: %w", err) 120 | } 121 | 122 | if !m.NoStart { 123 | err = m.startService(ctx, svc) 124 | if err != nil { 125 | log.Error().Err(err).Msg("Failed to start service") 126 | } 127 | } 128 | 129 | if !m.NoRevert { 130 | if svc.handle == nil { 131 | 132 | if err = m.Reconnect(ctx); err != nil { 133 | return err 134 | } 135 | svc, err = m.openService(ctx, svc.name) 136 | 137 | if err != nil { 138 | log.Error().Err(err).Msg("Failed to reopen service handle") 139 | return fmt.Errorf("reopen service: %w", err) 140 | } 141 | } 142 | req.BinaryPathName = bpn 143 | req.Service = svc.handle 144 | _, err := m.ctl.ChangeServiceConfigW(ctx, req) 145 | 146 | if err != nil { 147 | log.Error().Err(err).Msg("Failed to restore original service configuration") 148 | return fmt.Errorf("restore service config: %w", err) 149 | } 150 | log.Info().Msg("Restored original service configuration") 151 | } 152 | 153 | return 154 | } 155 | -------------------------------------------------------------------------------- /pkg/goexec/scmr/create.go: -------------------------------------------------------------------------------- 1 | package scmrexec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/FalconOpsLLC/goexec/internal/util" 7 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 8 | "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" 9 | "github.com/rs/zerolog" 10 | 11 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 12 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 13 | ) 14 | 15 | const ( 16 | MethodCreate = "Create" 17 | ) 18 | 19 | type ScmrCreate struct { 20 | Scmr 21 | goexec.Cleaner 22 | goexec.Executor 23 | 24 | IO goexec.ExecutionIO 25 | 26 | NoDelete bool 27 | NoStart bool 28 | ServiceName string 29 | DisplayName string 30 | } 31 | 32 | func (m *ScmrCreate) ensure() { 33 | if m.ServiceName == "" { 34 | m.ServiceName = util.RandomString() 35 | } 36 | if m.DisplayName == "" { 37 | m.DisplayName = m.ServiceName 38 | } 39 | } 40 | 41 | func (m *ScmrCreate) Execute(ctx context.Context, in *goexec.ExecutionIO) (err error) { 42 | m.ensure() 43 | 44 | log := zerolog.Ctx(ctx).With(). 45 | Str("service", m.ServiceName).Logger() 46 | 47 | svc := &service{name: m.ServiceName} 48 | 49 | resp, err := m.ctl.CreateServiceW(ctx, &svcctl.CreateServiceWRequest{ 50 | ServiceManager: m.scm, 51 | ServiceName: m.ServiceName, 52 | DisplayName: m.DisplayName, 53 | BinaryPathName: in.String(), 54 | ServiceType: ServiceWin32OwnProcess, 55 | StartType: ServiceDemandStart, 56 | DesiredAccess: ServiceAllAccess, // TODO: Replace 57 | }) 58 | 59 | if err != nil { 60 | log.Error().Err(err).Msg("Create service request failed") 61 | return fmt.Errorf("create service request: %w", err) 62 | } 63 | 64 | if resp.Return != 0 { 65 | log.Error().Err(err).Msg("Failed to create service") 66 | return fmt.Errorf("create service returned non-zero exit code: %02x", resp.Return) 67 | } 68 | 69 | if !m.NoDelete { 70 | m.AddCleaners(func(ctxInner context.Context) error { 71 | 72 | r, errInner := m.ctl.DeleteService(ctxInner, &svcctl.DeleteServiceRequest{ 73 | Service: svc.handle, 74 | }) 75 | if errInner != nil { 76 | return fmt.Errorf("delete service: %w", errInner) 77 | } 78 | if r.Return != 0 { 79 | return fmt.Errorf("delete service returned non-zero exit code: %02x", r.Return) 80 | } 81 | log.Info().Msg("Deleted service") 82 | 83 | return nil 84 | }) 85 | } 86 | 87 | m.AddCleaners(func(ctxInner context.Context) error { 88 | 89 | r, errInner := m.ctl.CloseService(ctxInner, &svcctl.CloseServiceRequest{ 90 | ServiceObject: svc.handle, 91 | }) 92 | if errInner != nil { 93 | return fmt.Errorf("close service: %w", errInner) 94 | } 95 | if r.Return != 0 { 96 | return fmt.Errorf("close service returned non-zero exit code: %02x", r.Return) 97 | } 98 | log.Info().Msg("Closed service handle") 99 | 100 | return nil 101 | }) 102 | 103 | log.Info().Msg("Created service") 104 | svc.handle = resp.Service 105 | 106 | if !m.NoStart { 107 | 108 | err = m.startService(ctx, svc) 109 | 110 | if err != nil { 111 | log.Error().Err(err).Msg("Failed to start service") 112 | return fmt.Errorf("start service: %w", err) 113 | } 114 | } 115 | if svc.handle == nil { 116 | 117 | if err = m.Reconnect(ctx); err != nil { 118 | return err 119 | } 120 | svc, err = m.openService(ctx, svc.name) 121 | 122 | if err != nil { 123 | log.Error().Err(err).Msg("Failed to reopen service handle") 124 | return fmt.Errorf("reopen service: %w", err) 125 | } 126 | } 127 | 128 | return 129 | } 130 | -------------------------------------------------------------------------------- /pkg/goexec/scmr/delete.go: -------------------------------------------------------------------------------- 1 | package scmrexec 2 | 3 | import ( 4 | "context" 5 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 6 | ) 7 | 8 | const ( 9 | MethodDelete = "Delete" 10 | ) 11 | 12 | type ScmrDelete struct { 13 | Scmr 14 | goexec.Cleaner 15 | 16 | IO goexec.ExecutionIO 17 | 18 | ServiceName string 19 | } 20 | 21 | func (m *ScmrDelete) Call(ctx context.Context) (err error) { 22 | 23 | svc, err := m.openService(ctx, m.ServiceName) 24 | if err != nil { 25 | return err 26 | } 27 | defer m.AddCleaners(func(ctxInner context.Context) error { return m.closeService(ctx, svc) }) 28 | 29 | return m.deleteService(ctx, svc) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/goexec/scmr/module.go: -------------------------------------------------------------------------------- 1 | package scmrexec 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/FalconOpsLLC/goexec/internal/util" 8 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 9 | "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" 10 | "github.com/oiweiwei/go-msrpc/dcerpc" 11 | "github.com/oiweiwei/go-msrpc/midl/uuid" 12 | "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" 13 | "github.com/rs/zerolog" 14 | 15 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 16 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 17 | ) 18 | 19 | type Scmr struct { 20 | goexec.Cleaner 21 | 22 | Client *dce.Client 23 | ctl svcctl.SvcctlClient 24 | scm *svcctl.Handle 25 | 26 | hostname string 27 | } 28 | 29 | const ( 30 | ModuleName = "SCMR" 31 | 32 | DefaultEndpoint = "ncacn_np:[svcctl]" 33 | ScmrUuid = "367ABB81-9844-35F1-AD32-98F038001003" 34 | ) 35 | 36 | func (m *Scmr) Connect(ctx context.Context) (err error) { 37 | 38 | if err = m.Client.Connect(ctx); err == nil { 39 | m.AddCleaners(m.Client.Close) 40 | } 41 | return 42 | } 43 | 44 | func (m *Scmr) Init(ctx context.Context) (err error) { 45 | 46 | log := zerolog.Ctx(ctx).With(). 47 | Str("module", ModuleName).Logger() 48 | 49 | if m.Client == nil || m.Client.Dce() == nil { 50 | return errors.New("DCE connection not initialized") 51 | } 52 | 53 | m.hostname, err = m.Client.Target.Hostname(ctx) 54 | if err != nil { 55 | log.Debug().Err(err).Msg("Failed to determine target hostname") 56 | } 57 | if m.hostname == "" { 58 | m.hostname = util.RandomHostname() 59 | } 60 | 61 | svcctlOpts := []dcerpc.Option{dcerpc.WithObjectUUID(uuid.MustParse(ScmrUuid))} 62 | 63 | if m.Client.Smb { 64 | svcctlOpts = append(svcctlOpts, dcerpc.WithInsecure()) 65 | } 66 | 67 | m.ctl, err = svcctl.NewSvcctlClient(ctx, m.Client.Dce(), svcctlOpts...) 68 | if err != nil { 69 | log.Error().Err(err).Msg("Failed to initialize SVCCTL client") 70 | return fmt.Errorf("create SVCCTL client: %w", err) 71 | } 72 | log.Info().Msg("Created SVCCTL client") 73 | 74 | resp, err := m.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{ 75 | MachineName: m.hostname, 76 | DatabaseName: "ServicesActive", 77 | DesiredAccess: ServiceAllAccess, 78 | }) 79 | if err != nil { 80 | log.Debug().Err(err).Msg("Failed to open SCM handle") 81 | return fmt.Errorf("open SCM handle: %w", err) 82 | } 83 | log.Info().Msg("Opened SCM handle") 84 | 85 | m.scm = resp.SCM 86 | 87 | return 88 | } 89 | 90 | func (m *Scmr) Reconnect(ctx context.Context) (err error) { 91 | 92 | if err = m.Client.Reconnect(ctx); err != nil { 93 | return fmt.Errorf("reconnect: %w", err) 94 | } 95 | if err = m.Init(ctx); err != nil { 96 | return fmt.Errorf("reconnect SCMR: %w", err) 97 | } 98 | return 99 | } 100 | 101 | // openService will a handle to the desired service 102 | func (m *Scmr) openService(ctx context.Context, name string) (svc *service, err error) { 103 | 104 | log := zerolog.Ctx(ctx) 105 | 106 | resp, err := m.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{ 107 | ServiceManager: m.scm, 108 | ServiceName: name, 109 | DesiredAccess: ServiceAllAccess, // TODO: dynamic 110 | }) 111 | if err != nil { 112 | log.Error().Err(err).Msg("Failed to open service handle") 113 | return nil, fmt.Errorf("open service: %w", err) 114 | } 115 | 116 | log.Info().Msg("Opened service handle") 117 | 118 | svc = new(service) 119 | svc.name = name 120 | svc.handle = resp.Service 121 | 122 | return 123 | } 124 | 125 | func (m *Scmr) startService(ctx context.Context, svc *service) error { 126 | 127 | log := zerolog.Ctx(ctx).With(). 128 | Str("service", svc.name).Logger() 129 | 130 | sr, err := m.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle}) 131 | 132 | if err != nil { 133 | 134 | if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case) 135 | log.Warn().Msg("Service execution deadline exceeded") 136 | svc.handle = nil 137 | return nil 138 | 139 | } else if sr.Return == ErrorServiceRequestTimeout { 140 | log.Info().Msg("Received request timeout. Execution was likely successful") 141 | return nil 142 | } 143 | 144 | log.Error().Err(err).Msg("Failed to start service") 145 | return fmt.Errorf("start service: %w", err) 146 | } 147 | log.Info().Msg("Service started successfully") 148 | return nil 149 | } 150 | 151 | func (m *Scmr) deleteService(ctx context.Context, svc *service) (err error) { 152 | 153 | log := zerolog.Ctx(ctx).With(). 154 | Str("service", svc.name).Logger() 155 | 156 | deleteResponse, err := m.ctl.DeleteService(ctx, &svcctl.DeleteServiceRequest{ 157 | Service: svc.handle, 158 | }) 159 | 160 | if err != nil { 161 | log.Error().Err(err).Msg("Failed to delete service") 162 | return fmt.Errorf("delete service: %w", err) 163 | } 164 | 165 | if deleteResponse.Return != 0 { 166 | log.Error().Err(err).Str("code", fmt.Sprintf("0x%02x", deleteResponse.Return)).Msg("Failed to delete service") 167 | return fmt.Errorf("delete service returned non-zero exit code: 0x%02x", deleteResponse.Return) 168 | } 169 | 170 | log.Info().Msg("Deleted service") 171 | return 172 | } 173 | 174 | func (m *Scmr) closeService(ctx context.Context, svc *service) (err error) { 175 | 176 | log := zerolog.Ctx(ctx).With(). 177 | Str("service", svc.name).Logger() 178 | 179 | closResponse, err := m.ctl.CloseService(ctx, &svcctl.CloseServiceRequest{ 180 | ServiceObject: svc.handle, 181 | }) 182 | 183 | if err != nil { 184 | log.Error().Err(err).Msg("Failed to close service handle") 185 | return fmt.Errorf("close service: %w", err) 186 | } 187 | 188 | if closResponse.Return != 0 { 189 | log.Error().Err(err).Str("code", fmt.Sprintf("0x%02x", closResponse.Return)).Msg("Failed to close service handle") 190 | return fmt.Errorf("close service returned non-zero exit code: 0x%02x", closResponse.Return) 191 | } 192 | 193 | log.Info().Msg("Closed service handle") 194 | return 195 | } 196 | -------------------------------------------------------------------------------- /pkg/goexec/scmr/scmr.go: -------------------------------------------------------------------------------- 1 | package scmrexec 2 | 3 | import ( 4 | "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" 5 | "golang.org/x/text/encoding/unicode" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | ErrorServiceRequestTimeout uint32 = 0x0000041d 11 | ErrorServiceNotActive uint32 = 0x00000426 12 | 13 | ServiceDemandStart uint32 = 0x00000003 14 | ServiceWin32OwnProcess uint32 = 0x00000010 15 | 16 | // https://learn.microsoft.com/en-us/windows/win32/services/service-security-and-access-rights 17 | 18 | ServiceQueryConfig uint32 = 0x00000001 19 | ServiceChangeConfig uint32 = 0x00000002 20 | ServiceStart uint32 = 0x00000010 21 | ServiceStop uint32 = 0x00000020 22 | ServiceDelete uint32 = 0x00010000 // special permission 23 | ServiceControlStop uint32 = 0x00000001 24 | ScManagerCreateService uint32 = 0x00000002 25 | 26 | /* 27 | // Windows error codes 28 | ERROR_FILE_NOT_FOUND uint32 = 0x00000002 29 | ERROR_SERVICE_DOES_NOT_EXIST uint32 = 0x00000424 30 | 31 | // Windows service/scm constants 32 | SERVICE_BOOT_START uint32 = 0x00000000 33 | SERVICE_SYSTEM_START uint32 = 0x00000001 34 | SERVICE_AUTO_START uint32 = 0x00000002 35 | SERVICE_DISABLED uint32 = 0x00000004 36 | 37 | // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/4e91ff36-ab5f-49ed-a43d-a308e72b0b3c 38 | SERVICE_CONTINUE_PENDING uint32 = 0x00000005 39 | SERVICE_PAUSE_PENDING uint32 = 0x00000006 40 | SERVICE_PAUSED uint32 = 0x00000007 41 | SERVICE_RUNNING uint32 = 0x00000004 42 | SERVICE_START_PENDING uint32 = 0x00000002 43 | SERVICE_STOP_PENDING uint32 = 0x00000003 44 | SERVICE_STOPPED uint32 = 0x00000001 45 | */ 46 | 47 | ServiceDeleteAccess = ServiceDelete 48 | ServiceModifyAccess = ServiceQueryConfig | ServiceChangeConfig | ServiceStop | ServiceStart | ServiceDelete 49 | ServiceCreateAccess = ScManagerCreateService | ServiceStart | ServiceStop | ServiceDelete 50 | ServiceAllAccess = ServiceCreateAccess | ServiceModifyAccess 51 | ) 52 | 53 | type service struct { 54 | name string 55 | handle *svcctl.Handle 56 | originalConfig *svcctl.QueryServiceConfigW 57 | } 58 | 59 | // parseDependencies will parse the dependencies returned from a RQueryServiceConfigW 60 | // response (svcctl.QueryServiceConfigWResponse) into a raw byte array compatible with 61 | // the lpDependencies field as defined in the microsoft docs. 62 | // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/3ab258d6-87b0-459e-8d83-a2cdd8038b78 63 | func parseDependencies(deps string) (out []byte) { 64 | if deps != "" && deps != "/" { 65 | 66 | if out, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes( 67 | []byte(strings.ReplaceAll(deps, "/", "\x00") + "\x00"), 68 | ); err == nil { 69 | return out 70 | } 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/goexec/smb/client.go: -------------------------------------------------------------------------------- 1 | package smb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/oiweiwei/go-smb2.fork" 8 | "github.com/rs/zerolog" 9 | "net" 10 | ) 11 | 12 | type Client struct { 13 | ClientOptions 14 | 15 | conn net.Conn 16 | sess *smb2.Session 17 | mount *smb2.Share 18 | 19 | connected bool 20 | share string 21 | } 22 | 23 | func (c *Client) Session() (sess *smb2.Session) { 24 | return c.sess 25 | } 26 | 27 | func (c *Client) String() string { 28 | return ClientName 29 | } 30 | 31 | func (c *Client) Logger(ctx context.Context) zerolog.Logger { 32 | return zerolog.Ctx(ctx).With().Str("client", c.String()).Logger() 33 | } 34 | 35 | func (c *Client) Mount(ctx context.Context, share string) (err error) { 36 | 37 | if c.sess == nil { 38 | return errors.New("SMB session not initialized") 39 | } 40 | 41 | c.mount, err = c.sess.Mount(share) 42 | zerolog.Ctx(ctx).Debug().Str("share", share).Msg("Mounted SMB share") 43 | c.share = share 44 | 45 | return 46 | } 47 | 48 | func (c *Client) Connect(ctx context.Context) (err error) { 49 | 50 | log := c.Logger(ctx) 51 | { 52 | if c.netDialer == nil { 53 | panic(fmt.Errorf("TCP dialer not initialized")) 54 | } 55 | if c.dialer == nil { 56 | panic(fmt.Errorf("%s dialer not initialized", c.String())) 57 | } 58 | } 59 | 60 | // Establish TCP connection 61 | c.conn, err = c.netDialer.Dial("tcp", net.JoinHostPort(c.Host, "445")) 62 | 63 | if err != nil { 64 | return err 65 | } 66 | 67 | log = log.With().Str("address", c.conn.RemoteAddr().String()).Logger() 68 | log.Debug().Msgf("Connected to %s server", c.String()) 69 | 70 | // Open SMB session 71 | c.sess, err = c.dialer.DialContext(ctx, c.conn) 72 | 73 | if err != nil { 74 | log.Error().Err(err).Msgf("Failed to open %s session", c.String()) 75 | return fmt.Errorf("dial %s: %w", c.String(), err) 76 | } 77 | log.Debug().Msgf("Opened %s session", c.String()) 78 | 79 | c.connected = true 80 | 81 | return 82 | } 83 | 84 | func (c *Client) Close(ctx context.Context) (err error) { 85 | 86 | log := c.Logger(ctx) 87 | 88 | c.connected = false 89 | 90 | // Close SMB session 91 | if c.sess != nil { 92 | defer func() { 93 | if err = c.sess.Logoff(); err != nil { 94 | log.Debug().Err(err).Msgf("Failed to discard SMB session") 95 | } else { 96 | log.Debug().Msg("Discarded SMB session") 97 | } 98 | }() 99 | 100 | } else if c.conn != nil { 101 | 102 | defer func() { 103 | if err = c.conn.Close(); err != nil { 104 | log.Debug().Err(err).Msgf("Failed to disconnect SMB client") 105 | } else { 106 | log.Debug().Msg("Disconnected SMB client") 107 | } 108 | }() 109 | } 110 | 111 | // Unmount SMB share 112 | if c.mount != nil { 113 | defer func() { 114 | if err = c.mount.Umount(); err != nil { 115 | log.Debug().Err(err).Msg("Failed to unmount share") 116 | } else { 117 | log.Debug().Msg("Unmounted file share") 118 | } 119 | c.share = "" 120 | }() 121 | } 122 | return 123 | } 124 | -------------------------------------------------------------------------------- /pkg/goexec/smb/default.go: -------------------------------------------------------------------------------- 1 | package smb 2 | 3 | import "github.com/oiweiwei/go-msrpc/smb2" 4 | 5 | const ( 6 | ClientName = "SMB" 7 | 8 | DefaultPort = 445 9 | DefaultDialect = smb2.SMB311 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/goexec/smb/input.go: -------------------------------------------------------------------------------- 1 | package smb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "io" 8 | "os" 9 | "path" 10 | "strings" 11 | ) 12 | 13 | type FileStager struct { 14 | goexec.Cleaner 15 | 16 | Client *Client 17 | 18 | Share string 19 | SharePath string 20 | File string 21 | relativePath string 22 | ForceReconnect bool 23 | DeleteStage bool 24 | } 25 | 26 | func (o *FileStager) Stage(ctx context.Context, reader io.Reader) (err error) { 27 | 28 | o.relativePath = path.Join( 29 | strings.ReplaceAll(pathPrefix.ReplaceAllString(o.SharePath, ""), `\`, "/"), 30 | strings.ReplaceAll(pathPrefix.ReplaceAllString(o.File, ""), `\`, "/"), 31 | ) 32 | 33 | if o.ForceReconnect || !o.Client.connected { 34 | err = o.Client.Connect(ctx) 35 | if err != nil { 36 | return 37 | } 38 | defer o.AddCleaners(o.Client.Close) 39 | } 40 | 41 | if o.ForceReconnect || o.Client.share != o.Share { 42 | err = o.Client.Mount(ctx, o.Share) 43 | if err != nil { 44 | return 45 | } 46 | } 47 | 48 | writer, err := o.Client.mount.OpenFile(o.relativePath, os.O_WRONLY, 0644) 49 | if err != nil { 50 | return fmt.Errorf("open remote file for writing: %w", err) 51 | } 52 | 53 | if _, err = io.Copy(writer, reader); err != nil { 54 | return 55 | } 56 | 57 | o.AddCleaners(func(_ context.Context) error { return writer.Close() }) 58 | 59 | if o.DeleteStage { 60 | o.AddCleaners(func(_ context.Context) error { 61 | return o.Client.mount.Remove(o.relativePath) 62 | }) 63 | } 64 | 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /pkg/goexec/smb/options.go: -------------------------------------------------------------------------------- 1 | package smb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 8 | "github.com/RedTeamPentesting/adauth/smbauth" 9 | msrpcSMB2 "github.com/oiweiwei/go-msrpc/smb2" 10 | "github.com/oiweiwei/go-smb2.fork" 11 | "net" 12 | ) 13 | 14 | var supportedDialects = map[msrpcSMB2.Dialect]msrpcSMB2.Dialect{ 15 | 2_0_2: msrpcSMB2.SMB202, 16 | 2_1_0: msrpcSMB2.SMB210, 17 | 3_0_0: msrpcSMB2.SMB300, 18 | 3_0_2: msrpcSMB2.SMB302, 19 | 3_1_1: msrpcSMB2.SMB311, 20 | 21 | 0x202: msrpcSMB2.SMB202, 22 | 0x210: msrpcSMB2.SMB210, 23 | 0x300: msrpcSMB2.SMB300, 24 | 0x302: msrpcSMB2.SMB302, 25 | 0x311: msrpcSMB2.SMB311, 26 | } 27 | 28 | // ClientOptions holds configuration settings for an SMB client 29 | type ClientOptions struct { 30 | goexec.ClientOptions 31 | goexec.AuthOptions 32 | 33 | // NoSign disables packet signing 34 | NoSign bool `json:"no_sign" yaml:"no_sign"` 35 | 36 | // NoSeal disables packet encryption 37 | NoSeal bool `json:"no_seal" yaml:"no_seal"` 38 | 39 | // Dialect sets the SMB dialect to be passed to smb2.WithDialect() 40 | Dialect msrpcSMB2.Dialect `json:"dialect" yaml:"dialect"` 41 | 42 | netDialer goexec.Dialer 43 | dialer *smb2.Dialer 44 | } 45 | 46 | func (c *Client) Parse(ctx context.Context) (err error) { 47 | 48 | var do []msrpcSMB2.DialerOption 49 | 50 | if c.Dialect != 0 { // Use specific dialect 51 | 52 | // Validate SMB dialect/version 53 | if d, ok := supportedDialects[c.Dialect]; ok { 54 | do = append(do, msrpcSMB2.WithDialect(d)) 55 | 56 | } else { 57 | return errors.New("unsupported SMB version") 58 | } 59 | } 60 | 61 | if c.Proxy == "" { 62 | c.netDialer = &net.Dialer{} 63 | 64 | } else { 65 | // Parse proxy URL 66 | c.netDialer, err = goexec.ParseProxyURI(c.Proxy) 67 | if err != nil { 68 | return err 69 | } 70 | } 71 | 72 | if !c.NoSeal { 73 | // Enable encryption 74 | do = append(do, msrpcSMB2.WithSeal()) 75 | } 76 | if !c.NoSign { 77 | // Enable signing 78 | do = append(do, msrpcSMB2.WithSign()) 79 | } 80 | 81 | // Validate authentication parameters 82 | c.dialer, err = smbauth.Dialer(ctx, c.Credential, c.Target, 83 | &smbauth.Options{ 84 | KerberosDialer: c.netDialer, 85 | SMBOptions: do, 86 | }) 87 | 88 | if err != nil { 89 | return fmt.Errorf("set %s auth: %w", ClientName, err) 90 | } 91 | 92 | c.Host = c.Target.AddressWithoutPort() 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/goexec/smb/output.go: -------------------------------------------------------------------------------- 1 | package smb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "github.com/rs/zerolog" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var ( 17 | DefaultOutputPollInterval = 1 * time.Second 18 | DefaultOutputPollTimeout = 60 * time.Second 19 | pathPrefix = regexp.MustCompile(`^([a-zA-Z]:)?[\\/]*`) 20 | ) 21 | 22 | type OutputFileFetcher struct { 23 | goexec.Cleaner 24 | 25 | Client *Client 26 | 27 | Share string 28 | SharePath string 29 | File string 30 | DeleteOutputFile bool 31 | ForceReconnect bool 32 | PollInterval time.Duration 33 | PollTimeout time.Duration 34 | 35 | relativePath string 36 | } 37 | 38 | func (o *OutputFileFetcher) GetOutput(ctx context.Context, writer io.Writer) (err error) { 39 | 40 | log := zerolog.Ctx(ctx) 41 | 42 | if o.PollInterval == 0 { 43 | o.PollInterval = DefaultOutputPollInterval 44 | } 45 | if o.PollTimeout == 0 { 46 | o.PollTimeout = DefaultOutputPollTimeout 47 | } 48 | 49 | shp := pathPrefix.ReplaceAllString(strings.ToLower(strings.ReplaceAll(o.SharePath, `\`, "/")), "") 50 | fp := pathPrefix.ReplaceAllString(strings.ToLower(strings.ReplaceAll(o.File, `\`, "/")), "") 51 | 52 | if o.relativePath, err = filepath.Rel(shp, fp); err != nil { 53 | return 54 | } 55 | 56 | log.Info().Str("path", o.relativePath).Msg("Fetching output file") 57 | 58 | if o.ForceReconnect || !o.Client.connected { 59 | err = o.Client.Connect(ctx) 60 | if err != nil { 61 | return 62 | } 63 | defer o.AddCleaners(o.Client.Close) 64 | } 65 | 66 | if o.ForceReconnect || o.Client.share != o.Share { 67 | err = o.Client.Mount(ctx, o.Share) 68 | if err != nil { 69 | return 70 | } 71 | } 72 | 73 | stopAt := time.Now().Add(o.PollTimeout) 74 | var reader io.ReadCloser 75 | 76 | for { 77 | if time.Now().After(stopAt) { 78 | return errors.New("execution output timeout") 79 | } 80 | if reader, err = o.Client.mount.OpenFile(o.relativePath, os.O_RDONLY, 0); err == nil { 81 | break 82 | } 83 | time.Sleep(o.PollInterval) 84 | } 85 | 86 | if _, err = io.Copy(writer, reader); err != nil { 87 | return 88 | } 89 | 90 | o.AddCleaners(func(_ context.Context) error { return reader.Close() }) 91 | 92 | if o.DeleteOutputFile { 93 | o.AddCleaners(func(_ context.Context) error { 94 | return o.Client.mount.Remove(o.relativePath) 95 | }) 96 | } 97 | 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/change.go: -------------------------------------------------------------------------------- 1 | package tschexec 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "fmt" 7 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 8 | "github.com/FalconOpsLLC/goexec/pkg/goexec/tsch/task" 9 | "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" 10 | "github.com/rs/zerolog" 11 | "regexp" 12 | "time" 13 | 14 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 15 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 16 | ) 17 | 18 | const ( 19 | FlagTaskUpdate uint32 = 0b_00000000_00000000_00000000_00000100 20 | MethodChange = "Change" 21 | DefaultWaitTime = 1 * time.Second 22 | ) 23 | 24 | type TschChange struct { 25 | Tsch 26 | goexec.Executor 27 | goexec.Cleaner 28 | 29 | IO goexec.ExecutionIO 30 | 31 | WorkingDirectory string 32 | NoStart bool 33 | NoRevert bool 34 | WaitTime time.Duration 35 | } 36 | 37 | func (m *TschChange) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { 38 | 39 | log := zerolog.Ctx(ctx).With(). 40 | Str("module", ModuleName). 41 | Str("method", MethodChange). 42 | Str("task", m.TaskPath). 43 | Logger() 44 | 45 | retrieveResponse, err := m.tsch.RetrieveTask(ctx, &itaskschedulerservice.RetrieveTaskRequest{ 46 | Path: m.TaskPath, 47 | }) 48 | 49 | if err != nil { 50 | log.Error().Err(err).Msg("Failed to retrieve task") 51 | return fmt.Errorf("retrieve task: %w", err) 52 | } 53 | if retrieveResponse.Return != 0 { 54 | log.Error().Err(err).Str("code", fmt.Sprintf("0x%02x", retrieveResponse.Return)). 55 | Msg("Failed to retrieve task") 56 | return fmt.Errorf("retrieve task returned non-zero exit code: %02x", retrieveResponse.Return) 57 | } 58 | 59 | log.Info().Msg("Successfully retrieved existing task definition") 60 | log.Debug().Str("xml", retrieveResponse.XML).Msg("Got task definition") 61 | 62 | tk := task.Task{} 63 | 64 | enc := regexp.MustCompile(`(?i)^<\?xml .*?\?>`) 65 | tkStr := enc.ReplaceAllString(retrieveResponse.XML, ``) 66 | 67 | if err = xml.Unmarshal([]byte(tkStr), &tk); err != nil { 68 | log.Error().Err(err).Msg("Failed to unmarshal task XML") 69 | 70 | return fmt.Errorf("unmarshal task XML: %w", err) 71 | } 72 | 73 | cmd := execIO.CommandLine() 74 | 75 | tk.Actions.Exec = append(tk.Actions.Exec, task.ExecAction{ 76 | Command: cmd[0], 77 | Arguments: cmd[1], 78 | WorkingDirectory: m.WorkingDirectory, 79 | }) 80 | 81 | doc, err := xml.Marshal(tk) 82 | 83 | if err != nil { 84 | log.Error().Err(err).Msg("failed to marshal task XML") 85 | return fmt.Errorf("marshal task: %w", err) 86 | } 87 | 88 | taskXml := TaskXmlHeader + string(doc) 89 | log.Debug().Str("xml", taskXml).Msg("Serialized new task") 90 | 91 | registerResponse, err := m.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ 92 | Path: m.TaskPath, 93 | XML: taskXml, 94 | Flags: FlagTaskUpdate, 95 | }) 96 | 97 | if !m.NoRevert { 98 | 99 | m.AddCleaners(func(ctxInner context.Context) error { 100 | 101 | revertResponse, err := m.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ 102 | Path: m.TaskPath, 103 | XML: retrieveResponse.XML, 104 | Flags: FlagTaskUpdate, 105 | }) 106 | 107 | if err != nil { 108 | return err 109 | } 110 | if revertResponse.Return != 0 { 111 | return fmt.Errorf("revert task definition returned non-zero exit code: %02x", revertResponse.Return) 112 | } 113 | return nil 114 | }) 115 | } 116 | 117 | if err != nil { 118 | log.Error().Err(err).Msg("Failed to update task") 119 | 120 | return fmt.Errorf("update task: %w", err) 121 | } 122 | if registerResponse.Return != 0 { 123 | log.Error().Err(err).Str("code", fmt.Sprintf("0x%02x", registerResponse.Return)).Msg("Failed to update task definition") 124 | 125 | return fmt.Errorf("update task returned non-zero exit code: %02x", registerResponse.Return) 126 | } 127 | log.Info().Msg("Successfully updated task definition") 128 | 129 | if !m.NoStart { 130 | 131 | runResponse, err := m.tsch.Run(ctx, &itaskschedulerservice.RunRequest{ 132 | Path: m.TaskPath, 133 | }) 134 | 135 | if err != nil { 136 | log.Error().Err(err).Msg("Failed to run modified task") 137 | 138 | return fmt.Errorf("run task: %w", err) 139 | } 140 | 141 | if ret := uint32(runResponse.Return); ret != 0 { 142 | log.Error().Str("code", fmt.Sprintf("0x%08x", ret)).Msg("Run task returned non-zero exit code") 143 | 144 | return fmt.Errorf("run task returned non-zero exit code: 0x%08x", ret) 145 | } 146 | 147 | log.Info().Msg("Successfully started modified task") 148 | } 149 | 150 | if m.WaitTime <= 0 { 151 | m.WaitTime = DefaultWaitTime 152 | } 153 | time.Sleep(m.WaitTime) 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/create.go: -------------------------------------------------------------------------------- 1 | package tschexec 2 | 3 | import ( 4 | "context" 5 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 6 | "github.com/rs/zerolog" 7 | "time" 8 | ) 9 | 10 | const ( 11 | MethodCreate = "Create" 12 | ) 13 | 14 | type TschCreate struct { 15 | Tsch 16 | goexec.Executor 17 | goexec.Cleaner 18 | 19 | IO goexec.ExecutionIO 20 | 21 | NoDelete bool 22 | CallDelete bool 23 | StartDelay time.Duration 24 | StopDelay time.Duration 25 | DeleteDelay time.Duration 26 | TimeOffset time.Duration 27 | // FEATURE: more opts 28 | } 29 | 30 | func (m *TschCreate) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { 31 | 32 | log := zerolog.Ctx(ctx).With(). 33 | Str("module", ModuleName). 34 | Str("method", MethodCreate). 35 | Str("task", m.TaskPath). 36 | Logger() 37 | 38 | startTime := time.Now().UTC().Add(m.StartDelay) 39 | stopTime := startTime.Add(m.StopDelay) 40 | 41 | trigger := taskTimeTrigger{ 42 | StartBoundary: startTime.Format(TaskXmlDurationFormat), 43 | Enabled: true, 44 | } 45 | 46 | var deleteAfter string 47 | 48 | if !m.NoDelete && !m.CallDelete { 49 | 50 | if m.StopDelay == 0 { 51 | m.StopDelay = time.Second // value is required, 1 second by default 52 | } 53 | trigger.EndBoundary = stopTime.Format(TaskXmlDurationFormat) 54 | deleteAfter = xmlDuration(m.DeleteDelay) 55 | } 56 | 57 | path, err := m.registerTask(ctx, 58 | ®isterOptions{ 59 | AllowStartOnDemand: true, 60 | AllowHardTerminate: true, 61 | Hidden: !m.NotHidden, 62 | triggers: taskTriggers{ 63 | TimeTriggers: []taskTimeTrigger{trigger}, 64 | }, 65 | DeleteAfter: deleteAfter, 66 | }, 67 | execIO, 68 | ) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if !m.NoDelete { 74 | if m.CallDelete { 75 | 76 | m.AddCleaners(func(ctxInner context.Context) error { 77 | 78 | log.Info().Msg("Waiting for task to start...") 79 | 80 | select { 81 | case <-ctxInner.Done(): 82 | log.Warn().Msg("Task deletion cancelled") 83 | 84 | case <-time.After(m.StartDelay + (5 * time.Second)): // 5 second buffer 85 | /* 86 | for { 87 | stat, err := m.tsch.GetLastRunInfo(ctx, &itaskschedulerservice.GetLastRunInfoRequest{ 88 | Path: path, 89 | }) 90 | if err != nil { 91 | log.Warn().Err(err).Msg("Failed to get last run info. Assuming task was executed") 92 | 93 | } else if stat.LastRuntime.AsTime().IsZero() { 94 | log.Warn().Msg("Task was not yet executed. Waiting 5 additional seconds") 95 | 96 | time.Sleep(5 * time.Second) 97 | continue 98 | } 99 | break 100 | } 101 | */ 102 | } 103 | return m.deleteTask(ctxInner, path) 104 | }) 105 | 106 | } else { 107 | log.Info().Time("when", stopTime).Msg("Task is scheduled to delete") 108 | } 109 | } 110 | return 111 | } 112 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/demand.go: -------------------------------------------------------------------------------- 1 | package tschexec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" 8 | "github.com/rs/zerolog" 9 | 10 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 11 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 12 | ) 13 | 14 | const ( 15 | MethodDemand = "Demand" 16 | ) 17 | 18 | type TschDemand struct { 19 | Tsch 20 | goexec.Executor 21 | goexec.Cleaner 22 | 23 | IO goexec.ExecutionIO 24 | 25 | NoDelete bool 26 | NoStart bool 27 | SessionId uint32 28 | } 29 | 30 | func (m *TschDemand) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { 31 | 32 | log := zerolog.Ctx(ctx).With(). 33 | Str("module", ModuleName). 34 | Str("method", MethodDemand). 35 | Str("task", m.TaskPath). 36 | Logger() 37 | 38 | path, err := m.registerTask(ctx, 39 | ®isterOptions{ 40 | AllowStartOnDemand: true, 41 | AllowHardTerminate: true, 42 | Hidden: !m.NotHidden, 43 | triggers: taskTriggers{}, 44 | }, 45 | execIO, 46 | ) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | log.Info().Msg("Task registered") 52 | 53 | if !m.NoDelete { 54 | m.AddCleaners(func(ctxInner context.Context) error { 55 | return m.deleteTask(ctxInner, path) 56 | }) 57 | } 58 | 59 | if !m.NoStart { 60 | 61 | var flags uint32 62 | if m.SessionId != 0 { 63 | flags |= 4 64 | } 65 | 66 | runResponse, err := m.tsch.Run(ctx, &itaskschedulerservice.RunRequest{ 67 | Path: path, 68 | Flags: flags, 69 | SessionID: m.SessionId, 70 | }) 71 | 72 | if err != nil { 73 | log.Error().Err(err).Msg("Failed to run task") 74 | return fmt.Errorf("run task: %w", err) 75 | } 76 | if ret := uint32(runResponse.Return); ret != 0 { 77 | log.Error().Str("code", fmt.Sprintf("0x%08x", ret)).Msg("Task returned non-zero exit code") 78 | return fmt.Errorf("task returned non-zero exit code: 0x%08x", ret) 79 | } 80 | 81 | log.Info().Msg("Task started successfully") 82 | } 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/module.go: -------------------------------------------------------------------------------- 1 | package tschexec 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "errors" 7 | "fmt" 8 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 9 | "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" 10 | "github.com/oiweiwei/go-msrpc/dcerpc" 11 | "github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1" 12 | "github.com/rs/zerolog" 13 | 14 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 15 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 16 | ) 17 | 18 | const ( 19 | ModuleName = "TSCH" 20 | ) 21 | 22 | type Tsch struct { 23 | goexec.Cleaner 24 | 25 | Client *dce.Client 26 | tsch itaskschedulerservice.TaskSchedulerServiceClient 27 | 28 | TaskPath string 29 | UserSid string 30 | NotHidden bool 31 | } 32 | 33 | type registerOptions struct { 34 | AllowStartOnDemand bool 35 | AllowHardTerminate bool 36 | StartWhenAvailable bool 37 | Hidden bool 38 | DeleteAfter string 39 | 40 | triggers taskTriggers 41 | } 42 | 43 | func (m *Tsch) Connect(ctx context.Context) (err error) { 44 | 45 | if err = m.Client.Connect(ctx); err == nil { 46 | m.AddCleaners(m.Client.Close) 47 | } 48 | return 49 | } 50 | 51 | func (m *Tsch) Init(ctx context.Context) (err error) { 52 | 53 | if m.Client.Dce() == nil { 54 | return errors.New("DCE connection not initialized") 55 | } 56 | 57 | // Create ITaskSchedulerService Client 58 | m.tsch, err = itaskschedulerservice.NewTaskSchedulerServiceClient(ctx, m.Client.Dce(), dcerpc.WithSeal()) 59 | return 60 | } 61 | 62 | func (m *Tsch) registerTask(ctx context.Context, opts *registerOptions, in *goexec.ExecutionIO) (path string, err error) { 63 | 64 | log := zerolog.Ctx(ctx).With(). 65 | Str("task", m.TaskPath). 66 | Logger() 67 | 68 | ctx = log.WithContext(ctx) 69 | 70 | principalId := "LocalSystem" 71 | 72 | settings := taskSettings{ 73 | MultipleInstancesPolicy: "IgnoreNew", 74 | IdleSettings: taskIdleSettings{ 75 | StopOnIdleEnd: true, 76 | RestartOnIdle: false, 77 | }, 78 | Enabled: true, 79 | Priority: 7, // a pretty standard value for scheduled tasks 80 | AllowHardTerminate: opts.AllowHardTerminate, 81 | AllowStartOnDemand: opts.AllowStartOnDemand, 82 | Hidden: opts.Hidden, 83 | StartWhenAvailable: opts.StartWhenAvailable, 84 | DeleteExpiredTaskAfter: opts.DeleteAfter, 85 | } 86 | 87 | principals := taskPrincipals{ 88 | Principals: []taskPrincipal{ 89 | { 90 | ID: principalId, 91 | UserID: m.UserSid, 92 | RunLevel: "HighestAvailable", 93 | }, 94 | }} 95 | 96 | cmdline := in.CommandLine() 97 | 98 | actions := taskActions{ 99 | Context: principalId, 100 | Exec: []taskActionExec{ 101 | { 102 | Command: cmdline[0], 103 | Arguments: cmdline[1], 104 | }, 105 | }, 106 | } 107 | 108 | def := simpleTask{ 109 | TaskVersion: TaskXmlVersion, 110 | TaskNamespace: TaskXmlNamespace, 111 | Triggers: opts.triggers, 112 | Actions: actions, 113 | Principals: principals, 114 | Settings: settings, 115 | } 116 | 117 | // Generate task XML content. See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f 118 | 119 | doc, err := xml.Marshal(def) 120 | 121 | if err != nil { 122 | log.Error().Err(err).Msg("failed to marshal task XML") 123 | return "", fmt.Errorf("marshal task: %w", err) 124 | } 125 | 126 | taskXml := TaskXmlHeader + string(doc) 127 | 128 | log.Debug().Str("content", taskXml).Msg("Generated task XML") 129 | 130 | registerResponse, err := m.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{ 131 | Path: m.TaskPath, 132 | XML: taskXml, 133 | Flags: 0, // FEATURE: dynamic 134 | SDDL: "", 135 | LogonType: 0, // FEATURE: dynamic 136 | CredsCount: 0, 137 | Creds: nil, 138 | }) 139 | 140 | if err != nil { 141 | log.Error().Err(err).Msg("Failed to register task") 142 | return "", fmt.Errorf("register task: %w", err) 143 | } 144 | log.Info().Msg("Scheduled task registered") 145 | 146 | return registerResponse.ActualPath, nil 147 | } 148 | 149 | func (m *Tsch) deleteTask(ctx context.Context, taskPath string) (err error) { 150 | 151 | log := zerolog.Ctx(ctx).With(). 152 | Str("path", taskPath).Logger() 153 | 154 | _, err = m.tsch.Delete(ctx, &itaskschedulerservice.DeleteRequest{ 155 | Path: taskPath, 156 | }) 157 | 158 | if err != nil { 159 | log.Error().Err(err).Msg("Failed to delete task") 160 | return fmt.Errorf("delete task: %w", err) 161 | } 162 | 163 | log.Info().Msg("Task deleted") 164 | 165 | return 166 | } 167 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/task/action.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "encoding/xml" 5 | ) 6 | 7 | // --------------------------------------------------------------------------- 8 | // shared base 9 | // --------------------------------------------------------------------------- 10 | 11 | // ActionType is the base for all actions (only carries the optional id attribute). 12 | type ActionType struct { 13 | XMLName xml.Name `xml:"-"` 14 | Id string `xml:"id,attr,omitempty"` 15 | } 16 | 17 | // --------------------------------------------------------------------------- 18 | // Exec 19 | // --------------------------------------------------------------------------- 20 | 21 | // ExecAction corresponds to (execActionType). 22 | type ExecAction struct { 23 | XMLName xml.Name `xml:"Exec"` 24 | ActionType 25 | 26 | // is the program or script to run. 27 | Command string `xml:"Command"` 28 | // are passed to the Command. 29 | Arguments string `xml:"Arguments,omitempty"` 30 | // sets the cwd for the process. 31 | WorkingDirectory string `xml:"WorkingDirectory,omitempty"` 32 | } 33 | 34 | // --------------------------------------------------------------------------- 35 | // ComHandler 36 | // --------------------------------------------------------------------------- 37 | 38 | // ComHandlerAction corresponds to (comHandlerActionType). 39 | type ComHandlerAction struct { 40 | XMLName xml.Name `xml:"ComHandler"` 41 | ActionType 42 | 43 | // is the COM class ID (GUID). 44 | ClassId string `xml:"ClassId"` 45 | // is passed into the handler (optional). 46 | Data string `xml:"Data,omitempty"` 47 | } 48 | 49 | // --------------------------------------------------------------------------- 50 | // SendEmail 51 | // --------------------------------------------------------------------------- 52 | 53 | // SendEmailAction corresponds to (sendEmailActionType). 54 | type SendEmailAction struct { 55 | XMLName xml.Name `xml:"SendEmail"` 56 | ActionType 57 | 58 | Server string `xml:"Server"` // SMTP server 59 | Subject string `xml:"Subject"` // email subject 60 | To string `xml:"To"` // semicolon‑separated 61 | Cc string `xml:"Cc,omitempty"` 62 | Bcc string `xml:"Bcc,omitempty"` 63 | ReplyTo string `xml:"ReplyTo,omitempty"` 64 | Body string `xml:"Body,omitempty"` 65 | // optional named header fields 66 | HeaderFields *NamedValues `xml:"HeaderFields,omitempty"` 67 | } 68 | 69 | // --------------------------------------------------------------------------- 70 | // ShowMessage 71 | // --------------------------------------------------------------------------- 72 | 73 | // ShowMessageAction corresponds to (showMessageActionType). 74 | type ShowMessageAction struct { 75 | XMLName xml.Name `xml:"ShowMessage"` 76 | ActionType 77 | 78 | Title string `xml:"Title"` // window title 79 | Message string `xml:"Message"` // body text 80 | } 81 | 82 | // --------------------------------------------------------------------------- 83 | // NamedValues (used by SendEmailAction.HeaderFields) 84 | // --------------------------------------------------------------------------- 85 | 86 | // NamedValues holds zero or more entries. 87 | type NamedValues struct { 88 | XMLName xml.Name //`xml:"HeaderFields"` 89 | Value []NamedValue `xml:"Value"` 90 | } 91 | 92 | // NamedValue is one name/value pair. 93 | type NamedValue struct { 94 | XMLName xml.Name `xml:"Value"` 95 | Name string `xml:"name,attr"` 96 | Value string `xml:",chardata"` 97 | } 98 | 99 | // --------------------------------------------------------------------------- 100 | // Actions container 101 | // --------------------------------------------------------------------------- 102 | 103 | // Actions corresponds to (actionsType). 104 | // It may contain any number of each action type, in any order, 105 | // and carries an optional Context attribute. 106 | type Actions struct { 107 | XMLName xml.Name `xml:"Actions"` 108 | 109 | // Context="" lets you override the default ("Author"). 110 | Context string `xml:"Context,attr,omitempty"` 111 | 112 | Exec []ExecAction `xml:"Exec,omitempty"` 113 | ComHandler []ComHandlerAction `xml:"ComHandler,omitempty"` 114 | SendEmail []SendEmailAction `xml:"SendEmail,omitempty"` 115 | ShowMessage []ShowMessageAction `xml:"ShowMessage,omitempty"` 116 | } 117 | 118 | /* 119 | // --------------------------------------------------------------------------- 120 | // Marshal / Unmarshal helpers 121 | // --------------------------------------------------------------------------- 122 | 123 | // MarshalXML satisfies xml.Marshaler. 124 | // It writes out the start tag (with optional Context attr), 125 | // then each child action in declaration order, then the end tag. 126 | func (a *Actions) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 127 | // prepare start element 128 | start.Name.Local = "Actions" 129 | if a.Context != "" { 130 | start.Attr = append(start.Attr, 131 | xml.Attr{Name: xml.Name{Local: "Context"}, Value: a.Context}, 132 | ) 133 | } 134 | // write 135 | if err := e.EncodeToken(start); err != nil { 136 | return err 137 | } 138 | // write children 139 | for _, act := range a.Exec { 140 | if err := e.Encode(act); err != nil { 141 | return err 142 | } 143 | } 144 | for _, act := range a.ComHandler { 145 | if err := e.Encode(act); err != nil { 146 | return err 147 | } 148 | } 149 | for _, act := range a.SendEmail { 150 | if err := e.Encode(act); err != nil { 151 | return err 152 | } 153 | } 154 | for _, act := range a.ShowMessage { 155 | if err := e.Encode(act); err != nil { 156 | return err 157 | } 158 | } 159 | // write 160 | if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { 161 | return err 162 | } 163 | return e.Flush() 164 | } 165 | 166 | // UnmarshalXML satisfies xml.Unmarshaler. 167 | // It reads the element (capturing Context attr), 168 | // then loops decoding any Exec, ComHandler, SendEmail, or ShowMessage children. 169 | func (a *Actions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 170 | // capture Context attribute 171 | for _, attr := range start.Attr { 172 | if attr.Name.Local == "Context" { 173 | a.Context = attr.Value 174 | } 175 | } 176 | 177 | // iterate tokens until 178 | for { 179 | tok, err := d.Token() 180 | if err != nil { 181 | return err 182 | } 183 | switch t := tok.(type) { 184 | case xml.StartElement: 185 | switch t.Name.Local { 186 | case "Exec": 187 | var act ExecAction 188 | if err := d.DecodeElement(&act, &t); err != nil { 189 | return err 190 | } 191 | a.Exec = append(a.Exec, act) 192 | 193 | case "ComHandler": 194 | var act ComHandlerAction 195 | if err := d.DecodeElement(&act, &t); err != nil { 196 | return err 197 | } 198 | a.ComHandler = append(a.ComHandler, act) 199 | 200 | case "SendEmail": 201 | var act SendEmailAction 202 | if err := d.DecodeElement(&act, &t); err != nil { 203 | return err 204 | } 205 | a.SendEmail = append(a.SendEmail, act) 206 | 207 | case "ShowMessage": 208 | var act ShowMessageAction 209 | if err := d.DecodeElement(&act, &t); err != nil { 210 | return err 211 | } 212 | a.ShowMessage = append(a.ShowMessage, act) 213 | 214 | default: 215 | // skip any unknown elements 216 | if err := d.Skip(); err != nil { 217 | return err 218 | } 219 | } 220 | 221 | case xml.EndElement: 222 | if t.Name.Local == start.Name.Local { 223 | // finished 224 | return nil 225 | } 226 | } 227 | } 228 | } 229 | */ 230 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/task/misc.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import "encoding/xml" 4 | 5 | // --------------------------------------------------------------------------- 6 | // RegistrationInfo (registrationInfoType) 7 | // --------------------------------------------------------------------------- 8 | 9 | // NamedValuePair represents one 10 | // entry within RegistrationInfo. 11 | type NamedValuePair struct { 12 | XMLName xml.Name `xml:"Identification"` 13 | Name string `xml:"name,attr"` 14 | Value string `xml:"value,attr"` 15 | } 16 | 17 | // RegistrationInfo corresponds to the element. 18 | // 19 | // Fields are all optional and appear in the same order as in the XSD. 20 | type RegistrationInfo struct { 21 | XMLName xml.Name `xml:"RegistrationInfo"` 22 | 23 | Date string `xml:"Date,omitempty"` // xs:dateTime 24 | Author string `xml:"Author,omitempty"` // xs:string 25 | Description string `xml:"Description,omitempty"` // xs:string 26 | URI string `xml:"URI,omitempty"` // xs:string 27 | Version string `xml:"Version,omitempty"` // xs:string 28 | Source string `xml:"Source,omitempty"` // xs:string 29 | Documentation string `xml:"Documentation,omitempty"` // xs:string 30 | SecurityDescriptor string `xml:"SecurityDescriptor,omitempty"` // xs:string (SDDL) 31 | Identification []NamedValuePair `xml:"Identification,omitempty"` // zero or more 32 | } 33 | 34 | // --------------------------------------------------------------------------- 35 | // Data (dataType) 36 | // --------------------------------------------------------------------------- 37 | 38 | // Data corresponds to the element under a TaskDefinition. 39 | // It can contain any well‑formed XML inside. 40 | type Data struct { 41 | XMLName xml.Name `xml:"Data"` 42 | InnerXML string `xml:",innerxml"` 43 | } 44 | 45 | // --------------------------------------------------------------------------- 46 | // Principal (principalType) 47 | // --------------------------------------------------------------------------- 48 | 49 | // RunLevelType enumerates the RunLevel element values. 50 | type RunLevelType string 51 | 52 | const ( 53 | RunLevelLeastPrivilege RunLevelType = "LeastPrivilege" 54 | RunLevelHighestAvailable RunLevelType = "HighestAvailable" 55 | ) 56 | 57 | // LogonType enumerates the LogonType element values. 58 | type LogonType string 59 | 60 | const ( 61 | LogonTypeNone LogonType = "None" 62 | LogonTypePassword LogonType = "Password" 63 | LogonTypeInteractiveToken LogonType = "InteractiveToken" 64 | LogonTypeS4U LogonType = "S4U" 65 | LogonTypeVirtualAccount LogonType = "VirtualAccount" 66 | LogonTypeGroup LogonType = "Group" 67 | ) 68 | 69 | // --------------------------------------------------------------------------- 70 | // Principals container (principalsType) 71 | // --------------------------------------------------------------------------- 72 | 73 | // Principals corresponds to the element. 74 | // It holds one or more entries. 75 | type Principals struct { 76 | XMLName xml.Name `xml:"Principals"` 77 | Principal []Principal `xml:"Principal"` 78 | } 79 | 80 | // Principal corresponds to the element within . 81 | type Principal struct { 82 | XMLName xml.Name `xml:"Principal"` 83 | Id string `xml:"id,attr,omitempty"` 84 | 85 | UserId string `xml:"UserId,omitempty"` // xs:string 86 | GroupId string `xml:"GroupId,omitempty"` // xs:string 87 | RunLevel RunLevelType `xml:"RunLevel,omitempty"` // default="LeastPrivilege" 88 | LogonType LogonType `xml:"LogonType,omitempty"` // default="InteractiveToken" 89 | DisplayName string `xml:"DisplayName,omitempty"` // xs:string 90 | } 91 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/task/settings.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import "encoding/xml" 4 | 5 | // Settings mirrors the element (settingsType). 6 | type Settings struct { 7 | XMLName xml.Name `xml:"Settings"` 8 | 9 | AllowStartOnDemand bool `xml:"AllowStartOnDemand,omitempty"` 10 | RestartOnFailure *RestartOnFailure `xml:"RestartOnFailure,omitempty"` 11 | MultipleInstancesPolicy MultipleInstancesPolicy `xml:"MultipleInstancesPolicy,omitempty"` 12 | DisallowStartIfOnBatteries bool `xml:"DisallowStartIfOnBatteries,omitempty"` 13 | StopIfGoingOnBatteries bool `xml:"StopIfGoingOnBatteries,omitempty"` 14 | AllowHardTerminate bool `xml:"AllowHardTerminate,omitempty"` 15 | StartWhenAvailable bool `xml:"StartWhenAvailable,omitempty"` 16 | NetworkProfileName string `xml:"NetworkProfileName,omitempty"` 17 | RunOnlyIfNetworkAvailable bool `xml:"RunOnlyIfNetworkAvailable,omitempty"` 18 | WakeToRun bool `xml:"WakeToRun,omitempty"` 19 | Enabled bool `xml:"Enabled,omitempty"` 20 | Hidden bool `xml:"Hidden,omitempty"` 21 | DeleteExpiredTaskAfter string `xml:"DeleteExpiredTaskAfter,omitempty"` 22 | IdleSettings *IdleSettings `xml:"IdleSettings,omitempty"` 23 | NetworkSettings *NetworkSettings `xml:"NetworkSettings,omitempty"` 24 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 25 | Priority byte `xml:"Priority,omitempty"` 26 | RunOnlyIfIdle bool `xml:"RunOnlyIfIdle,omitempty"` 27 | UseUnifiedSchedulingEngine bool `xml:"UseUnifiedSchedulingEngine,omitempty"` 28 | DisallowStartOnRemoteAppSession bool `xml:"DisallowStartOnRemoteAppSession,omitempty"` 29 | } 30 | 31 | // RestartOnFailure corresponds to (restartType), 32 | // retrying a failed task. 33 | type RestartOnFailure struct { 34 | XMLName xml.Name `xml:"RestartOnFailure"` 35 | Interval string `xml:"Interval"` // xs:duration (min PT1M, max P31D) 36 | Count uint8 `xml:"Count"` // unsignedByte ≥1 37 | } 38 | 39 | // MultipleInstancesPolicy enumerates policies for concurrent task instances. 40 | type MultipleInstancesPolicy string 41 | 42 | const ( 43 | Parallel MultipleInstancesPolicy = "Parallel" 44 | Queue MultipleInstancesPolicy = "Queue" 45 | IgnoreNew MultipleInstancesPolicy = "IgnoreNew" 46 | StopExisting MultipleInstancesPolicy = "StopExisting" 47 | ) 48 | 49 | // IdleSettings corresponds to (idleSettingsType), 50 | // controlling idle‐based execution. 51 | type IdleSettings struct { 52 | XMLName xml.Name `xml:"IdleSettings"` 53 | StopOnIdleEnd bool `xml:"StopOnIdleEnd,omitempty"` 54 | RestartOnIdle bool `xml:"RestartOnIdle,omitempty"` 55 | Duration string `xml:"Duration,omitempty"` // xs:duration (deprecated) 56 | WaitTimeout string `xml:"WaitTimeout,omitempty"` // xs:duration (deprecated) 57 | } 58 | 59 | // NetworkSettings corresponds to (networkSettingsType), 60 | // specifying which network profile to await. 61 | type NetworkSettings struct { 62 | XMLName xml.Name `xml:"NetworkSettings"` 63 | Name string `xml:"Name,omitempty"` // nonEmptyString 64 | Id string `xml:"Id,omitempty"` // guidType 65 | } 66 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/task/task.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import "encoding/xml" 4 | 5 | // --------------------------------------------------------------------------- 6 | // Task (TaskDefinitionType / root element) 7 | // --------------------------------------------------------------------------- 8 | 9 | // Task represents the root element (type TaskDefinitionType). 10 | // It pulls together RegistrationInfo, Triggers, Principals, Settings, Actions, and Data. 11 | type Task struct { 12 | XMLName xml.Name `xml:"Task"` 13 | Version string `xml:"version,attr"` // required 14 | Xmlns string `xml:"xmlns,attr,omitempty"` // e.g. "http://schemas.microsoft.com/windows/2004/02/mit/task" 15 | 16 | RegistrationInfo *RegistrationInfo `xml:"RegistrationInfo,omitempty"` 17 | Triggers *Triggers `xml:"Triggers,omitempty"` 18 | Principals *Principals `xml:"Principals,omitempty"` 19 | Settings *Settings `xml:"Settings,omitempty"` 20 | Actions *Actions `xml:"Actions"` // required 21 | Data *Data `xml:"Data,omitempty"` 22 | } 23 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/task/trigger.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import "encoding/xml" 4 | 5 | // Triggers corresponds to the container (triggersType) 6 | // and may hold any number of each trigger type, in schema order. 7 | type Triggers struct { 8 | XMLName xml.Name `xml:"Triggers"` 9 | Boot []BootTrigger `xml:"BootTrigger,omitempty"` 10 | Time []TimeTrigger `xml:"TimeTrigger,omitempty"` 11 | Calendar []CalendarTrigger `xml:"CalendarTrigger,omitempty"` 12 | Event []EventTrigger `xml:"EventTrigger,omitempty"` 13 | Idle []IdleTrigger `xml:"IdleTrigger,omitempty"` 14 | Logon []LogonTrigger `xml:"LogonTrigger,omitempty"` 15 | Registration []RegistrationTrigger `xml:"RegistrationTrigger,omitempty"` 16 | SessionStateChange []SessionStateChangeTrigger `xml:"SessionStateChangeTrigger,omitempty"` 17 | } 18 | 19 | // Repetition corresponds to the element (repetitionType), 20 | // defining how often and for how long a trigger will re‑fire. 21 | type Repetition struct { 22 | XMLName xml.Name `xml:"Repetition"` 23 | Interval string `xml:"Interval"` // duration, e.g. PT5M 24 | StopAtDurationEnd bool `xml:"StopAtDurationEnd,omitempty"` // default=false 25 | Duration string `xml:"Duration,omitempty"` // duration, max span 26 | } 27 | 28 | // BootTrigger starts a task when the system boots. 29 | // Inherits StartBoundary, EndBoundary, Enabled, Repetition, ExecutionTimeLimit. 30 | type BootTrigger struct { 31 | XMLName xml.Name `xml:"BootTrigger"` 32 | Id string `xml:"id,attr,omitempty"` 33 | StartBoundary string `xml:"StartBoundary"` 34 | EndBoundary string `xml:"EndBoundary,omitempty"` 35 | Enabled bool `xml:"Enabled,omitempty"` 36 | Repetition *Repetition `xml:"Repetition,omitempty"` 37 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 38 | Delay string `xml:"Delay,omitempty"` // duration after boot 39 | } 40 | 41 | // TimeTrigger fires once at a given time. 42 | // Adds RandomDelay to the base trigger. 43 | type TimeTrigger struct { 44 | XMLName xml.Name `xml:"TimeTrigger"` 45 | Id string `xml:"id,attr,omitempty"` 46 | StartBoundary string `xml:"StartBoundary"` 47 | EndBoundary string `xml:"EndBoundary,omitempty"` 48 | Enabled bool `xml:"Enabled,omitempty"` 49 | Repetition *Repetition `xml:"Repetition,omitempty"` 50 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 51 | RandomDelay string `xml:"RandomDelay,omitempty"` // optional jitter 52 | } 53 | 54 | // CalendarTrigger covers daily, weekly, monthly & DOW schedules. 55 | type CalendarTrigger struct { 56 | XMLName xml.Name `xml:"CalendarTrigger"` 57 | Id string `xml:"id,attr,omitempty"` 58 | StartBoundary string `xml:"StartBoundary"` 59 | EndBoundary string `xml:"EndBoundary,omitempty"` 60 | Enabled bool `xml:"Enabled,omitempty"` 61 | Repetition *Repetition `xml:"Repetition,omitempty"` 62 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 63 | RandomDelay string `xml:"RandomDelay,omitempty"` 64 | ScheduleByDay *DailySchedule `xml:"ScheduleByDay,omitempty"` 65 | ScheduleByWeek *WeeklySchedule `xml:"ScheduleByWeek,omitempty"` 66 | ScheduleByMonth *MonthlySchedule `xml:"ScheduleByMonth,omitempty"` 67 | ScheduleByMonthDayOfWeek *MonthlyDOWSchedule `xml:"ScheduleByMonthDayOfWeek,omitempty"` 68 | } 69 | 70 | // Support types for CalendarTrigger: 71 | 72 | // DailySchedule (dailyScheduleType): interval in days. 73 | type DailySchedule struct { 74 | DaysInterval int `xml:"DaysInterval,omitempty"` 75 | } 76 | 77 | // WeeklySchedule (weeklyScheduleType): weeks interval + days flag. 78 | type WeeklySchedule struct { 79 | WeeksInterval int `xml:"WeeksInterval,omitempty"` 80 | DaysOfWeek *DaysOfWeek `xml:"DaysOfWeek,omitempty"` 81 | } 82 | 83 | // MonthlySchedule (monthlyScheduleType): specific month days + months. 84 | type MonthlySchedule struct { 85 | DaysOfMonth *DaysOfMonth `xml:"DaysOfMonth,omitempty"` 86 | Months *Months `xml:"Months,omitempty"` 87 | } 88 | 89 | // MonthlyDOWSchedule (monthlyDayOfWeekScheduleType): weeks of month + days + months. 90 | type MonthlyDOWSchedule struct { 91 | Weeks *Weeks `xml:"Weeks,omitempty"` 92 | DaysOfWeek DaysOfWeek `xml:"DaysOfWeek"` 93 | Months *Months `xml:"Months,omitempty"` 94 | } 95 | 96 | // DaysOfWeek (daysOfWeekType): a set of empty elements indicating which weekdays. 97 | type DaysOfWeek struct { 98 | Monday *struct{} `xml:"Monday,omitempty"` 99 | Tuesday *struct{} `xml:"Tuesday,omitempty"` 100 | Wednesday *struct{} `xml:"Wednesday,omitempty"` 101 | Thursday *struct{} `xml:"Thursday,omitempty"` 102 | Friday *struct{} `xml:"Friday,omitempty"` 103 | Saturday *struct{} `xml:"Saturday,omitempty"` 104 | Sunday *struct{} `xml:"Sunday,omitempty"` 105 | } 106 | 107 | // DaysOfMonth (daysOfMonthType): list of numeric days in a month. 108 | type DaysOfMonth struct { 109 | Day []int `xml:"Day,omitempty"` 110 | } 111 | 112 | // Months (monthsType): empty elements for each month. 113 | type Months struct { 114 | January *struct{} `xml:"January,omitempty"` 115 | February *struct{} `xml:"February,omitempty"` 116 | March *struct{} `xml:"March,omitempty"` 117 | April *struct{} `xml:"April,omitempty"` 118 | May *struct{} `xml:"May,omitempty"` 119 | June *struct{} `xml:"June,omitempty"` 120 | July *struct{} `xml:"July,omitempty"` 121 | August *struct{} `xml:"August,omitempty"` 122 | September *struct{} `xml:"September,omitempty"` 123 | October *struct{} `xml:"October,omitempty"` 124 | November *struct{} `xml:"November,omitempty"` 125 | December *struct{} `xml:"December,omitempty"` 126 | } 127 | 128 | // Weeks (weeksType): list of "1"–"4" or "Last". 129 | type Weeks struct { 130 | Week []string `xml:"Week,omitempty"` 131 | } 132 | 133 | // EventTrigger fires on matching Windows events. 134 | type EventTrigger struct { 135 | XMLName xml.Name `xml:"EventTrigger"` 136 | Id string `xml:"id,attr,omitempty"` 137 | StartBoundary string `xml:"StartBoundary,omitempty"` 138 | EndBoundary string `xml:"EndBoundary,omitempty"` 139 | Enabled bool `xml:"Enabled,omitempty"` 140 | Repetition *Repetition `xml:"Repetition,omitempty"` 141 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 142 | Subscription string `xml:"Subscription"` // XPath query 143 | Delay string `xml:"Delay,omitempty"` 144 | ValueQueries *NamedValues `xml:"ValueQueries,omitempty"` 145 | } 146 | 147 | // IdleTrigger fires when the system goes idle. 148 | type IdleTrigger struct { 149 | XMLName xml.Name `xml:"IdleTrigger"` 150 | Id string `xml:"id,attr,omitempty"` 151 | StartBoundary string `xml:"StartBoundary"` 152 | EndBoundary string `xml:"EndBoundary,omitempty"` 153 | Enabled bool `xml:"Enabled,omitempty"` 154 | Repetition *Repetition `xml:"Repetition,omitempty"` 155 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 156 | } 157 | 158 | // LogonTrigger fires on user logon (optionally scoped by UserId). 159 | type LogonTrigger struct { 160 | XMLName xml.Name `xml:"LogonTrigger"` 161 | Id string `xml:"id,attr,omitempty"` 162 | StartBoundary string `xml:"StartBoundary"` 163 | EndBoundary string `xml:"EndBoundary,omitempty"` 164 | Enabled bool `xml:"Enabled,omitempty"` 165 | Repetition *Repetition `xml:"Repetition,omitempty"` 166 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 167 | UserId string `xml:"UserId,omitempty"` 168 | Delay string `xml:"Delay,omitempty"` 169 | } 170 | 171 | // RegistrationTrigger fires when the task is registered or updated. 172 | type RegistrationTrigger struct { 173 | XMLName xml.Name `xml:"RegistrationTrigger"` 174 | Id string `xml:"id,attr,omitempty"` 175 | StartBoundary string `xml:"StartBoundary"` 176 | EndBoundary string `xml:"EndBoundary,omitempty"` 177 | Enabled bool `xml:"Enabled,omitempty"` 178 | Repetition *Repetition `xml:"Repetition,omitempty"` 179 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 180 | Delay string `xml:"Delay,omitempty"` 181 | } 182 | 183 | // SessionStateChangeTrigger fires on terminal‑server session changes. 184 | type SessionStateChangeTrigger struct { 185 | XMLName xml.Name `xml:"SessionStateChangeTrigger"` 186 | Id string `xml:"id,attr,omitempty"` 187 | StartBoundary string `xml:"StartBoundary,omitempty"` 188 | EndBoundary string `xml:"EndBoundary,omitempty"` 189 | Enabled bool `xml:"Enabled,omitempty"` 190 | Repetition *Repetition `xml:"Repetition,omitempty"` 191 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 192 | StateChange string `xml:"StateChange"` // e.g. “Connect” or “Disconnect” 193 | UserId string `xml:"UserId,omitempty"` 194 | Delay string `xml:"Delay,omitempty"` 195 | } 196 | -------------------------------------------------------------------------------- /pkg/goexec/tsch/tsch.go: -------------------------------------------------------------------------------- 1 | package tschexec 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "regexp" 7 | "time" 8 | ) 9 | 10 | const ( 11 | TaskXmlHeader = `` 12 | TaskXmlNamespace = "http://schemas.microsoft.com/windows/2004/02/mit/task" 13 | TaskXmlVersion = "1.2" 14 | TaskXmlDurationFormat = "2006-01-02T15:04:05.9999999Z" 15 | ) 16 | 17 | var ( 18 | TaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`) 19 | TaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`) 20 | ) 21 | 22 | // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f 23 | 24 | type taskTriggers struct { 25 | XMLName xml.Name `xml:"Triggers"` 26 | TimeTriggers []taskTimeTrigger `xml:"TimeTrigger,omitempty"` 27 | } 28 | 29 | type taskTimeTrigger struct { 30 | XMLName xml.Name `xml:"TimeTrigger"` 31 | StartBoundary string `xml:"StartBoundary,omitempty"` // Derived from time.Time 32 | EndBoundary string `xml:"EndBoundary,omitempty"` // Derived from time.Time; must be > StartBoundary 33 | Enabled bool `xml:"Enabled"` 34 | } 35 | 36 | type taskIdleSettings struct { 37 | XMLName xml.Name `xml:"IdleSettings"` 38 | StopOnIdleEnd bool `xml:"StopOnIdleEnd"` 39 | RestartOnIdle bool `xml:"RestartOnIdle"` 40 | } 41 | 42 | type taskSettings struct { 43 | XMLName xml.Name `xml:"Settings"` 44 | Enabled bool `xml:"Enabled"` 45 | Hidden bool `xml:"Hidden"` 46 | DisallowStartIfOnBatteries bool `xml:"DisallowStartIfOnBatteries"` 47 | StopIfGoingOnBatteries bool `xml:"StopIfGoingOnBatteries"` 48 | AllowHardTerminate bool `xml:"AllowHardTerminate"` 49 | RunOnlyIfNetworkAvailable bool `xml:"RunOnlyIfNetworkAvailable"` 50 | AllowStartOnDemand bool `xml:"AllowStartOnDemand"` 51 | WakeToRun bool `xml:"WakeToRun"` 52 | RunOnlyIfIdle bool `xml:"RunOnlyIfIdle"` 53 | StartWhenAvailable bool `xml:"StartWhenAvailable"` 54 | Priority int `xml:"Priority,omitempty"` // 1 to 10 inclusive 55 | MultipleInstancesPolicy string `xml:"MultipleInstancesPolicy,omitempty"` 56 | ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"` 57 | DeleteExpiredTaskAfter string `xml:"DeleteExpiredTaskAfter,omitempty"` // Derived from time.Duration 58 | IdleSettings taskIdleSettings `xml:"IdleSettings,omitempty"` 59 | } 60 | 61 | type taskActionExec struct { 62 | XMLName xml.Name `xml:"Exec"` 63 | Command string `xml:"Command"` 64 | Arguments string `xml:"Arguments,omitempty"` 65 | } 66 | 67 | type taskActions struct { 68 | XMLName xml.Name `xml:"Actions"` 69 | Context string `xml:"Context,attr"` 70 | Exec []taskActionExec `xml:"Exec,omitempty"` 71 | } 72 | 73 | type taskPrincipals struct { 74 | XMLName xml.Name `xml:"Principals"` 75 | Principals []taskPrincipal `xml:"Principal,omitempty"` 76 | } 77 | 78 | type taskPrincipal struct { 79 | XMLName xml.Name `xml:"Principal"` 80 | ID string `xml:"id,attr"` 81 | UserID string `xml:"UserId"` 82 | RunLevel string `xml:"RunLevel"` 83 | } 84 | 85 | type simpleTask struct { 86 | XMLName xml.Name `xml:"Task"` 87 | TaskVersion string `xml:"version,attr"` 88 | TaskNamespace string `xml:"xmlns,attr"` 89 | Triggers taskTriggers `xml:"Triggers"` 90 | Actions taskActions `xml:"Actions"` 91 | Principals taskPrincipals `xml:"Principals"` 92 | Settings taskSettings `xml:"Settings"` 93 | } 94 | 95 | /* 96 | 97 | // newSettings just creates a taskSettings instance with the necessary values + a few dynamic ones 98 | func newSettings(terminate, onDemand, startWhenAvailable bool) *taskSettings { 99 | return &taskSettings{ 100 | MultipleInstancesPolicy: "IgnoreNew", 101 | AllowHardTerminate: terminate, 102 | IdleSettings: taskIdleSettings{ 103 | StopOnIdleEnd: true, 104 | RestartOnIdle: false, 105 | }, 106 | AllowStartOnDemand: onDemand, 107 | Enabled: true, 108 | Hidden: true, 109 | Priority: 7, // a pretty standard value for scheduled tasks 110 | StartWhenAvailable: startWhenAvailable, 111 | } 112 | } 113 | 114 | // newTask creates a task with any static values filled 115 | func newTask(se *taskSettings, pr []taskPrincipal, tr taskTriggers, cmd, args string) *simpleTask { 116 | if se == nil { 117 | se = newSettings(true, true, false) 118 | } 119 | if pr == nil || len(pr) == 0 { 120 | pr = []taskPrincipal{ 121 | { 122 | ID: "1", 123 | UserID: "S-1-5-18", 124 | RunLevel: "HighestAvailable", 125 | }, 126 | } 127 | } 128 | return &simpleTask{ 129 | TaskVersion: "1.2", 130 | TaskNamespace: "http://schemas.microsoft.com/windows/2004/02/mit/task", 131 | Triggers: tr, 132 | Principals: taskPrincipals{Principals: pr}, 133 | Settings: *se, 134 | Actions: taskActions{ 135 | Context: pr[0].ID, 136 | Exec: []taskActionExec{ 137 | { 138 | Command: cmd, 139 | Arguments: args, 140 | }, 141 | }, 142 | }, 143 | } 144 | } 145 | */ 146 | 147 | // xmlDuration is a *very* simple implementation of xs:duration - only accepts +seconds 148 | func xmlDuration(dur time.Duration) string { 149 | if s := int(dur.Seconds()); s >= 0 { 150 | return fmt.Sprintf(`PT%dS`, s) 151 | } 152 | return `PT0S` 153 | } 154 | 155 | // ValidateTaskName will validate the provided task name according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1 156 | func ValidateTaskName(taskName string) error { 157 | if !TaskNameRegex.MatchString(taskName) { 158 | return fmt.Errorf("invalid task name: %s", taskName) 159 | } 160 | return nil 161 | } 162 | 163 | // ValidateTaskPath will validate the provided task path according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1 164 | func ValidateTaskPath(taskPath string) error { 165 | if !TaskPathRegex.MatchString(taskPath) { 166 | return fmt.Errorf("invalid task path: %s", taskPath) 167 | } 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /pkg/goexec/wmi/call.go: -------------------------------------------------------------------------------- 1 | package wmiexec 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/rs/zerolog" 8 | "io" 9 | ) 10 | 11 | type WmiCall struct { 12 | Wmi 13 | 14 | Class string 15 | Method string 16 | Args map[string]any 17 | 18 | Out io.Writer 19 | } 20 | 21 | func (m *WmiCall) Call(ctx context.Context) (err error) { 22 | var outMap map[string]any 23 | 24 | if outMap, err = m.query(ctx, m.Class, m.Method, m.Args); err != nil { 25 | return 26 | } 27 | zerolog.Ctx(ctx).Info().Msg("WMI call successful") 28 | 29 | out, err := json.Marshal(outMap) 30 | 31 | if m.Out != nil { 32 | // Write output with a trailing line feed 33 | if _, err = m.Out.Write(append(out, 0x0a)); err != nil { 34 | return fmt.Errorf("write output: %w", err) 35 | } 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /pkg/goexec/wmi/module.go: -------------------------------------------------------------------------------- 1 | package wmiexec 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 8 | "github.com/FalconOpsLLC/goexec/pkg/goexec/dce" 9 | "github.com/oiweiwei/go-msrpc/dcerpc" 10 | "github.com/oiweiwei/go-msrpc/msrpc/dcom" 11 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0" 12 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi" 13 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemlevel1login/v0" 14 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0" 15 | "github.com/oiweiwei/go-msrpc/msrpc/dcom/wmio/query" 16 | "github.com/rs/zerolog" 17 | 18 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/ntstatus" 19 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/win32" 20 | _ "github.com/oiweiwei/go-msrpc/msrpc/erref/wmi" 21 | ) 22 | 23 | const ( 24 | ModuleName = "WMI" 25 | DefaultEndpoint = "ncacn_ip_tcp:[135]" 26 | ) 27 | 28 | type Wmi struct { 29 | goexec.Cleaner 30 | Client *dce.Client 31 | 32 | Resource string 33 | 34 | servicesClient iwbemservices.ServicesClient 35 | } 36 | 37 | func (m *Wmi) Connect(ctx context.Context) (err error) { 38 | 39 | if err = m.Client.Connect(ctx); err == nil { 40 | m.AddCleaners(m.Client.Close) 41 | } 42 | return 43 | } 44 | 45 | func (m *Wmi) Init(ctx context.Context) (err error) { 46 | 47 | log := zerolog.Ctx(ctx).With(). 48 | Str("module", ModuleName).Logger() 49 | 50 | if m.Client == nil || m.Client.Dce() == nil { 51 | return errors.New("DCE connection not initialized") 52 | } 53 | 54 | actClient, err := iactivation.NewActivationClient(ctx, m.Client.Dce()) 55 | if err != nil { 56 | log.Error().Err(err).Msg("Failed to initialize IActivation client") 57 | return fmt.Errorf("create IActivation client: %w", err) 58 | } 59 | 60 | actResponse, err := actClient.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{ 61 | ORPCThis: ORPCThis, 62 | ClassID: wmi.Level1LoginClassID.GUID(), 63 | IIDs: []*dcom.IID{iwbemlevel1login.Level1LoginIID}, 64 | RequestedProtocolSequences: []uint16{ProtocolSequenceRPC}, // FEATURE: Named pipe support? 65 | }) 66 | if err != nil { 67 | log.Error().Err(err).Msg("Failed to activate remote object") 68 | return fmt.Errorf("request remote activation: %w", err) 69 | } 70 | if actResponse.HResult != 0 { 71 | return fmt.Errorf("remote activation failed with code %d", actResponse.HResult) 72 | } 73 | 74 | log.Info().Msg("Remote activation succeeded") 75 | 76 | var newOpts []dcerpc.Option 77 | 78 | for _, bind := range actResponse.OXIDBindings.GetStringBindings() { 79 | stringBinding, err := dcerpc.ParseStringBinding(bind.String()) 80 | if err != nil { 81 | log.Debug().Err(err).Msg("Failed to parse string binding") 82 | continue 83 | } 84 | // Only consider ncacn_ip_tcp endpoints 85 | if stringBinding.ProtocolSequence == dcerpc.ProtocolSequenceIPTCP { 86 | stringBinding.NetworkAddress = m.Client.Target.AddressWithoutPort() 87 | newOpts = append(newOpts, dcerpc.WithEndpoint(stringBinding.String())) 88 | } 89 | } 90 | 91 | if err = m.Client.Reconnect(ctx, newOpts...); err != nil { 92 | log.Error().Err(err).Msg("Failed to connect to remote instance") 93 | return fmt.Errorf("connect remote instance: %w", err) 94 | } 95 | 96 | log.Info().Msg("Connected to remote instance") 97 | 98 | ipid := actResponse.InterfaceData[0].GetStandardObjectReference().Std.IPID 99 | loginClient, err := iwbemlevel1login.NewLevel1LoginClient(ctx, m.Client.Dce(), dcom.WithIPID(ipid)) 100 | 101 | if err != nil { 102 | log.Error().Err(err).Msg("Failed to create IWbemLevel1Login client") 103 | return fmt.Errorf("create IWbemLevel1Login client: %w", err) 104 | } 105 | 106 | login, err := loginClient.NTLMLogin(ctx, &iwbemlevel1login.NTLMLoginRequest{ 107 | This: ORPCThis, 108 | NetworkResource: m.Resource, 109 | }) 110 | 111 | if err != nil { 112 | log.Error().Err(err).Msg("Failed to login on remote instance") 113 | return fmt.Errorf("login: IWbemLevel1Login::NTLMLogin: %w", err) 114 | } 115 | 116 | log.Info().Msg("Completed NTLMLogin operation") 117 | 118 | ipid = login.Namespace.InterfacePointer().IPID() 119 | m.servicesClient, err = iwbemservices.NewServicesClient(ctx, m.Client.Dce(), dcom.WithIPID(ipid)) 120 | 121 | if err != nil { 122 | log.Error().Err(err).Msg("Failed to create services client") 123 | return fmt.Errorf("create IWbemServices client: %w", err) 124 | } 125 | 126 | log.Info().Msg("Initialized services client") 127 | 128 | return 129 | } 130 | 131 | func (m *Wmi) query(ctx context.Context, class, method string, values map[string]any) (map[string]any, error) { 132 | if m.servicesClient == nil { 133 | return nil, errors.New("module has not been initialized") 134 | } 135 | if out, err := query.NewBuilder(ctx, m.servicesClient, ComVersion). 136 | Spawn(class). // The class to instantiate (i.e., Win32_Process) 137 | Method(method). // The method to call (i.e., Create) 138 | Values(values). // The values to pass to method 139 | Exec(). 140 | Object(); err == nil { 141 | return out.Values(), err 142 | } else { 143 | return nil, fmt.Errorf("spawn WMI query: %w", err) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /pkg/goexec/wmi/proc.go: -------------------------------------------------------------------------------- 1 | package wmiexec 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/FalconOpsLLC/goexec/pkg/goexec" 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | const ( 11 | MethodProc = "Proc" 12 | ) 13 | 14 | type WmiProc struct { 15 | Wmi 16 | IO goexec.ExecutionIO 17 | WorkingDirectory string 18 | } 19 | 20 | func (m *WmiProc) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) { 21 | 22 | log := zerolog.Ctx(ctx).With(). 23 | Str("module", ModuleName). 24 | Str("method", MethodProc). 25 | Logger() 26 | ctx = log.WithContext(ctx) 27 | 28 | if execIO == nil { 29 | return errors.New("execution IO is nil") 30 | } 31 | 32 | out, err := m.query(ctx, 33 | "Win32_Process", 34 | "Create", 35 | map[string]any{ 36 | "CommandLine": execIO.String(), 37 | "WorkingDir": m.WorkingDirectory, 38 | }, 39 | ) 40 | if err != nil { 41 | return 42 | } 43 | 44 | if pid, ok := out["ProcessId"].(uint32); pid != 0 { 45 | log = log.With().Uint32("pid", pid).Logger() 46 | 47 | } else if !ok { 48 | return errors.New("process creation failed") 49 | } 50 | log.Info().Err(err).Msg("Process created") 51 | 52 | if ret, ok := out["ReturnValue"].(uint32); ret != 0 { 53 | log.Error().Err(err).Uint32("return", ret).Msg("Process returned non-zero exit code") 54 | 55 | } else if !ok { 56 | return errors.New("invalid call response") 57 | } 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /pkg/goexec/wmi/wmi.go: -------------------------------------------------------------------------------- 1 | package wmiexec 2 | 3 | import "github.com/oiweiwei/go-msrpc/msrpc/dcom" 4 | 5 | const ( 6 | ProtocolSequenceRPC uint16 = 7 7 | ProtocolSequenceNP uint16 = 15 8 | ) 9 | 10 | var ( 11 | ComVersion = &dcom.COMVersion{ 12 | MajorVersion: 5, 13 | MinorVersion: 7, 14 | } 15 | ORPCThis = &dcom.ORPCThis{Version: ComVersion} 16 | ) 17 | --------------------------------------------------------------------------------