rewrote everything lol

This commit is contained in:
Bryan McNulty
2025-04-16 12:11:58 -05:00
parent a827b67d47
commit 55eb4275fb
58 changed files with 2894 additions and 2034 deletions
+1
View File
@@ -5,6 +5,7 @@ WORKDIR /go/src/
COPY cmd/ cmd/
COPY internal/ internal/
COPY pkg/ pkg/
COPY main.go go.mod go.sum ./
ENV CGO_ENABLED=0
+3 -1
View File
@@ -8,13 +8,14 @@
- [X] Session hijacking
- [ ] Add command to tsch - update task if it already exists. See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167 (`flags` argument)
- [ ] Add more trigger types
- [ ] Generate random name/path
### SCMR
- [X] Clean up SCMR module
- [X] add dynamic string binding support
- [X] general clean up. Use TSCH & WMI as reference
- [ ] Output
- [X] Output
- [ ] Fix SCMR `change` method so that dependencies field isn't permanently overwritten
### DCOM
@@ -41,6 +42,7 @@
## Resolve Eventually
- [ ] Packet stub encryption for ncacn_ip_tcp doesn't appear to be working...
- [ ] `--shell` option
- [ ] Add Go tests
- [ ] ability to specify multiple targets
+126
View File
@@ -0,0 +1,126 @@
package cmd
import (
"context"
"errors"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func registerRpcFlags(cmd *cobra.Command) {
rpcFlags := pflag.NewFlagSet("RPC", pflag.ExitOnError)
rpcFlags.BoolVar(&rpcClient.NoEpm, "no-epm", false, "Do not use EPM to automatically detect endpoints")
//rpcFlags.BoolVar(&rpcClient.Options.EpmAuto, "epm-auto", false, "Automatically detect endpoints instead of using the module defaults")
rpcFlags.BoolVar(&rpcClient.NoSign, "no-sign", false, "Disable signing on DCE messages")
rpcFlags.BoolVar(&rpcClient.NoSeal, "no-seal", false, "Disable packet stub encryption on DCE messages")
rpcFlags.StringVar(&rpcClient.Filter, "epm-filter", "", "String binding to filter endpoints returned by EPM")
rpcFlags.StringVar(&rpcClient.Endpoint, "endpoint", "", "Explicit RPC endpoint definition")
cmd.PersistentFlags().AddFlagSet(rpcFlags)
cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter")
cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter")
}
func registerProcessExecutionArgs(cmd *cobra.Command) {
group := pflag.NewFlagSet("Execution", pflag.ExitOnError)
group.StringVarP(&exec.Input.Arguments, "args", "a", "", "Command line arguments")
group.StringVarP(&exec.Input.CommandLine, "command", "c", "", "Windows process command line (executable & arguments)")
group.StringVarP(&exec.Input.Executable, "executable", "e", "", "Windows executable to invoke")
cmd.PersistentFlags().AddFlagSet(group)
cmd.MarkFlagsOneRequired("executable", "command")
cmd.MarkFlagsMutuallyExclusive("executable", "command")
}
func registerExecutionOutputArgs(cmd *cobra.Command) {
group := pflag.NewFlagSet("Output", pflag.ExitOnError)
group.StringVarP(&outputPath, "output", "o", "", `Fetch execution output to file or "-" for standard output`)
group.StringVarP(&outputMethod, "output-method", "m", "smb", "Method to fetch execution output")
group.StringVar(&exec.Output.RemotePath, "remote-output", "", "Location to temporarily store output on remote filesystem")
group.BoolVar(&exec.Output.NoDelete, "no-delete-output", false, "Preserve output file on remote filesystem")
cmd.PersistentFlags().AddFlagSet(group)
}
func args(reqs ...func(*cobra.Command, []string) error) (fn func(*cobra.Command, []string) error) {
return func(cmd *cobra.Command, args []string) (err error) {
for _, req := range reqs {
if err = req(cmd, args); err != nil {
return
}
}
return
}
}
func argsTarget(proto string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) (err error) {
if len(args) != 1 {
return errors.New("command require exactly one positional argument: [target]")
}
if credential, target, err = authOpts.WithTarget(context.TODO(), proto, args[0]); err != nil {
return fmt.Errorf("failed to parse target: %w", err)
}
if credential == nil {
return errors.New("no credentials supplied")
}
if target == nil {
return errors.New("no target supplied")
}
return
}
}
func argsSmbClient() func(cmd *cobra.Command, args []string) error {
return args(
argsTarget("cifs"),
func(_ *cobra.Command, _ []string) error {
smbClient.Credential = credential
smbClient.Target = target
smbClient.Proxy = proxy
return smbClient.Parse(context.TODO())
},
)
}
func argsRpcClient(proto string) func(cmd *cobra.Command, args []string) error {
return args(
argsTarget(proto),
func(cmd *cobra.Command, args []string) (err error) {
rpcClient.Target = target
rpcClient.Credential = credential
rpcClient.Proxy = proxy
return rpcClient.Parse(context.TODO())
},
)
}
func argsOutput(methods ...string) func(cmd *cobra.Command, args []string) error {
var as []func(*cobra.Command, []string) error
for _, method := range methods {
if method == "smb" {
as = append(as, argsSmbClient())
}
}
return args(as...)
}
+53 -49
View File
@@ -1,38 +1,37 @@
package cmd
import (
"github.com/FalconOpsLLC/goexec/internal/exec"
dcomexec "github.com/FalconOpsLLC/goexec/internal/exec/dcom"
"github.com/spf13/cobra"
"context"
dcomexec "github.com/FalconOpsLLC/goexec/pkg/goexec/dcom"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/spf13/cobra"
)
func dcomCmdInit() {
registerRpcFlags(dcomCmd)
dcomMmcCmdInit()
dcomCmd.AddCommand(dcomMmcCmd)
registerRpcFlags(dcomCmd)
dcomMmcCmdInit()
dcomCmd.AddCommand(dcomMmcCmd)
}
func dcomMmcCmdInit() {
dcomMmcCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke")
dcomMmcCmd.Flags().StringVarP(&workingDirectory, "directory", "d", `C:\`, "Working directory")
dcomMmcCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Process command line")
dcomMmcCmd.Flags().StringVar(&windowState, "window", "Minimized", "Window state")
dcomMmcCmd.Flags().StringVarP(&command, "command", "c", ``, "Windows executable & arguments to run")
dcomMmcCmd.Flags().StringVarP(&dcomMmc.WorkingDirectory, "directory", "d", `C:\`, "Working directory")
dcomMmcCmd.Flags().StringVar(&dcomMmc.WindowState, "window", "Minimized", "Window state")
dcomMmcCmd.MarkFlagsOneRequired("executable", "command")
dcomMmcCmd.MarkFlagsMutuallyExclusive("executable", "command")
registerProcessExecutionArgs(dcomMmcCmd)
}
var (
dcomCmd = &cobra.Command{
Use: "dcom",
Short: "Establish execution via DCOM",
Args: cobra.NoArgs,
}
dcomMmcCmd = &cobra.Command{
Use: "mmc [target]",
Short: "Establish execution via the DCOM MMC20.Application object",
Long: `Description:
dcomMmc dcomexec.DcomMmc
dcomCmd = &cobra.Command{
Use: "dcom",
Short: "Establish execution via DCOM",
Args: cobra.NoArgs,
}
dcomMmcCmd = &cobra.Command{
Use: "mmc [target]",
Short: "Establish execution via the DCOM MMC20.Application object",
Long: `Description:
The mmc method uses the exposed MMC20.Application object to call Document.ActiveView.ShellExec,
and ultimately execute system commands.
@@ -42,34 +41,39 @@ References:
https://github.com/fortra/impacket/blob/master/examples/dcomexec.py
https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mmc/view-executeshellcommand
`,
Args: needsRpcTarget("host"),
Run: func(cmd *cobra.Command, args []string) {
Args: argsRpcClient("host"),
Run: func(cmd *cobra.Command, args []string) {
var err error
ctx = log.With().
Str("module", "dcom").
Str("method", "mmc").
Logger().WithContext(ctx)
ctx := gssapi.NewSecurityContext(context.Background())
module := dcomexec.Module{}
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
execCfg := &exec.ExecutionConfig{
ExecutableName: executable,
ExecutableArgs: executableArgs,
ExecutionMethod: dcomexec.MethodMmc,
ctx = log.With().
Str("module", "dcom").
Str("method", "mmc").
Logger().
WithContext(ctx)
ExecutionMethodConfig: dcomexec.MethodMmcConfig{
WorkingDirectory: workingDirectory,
WindowState: windowState,
},
}
if err := module.Connect(ctx, creds, target, connCfg); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
} else if err = module.Exec(ctx, execCfg); err != nil {
log.Fatal().Err(err).Msg("Execution failed")
}
},
}
if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
defer func() {
closeErr := rpcClient.Close(ctx)
if closeErr != nil {
log.Error().Err(closeErr).Msg("Failed to close connection")
}
}()
if err = dcomMmc.Init(ctx, &rpcClient); err != nil {
log.Error().Err(err).Msg("Module initialization failed")
returnCode = 1
return
}
if err = dcomMmc.Execute(ctx, exec.Input); err != nil {
log.Error().Err(err).Msg("Execution failed")
returnCode = 1
}
},
}
)
+74 -90
View File
@@ -1,122 +1,105 @@
package cmd
import (
"context"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/util"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
"github.com/FalconOpsLLC/goexec/pkg/goexec/smb"
"github.com/RedTeamPentesting/adauth"
"github.com/oiweiwei/go-msrpc/ssp"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"net/url"
"os"
"regexp"
"strings"
)
var (
//logFile string
log zerolog.Logger
ctx context.Context
authOpts *adauth.Options
debug bool
logJson bool
returnCode int
outputMethod string
outputPath string
proxy string
hostname string
proxyStr string
proxyUrl *url.URL
log zerolog.Logger
// Root flags
unsafe bool // not implemented
debug bool
rpcClient dce.Client
smbClient smb.Client
// Generic flags
command string
executable string
executablePath string
executableArgs string
workingDirectory string
windowState string
exec = goexec.ExecutionIO{
Input: new(goexec.ExecutionInput),
Output: new(goexec.ExecutionOutput),
}
authOpts *adauth.Options
credential *adauth.Credential
target *adauth.Target
rootCmd = &cobra.Command{
Use: "goexec",
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
// For modules that require a full executable path
if executablePath != "" && !regexp.MustCompile(`^([a-zA-Z]:)?\\`).MatchString(executablePath) {
return fmt.Errorf("executable path (-e) must be an absolute Windows path, i.e. C:\\Windows\\System32\\cmd.exe")
Use: "goexec",
Short: `Windows remote execution multitool`,
Long: `TODO`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if logJson {
log = zerolog.New(os.Stderr)
} else {
log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
}
if command != "" {
p := strings.SplitN(command, " ", 2)
executable = p[0]
if len(p) > 1 {
executableArgs = p[1]
}
}
log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel).With().Timestamp().Logger()
log = log.Level(zerolog.InfoLevel).With().Timestamp().Logger()
if debug {
log = log.Level(zerolog.DebugLevel)
}
return
if outputMethod == "smb" {
if exec.Output.RemotePath == "" {
exec.Output.RemotePath = util.RandomWindowsTempFile()
}
exec.Output.Provider = &smb.OutputFileFetcher{
Client: &smbClient,
Share: `C$`,
File: exec.Output.RemotePath,
}
}
},
}
)
func needs(reqs ...func(*cobra.Command, []string) error) (fn func(*cobra.Command, []string) error) {
return func(cmd *cobra.Command, args []string) (err error) {
for _, req := range reqs {
if err = req(cmd, args); err != nil {
return
}
}
return
}
}
func needsTarget(proto string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) (err error) {
if proxyStr != "" {
if proxyUrl, err = url.Parse(proxyStr); err != nil {
return fmt.Errorf("failed to parse proxy URL %q: %w", proxyStr, err)
}
}
if len(args) != 1 {
return fmt.Errorf("command require exactly one positional argument: [target]")
}
if creds, target, err = authOpts.WithTarget(ctx, proto, args[0]); err != nil {
return fmt.Errorf("failed to parse target: %w", err)
}
if creds == nil {
return fmt.Errorf("no credentials supplied")
}
if target == nil {
return fmt.Errorf("no target supplied")
}
if hostname, err = target.Hostname(ctx); err != nil {
log.Debug().Err(err).Msg("Could not get target hostname")
}
return
}
}
func init() {
ctx = context.Background()
// Cobra init
{
cobra.EnableCommandSorting = false
cobra.EnableCommandSorting = false
rootCmd.InitDefaultVersionFlag()
rootCmd.InitDefaultHelpCmd()
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging")
rootCmd.PersistentFlags().BoolVar(&logJson, "log-json", false, "Log in JSON format")
rootCmd.InitDefaultVersionFlag()
rootCmd.InitDefaultHelpCmd()
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging")
rootCmd.PersistentFlags().StringVarP(&proxyStr, "proxy", "x", "", "Proxy URL")
rootCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "[NOT IMPLEMENTED] Don't ask for permission to run unsafe actions")
dcomCmdInit()
rootCmd.AddCommand(dcomCmd)
authOpts = &adauth.Options{Debug: log.Debug().Msgf}
authOpts.RegisterFlags(rootCmd.PersistentFlags())
wmiCmdInit()
rootCmd.AddCommand(wmiCmd)
scmrCmdInit()
rootCmd.AddCommand(scmrCmd)
tschCmdInit()
rootCmd.AddCommand(tschCmd)
wmiCmdInit()
rootCmd.AddCommand(wmiCmd)
dcomCmdInit()
rootCmd.AddCommand(dcomCmd)
scmrCmdInit()
rootCmd.AddCommand(scmrCmd)
tschCmdInit()
rootCmd.AddCommand(tschCmd)
}
// Auth init
{
gssapi.AddMechanism(ssp.SPNEGO)
gssapi.AddMechanism(ssp.NTLM)
gssapi.AddMechanism(ssp.KRB5)
authOpts = &adauth.Options{Debug: log.Debug().Msgf}
authOpts.RegisterFlags(rootCmd.PersistentFlags())
}
}
func Execute() {
@@ -124,4 +107,5 @@ func Execute() {
fmt.Println(err)
os.Exit(1)
}
os.Exit(returnCode)
}
-86
View File
@@ -1,86 +0,0 @@
package cmd
import (
"fmt"
"github.com/FalconOpsLLC/goexec/internal/client/dce"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/net/proxy"
"regexp"
)
func needsRpcTarget(proto string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) (err error) {
if err = needsTarget(proto)(cmd, args); err != nil {
return err
}
if proxyUrl != nil {
if netDialer, err := proxy.FromURL(proxyUrl, nil); err != nil {
return fmt.Errorf("proxy dialer from URL: %w", err)
} else if dceDialer, ok := netDialer.(dcerpc.Dialer); !ok {
return fmt.Errorf("failed to cast %T to dcerpc.Dialer", netDialer)
} else {
dceConfig.Options = append(dceConfig.Options, dcerpc.WithDialer(dceDialer))
}
}
if argDceStringBinding != "" {
dceConfig.Endpoint, err = dcerpc.ParseStringBinding(argDceStringBinding)
if err != nil {
return fmt.Errorf("failed to parse RPC endpoint: %w", err)
}
dceConfig.NoEpm = true // If an explicit endpoint is set, don't use EPM
} else if argDceEpmFilter != "" {
// This ensures that filters like "ncacn_ip_tcp" will be made into a valid binding (i.e. "ncacn_ip_tcp:")
if ok, err := regexp.MatchString(`^\w+$`, argDceEpmFilter); err == nil && ok {
argDceEpmFilter += ":"
}
dceConfig.EpmFilter, err = dcerpc.ParseStringBinding(argDceEpmFilter)
if err != nil {
return fmt.Errorf("failed to parse EPM filter: %w", err)
}
}
if hostname != "" {
dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithTargetName(fmt.Sprintf("%s/%s", proto, hostname)))
}
if !argDceNoSign {
dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithSign())
dceConfig.EpmOptions = append(dceConfig.EpmOptions, dcerpc.WithSign())
}
if argDceNoSeal {
dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithInsecure())
} else {
dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
dceConfig.EpmOptions = append(dceConfig.EpmOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
}
return nil
}
}
var (
// DCE arguments
argDceStringBinding string
argDceEpmFilter string
argDceNoSeal bool
argDceNoSign bool
// DCE options
dceStringBinding *dcerpc.StringBinding
dceConfig dce.ConnectionMethodDCEConfig
)
func registerRpcFlags(cmd *cobra.Command) {
rpcFlags := pflag.NewFlagSet("RPC", pflag.ExitOnError)
rpcFlags.BoolVar(&dceConfig.NoEpm, "no-epm", false, "Do not use EPM to automatically detect endpoints")
rpcFlags.BoolVar(&dceConfig.EpmAuto, "epm-auto", false, "Automatically detect endpoints instead of using the module defaults")
rpcFlags.BoolVar(&argDceNoSign, "no-sign", false, "Disable signing on DCE messages")
rpcFlags.BoolVar(&argDceNoSeal, "no-seal", false, "Disable packet stub encryption on DCE messages")
rpcFlags.StringVarP(&argDceEpmFilter, "epm-filter", "F", "", "String binding to filter endpoints returned by EPM")
rpcFlags.StringVar(&argDceStringBinding, "endpoint", "", "Explicit RPC endpoint definition")
cmd.PersistentFlags().AddFlagSet(rpcFlags)
cmd.MarkFlagsMutuallyExclusive("endpoint", "epm-filter")
cmd.MarkFlagsMutuallyExclusive("no-epm", "epm-filter")
}
+126 -119
View File
@@ -1,14 +1,12 @@
package cmd
import (
"github.com/FalconOpsLLC/goexec/internal/exec"
"context"
"github.com/FalconOpsLLC/goexec/internal/util"
"github.com/FalconOpsLLC/goexec/internal/windows"
"github.com/RedTeamPentesting/adauth"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/spf13/cobra"
scmrexec "github.com/FalconOpsLLC/goexec/internal/exec/scmr"
scmrexec "github.com/FalconOpsLLC/goexec/pkg/goexec/scmr"
)
func scmrCmdInit() {
@@ -22,53 +20,55 @@ func scmrCmdInit() {
}
func scmrCreateCmdInit() {
scmrCreateCmd.Flags().StringVarP(&scmrDisplayName, "display-name", "n", "", "Display name of service to create")
scmrCreateCmd.Flags().StringVarP(&scmrServiceName, "service-name", "s", "", "Name of service to create")
scmrCreateCmd.Flags().BoolVar(&scmrNoDelete, "no-delete", false, "Don't delete service after execution")
scmrCreateCmd.Flags().BoolVar(&scmrNoStart, "no-start", false, "Don't start service")
scmrCreateCmd.Flags().StringVarP(&executablePath, "executable-path", "f", "", "Full path to a remote Windows executable file")
scmrCreateCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to the executable")
scmrCreateCmd.Flags().BoolVarP(&scmrOutput, "output", "O", false, "Fetch program output")
scmrCreateCmd.Flags().StringVarP(&scmrCreate.DisplayName, "display-name", "n", "", "Display name of service to create")
scmrCreateCmd.Flags().StringVarP(&scmrCreate.ServiceName, "service-name", "s", "", "Name of service to create")
scmrCreateCmd.Flags().BoolVar(&scmrCreate.NoDelete, "no-delete", false, "Don't delete service after execution")
scmrCreateCmd.Flags().BoolVar(&scmrCreate.NoStart, "no-start", false, "Don't start service")
scmrCreateCmd.Flags().StringVarP(&exec.Input.ExecutablePath, "executable-path", "f", "", "Full path to a remote Windows executable")
scmrCreateCmd.Flags().StringVarP(&exec.Input.Arguments, "args", "a", "", "Arguments to pass to the executable")
scmrCreateCmd.MarkFlagsMutuallyExclusive("no-delete", "no-start")
if err := scmrCreateCmd.MarkFlagRequired("executable-path"); err != nil {
panic(err)
}
}
func scmrChangeCmdInit() {
scmrChangeCmd.Flags().StringVarP(&scmrDisplayName, "display-name", "n", "", "Display name of service to create")
scmrChangeCmd.Flags().BoolVar(&scmrNoStart, "no-start", false, "Don't start service")
scmrChangeCmd.Flags().StringVarP(&scmrServiceName, "service-name", "s", "", "Name of service to modify")
scmrChangeCmd.Flags().StringVarP(&executablePath, "executable-path", "f", "", "Full path to remote Windows executable")
scmrChangeCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable")
scmrChangeCmd.Flags().BoolVar(&scmrChange.NoStart, "no-start", false, "Don't start service")
scmrChangeCmd.Flags().StringVarP(&scmrChange.ServiceName, "service-name", "s", "", "Name of service to modify")
scmrChangeCmd.Flags().StringVarP(&exec.Input.ExecutablePath, "executable-path", "f", "", "Full path to remote Windows executable")
scmrChangeCmd.Flags().StringVarP(&exec.Input.Arguments, "args", "a", "", "Arguments to pass to executable")
if err := scmrChangeCmd.MarkFlagRequired("service-name"); err != nil {
panic(err)
}
if err := scmrCreateCmd.MarkFlagRequired("executable-path"); err != nil {
panic(err)
}
}
func scmrDeleteCmdInit() {
scmrDeleteCmd.Flags().StringArrayVarP(&scmrServiceNames, "service-name", "s", scmrServiceNames, "Name of service(s) to delete")
scmrDeleteCmd.Flags().StringVarP(&scmrDelete.ServiceName, "service-name", "s", scmrDelete.ServiceName, "Name of service to delete")
if err := scmrDeleteCmd.MarkFlagRequired("service-name"); err != nil {
panic(err)
}
}
var (
// scmr arguments
scmrServiceName string
scmrServiceNames []string
scmrDisplayName string
scmrNoDelete bool
scmrNoStart bool
scmrOutput bool
creds *adauth.Credential
target *adauth.Target
scmrCreate scmrexec.ScmrCreate
scmrChange scmrexec.ScmrChange
scmrDelete scmrexec.ScmrDelete
scmrCmd = &cobra.Command{
Use: "scmr",
Short: "Establish execution via SCMR",
Args: cobra.NoArgs,
}
scmrCreateCmd = &cobra.Command{
Use: "create [target]",
Short: "Create & run a new Windows service to gain execution",
@@ -79,138 +79,145 @@ var (
References:
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/6a8ca926-9477-4dd4-b766-692fab07227e
`,
Args: needs(needsTarget("host"), needsRpcTarget("host")),
Args: argsRpcClient("cifs"),
Run: func(cmd *cobra.Command, args []string) {
var err error
if scmrServiceName == "" {
log.Warn().Msg("No service name was specified, using random string")
scmrServiceName = util.RandomString()
}
if scmrNoDelete {
log.Warn().Msg("Service will not be deleted after execution")
}
if scmrDisplayName == "" {
log.Debug().Msg("No display name specified, using service name as display name")
scmrDisplayName = scmrServiceName
}
ctx := gssapi.NewSecurityContext(context.Background())
executor := scmrexec.Module{}
cleanCfg := &exec.CleanupConfig{
CleanupMethod: scmrexec.CleanupMethodDelete,
CleanupMethodConfig: scmrexec.CleanupMethodDeleteConfig{},
}
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
execCfg := &exec.ExecutionConfig{
ExecutablePath: executablePath,
ExecutableArgs: executableArgs,
ReturnOutput: scmrOutput,
ExecutionMethod: scmrexec.MethodCreate,
ExecutionMethodConfig: scmrexec.MethodCreateConfig{
NoDelete: scmrNoDelete,
ServiceName: util.RandomStringIfBlank(scmrServiceName),
DisplayName: scmrDisplayName,
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
StartType: windows.SERVICE_DEMAND_START,
},
}
ctx = log.With().
Str("module", "scmr").
Str("method", "create").
Logger().WithContext(ctx)
Logger().
WithContext(ctx)
if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
if scmrCreate.ServiceName == "" {
log.Warn().Msg("No service name was provided. Using a random string")
scmrCreate.ServiceName = util.RandomString()
}
if scmrCreate.NoDelete {
log.Warn().Msg("Service will not be deleted after execution")
}
if scmrCreate.DisplayName == "" {
log.Debug().Msg("No display name specified, using service name as display name")
scmrCreate.DisplayName = scmrCreate.ServiceName
}
if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
if !scmrNoDelete {
defer func() {
if err := executor.Cleanup(ctx, cleanCfg); err != nil {
log.Error().Err(err).Msg("Cleanup failed")
}
}()
defer func() {
closeErr := rpcClient.Close(ctx)
if closeErr != nil {
log.Error().Err(closeErr).Msg("Failed to close connection")
}
}()
defer func() {
cleanErr := scmrCreate.Clean(ctx)
if cleanErr != nil {
log.Warn().Err(cleanErr).Msg("Clean operation failed")
}
}()
if err = scmrCreate.Init(ctx, &rpcClient); err != nil {
log.Error().Err(err).Msg("Module initialization failed")
returnCode = 2
return
}
if err := executor.Exec(ctx, execCfg); err != nil {
if err = scmrCreate.Execute(ctx, exec.Input); err != nil {
log.Error().Err(err).Msg("Execution failed")
returnCode = 4
}
},
}
scmrChangeCmd = &cobra.Command{
Use: "change [target]",
Short: "Change an existing Windows service to gain execution",
Args: needs(needsTarget("host"), needsRpcTarget("host")),
Args: argsRpcClient("cifs"),
Run: func(cmd *cobra.Command, args []string) {
var err error
executor := scmrexec.Module{}
cleanCfg := &exec.CleanupConfig{
CleanupMethod: scmrexec.CleanupMethodRevert,
CleanupMethodConfig: scmrexec.CleanupMethodRevertConfig{},
}
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
execCfg := &exec.ExecutionConfig{
ExecutablePath: executablePath,
ExecutableArgs: executableArgs,
ExecutionMethod: scmrexec.MethodChange,
ctx := gssapi.NewSecurityContext(context.Background())
ExecutionMethodConfig: scmrexec.MethodChangeConfig{
NoStart: scmrNoStart,
ServiceName: scmrServiceName,
},
}
ctx = log.With().
Str("module", "scmr").
Str("method", "change").
Logger().WithContext(ctx)
Logger().
WithContext(ctx)
if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
if !scmrNoDelete {
defer func() {
if err := executor.Cleanup(ctx, cleanCfg); err != nil {
log.Error().Err(err).Msg("Cleanup failed")
}
}()
defer func() {
closeErr := rpcClient.Close(ctx)
if closeErr != nil {
log.Error().Err(closeErr).Msg("Failed to close connection")
}
}()
defer func() {
cleanErr := scmrChange.Clean(ctx)
if cleanErr != nil {
log.Warn().Err(cleanErr).Msg("Clean operation failed")
}
}()
if err = scmrChange.Init(ctx, &rpcClient); err != nil {
log.Error().Err(err).Msg("Module initialization failed")
returnCode = 2
return
}
if err := executor.Exec(ctx, execCfg); err != nil {
if err = scmrChange.Execute(ctx, exec.Input); err != nil {
log.Error().Err(err).Msg("Execution failed")
returnCode = 4
}
},
}
scmrDeleteCmd = &cobra.Command{
Use: "delete [target]",
Short: "Delete an existing Windows service",
Long: `Description:
TODO
`,
Args: needs(needsTarget("host"), needsRpcTarget("host")),
Run: func(cmd *cobra.Command, args []string) {
dceConfig.DceOptions = append(dceConfig.DceOptions, dcerpc.WithInsecure())
Long: `TODO`,
Args: argsRpcClient("cifs"),
Run: func(cmd *cobra.Command, args []string) {
var err error
ctx := gssapi.NewSecurityContext(context.Background())
executor := scmrexec.Module{}
cleanCfg := &exec.CleanupConfig{
CleanupMethod: scmrexec.CleanupMethodDelete,
CleanupMethodConfig: scmrexec.CleanupMethodDeleteConfig{ServiceNames: scmrServiceNames},
}
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
ctx = log.With().
Str("module", "scmr").
Str("method", "delete").
Logger().WithContext(ctx)
Logger().
WithContext(ctx)
if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
} else if err = executor.Cleanup(ctx, cleanCfg); err != nil {
log.Fatal().Err(err).Msg("Delete failed")
defer func() {
closeErr := rpcClient.Close(ctx)
if closeErr != nil {
log.Error().Err(closeErr).Msg("Failed to close connection")
}
}()
if err = scmrDelete.Init(ctx, &rpcClient); err != nil {
log.Error().Err(err).Msg("Module initialization failed")
returnCode = 2
}
if err = scmrDelete.Clean(ctx); err != nil {
log.Warn().Err(err).Msg("Clean failed")
returnCode = 4
}
},
}
+145 -168
View File
@@ -1,102 +1,147 @@
package cmd
import (
"fmt"
"github.com/FalconOpsLLC/goexec/internal/exec"
"github.com/FalconOpsLLC/goexec/internal/exec/tsch"
"context"
"github.com/FalconOpsLLC/goexec/internal/util"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
tschexec "github.com/FalconOpsLLC/goexec/pkg/goexec/tsch"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/spf13/cobra"
"regexp"
"io"
"os"
"time"
)
func tschCmdInit() {
registerRpcFlags(tschCmd)
tschDeleteCmdInit()
tschCmd.AddCommand(tschDeleteCmd)
tschRegisterCmdInit()
tschCmd.AddCommand(tschRegisterCmd)
tschDemandCmdInit()
tschCmd.AddCommand(tschDemandCmd)
tschCreateCmdInit()
tschCmd.AddCommand(tschCreateCmd)
}
func tschDeleteCmdInit() {
tschDeleteCmd.Flags().StringVarP(&tschTaskPath, "path", "t", "", "Scheduled task path")
if err := tschDeleteCmd.MarkFlagRequired("path"); err != nil {
panic(err)
func argsTschTaskIdentifiers(name, path string) error {
switch {
case path != "":
return tschexec.ValidateTaskPath(path)
case name != "":
return tschexec.ValidateTaskName(name)
default:
}
return nil
}
func argsTschDemand(_ *cobra.Command, _ []string) error {
return argsTschTaskIdentifiers(tschDemand.TaskName, tschDemand.TaskPath)
}
func argsTschCreate(_ *cobra.Command, _ []string) error {
return argsTschTaskIdentifiers(tschCreate.TaskName, tschCreate.TaskPath)
}
func tschDemandCmdInit() {
tschDemandCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke")
tschDemandCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable")
tschDemandCmd.Flags().StringVarP(&tschTaskName, "name", "n", "", "Target task name")
tschDemandCmd.Flags().BoolVar(&tschNoDelete, "no-delete", false, "Don't delete task after execution")
tschDemandCmd.Flags().Uint32Var(&tschSessionId, "session-id", 0, "Hijack existing session")
if err := tschDemandCmd.MarkFlagRequired("executable"); err != nil {
panic(err)
}
tschDemandCmd.Flags().StringVarP(&tschDemand.TaskName, "name", "t", "", "Name of task to register")
tschDemandCmd.Flags().StringVarP(&tschDemand.TaskPath, "path", "P", "", "Path of task to register")
tschDemandCmd.Flags().Uint32Var(&tschDemand.SessionId, "session", 0, "Hijack existing session given the session ID")
tschDemandCmd.Flags().BoolVar(&tschDemand.NoDelete, "no-delete", false, "Don't delete task after execution")
tschDemandCmd.Flags().StringVar(&tschDemand.UserSid, "sid", "S-1-5-18", "User SID to impersonate")
registerProcessExecutionArgs(tschDemandCmd)
registerExecutionOutputArgs(tschDemandCmd)
tschDemandCmd.MarkFlagsMutuallyExclusive("name", "path")
}
func tschRegisterCmdInit() {
tschRegisterCmd.Flags().StringVarP(&executable, "executable", "e", "", "Remote Windows executable to invoke")
tschRegisterCmd.Flags().StringVarP(&executableArgs, "args", "a", "", "Arguments to pass to executable")
tschRegisterCmd.Flags().StringVarP(&tschTaskName, "name", "n", "", "Target task name")
tschRegisterCmd.Flags().DurationVar(&tschStopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This will not stop the process spawned by the task")
tschRegisterCmd.Flags().DurationVarP(&tschDelay, "delay-start", "d", 5*time.Second, "Delay between task registration and execution")
tschRegisterCmd.Flags().DurationVarP(&tschDeleteDelay, "delay-delete", "D", 0*time.Second, "Delay between task termination and deletion")
tschRegisterCmd.Flags().BoolVar(&tschNoDelete, "no-delete", false, "Don't delete task after execution")
tschRegisterCmd.Flags().BoolVar(&tschCallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task")
func tschCreateCmdInit() {
tschCreateCmd.Flags().StringVarP(&tschCreate.TaskName, "name", "t", "", "Name of task to register")
tschCreateCmd.Flags().StringVarP(&tschCreate.TaskPath, "path", "P", "", "Path of task to register")
tschCreateCmd.Flags().DurationVar(&tschCreate.StopDelay, "delay-stop", 5*time.Second, "Delay between task execution and termination. This will not stop the process spawned by the task")
tschCreateCmd.Flags().DurationVar(&tschCreate.StartDelay, "start-delay", 5*time.Second, "Delay between task registration and execution")
tschCreateCmd.Flags().DurationVar(&tschCreate.DeleteDelay, "delete-delay", 0*time.Second, "Delay between task termination and deletion")
tschCreateCmd.Flags().BoolVar(&tschCreate.NoDelete, "no-delete", false, "Don't delete task after execution")
tschCreateCmd.Flags().BoolVar(&tschCreate.CallDelete, "call-delete", false, "Directly call SchRpcDelete to delete task")
tschCreateCmd.Flags().StringVar(&tschCreate.UserSid, "sid", "S-1-5-18", "User SID to impersonate")
tschRegisterCmd.MarkFlagsMutuallyExclusive("no-delete", "delay-delete")
tschRegisterCmd.MarkFlagsMutuallyExclusive("no-delete", "call-delete")
tschRegisterCmd.MarkFlagsMutuallyExclusive("delay-delete", "call-delete")
registerProcessExecutionArgs(tschCreateCmd)
if err := tschRegisterCmd.MarkFlagRequired("executable"); err != nil {
panic(err)
}
}
func tschArgs(principal string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
if tschTaskPath != "" && !tschTaskPathRegex.MatchString(tschTaskPath) {
return fmt.Errorf("invalid task path: %s", tschTaskPath)
}
if tschTaskName != "" {
if !tschTaskNameRegex.MatchString(tschTaskName) {
return fmt.Errorf("invalid task name: %s", tschTaskName)
} else if tschTaskPath == "" {
tschTaskPath = `\` + tschTaskName
}
}
return needsRpcTarget(principal)(cmd, args)
}
tschCreateCmd.MarkFlagsMutuallyExclusive("name", "path")
}
var (
tschSessionId uint32
tschNoDelete bool
tschCallDelete bool
tschDeleteDelay time.Duration
tschStopDelay time.Duration
tschDelay time.Duration
tschTaskName string
tschTaskPath string
tschTaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`)
tschTaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`)
tschDemand tschexec.TschDemand
tschCreate tschexec.TschCreate
tschCmd = &cobra.Command{
Use: "tsch",
Short: "Establish execution via TSCH",
Short: "Establish execution via Windows Task Scheduler (MS-TSCH)",
Args: cobra.NoArgs,
}
tschRegisterCmd = &cobra.Command{
Use: "register [target]",
Short: "Register a remote scheduled task with an automatic start time",
tschDemandCmd = &cobra.Command{
Use: "demand [target]",
Short: "Register a remote scheduled task and demand immediate start",
Long: `Description:
The register method calls SchRpcRegisterTask to register a scheduled task
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.
References:
SchRpcRegisterTask - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167
SchRpcRun - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/77f2250d-500a-40ee-be18-c82f7079c4f0
`,
Args: args(
argsRpcClient("cifs"),
argsSmbClient(),
argsTschDemand,
),
Run: func(cmd *cobra.Command, args []string) {
var err error
tschDemand.Client = &rpcClient
tschDemand.IO = exec
if tschDemand.TaskName == "" && tschDemand.TaskPath == "" {
tschDemand.TaskPath = `\` + util.RandomString()
}
ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO()))
var writer io.WriteCloser
if outputPath == "-" {
writer = os.Stdout
} else if outputPath != "" {
if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
log.Fatal().Err(err).Msg("Failed to open output file")
}
defer writer.Close()
}
if err = goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil {
log.Fatal().Err(err).Msg("Operation failed")
}
if outputPath != "" {
if reader, err := tschDemand.GetOutput(ctx); err == nil {
_, err = io.Copy(writer, reader)
} else {
log.Error().Err(err).Msg("Failed to get process execution output")
returnCode = 2
}
}
},
}
tschCreateCmd = &cobra.Command{
Use: "create [target]",
Short: "Create a remote scheduled task with an automatic start time",
Long: `Description:
The create method calls SchRpcRegisterTask to register a scheduled task
with an automatic start time.This method avoids directly calling SchRpcRun,
and can even avoid calling SchRpcDelete by populating the DeleteExpiredTaskAfter
Setting.
@@ -107,117 +152,49 @@ References:
SchRpcDelete - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/360bb9b1-dd2a-4b36-83ee-21f12cb97cff
DeleteExpiredTaskAfter - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/6bfde6fe-440e-4ddd-b4d6-c8fc0bc06fae
`,
Args: tschArgs("cifs"),
Args: args(
argsRpcClient("cifs"),
argsSmbClient(),
argsTschCreate,
),
Run: func(cmd *cobra.Command, args []string) {
var err error
log = log.With().
Str("module", "tsch").
Str("method", "register").
Logger()
if tschNoDelete {
log.Warn().Msg("Task will not be deleted after execution")
tschCreate.Tsch.Client = &rpcClient
tschCreate.IO = exec
if tschCreate.TaskName == "" && tschDemand.TaskPath == "" {
tschCreate.TaskPath = `\` + util.RandomString()
}
module := tschexec.Module{}
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
execCfg := &exec.ExecutionConfig{
ExecutableName: executable,
ExecutableArgs: executableArgs,
ExecutionMethod: tschexec.MethodRegister,
ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO()))
ExecutionMethodConfig: tschexec.MethodRegisterConfig{
NoDelete: tschNoDelete,
CallDelete: tschCallDelete,
StartDelay: tschDelay,
StopDelay: tschStopDelay,
DeleteDelay: tschDeleteDelay,
TaskPath: tschTaskPath,
},
}
if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
} else if err = module.Exec(log.WithContext(ctx), execCfg); err != nil {
log.Fatal().Err(err).Msg("Execution failed")
}
},
}
tschDemandCmd = &cobra.Command{
Use: "demand [target]",
Short: "Register a remote scheduled task and demand immediate start",
Long: `Description:
Similar to the register 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.
var writer io.WriteCloser
References:
SchRpcRegisterTask - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167
SchRpcRun - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/77f2250d-500a-40ee-be18-c82f7079c4f0
`,
Args: tschArgs("cifs"),
Run: func(cmd *cobra.Command, args []string) {
if outputPath == "-" {
writer = os.Stdout
log = log.With().
Str("module", "tsch").
Str("method", "register").
Logger()
if tschNoDelete {
log.Warn().Msg("Task will not be deleted after execution")
}
module := tschexec.Module{}
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
execCfg := &exec.ExecutionConfig{
ExecutableName: executable,
ExecutableArgs: executableArgs,
ExecutionMethod: tschexec.MethodDemand,
} else if outputPath != "" {
ExecutionMethodConfig: tschexec.MethodDemandConfig{
NoDelete: tschNoDelete,
TaskPath: tschTaskPath,
SessionId: tschSessionId,
},
if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
log.Fatal().Err(err).Msg("Failed to open output file")
}
defer writer.Close()
}
if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
} else if err = module.Exec(log.WithContext(ctx), execCfg); err != nil {
log.Fatal().Err(err).Msg("Execution failed")
}
},
}
tschDeleteCmd = &cobra.Command{
Use: "delete [target]",
Short: "Manually delete a scheduled task",
Long: `Description:
The delete method manually deletes a scheduled task by calling SchRpcDelete
References:
SchRpcDelete - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/360bb9b1-dd2a-4b36-83ee-21f12cb97cff
`,
Args: tschArgs("cifs"),
Run: func(cmd *cobra.Command, args []string) {
log = log.With().
Str("module", "tsch").
Str("method", "delete").
Logger()
if err = goexec.ExecuteCleanMethod(ctx, &tschDemand, &exec); err != nil {
log.Fatal().Err(err).Msg("Operation failed")
}
module := tschexec.Module{}
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
cleanCfg := &exec.CleanupConfig{
CleanupMethod: tschexec.MethodDelete,
CleanupMethodConfig: tschexec.MethodDeleteConfig{TaskPath: tschTaskPath},
}
if err := module.Connect(log.WithContext(ctx), creds, target, connCfg); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
} else if err := module.Cleanup(log.WithContext(ctx), cleanCfg); err != nil {
log.Fatal().Err(err).Msg("Cleanup failed")
if outputPath != "" {
if reader, err := tschDemand.GetOutput(ctx); err == nil {
_, err = io.Copy(writer, reader)
} else {
log.Error().Err(err).Msg("Failed to get process execution output")
returnCode = 2
}
}
},
}
+91 -82
View File
@@ -1,52 +1,65 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/exec"
wmiexec "github.com/FalconOpsLLC/goexec/internal/exec/wmi"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
wmiexec "github.com/FalconOpsLLC/goexec/pkg/goexec/wmi"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/spf13/cobra"
"io"
"os"
)
func wmiCmdInit() {
registerRpcFlags(wmiCmd)
wmiCallCmdInit()
wmiCmd.AddCommand(wmiCallCmd)
wmiProcessCmdInit()
wmiCmd.AddCommand(wmiProcessCmd)
wmiProcCmdInit()
wmiCmd.AddCommand(wmiProcCmd)
}
func wmiCallArgs(_ *cobra.Command, _ []string) error {
return json.Unmarshal([]byte(wmiArguments), &wmiCall.Args)
}
func wmiCallCmdInit() {
wmiCallCmd.Flags().StringVarP(&dceConfig.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace")
wmiCallCmd.Flags().StringVarP(&wmi.Class, "class", "C", "", `WMI class to instantiate (i.e. "Win32_Process")`)
wmiCallCmd.Flags().StringVarP(&wmi.Method, "method", "m", "", `WMI Method to call (i.e. "Create")`)
wmiCallCmd.Flags().StringVarP(&wmi.Args, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`)
wmiCallCmd.Flags().StringVarP(&wmiCall.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace")
wmiCallCmd.Flags().StringVarP(&wmiCall.Class, "class", "C", "", `WMI class to instantiate (i.e. "Win32_Process")`)
wmiCallCmd.Flags().StringVarP(&wmiCall.Method, "method", "m", "", `WMI Method to call (i.e. "Create")`)
wmiCallCmd.Flags().StringVarP(&wmiArguments, "args", "A", "{}", `WMI Method argument(s) in JSON dictionary format (i.e. {"CommandLine":"calc.exe"})`)
if err := wmiCallCmd.MarkFlagRequired("class"); err != nil {
panic(err)
}
if err := wmiCallCmd.MarkFlagRequired("method"); err != nil {
panic(err)
}
}
func wmiProcessCmdInit() {
wmiProcessCmd.Flags().StringVarP(&command, "command", "c", "", "Process command line")
wmiProcessCmd.Flags().StringVarP(&workingDirectory, "directory", "d", `C:\`, "Working directory")
if err := wmiProcessCmd.MarkFlagRequired("command"); err != nil {
panic(err)
}
func wmiProcCmdInit() {
wmiProcCmd.Flags().StringVarP(&wmiProc.Resource, "namespace", "n", "//./root/cimv2", "WMI namespace")
wmiProcCmd.Flags().StringVarP(&wmiProc.WorkingDirectory, "directory", "d", `C:\`, "Working directory")
registerProcessExecutionArgs(wmiProcCmd)
registerExecutionOutputArgs(wmiProcCmd)
}
var (
wmi struct {
Class string
Method string
Args string
}
wmiMethodArgsMap map[string]any
wmiCall = wmiexec.WmiCall{}
wmiProc = wmiexec.WmiProc{}
wmiArguments string
wmiCmd = &cobra.Command{
Use: "wmi",
Short: "Establish execution via WMI",
Short: "Establish execution via wmi",
Args: cobra.NoArgs,
}
wmiCallCmd = &cobra.Command{
Use: "call",
Short: "Execute specified WMI method",
@@ -56,52 +69,48 @@ var (
References:
https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-classes
`,
Args: needs(needsTarget("cifs"), needsRpcTarget("cifs"), func(cmd *cobra.Command, args []string) (err error) {
if err = json.Unmarshal([]byte(wmi.Args), &wmiMethodArgsMap); err != nil {
err = fmt.Errorf("parse JSON arguments: %w", err)
}
return
}),
Run: func(cmd *cobra.Command, args []string) {
executor := wmiexec.Module{}
cleanCfg := &exec.CleanupConfig{} // TODO
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
`,
Args: args(argsRpcClient("host"), wmiCallArgs),
execCfg := &exec.ExecutionConfig{
ExecutableName: executable,
ExecutableArgs: executableArgs,
ExecutionMethod: wmiexec.MethodCall,
ExecutionMethodConfig: wmiexec.MethodCallConfig{
Class: wmi.Class,
Method: wmi.Method,
Arguments: wmiMethodArgsMap,
},
}
Run: func(cmd *cobra.Command, args []string) {
var err error
ctx := gssapi.NewSecurityContext(context.Background())
ctx = log.With().
Str("module", "wmi").
Str("method", "proc").
Logger().WithContext(ctx)
Str("method", "call").
Logger().
WithContext(ctx)
if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
if err = rpcClient.Connect(ctx); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
defer func() {
if err := executor.Cleanup(ctx, cleanCfg); err != nil {
log.Error().Err(err).Msg("Cleanup failed")
closeErr := rpcClient.Close(ctx)
if closeErr != nil {
log.Error().Err(closeErr).Msg("Failed to close connection")
}
}()
if err := executor.Exec(ctx, execCfg); err != nil {
log.Error().Err(err).Msg("Execution failed")
if err = wmiCall.Init(ctx); err != nil {
log.Error().Err(err).Msg("Module initialization failed")
returnCode = 2
return
}
out, err := wmiCall.Call(ctx)
if err != nil {
log.Error().Err(err).Msg("Call failed")
returnCode = 4
return
}
fmt.Println(string(out))
},
}
wmiProcessCmd = &cobra.Command{
wmiProcCmd = &cobra.Command{
Use: "proc",
Short: "Start a Windows process",
Long: `Description:
@@ -112,41 +121,41 @@ References:
References:
https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/create-method-in-class-win32-process
`,
Args: needs(needsTarget("cifs"), needsRpcTarget("cifs")),
Args: args(argsOutput("smb"), argsRpcClient("host")),
Run: func(cmd *cobra.Command, args []string) {
var err error
executor := wmiexec.Module{}
cleanCfg := &exec.CleanupConfig{} // TODO
connCfg := &exec.ConnectionConfig{
ConnectionMethod: exec.ConnectionMethodDCE,
ConnectionMethodConfig: dceConfig,
}
execCfg := &exec.ExecutionConfig{
ExecutableName: executable,
ExecutableArgs: executableArgs,
ExecutionMethod: wmiexec.MethodProcess,
wmiProc.Client = &rpcClient
wmiProc.IO = exec
ExecutionMethodConfig: wmiexec.MethodProcessConfig{
Command: command,
WorkingDirectory: workingDirectory,
},
}
ctx := log.WithContext(gssapi.NewSecurityContext(context.TODO()))
ctx = log.With().
Str("module", "wmi").
Str("method", "proc").
Logger().WithContext(ctx)
var writer io.WriteCloser
if err := executor.Connect(ctx, creds, target, connCfg); err != nil {
log.Fatal().Err(err).Msg("Connection failed")
}
defer func() {
if err := executor.Cleanup(ctx, cleanCfg); err != nil {
log.Error().Err(err).Msg("Cleanup failed")
if outputPath == "-" {
writer = os.Stdout
} else if outputPath != "" {
if writer, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
log.Fatal().Err(err).Msg("Failed to open output file")
}
defer writer.Close()
}
if err = goexec.ExecuteCleanMethod(ctx, &wmiProc, &exec); err != nil {
log.Fatal().Err(err).Msg("Operation failed")
}
if outputPath != "" {
if reader, err := wmiProc.GetOutput(ctx); err == nil {
_, err = io.Copy(writer, reader)
} else {
log.Error().Err(err).Msg("Failed to get process execution output")
returnCode = 2
}
}()
if err := executor.Exec(ctx, execCfg); err != nil {
log.Error().Err(err).Msg("Execution failed")
}
},
}
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/FalconOpsLLC/goexec
go 1.24.1
require (
github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877
github.com/RedTeamPentesting/adauth v0.2.0
github.com/google/uuid v1.6.0
github.com/oiweiwei/go-msrpc v1.2.5
github.com/rs/zerolog v1.34.0
+2
View File
@@ -1,5 +1,7 @@
github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877 h1:n5V0EER+EvvmUZitR5zGFUMyoIHnI12SWFMciI7kh70=
github.com/RedTeamPentesting/adauth v0.1.1-0.20250304075117-acd47d454877/go.mod h1:iHf/fY7CueB7qLHZ5YgTZXvrVCSLJy4+tAifOSNLAFQ=
github.com/RedTeamPentesting/adauth v0.2.0 h1:pNb4xcEd/oG/JY8aXkBm8Y0veVaUA/CsKFhgEgNCPik=
github.com/RedTeamPentesting/adauth v0.2.0/go.mod h1:AhWZEgl98YkKEta6kXTOSocQh2SbJJZg4nKqF+uY4bc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-74
View File
@@ -1,74 +0,0 @@
package dce
import (
"context"
"errors"
"fmt"
"github.com/RedTeamPentesting/adauth"
"github.com/RedTeamPentesting/adauth/dcerpcauth"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/midl/uuid"
"github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3"
"github.com/oiweiwei/go-msrpc/ssp/gssapi"
"github.com/rs/zerolog"
)
type ConnectionMethodDCEConfig struct {
NoEpm bool // NoEpm disables EPM
EpmAuto bool // EpmAuto will find any suitable endpoint, without any filter
Endpoint *dcerpc.StringBinding // Endpoint is the explicit endpoint passed to dcerpc.WithEndpoint for use without EPM
EpmFilter *dcerpc.StringBinding // EpmFilter is the rough filter used to pick an EPM endpoint
Options []dcerpc.Option // Options stores the options that will be passed to all dialers
DceOptions []dcerpc.Option // DceOptions are the options passed to dcerpc.Dial
EpmOptions []dcerpc.Option // EpmOptions are the options passed to epm.EndpointMapper
Resource string // Resource stores the target network resource (usually for DCOM)
}
func (cfg *ConnectionMethodDCEConfig) GetDce(ctx context.Context, cred *adauth.Credential, target *adauth.Target, endpoint, object string, arbOpts ...dcerpc.Option) (cc dcerpc.Conn, err error) {
log := zerolog.Ctx(ctx).With().
Str("client", "DCERPC").Logger()
// Mandatory logging
dceOpts := append(append(cfg.Options, arbOpts...), append(cfg.DceOptions, dcerpc.WithLogger(log))...)
epmOpts := append(cfg.Options, append(cfg.EpmOptions, dcerpc.WithLogger(log))...)
ctx = gssapi.NewSecurityContext(ctx)
auth, err := dcerpcauth.AuthenticationOptions(ctx, cred, target, &dcerpcauth.Options{})
if err != nil {
log.Error().Err(err).Msg("Failed to parse authentication options")
return nil, fmt.Errorf("parse auth options: %w", err)
}
addr := target.AddressWithoutPort()
log = log.With().Str("address", addr).Logger()
if object != "" {
if id, err := uuid.Parse(object); err != nil {
log.Error().Err(err).Msg("Failed to parse input object UUID")
} else {
dceOpts = append(dceOpts, dcerpc.WithObjectUUID(id))
}
}
if cfg.Endpoint != nil {
dceOpts = append(dceOpts, dcerpc.WithEndpoint(cfg.Endpoint.String()))
log.Debug().Str("binding", cfg.Endpoint.String()).Msg("Using endpoint")
} else if !cfg.NoEpm {
dceOpts = append(dceOpts, epm.EndpointMapper(ctx, addr, append(epmOpts, auth...)...))
log.Debug().Msg("Using endpoint mapper")
if cfg.EpmFilter != nil {
dceOpts = append(dceOpts, dcerpc.WithEndpoint(cfg.EpmFilter.String()))
log.Debug().Str("filter", cfg.EpmFilter.String()).Msg("Using endpoint filter")
}
} else if endpoint != "" {
dceOpts = append(dceOpts, dcerpc.WithEndpoint(endpoint))
log.Debug().Str("endpoint", endpoint).Msg("Using default endpoint")
} else {
log.Err(err).Msg("Invalid DCE connection options")
return nil, errors.New("get DCE: invalid connection options")
}
return dcerpc.Dial(ctx, target.AddressWithoutPort(), append(dceOpts, auth...)...)
}
-181
View File
@@ -1,181 +0,0 @@
package dcomexec
import (
"context"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/client/dce"
"github.com/FalconOpsLLC/goexec/internal/exec"
"github.com/RedTeamPentesting/adauth"
guuid "github.com/google/uuid"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/midl/uuid"
"github.com/oiweiwei/go-msrpc/msrpc/dcom"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0"
"github.com/oiweiwei/go-msrpc/msrpc/dtyp"
"github.com/rs/zerolog"
)
const (
DefaultDcomEndpoint = "ncacn_ip_tcp:[135]"
)
var (
MmcUuid = uuid.MustParse("49B2791A-B1AE-4C90-9B8E-E860BA07F889")
ShellWindowsUuid = uuid.MustParse("9BA05972-F6A8-11CF-A442-00A0C90A8F39")
RandCid = dcom.CID(*dtyp.GUIDFromUUID(uuid.MustParse(guuid.NewString())))
IDispatchIID = &dcom.IID{
Data1: 0x20400,
Data2: 0x0,
Data3: 0x0,
Data4: []byte{0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46},
}
ComVersion = &dcom.COMVersion{
MajorVersion: 5,
MinorVersion: 7,
}
MmcClsid = dcom.ClassID(*dtyp.GUIDFromUUID(MmcUuid))
ORPCThis = &dcom.ORPCThis{
Version: ComVersion,
CID: &RandCid,
}
)
func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("method", ccfg.ConnectionMethod).
Str("func", "Exec").Logger()
if ccfg.ConnectionMethod == exec.ConnectionMethodDCE {
if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok {
return errors.New("invalid configuration for DCE connection method")
} else {
opts := []dcerpc.Option{dcerpc.WithSign(), dcerpc.WithSecurityLevel(0)}
// Create DCE connection
if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultDcomEndpoint, "", opts...); err != nil {
log.Error().Err(err).Msg("Failed to initialize DCE dialer")
return fmt.Errorf("create DCE dialer: %w", err)
}
inst := &dcom.InstantiationInfoData{
ClassID: &MmcClsid,
IID: []*dcom.IID{IDispatchIID},
ClientCOMVersion: ComVersion,
}
scm := &dcom.SCMRequestInfoData{
RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{
RequestedProtocolSequences: []uint16{7},
},
}
loc := &dcom.LocationInfoData{}
ac := &dcom.ActivationContextInfoData{}
ap := &dcom.ActivationProperties{
DestinationContext: 2,
Properties: []dcom.ActivationProperty{inst, ac, loc, scm},
}
apin, err := ap.ActivationPropertiesIn()
if err != nil {
return err
}
act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, mod.dce)
if err != nil {
return err
}
cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{
ORPCThis: &dcom.ORPCThis{
Version: ComVersion,
Flags: 1,
CID: &RandCid,
},
ActPropertiesIn: apin,
})
if err != nil {
return err
}
log.Info().Msg("RemoteCreateInstance succeeded")
apout := &dcom.ActivationProperties{}
if err = apout.Parse(cr.ActPropertiesOut); err != nil {
return err
}
si := apout.SCMReplyInfoData()
pi := apout.PropertiesOutInfo()
if si == nil {
return fmt.Errorf("remote create instance response: SCMReplyInfoData is nil")
}
if pi == nil {
return fmt.Errorf("remote create instance response: PropertiesOutInfo is nil")
}
oIPID := pi.InterfaceData[0].IPID()
opts = append(opts, si.RemoteReply.OXIDBindings.EndpointsByProtocol("ncacn_ip_tcp")...) // TODO
mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultDcomEndpoint, "", opts...)
if err != nil {
return err
}
log.Info().Msg("created new DCERPC dialer")
mod.dc, err = idispatch.NewDispatchClient(ctx, mod.dce, dcom.WithIPID(oIPID))
if err != nil {
return err
}
log.Info().Msg("created IDispatch client")
}
}
return
}
func (mod *Module) Exec(ctx context.Context, ecfg *exec.ExecutionConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("method", ecfg.ExecutionMethod).
Str("func", "Exec").Logger()
if ecfg.ExecutionMethod == MethodMmc {
if cfg, ok := ecfg.ExecutionMethodConfig.(MethodMmcConfig); !ok {
return errors.New("invalid configuration")
} else {
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/mmc/view-executeshellcommand
method := "Document.ActiveView.ExecuteShellCommand"
log = log.With().Str("classMethod", method).Logger()
log.Info().
Str("executable", ecfg.ExecutableName).
Str("arguments", ecfg.ExecutableArgs).Msg("Attempting execution")
command := &oaut.Variant{
Size: 5,
VT: 8,
VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: ecfg.ExecutableName}}},
}
directory := &oaut.Variant{
Size: 5,
VT: 8,
VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: cfg.WorkingDirectory}}},
}
parameters := &oaut.Variant{
Size: 5,
VT: 8,
VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: ecfg.ExecutableArgs}}},
}
windowState := &oaut.Variant{
Size: 5,
VT: 8,
VarUnion: &oaut.Variant_VarUnion{Value: &oaut.Variant_VarUnion_BSTR{BSTR: &oaut.String{Data: cfg.WindowState}}},
}
// Arguments must be passed in reverse order
if _, err := callMethod(ctx, mod.dc, method, windowState, parameters, directory, command); err != nil {
log.Error().Err(err).Msg("Failed to call method")
return fmt.Errorf("call %q: %w", method, err)
}
log.Info().Msg("Method call successful")
}
}
return nil
}
-21
View File
@@ -1,21 +0,0 @@
package dcomexec
import (
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0"
)
type Module struct {
dce dcerpc.Conn
dc idispatch.DispatchClient
hostname string
}
type MethodMmcConfig struct {
WorkingDirectory string
WindowState string
}
const (
MethodMmc string = "mmc"
)
-54
View File
@@ -1,54 +0,0 @@
package exec
import (
"context"
"fmt"
"github.com/RedTeamPentesting/adauth"
"strings"
)
const (
ConnectionMethodDCE = "dcerpc"
)
type ConnectionConfig struct {
ConnectionMethod string
ConnectionMethodConfig interface{}
}
type CleanupConfig struct {
CleanupMethod string
CleanupMethodConfig interface{}
}
type ExecutionConfig struct {
ExecutableName string // ExecutableName represents the name of the executable; i.e. "notepad.exe", "calc"
ExecutablePath string // ExecutablePath represents the full path to the executable; i.e. `C:\Windows\explorer.exe`
ExecutableArgs string // ExecutableArgs represents the arguments to be passed to the executable during execution; i.e. "/C whoami"
ExecutionMethod string // ExecutionMethod represents the specific execution strategy used by the module.
ExecutionMethodConfig interface{}
ReturnOutput bool
}
type ShellConfig struct {
ShellName string // ShellName specifies the name of the shell executable; i.e. "cmd.exe", "powershell"
ShellPath string // ShellPath is the full Windows path to the shell executable; i.e. `C:\Windows\System32\cmd.exe`
}
type Module interface {
Connect(context.Context, *adauth.Credential, *adauth.Target, *ConnectionConfig) error
Exec(context.Context, *ExecutionConfig) error
Cleanup(context.Context, *CleanupConfig) error
}
func (cfg *ExecutionConfig) GetRawCommand() string {
executable := cfg.ExecutablePath
if strings.Contains(executable, " ") {
executable = fmt.Sprintf("%q", executable)
}
if cfg.ExecutableArgs != "" {
return executable + " " + cfg.ExecutableArgs
}
return executable
}
-345
View File
@@ -1,345 +0,0 @@
package scmrexec
import (
"context"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/client/dce"
"github.com/FalconOpsLLC/goexec/internal/exec"
"github.com/FalconOpsLLC/goexec/internal/util"
"github.com/FalconOpsLLC/goexec/internal/windows"
"github.com/RedTeamPentesting/adauth"
"github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
"github.com/rs/zerolog"
)
const (
ScmrDefaultEndpoint = "ncacn_np:[svcctl]"
ScmrDefaultObject = "367ABB81-9844-35F1-AD32-98F038001003"
)
func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("func", "Connect").Logger()
if ccfg.ConnectionMethod == exec.ConnectionMethodDCE {
if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok {
return fmt.Errorf("invalid configuration for DCE connection method")
} else {
// Fetch target hostname - for opening SCM handle
if mod.hostname, err = target.Hostname(ctx); err != nil {
log.Debug().Err(err).Msg("Failed to get target hostname")
mod.hostname = util.RandomHostname()
err = nil
}
connect := func(ctx context.Context) error {
// Create DCE connection
if mod.dce, err = cfg.GetDce(ctx, creds, target, ScmrDefaultEndpoint, ScmrDefaultObject); err != nil {
log.Error().Err(err).Msg("Failed to initialize DCE dialer")
return fmt.Errorf("create DCE dialer: %w", err)
}
log.Info().Msg("DCE dialer initialized")
// Create SVCCTL client
mod.ctl, err = svcctl.NewSvcctlClient(ctx, mod.dce)
if err != nil {
log.Error().Err(err).Msg("Failed to initialize SCMR client")
return fmt.Errorf("init SCMR client: %w", err)
}
log.Info().Msg("DCE connection successful")
return nil
}
mod.reconnect = func(c context.Context) error {
mod.dce = nil
mod.ctl = nil
return connect(c)
}
return connect(ctx)
}
} else {
return errors.New("unsupported connection method")
}
}
func (mod *Module) Cleanup(ctx context.Context, ccfg *exec.CleanupConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("method", ccfg.CleanupMethod).
Str("func", "Cleanup").Logger()
if len(mod.services) == 0 {
if cfg, ok := ccfg.CleanupMethodConfig.(CleanupMethodDeleteConfig); ok && len(cfg.ServiceNames) > 0 {
for _, svcName := range cfg.ServiceNames {
if svcName != "" {
mod.services = append(mod.services, remoteService{name: svcName})
}
}
}
}
if mod.dce == nil || mod.ctl == nil {
// Try to reconnect
if err := mod.reconnect(ctx); err != nil {
log.Error().Err(err).Msg("Reconnect failed")
return err
}
log.Info().Msg("Reconnect successful")
}
if mod.scm == nil {
// Open a handle to SCM (again)
if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{
MachineName: util.CheckNullString(mod.hostname),
DatabaseName: "ServicesActive\x00",
DesiredAccess: ServiceAllAccess, // TODO: Replace
}); err != nil {
log.Error().Err(err).Msg("Failed to open an SCM handle")
return err
} else {
mod.scm = resp.SCM
log.Info().Msg("Opened an SCM handle")
}
}
for _, rsvc := range mod.services {
log = log.With().Str("service", rsvc.name).Logger()
if rsvc.handle == nil {
// Open a handle to the service in question
if or, err := mod.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{
ServiceManager: mod.scm,
ServiceName: rsvc.name,
DesiredAccess: windows.SERVICE_DELETE | windows.SERVICE_CHANGE_CONFIG,
}); err != nil {
log.Error().Err(err).Msg("Failed to open a service handle")
continue
} else {
rsvc.handle = or.Service
}
log.Info().Msg("Service handle opened")
}
if ccfg.CleanupMethod == CleanupMethodDelete {
// Delete the service
if _, err = mod.ctl.DeleteService(ctx, &svcctl.DeleteServiceRequest{Service: rsvc.handle}); err != nil {
log.Error().Err(err).Msg("Failed to delete service")
continue
}
log.Info().Msg("Service deleted successfully")
} else if ccfg.CleanupMethod == CleanupMethodRevert {
// Revert the service configuration & state
log.Info().Msg("Attempting to revert service configuration")
if _, err = mod.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{
Service: rsvc.handle,
//Dependencies: []byte(rsvc.originalConfig.Dependencies), // TODO: ensure this works
ServiceType: rsvc.originalConfig.ServiceType,
StartType: rsvc.originalConfig.StartType,
ErrorControl: rsvc.originalConfig.ErrorControl,
BinaryPathName: rsvc.originalConfig.BinaryPathName,
LoadOrderGroup: rsvc.originalConfig.LoadOrderGroup,
ServiceStartName: rsvc.originalConfig.ServiceStartName,
DisplayName: rsvc.originalConfig.DisplayName,
TagID: rsvc.originalConfig.TagID,
}); err != nil {
log.Error().Err(err).Msg("Failed to revert service configuration")
continue
}
log.Info().Msg("Service configuration reverted")
}
if _, err = mod.ctl.CloseService(ctx, &svcctl.CloseServiceRequest{ServiceObject: rsvc.handle}); err != nil {
log.Warn().Err(err).Msg("Failed to close service handle")
return nil
}
log.Info().Msg("Closed service handle")
}
return
}
func (mod *Module) Exec(ctx context.Context, ecfg *exec.ExecutionConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("method", ecfg.ExecutionMethod).
Str("func", "Exec").Logger()
if ecfg.ExecutionMethod == MethodCreate {
if cfg, ok := ecfg.ExecutionMethodConfig.(MethodCreateConfig); !ok {
return errors.New("invalid configuration")
} else {
svc := remoteService{
name: cfg.ServiceName,
}
// Open a handle to SCM
if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{
MachineName: util.CheckNullString(mod.hostname),
DatabaseName: "ServicesActive\x00",
DesiredAccess: ServiceAllAccess, // TODO: Replace
}); err != nil {
log.Debug().Err(err).Msg("Failed to open SCM handle")
return fmt.Errorf("open SCM handle: %w", err)
} else {
mod.scm = resp.SCM
log.Info().Msg("Opened SCM handle")
}
// Create service
serviceName := util.RandomStringIfBlank(svc.name)
resp, err := mod.ctl.CreateServiceW(ctx, &svcctl.CreateServiceWRequest{
ServiceManager: mod.scm,
ServiceName: serviceName,
DisplayName: util.RandomStringIfBlank(cfg.DisplayName),
BinaryPathName: ecfg.GetRawCommand(),
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
StartType: windows.SERVICE_DEMAND_START,
DesiredAccess: ServiceAllAccess, // TODO: Replace
})
if err != nil || resp == nil || resp.Return != 0 {
log.Error().Err(err).Msg("Failed to create service")
return fmt.Errorf("create service: %w", err)
}
defer func() {
mod.services = append(mod.services, svc)
}()
svc.handle = resp.Service
log = log.With().
Str("service", serviceName).Logger()
log.Info().Msg("Service created")
// Start the service
sr, err := mod.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle})
if err != nil {
if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case)
log.Warn().Err(err).Msg("Service execution deadline exceeded")
// Connection closes, so we nullify the client variables and handles
mod.dce = nil
mod.ctl = nil
mod.scm = nil
svc.handle = nil
} else if sr != nil && sr.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT { // Check for request timeout
log.Info().Msg("Received request timeout. Execution was likely successful")
} else {
log.Error().Err(err).Msg("Failed to start service")
return fmt.Errorf("start service: %w", err)
}
// Inform the caller that execution was likely successful despite error
err = nil
} else {
log.Info().Msg("Started service")
}
}
} else if ecfg.ExecutionMethod == MethodChange {
if cfg, ok := ecfg.ExecutionMethodConfig.(MethodChangeConfig); !ok {
return errors.New("invalid configuration")
} else {
svc := remoteService{
name: cfg.ServiceName,
}
// Open a handle to SCM
if resp, err := mod.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{
MachineName: util.CheckNullString(mod.hostname),
DatabaseName: "ServicesActive\x00",
DesiredAccess: ServiceAllAccess, // TODO: Replace
}); err != nil {
log.Debug().Err(err).Msg("Failed to open SCM handle")
return fmt.Errorf("open SCM handle: %w", err)
} else {
mod.scm = resp.SCM
log.Info().Msg("Opened SCM handle")
}
// Open a handle to the desired service
if resp, err := mod.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{
ServiceManager: mod.scm,
ServiceName: svc.name,
DesiredAccess: ServiceAllAccess, // TODO: Replace
}); err != nil {
log.Error().Err(err).Msg("Failed to open service handle")
return fmt.Errorf("open service: %w", err)
} else {
svc.handle = resp.Service
}
// Note original service status
if resp, err := mod.ctl.QueryServiceStatus(ctx, &svcctl.QueryServiceStatusRequest{
Service: svc.handle,
}); err != nil {
log.Warn().Err(err).Msg("Failed to get service status")
} else {
svc.originalState = resp.ServiceStatus
}
// Note original service configuration
if resp, err := mod.ctl.QueryServiceConfigW(ctx, &svcctl.QueryServiceConfigWRequest{
Service: svc.handle,
BufferLength: 8 * 1024,
}); err != nil {
log.Error().Err(err).Msg("Failed to fetch service configuration")
return fmt.Errorf("get service config: %w", err)
} else {
log.Info().Str("binaryPath", resp.ServiceConfig.BinaryPathName).Msg("Fetched original service configuration")
svc.originalConfig = resp.ServiceConfig
}
// Stop service if its running
if svc.originalState == nil || svc.originalState.CurrentState != windows.SERVICE_STOPPED {
if resp, err := mod.ctl.ControlService(ctx, &svcctl.ControlServiceRequest{
Service: svc.handle,
Control: windows.SERVICE_STOPPED,
}); err != nil {
if resp != nil && resp.Return == windows.ERROR_SERVICE_NOT_ACTIVE {
log.Info().Msg("Service is already stopped")
} else {
log.Error().Err(err).Msg("Failed to stop service")
}
} else {
log.Info().Msg("Service stopped")
}
}
defer func() {
mod.services = append(mod.services, svc)
}()
// Change service configuration
if _, err = mod.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{
Service: svc.handle,
BinaryPathName: ecfg.GetRawCommand(),
//Dependencies: []byte(svc.originalConfig.Dependencies), // TODO: ensure this works
ServiceType: svc.originalConfig.ServiceType,
StartType: windows.SERVICE_DEMAND_START,
ErrorControl: svc.originalConfig.ErrorControl,
LoadOrderGroup: svc.originalConfig.LoadOrderGroup,
ServiceStartName: svc.originalConfig.ServiceStartName,
DisplayName: svc.originalConfig.DisplayName,
TagID: svc.originalConfig.TagID,
}); err != nil {
log.Error().Err(err).Msg("Failed to change service configuration")
return fmt.Errorf("change service configuration: %w", err)
}
log.Info().Msg("Successfully altered service configuration")
if !cfg.NoStart {
if resp, err := mod.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle}); err != nil {
if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case)
log.Warn().Err(err).Msg("Service execution deadline exceeded")
// Connection closes, so we nullify the client variables and handles
mod.dce = nil
mod.ctl = nil
mod.scm = nil
svc.handle = nil
} else if resp != nil && resp.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT { // Check for request timeout
log.Info().Err(err).Msg("Received request timeout. Execution was likely successful")
} else {
log.Error().Err(err).Msg("Failed to start service")
return fmt.Errorf("start service: %w", err)
}
}
}
}
}
return
}
-44
View File
@@ -1,44 +0,0 @@
package scmrexec
import (
"context"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
)
const (
MethodCreate = "create"
MethodChange = "change"
CleanupMethodDelete = "delete"
CleanupMethodRevert = "revert"
)
type Module struct {
hostname string // The target hostname
dce dcerpc.Conn
reconnect func(context.Context) error
ctl svcctl.SvcctlClient
scm *svcctl.Handle
services []remoteService
}
type MethodCreateConfig struct {
NoDelete bool
ServiceName string
DisplayName string
ServiceType uint32
StartType uint32
}
type MethodChangeConfig struct {
NoStart bool
ServiceName string
}
type CleanupMethodDeleteConfig struct {
ServiceNames []string
}
type CleanupMethodRevertConfig struct{}
-25
View File
@@ -1,25 +0,0 @@
package scmrexec
import (
"context"
"github.com/FalconOpsLLC/goexec/internal/windows"
"github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
)
const (
ServiceDeleteAccess uint32 = windows.SERVICE_DELETE
ServiceModifyAccess uint32 = windows.SERVICE_QUERY_CONFIG | windows.SERVICE_CHANGE_CONFIG | windows.SERVICE_STOP | windows.SERVICE_START | windows.SERVICE_DELETE
ServiceCreateAccess uint32 = windows.SC_MANAGER_CREATE_SERVICE | windows.SERVICE_START | windows.SERVICE_STOP | windows.SERVICE_DELETE
ServiceAllAccess uint32 = ServiceCreateAccess | ServiceModifyAccess
)
type remoteService struct {
name string
handle *svcctl.Handle
originalConfig *svcctl.QueryServiceConfigW
originalState *svcctl.ServiceStatus
}
func (mod *Module) parseServiceDependencies(ctx context.Context) (err error) {
return nil // TODO
}
-189
View File
@@ -1,189 +0,0 @@
package tschexec
import (
"context"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/client/dce"
"github.com/FalconOpsLLC/goexec/internal/exec"
"github.com/FalconOpsLLC/goexec/internal/util"
"github.com/RedTeamPentesting/adauth"
"github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
"github.com/rs/zerolog"
"time"
)
const (
TschDefaultEndpoint = "ncacn_np:[atsvc]"
TschDefaultObject = "86D35949-83C9-4044-B424-DB363231FD0C"
)
// Connect to the target & initialize DCE & TSCH clients
func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("func", "Connect").Logger()
if ccfg.ConnectionMethod == exec.ConnectionMethodDCE {
if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok {
return fmt.Errorf("invalid configuration for DCE connection method")
} else {
// Create DCERPC dialer
mod.dce, err = cfg.GetDce(ctx, creds, target, TschDefaultEndpoint, TschDefaultObject)
if err != nil {
log.Error().Err(err).Msg("Failed to create DCERPC dialer")
return fmt.Errorf("create DCERPC dialer: %w", err)
}
// Create ITaskSchedulerService client
mod.tsch, err = itaskschedulerservice.NewTaskSchedulerServiceClient(ctx, mod.dce)
if err != nil {
log.Error().Err(err).Msg("Failed to initialize TSCH client")
return fmt.Errorf("init TSCH client: %w", err)
}
log.Info().Msg("DCE connection successful")
}
} else {
return errors.New("unsupported connection method")
}
return
}
func (mod *Module) Cleanup(ctx context.Context, ccfg *exec.CleanupConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("method", ccfg.CleanupMethod).
Str("func", "Cleanup").Logger()
if ccfg.CleanupMethod == MethodDelete {
if cfg, ok := ccfg.CleanupMethodConfig.(MethodDeleteConfig); !ok {
return errors.New("invalid configuration")
} else {
log = log.With().Str("task", cfg.TaskPath).Logger()
log.Info().Msg("Manually deleting task")
if err = mod.deleteTask(ctx, cfg.TaskPath); err == nil {
log.Info().Msg("Task deleted successfully")
}
}
} else if ccfg.CleanupMethod == "" {
return nil
} else {
return fmt.Errorf("unsupported cleanup method")
}
return
}
func (mod *Module) Exec(ctx context.Context, ecfg *exec.ExecutionConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("method", ecfg.ExecutionMethod).
Str("func", "Exec").Logger()
if ecfg.ExecutionMethod == MethodRegister {
if cfg, ok := ecfg.ExecutionMethodConfig.(MethodRegisterConfig); !ok {
return errors.New("invalid configuration")
} else {
startTime := time.Now().UTC().Add(cfg.StartDelay)
stopTime := startTime.Add(cfg.StopDelay)
tr := taskTimeTrigger{
StartBoundary: startTime.Format(TaskXMLDurationFormat),
Enabled: true,
}
tk := newTask(nil, nil, triggers{TimeTriggers: []taskTimeTrigger{tr}}, ecfg.ExecutableName, ecfg.ExecutableArgs)
if !cfg.NoDelete && !cfg.CallDelete {
if cfg.StopDelay == 0 {
cfg.StopDelay = time.Second
}
tk.Settings.DeleteExpiredTaskAfter = xmlDuration(cfg.DeleteDelay)
tk.Triggers.TimeTriggers[0].EndBoundary = stopTime.Format(TaskXMLDurationFormat)
}
taskPath := cfg.TaskPath
if taskPath == "" {
log.Debug().Msg("Task path not defined. Using random path")
taskPath = `\` + util.RandomString()
}
// The taskPath is changed here to the *actual path returned by SchRpcRegisterTask
taskPath, err = mod.registerTask(ctx, *tk, taskPath)
if err != nil {
return fmt.Errorf("call registerTask: %w", err)
}
if !cfg.NoDelete {
if cfg.CallDelete {
defer mod.deleteTask(ctx, taskPath)
log.Info().Dur("ms", cfg.StartDelay).Msg("Waiting for task to run")
select {
case <-ctx.Done():
log.Warn().Msg("Cancelling execution")
return err
case <-time.After(cfg.StartDelay + time.Second): // + one second for good measure
for {
if stat, err := mod.tsch.GetLastRunInfo(ctx, &itaskschedulerservice.GetLastRunInfoRequest{Path: taskPath}); err != nil {
log.Warn().Err(err).Msg("Failed to get last run info. Assuming task was executed")
break
} else if stat.LastRuntime.AsTime().IsZero() {
log.Warn().Msg("Task was not yet run. Waiting 10 additional seconds")
time.Sleep(10 * time.Second)
} else {
break
}
}
break
}
} else {
log.Info().Time("when", stopTime).Msg("Task is scheduled to delete")
}
}
}
} else if ecfg.ExecutionMethod == MethodDemand {
if cfg, ok := ecfg.ExecutionMethodConfig.(MethodDemandConfig); !ok {
return errors.New("invalid configuration")
} else {
taskPath := cfg.TaskPath
if taskPath == "" {
log.Debug().Msg("Task path not defined. Using random path")
taskPath = `\` + util.RandomString()
}
st := newSettings(true, true, false)
tk := newTask(st, nil, triggers{}, ecfg.ExecutableName, ecfg.ExecutableArgs)
// The taskPath is changed here to the *actual path returned by SchRpcRegisterTask
taskPath, err = mod.registerTask(ctx, *tk, taskPath)
if err != nil {
return fmt.Errorf("call registerTask: %w", err)
}
if !cfg.NoDelete {
defer mod.deleteTask(ctx, taskPath)
}
var flags uint32
if cfg.SessionId != 0 {
flags |= 4
}
_, err := mod.tsch.Run(ctx, &itaskschedulerservice.RunRequest{
Path: taskPath,
Flags: flags,
SessionID: cfg.SessionId,
})
if err != nil {
log.Error().Str("task", taskPath).Err(err).Msg("Failed to run task")
return fmt.Errorf("force run task: %w", err)
}
log.Info().Str("task", taskPath).Msg("Started task")
}
} else {
return fmt.Errorf("method '%s' not implemented", ecfg.ExecutionMethod)
}
return nil
}
-44
View File
@@ -1,44 +0,0 @@
package tschexec
import (
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
"time"
)
type Module struct {
// dce holds the working DCE connection interface
dce dcerpc.Conn
// tsch holds the ITaskSchedulerService client
tsch itaskschedulerservice.TaskSchedulerServiceClient
}
type MethodRegisterConfig struct {
NoDelete bool
CallDelete bool
TaskPath string
StartDelay time.Duration
StopDelay time.Duration
DeleteDelay time.Duration
}
type MethodDemandConfig struct {
NoDelete bool
CallDelete bool
SessionId uint32
TaskName string
TaskPath string
StopDelay time.Duration
DeleteDelay time.Duration
}
type MethodDeleteConfig struct {
TaskPath string
}
const (
MethodRegister string = "register"
MethodDemand string = "demand"
MethodDelete string = "delete"
MethodChange string = "update"
)
-85
View File
@@ -1,85 +0,0 @@
package tschexec
import (
"fmt"
"regexp"
"time"
)
var (
TaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`)
TaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`)
)
// newSettings just creates a settings instance with the necessary values + a few dynamic ones
func newSettings(terminate, onDemand, startWhenAvailable bool) *settings {
return &settings{
MultipleInstancesPolicy: "IgnoreNew",
AllowHardTerminate: terminate,
IdleSettings: idleSettings{
StopOnIdleEnd: true,
RestartOnIdle: false,
},
AllowStartOnDemand: onDemand,
Enabled: true,
Hidden: true,
Priority: 7, // a pretty standard value for scheduled tasks
StartWhenAvailable: startWhenAvailable,
}
}
// newTask creates a task with any static values filled
func newTask(se *settings, pr []principal, tr triggers, cmd, args string) *task {
if se == nil {
se = newSettings(true, true, false)
}
if pr == nil || len(pr) == 0 {
pr = []principal{
{
ID: "1",
UserID: "S-1-5-18",
RunLevel: "HighestAvailable",
},
}
}
return &task{
TaskVersion: "1.2",
TaskNamespace: "http://schemas.microsoft.com/windows/2004/02/mit/task",
Triggers: tr,
Principals: principals{Principals: pr},
Settings: *se,
Actions: actions{
Context: pr[0].ID,
Exec: []actionExec{
{
Command: cmd,
Arguments: args,
},
},
},
}
}
// xmlDuration is a *very* simple implementation of xs:duration - only accepts +seconds
func xmlDuration(dur time.Duration) string {
if s := int(dur.Seconds()); s >= 0 {
return fmt.Sprintf(`PT%dS`, s)
}
return `PT0S`
}
// ValidateTaskName will validate the provided task name according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1
func ValidateTaskName(taskName string) error {
if !TaskNameRegex.MatchString(taskName) {
return fmt.Errorf("invalid task name: %s", taskName)
}
return nil
}
// ValidateTaskPath will validate the provided task path according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1
func ValidateTaskPath(taskPath string) error {
if !TaskPathRegex.MatchString(taskPath) {
return fmt.Errorf("invalid task path: %s", taskPath)
}
return nil
}
-141
View File
@@ -1,141 +0,0 @@
package tschexec
import (
"context"
"encoding/xml"
"fmt"
"github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
"github.com/rs/zerolog"
)
const (
TaskXMLDurationFormat = "2006-01-02T15:04:05.9999999Z"
TaskXMLHeader = `<?xml version="1.0" encoding="UTF-16"?>`
)
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f
type triggers struct {
XMLName xml.Name `xml:"Triggers"`
TimeTriggers []taskTimeTrigger `xml:"TimeTrigger,omitempty"`
}
type taskTimeTrigger struct {
XMLName xml.Name `xml:"TimeTrigger"`
StartBoundary string `xml:"StartBoundary,omitempty"` // Derived from time.Time
EndBoundary string `xml:"EndBoundary,omitempty"` // Derived from time.Time; must be > StartBoundary
Enabled bool `xml:"Enabled"`
}
type idleSettings struct {
XMLName xml.Name `xml:"IdleSettings"`
StopOnIdleEnd bool `xml:"StopOnIdleEnd"`
RestartOnIdle bool `xml:"RestartOnIdle"`
}
type settings struct {
XMLName xml.Name `xml:"Settings"`
Enabled bool `xml:"Enabled"`
Hidden bool `xml:"Hidden"`
DisallowStartIfOnBatteries bool `xml:"DisallowStartIfOnBatteries"`
StopIfGoingOnBatteries bool `xml:"StopIfGoingOnBatteries"`
AllowHardTerminate bool `xml:"AllowHardTerminate"`
RunOnlyIfNetworkAvailable bool `xml:"RunOnlyIfNetworkAvailable"`
AllowStartOnDemand bool `xml:"AllowStartOnDemand"`
WakeToRun bool `xml:"WakeToRun"`
RunOnlyIfIdle bool `xml:"RunOnlyIfIdle"`
StartWhenAvailable bool `xml:"StartWhenAvailable"`
Priority int `xml:"Priority,omitempty"` // 1 to 10 inclusive
MultipleInstancesPolicy string `xml:"MultipleInstancesPolicy,omitempty"`
ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"`
DeleteExpiredTaskAfter string `xml:"DeleteExpiredTaskAfter,omitempty"` // Derived from time.Duration
IdleSettings idleSettings `xml:"IdleSettings,omitempty"`
}
type actionExec struct {
XMLName xml.Name `xml:"Exec"`
Command string `xml:"Command"`
Arguments string `xml:"Arguments,omitempty"`
}
type actions struct {
XMLName xml.Name `xml:"Actions"`
Context string `xml:"Context,attr"`
Exec []actionExec `xml:"Exec,omitempty"`
}
type principals struct {
XMLName xml.Name `xml:"Principals"`
Principals []principal `xml:"Principal,omitempty"`
}
type principal struct {
XMLName xml.Name `xml:"Principal"`
ID string `xml:"id,attr"`
UserID string `xml:"UserId"`
RunLevel string `xml:"RunLevel"`
}
type task struct {
XMLName xml.Name `xml:"Task"`
TaskVersion string `xml:"version,attr"`
TaskNamespace string `xml:"xmlns,attr"`
Triggers triggers `xml:"Triggers"`
Actions actions `xml:"Actions"`
Principals principals `xml:"Principals"`
Settings settings `xml:"Settings"`
}
// registerTask serializes and submits the provided task structure
func (mod *Module) registerTask(ctx context.Context, taskDef task, taskPath string) (path string, err error) {
var taskXml string
log := zerolog.Ctx(ctx).With().
Str("module", "tsch").
Str("func", "createTask").Logger()
// Generate task XML content. See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f
{
doc, err := xml.Marshal(taskDef)
if err != nil {
log.Error().Err(err).Msg("failed to marshal task XML")
return "", fmt.Errorf("marshal task: %w", err)
}
taskXml = TaskXMLHeader + string(doc)
log.Debug().Str("content", taskXml).Msg("Generated task XML")
}
// Submit task
{
response, err := mod.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{
Path: taskPath,
XML: taskXml,
Flags: 0, // TODO
LogonType: 0, // TASK_LOGON_NONE
CredsCount: 0,
Creds: nil,
})
if err != nil {
log.Error().Err(err).Msg("Failed to register task")
return "", fmt.Errorf("register task: %w", err)
}
log.Info().Str("path", taskPath).Msg("Task created successfully")
path = response.ActualPath
}
return
}
func (mod *Module) deleteTask(ctx context.Context, taskPath string) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", "tsch").
Str("path", taskPath).
Str("func", "deleteTask").Logger()
if _, err = mod.tsch.Delete(ctx, &itaskschedulerservice.DeleteRequest{Path: taskPath}); err != nil {
log.Error().Err(err).Msg("Failed to delete task")
return fmt.Errorf("delete task: %w", err)
}
log.Info().Msg("Task deleted successfully")
return
}
-166
View File
@@ -1,166 +0,0 @@
package wmiexec
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/client/dce"
"github.com/FalconOpsLLC/goexec/internal/exec"
"github.com/RedTeamPentesting/adauth"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/dcom"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemlevel1login/v0"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0"
"github.com/rs/zerolog"
)
const (
ProtocolSequenceRPC uint16 = 7
ProtocolSequenceNP uint16 = 15
DefaultWmiEndpoint string = "ncacn_ip_tcp:[135]"
)
var (
ComVersion = &dcom.COMVersion{
MajorVersion: 5,
MinorVersion: 7,
}
ORPCThis = &dcom.ORPCThis{Version: ComVersion}
)
func (mod *Module) Cleanup(ctx context.Context, _ *exec.CleanupConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", "tsch").
Str("func", "Cleanup").Logger()
if err = mod.dce.Close(ctx); err != nil {
log.Warn().Err(err).Msg("Failed to close DCERPC connection")
}
mod.sc = nil
mod.dce = nil
return
}
func (mod *Module) Connect(ctx context.Context, creds *adauth.Credential, target *adauth.Target, ccfg *exec.ConnectionConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("method", ccfg.ConnectionMethod).
Str("func", "Connect").Logger()
if cfg, ok := ccfg.ConnectionMethodConfig.(dce.ConnectionMethodDCEConfig); !ok {
return errors.New("invalid configuration for DCE connection method")
} else {
var dceOpts []dcerpc.Option
// Create DCE connection
if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultWmiEndpoint, "", dceOpts...); err != nil {
log.Error().Err(err).Msg("Failed to initialize DCE dialer")
return fmt.Errorf("create DCE dialer: %w", err)
}
ia, err := iactivation.NewActivationClient(ctx, mod.dce)
if err != nil {
log.Error().Err(err).Msg("Failed to create activation client")
return fmt.Errorf("create activation client: %w", err)
}
act, err := ia.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{
ORPCThis: ORPCThis,
ClassID: wmi.Level1LoginClassID.GUID(),
IIDs: []*dcom.IID{iwbemlevel1login.Level1LoginIID},
RequestedProtocolSequences: []uint16{ProtocolSequenceRPC}, // TODO: Named pipe support
})
if err != nil {
return fmt.Errorf("request remote activation: %w", err)
}
if act.HResult != 0 {
return fmt.Errorf("remote activation failed with code %d", act.HResult)
}
retBinds := act.OXIDBindings.GetStringBindings()
if len(act.InterfaceData) < 1 || len(retBinds) < 1 {
return errors.New("remote activation failed")
}
ipid := act.InterfaceData[0].GetStandardObjectReference().Std.IPID
for _, b := range retBinds {
sb, err := dcerpc.ParseStringBinding("ncacn_ip_tcp:" + b.NetworkAddr)
if err != nil {
log.Debug().Err(err).Msg("Failed to parse string binding")
}
sb.NetworkAddress = target.AddressWithoutPort()
dceOpts = append(dceOpts, dcerpc.WithEndpoint(sb.String()))
}
if mod.dce, err = cfg.GetDce(ctx, creds, target, DefaultWmiEndpoint, "", dceOpts...); err != nil {
log.Error().Err(err).Msg("Failed to initialize secondary DCE dialer")
}
loginClient, err := iwbemlevel1login.NewLevel1LoginClient(ctx, mod.dce, dcom.WithIPID(ipid))
if err != nil {
return fmt.Errorf("initialize wbem login client: %w", err)
}
login, err := loginClient.NTLMLogin(ctx, &iwbemlevel1login.NTLMLoginRequest{
This: ORPCThis,
NetworkResource: cfg.Resource,
})
if err != nil {
return fmt.Errorf("ntlm login: %w", err)
}
mod.sc, err = iwbemservices.NewServicesClient(ctx, mod.dce, dcom.WithIPID(login.Namespace.InterfacePointer().IPID()))
if err != nil {
return fmt.Errorf("create services client: %w", err)
}
}
return
}
func (mod *Module) Exec(ctx context.Context, ecfg *exec.ExecutionConfig) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", "tsch").
Str("method", ecfg.ExecutionMethod).Logger()
if ecfg.ExecutionMethod == MethodCall {
if cfg, ok := ecfg.ExecutionMethodConfig.(MethodCallConfig); !ok {
return errors.New("invalid execution configuration")
} else {
out, err := mod.query(ctx, cfg.Class, cfg.Method, cfg.Arguments)
if err != nil {
return fmt.Errorf("query: %w", err)
}
if outJson, err := json.Marshal(out); err != nil {
log.Error().Err(err).Msg("failed to marshal call output")
} else {
fmt.Println(string(outJson))
}
}
} else if ecfg.ExecutionMethod == MethodProcess {
if cfg, ok := ecfg.ExecutionMethodConfig.(MethodProcessConfig); !ok {
return errors.New("invalid execution configuration")
} else {
out, err := mod.query(ctx, "Win32_Process", "Create", map[string]any{
"CommandLine": cfg.Command,
"WorkingDir": cfg.WorkingDirectory,
})
if err != nil {
return fmt.Errorf("query: %w", err)
}
if pid, ok := out["ProcessId"]; ok && pid != nil {
log.Info().
Any("PID", pid).
Any("return", out["ReturnValue"]).
Msg("Process created")
} else {
log.Error().
Any("return", out["ReturnValue"]).
Msg("Process creation failed")
return errors.New("failed to create process")
}
}
} else {
return errors.New("unsupported execution method")
}
return nil
}
-33
View File
@@ -1,33 +0,0 @@
package wmiexec
import (
"github.com/RedTeamPentesting/adauth"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0"
"github.com/rs/zerolog"
)
type Module struct {
creds *adauth.Credential
target *adauth.Target
log zerolog.Logger
dce dcerpc.Conn
sc iwbemservices.ServicesClient
}
type MethodCallConfig struct {
Class string
Method string
Arguments map[string]any
}
type MethodProcessConfig struct {
Command string
WorkingDirectory string
}
const (
MethodCall = "call"
MethodProcess = "process"
)
-26
View File
@@ -1,26 +0,0 @@
package wmiexec
import (
"context"
"errors"
"fmt"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmio/query"
)
func (mod *Module) query(ctx context.Context, class, method string, values map[string]any) (outValues map[string]any, err error) {
outValues = make(map[string]any)
if mod.sc == nil {
err = errors.New("module has not been initialized")
return
}
if out, err := query.NewBuilder(ctx, mod.sc, ComVersion).
Spawn(class). // The class to instantiate (i.e. Win32_Process)
Method(method). // The method to call (i.e. Create)
Values(values). // The values to pass to method
Exec().
Object(); err == nil {
return out.Values(), err
}
err = fmt.Errorf("(*query.Builder).Spawn: %w", err)
return
}
+5
View File
@@ -1,6 +1,7 @@
package util
import (
"github.com/google/uuid"
"math/rand" // not crypto secure
"regexp"
"strings"
@@ -23,6 +24,10 @@ func RandomHostname() (hostname string) {
}
}
func RandomWindowsTempFile() string {
return `\Windows\Temp\` + strings.ToUpper(uuid.New().String())
}
func RandomString() string {
return RandomStringFromCharset(randStringCharset, rand.Intn(10)+6)
}
+11
View File
@@ -0,0 +1,11 @@
package goexec
import (
"github.com/RedTeamPentesting/adauth"
)
// AuthOptions holds Windows / Active Directory authentication parameters
type AuthOptions struct {
Target *adauth.Target
Credential *adauth.Credential
}
+31
View File
@@ -0,0 +1,31 @@
package goexec
import (
"context"
"github.com/rs/zerolog"
)
type CleanProvider interface {
Clean(ctx context.Context) (err error)
}
type Cleaner struct {
workers []func(ctx context.Context) error
}
func (c *Cleaner) AddCleaner(worker func(ctx context.Context) error) {
c.workers = append(c.workers, worker)
}
func (c *Cleaner) Clean(ctx context.Context) (err error) {
log := zerolog.Ctx(ctx).With().
Str("component", "cleaner").Logger()
for _, worker := range c.workers {
if err = worker(log.WithContext(ctx)); err != nil {
log.Warn().Err(err).Msg("Clean worker failed")
}
}
return
}
+26
View File
@@ -0,0 +1,26 @@
package goexec
import "context"
// Client represents an application layer network client
type Client interface {
// Connect establishes a connection to the remote server
Connect(ctx context.Context) error
// Close terminates the active connection and frees allocated resources
Close(ctx context.Context) error
}
// ClientOptions represents configuration options for a Client
type ClientOptions struct {
// Proxy specifies the URI of the proxy server to route client requests through
Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"`
// Host specifies the hostname or IP address that the client should connect to
Host string `json:"host" yaml:"host"`
// Port specifies the network port on Host that the client will connect to
Port uint16 `json:"port" yaml:"port"`
}
+89
View File
@@ -0,0 +1,89 @@
package dce
import (
"context"
"fmt"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3"
"github.com/rs/zerolog"
)
type Client struct {
Options
conn dcerpc.Conn
hostname string
}
func NewClient() *Client {
return new(Client)
}
func (c *Client) String() string {
return ClientName
}
func (c *Client) Reconnect(ctx context.Context, opts ...dcerpc.Option) (err error) {
c.DcerpcOptions = append(c.DcerpcOptions, opts...)
return c.Connect(ctx)
}
func (c *Client) Dce() (dce dcerpc.Conn) {
return c.conn
}
func (c *Client) Logger(ctx context.Context) (log zerolog.Logger) {
return zerolog.Ctx(ctx).With().
Str("client", c.String()).Logger()
}
func (c *Client) Connect(ctx context.Context) (err error) {
log := c.Logger(ctx)
ctx = log.WithContext(ctx)
var do, eo []dcerpc.Option
do = append(do, c.DcerpcOptions...)
do = append(do, c.authOptions...)
if !c.NoSign {
do = append(do, dcerpc.WithSign())
eo = append(eo, dcerpc.WithSign())
}
if !c.NoSeal {
do = append(do, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
eo = append(eo, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
}
if !c.NoLog {
do = append(do, dcerpc.WithLogger(log))
eo = append(eo, dcerpc.WithLogger(log))
}
if !c.NoEpm {
log.Debug().Msg("Using endpoint mapper")
eo = append(eo, c.epmOptions...)
eo = append(eo, c.authOptions...)
do = append(do, epm.EndpointMapper(ctx, c.Host, eo...))
}
for _, e := range c.stringBindings {
do = append(do, dcerpc.WithEndpoint(e.String()))
}
if c.conn, err = dcerpc.Dial(ctx, c.Host, do...); err != nil {
log.Error().Err(err).Msgf("Failed to connect to %s endpoint", c.String())
return fmt.Errorf("dial %s: %w", c.String(), err)
}
return
}
func (c *Client) Close(ctx context.Context) (err error) {
return c.conn.Close(ctx)
}
+5
View File
@@ -0,0 +1,5 @@
package dce
const (
ClientName = "DCE"
)
+108
View File
@@ -0,0 +1,108 @@
package dce
import (
"context"
"fmt"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/RedTeamPentesting/adauth/dcerpcauth"
"github.com/oiweiwei/go-msrpc/dcerpc"
"net"
)
type Options struct {
goexec.ClientOptions
goexec.AuthOptions
// NoSign disables packet signing by omitting dcerpc.WithSign()
NoSign bool `json:"no_sign" yaml:"no_sign"`
// NoSeal disables packet stub encryption by omitting dcerpc.WithSeal()
NoSeal bool `json:"no_seal" yaml:"no_seal"`
// NoLog disables logging by omitting dcerpc.WithLogger(...)
NoLog bool `json:"no_log" yaml:"no_log"`
// NoEpm disables DCE endpoint mapper communications
NoEpm bool `json:"no_epm" yaml:"no_epm"`
// Endpoint stores the explicit DCE string binding to use
Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
// Filter stores the filter for returned endpoints from an endpoint mapper
Filter string `json:"filter,omitempty" yaml:"filter,omitempty"`
netDialer goexec.Dialer
dialer dcerpc.Dialer
authOptions []dcerpc.Option
DcerpcOptions []dcerpc.Option
epmOptions []dcerpc.Option
stringBindings []*dcerpc.StringBinding
}
func (c *Client) Parse(ctx context.Context) (err error) {
// Reset internals
{
c.netDialer = nil
c.dialer = nil
c.stringBindings = []*dcerpc.StringBinding{}
c.authOptions = []dcerpc.Option{}
c.DcerpcOptions = []dcerpc.Option{}
c.epmOptions = []dcerpc.Option{
dcerpc.WithSign(), // Require signing for EPM
}
}
if !c.NoSeal {
// Enable encryption
c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
c.epmOptions = append(c.epmOptions, dcerpc.WithSeal(), dcerpc.WithSecurityLevel(dcerpc.AuthLevelPktPrivacy))
}
if !c.NoSign {
// Enable signing
c.DcerpcOptions = append(c.DcerpcOptions, dcerpc.WithSign())
//c.epmOptions = append(c.epmOptions, dcerpc.WithSign())
}
// Parse DCERPC endpoint
if c.Endpoint != "" {
sb, err := dcerpc.ParseStringBinding(c.Endpoint)
if err != nil {
return err
}
c.stringBindings = append(c.stringBindings, sb)
}
// Parse EPM filter
if c.Filter != "" {
sb, err := dcerpc.ParseStringBinding(c.Filter)
if err != nil {
return err
}
c.stringBindings = append(c.stringBindings, sb)
}
if c.Proxy == "" {
c.netDialer = &net.Dialer{} // FUTURE: additional dial c
} else {
// Parse proxy URL
d, err := goexec.ParseProxyURI(c.Proxy)
if err != nil {
return err
}
var ok bool
if c.dialer, ok = d.(dcerpc.Dialer); !ok {
return fmt.Errorf("cannot cast %T to dcerpc.Dialer", d)
}
}
// Parse authentication parameters
if c.authOptions, err = dcerpcauth.AuthenticationOptions(ctx, c.Credential, c.Target, &dcerpcauth.Options{}); err != nil {
return fmt.Errorf("parse auth c: %w", err)
}
c.Host = c.Target.AddressWithoutPort()
return
}
+34
View File
@@ -0,0 +1,34 @@
package dcomexec
import (
guuid "github.com/google/uuid"
"github.com/oiweiwei/go-msrpc/midl/uuid"
"github.com/oiweiwei/go-msrpc/msrpc/dcom"
"github.com/oiweiwei/go-msrpc/msrpc/dtyp"
)
const (
LcEnglishUs uint32 = 0x409
)
var (
ShellWindowsUuid = uuid.MustParse("9BA05972-F6A8-11CF-A442-00A0C90A8F39")
Mmc20Uuid = uuid.MustParse("49B2791A-B1AE-4C90-9B8E-E860BA07F889")
RandCid = dcom.CID(*dtyp.GUIDFromUUID(uuid.MustParse(guuid.NewString())))
IDispatchIID = &dcom.IID{
Data1: 0x20400,
Data2: 0x0,
Data3: 0x0,
Data4: []byte{0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46},
}
ComVersion = &dcom.COMVersion{
MajorVersion: 5,
MinorVersion: 7,
}
MmcClsid = dcom.ClassID(*dtyp.GUIDFromUUID(Mmc20Uuid))
ORPCThis = &dcom.ORPCThis{
Version: ComVersion,
CID: &RandCid,
}
)
+50
View File
@@ -0,0 +1,50 @@
package dcomexec
import (
"context"
"fmt"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/rs/zerolog"
)
const (
MethodMmc = "MMC" // MMC20.Application::Document.ActiveView.ExecuteShellCommand
)
type DcomMmc struct {
DcomExec
WorkingDirectory string
WindowState string
}
// Execute will perform command execution via the MMC20.Application DCOM object.
func (m *DcomMmc) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).
Str("method", MethodMmc).
Logger()
method := "Document.ActiveView.ExecuteShellCommand"
var args = in.Arguments
if args == "" {
args = " " // the process arguments can't be a blank string
}
// Arguments must be passed in reverse order
if _, err := callComMethod(ctx,
m.dispatchClient,
method,
stringToVariant(m.WindowState),
stringToVariant(in.Arguments),
stringToVariant(m.WorkingDirectory),
stringToVariant(in.Executable)); err != nil {
log.Error().Err(err).Msg("Failed to call method")
return fmt.Errorf("call %q: %w", method, err)
}
log.Info().Msg("Method call successful")
return
}
+111
View File
@@ -0,0 +1,111 @@
package dcomexec
import (
"context"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/dcom"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/iremotescmactivator/v0"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/oaut/idispatch/v0"
"github.com/rs/zerolog"
)
const (
ModuleName = "DCOM"
)
type DcomExec struct {
client *dce.Client
dispatchClient idispatch.DispatchClient
}
func (m *DcomExec) Init(ctx context.Context, c *dce.Client) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).Logger()
m.client = c
if m.client.Dce() == nil {
return errors.New("DCE connection not initialized")
}
opts := []dcerpc.Option{
dcerpc.WithSign(),
}
inst := &dcom.InstantiationInfoData{
ClassID: &MmcClsid,
IID: []*dcom.IID{IDispatchIID},
ClientCOMVersion: ComVersion,
}
ac := &dcom.ActivationContextInfoData{}
loc := &dcom.LocationInfoData{}
scm := &dcom.SCMRequestInfoData{
RemoteRequest: &dcom.CustomRemoteRequestSCMInfo{
RequestedProtocolSequences: []uint16{7},
},
}
ap := &dcom.ActivationProperties{
DestinationContext: 2,
Properties: []dcom.ActivationProperty{inst, ac, loc, scm},
}
apin, err := ap.ActivationPropertiesIn()
if err != nil {
return err
}
act, err := iremotescmactivator.NewRemoteSCMActivatorClient(ctx, m.client.Dce())
if err != nil {
return err
}
cr, err := act.RemoteCreateInstance(ctx, &iremotescmactivator.RemoteCreateInstanceRequest{
ORPCThis: &dcom.ORPCThis{
Version: ComVersion,
Flags: 1,
CID: &RandCid,
},
ActPropertiesIn: apin,
})
if err != nil {
return err
}
log.Info().Msg("RemoteCreateInstance succeeded")
apout := new(dcom.ActivationProperties)
if err = apout.Parse(cr.ActPropertiesOut); err != nil {
return err
}
si := apout.SCMReplyInfoData()
pi := apout.PropertiesOutInfo()
if si == nil {
return fmt.Errorf("remote create instance response: SCMReplyInfoData is nil")
}
if pi == nil {
return fmt.Errorf("remote create instance response: PropertiesOutInfo is nil")
}
oIPID := pi.InterfaceData[0].IPID()
opts = append(opts, si.RemoteReply.OXIDBindings.EndpointsByProtocol("ncacn_ip_tcp")...) // TODO
err = c.Reconnect(ctx, opts...)
if err != nil {
return err
}
log.Info().Msg("created new DCERPC dialer")
m.dispatchClient, err = idispatch.NewDispatchClient(ctx, c.Dce(), dcom.WithIPID(oIPID))
if err != nil {
return err
}
log.Info().Msg("created IDispatch client")
return
}
@@ -10,56 +10,76 @@ import (
"strings"
)
const (
LC_ENGLISH_US uint32 = 0x409
)
func callComMethod(ctx context.Context, dc idispatch.DispatchClient, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) {
func callMethod(ctx context.Context, dc idispatch.DispatchClient, method string, args ...*oaut.Variant) (ir *idispatch.InvokeResponse, err error) {
parts := strings.Split(method, ".")
var id *dcom.IPID
var gr *idispatch.GetIDsOfNamesResponse
for i, obj := range parts {
var opts []dcerpc.CallOption
if id != nil {
opts = append(opts, dcom.WithIPID(id))
}
gr, err = dc.GetIDsOfNames(ctx, &idispatch.GetIDsOfNamesRequest{
This: ORPCThis,
IID: &dcom.IID{},
Names: []string{obj + "\x00"},
LocaleID: LC_ENGLISH_US,
LocaleID: LcEnglishUs,
Names: []string{obj + "\x00"},
}, opts...)
if err != nil {
return nil, fmt.Errorf("get dispatch ID of name %q: %w", obj, err)
}
if len(gr.DispatchID) < 1 {
return nil, fmt.Errorf("dispatch ID of name %q not found", obj)
}
irq := &idispatch.InvokeRequest{
This: ORPCThis,
This: ORPCThis,
IID: &dcom.IID{},
LocaleID: LcEnglishUs,
DispatchIDMember: gr.DispatchID[0],
IID: &dcom.IID{},
LocaleID: LC_ENGLISH_US,
}
if i >= len(parts)-1 {
irq.Flags = 1
irq.DispatchParams = &oaut.DispatchParams{ArgsCount: uint32(len(args)), Args: args}
return dc.Invoke(ctx, irq, opts...)
}
irq.Flags = 2
ir, err = dc.Invoke(ctx, irq, opts...)
if err != nil {
return nil, fmt.Errorf("get properties of object %q: %w", obj, err)
}
di, ok := ir.VarResult.VarUnion.GetValue().(*oaut.Dispatch)
if !ok {
return nil, fmt.Errorf("invalid dispatch object for %q", obj)
}
id = di.InterfacePointer().GetStandardObjectReference().Std.IPID
}
return
}
func stringToVariant(s string) *oaut.Variant {
return &oaut.Variant{
Size: 5,
VT: 8,
VarUnion: &oaut.Variant_VarUnion{
Value: &oaut.Variant_VarUnion_BSTR{
BSTR: &oaut.String{
Data: s,
},
},
},
}
}
+14
View File
@@ -0,0 +1,14 @@
package goexec
import "context"
type ExecutionProvider interface {
Execute(ctx context.Context, in *ExecutionInput) (err error)
}
type Executor struct{}
type CleanExecutionProvider interface {
ExecutionProvider
CleanProvider
}
+87
View File
@@ -0,0 +1,87 @@
package goexec
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
)
type OutputProvider interface {
GetOutput(ctx context.Context) (out io.ReadCloser, err error)
}
type ExecutionIO struct {
Cleaner
Input *ExecutionInput
Output *ExecutionOutput
}
type ExecutionOutput struct {
NoDelete bool
RemotePath string
Provider OutputProvider
}
type ExecutionInput struct {
FilePath string
Executable string
ExecutablePath string
Arguments string
CommandLine string
}
func (execIO *ExecutionIO) GetOutput(ctx context.Context) (out io.ReadCloser, err error) {
if execIO.Output.Provider != nil {
return execIO.Output.Provider.GetOutput(ctx)
}
return nil, errors.New("output provider not set")
}
func (execIO *ExecutionIO) CommandLine() string {
return execIO.Input.Command()
}
func (execIO *ExecutionIO) String() (cmd string) {
cmd = execIO.Input.Command()
if execIO.Output.Provider != nil && execIO.Output.RemotePath != "" {
return fmt.Sprintf(`C:\Windows\System32\cmd.exe /C %s > %s`, cmd, execIO.Output.RemotePath)
}
return
}
func (i *ExecutionInput) Command() string {
if i.CommandLine == "" {
if i.ExecutablePath != "" {
i.CommandLine = i.ExecutablePath
} else if i.Executable != "" {
i.CommandLine = i.Executable
}
if i.Arguments != "" {
i.CommandLine += " " + i.Arguments
}
}
return i.CommandLine
}
func (i *ExecutionInput) String() string {
return i.Command()
}
func (i *ExecutionInput) UploadReader(_ context.Context) (reader io.Reader, err error) {
if i.FilePath != "" {
return os.OpenFile(i.FilePath, os.O_RDONLY, 0)
}
return bytes.NewBufferString(i.Command()), nil
}
+72
View File
@@ -0,0 +1,72 @@
package goexec
import (
"context"
"fmt"
"github.com/rs/zerolog"
)
type Method interface{}
type RemoteMethod interface {
Connect(ctx context.Context) error
Init(ctx context.Context) error
}
type RemoteExecuteMethod interface {
RemoteMethod
Execute(ctx context.Context, io *ExecutionIO) error
}
type RemoteCleanMethod interface {
RemoteMethod
Clean(ctx context.Context) error
}
type RemoteExecuteCleanMethod interface {
RemoteExecuteMethod
Clean(ctx context.Context) error
}
type RemoteExecuteCleanMethodWithOutput interface {
RemoteExecuteCleanMethod
OutputProvider
}
func ExecuteMethod(ctx context.Context, module RemoteExecuteMethod, execIO *ExecutionIO) (err error) {
log := zerolog.Ctx(ctx)
if err = module.Connect(ctx); err != nil {
log.Error().Err(err).Msg("Connection failed")
return fmt.Errorf("connect: %w", err)
}
if err = module.Init(ctx); err != nil {
log.Error().Err(err).Msg("Module initialization failed")
return fmt.Errorf("init module: %w", err)
}
if err = module.Execute(ctx, execIO); err != nil {
log.Error().Err(err).Msg("Execution failed")
return fmt.Errorf("execute: %w", err)
}
return
}
func ExecuteCleanMethod(ctx context.Context, module RemoteExecuteCleanMethod, execIO *ExecutionIO) (err error) {
log := zerolog.Ctx(ctx)
defer func() {
if err = module.Clean(ctx); err != nil {
log.Error().Err(err).Msg("Module cleanup failed")
}
}()
return ExecuteMethod(ctx, module, execIO)
}
func ExecuteCleanMethodWithOutput(ctx context.Context, module RemoteExecuteCleanMethodWithOutput, execIO *ExecutionIO) (err error) {
return ExecuteCleanMethod(ctx, module, execIO)
}
+25
View File
@@ -0,0 +1,25 @@
package goexec
import "context"
type Module interface {
Init(ctx context.Context) error
}
type ExecutionModule interface {
Module
ExecutionProvider
}
type CleanExecutionModule interface {
Module
ExecutionProvider
CleanProvider
}
type CleanExecutionOutputModule interface {
Module
ExecutionProvider
CleanProvider
OutputProvider
}
+33
View File
@@ -0,0 +1,33 @@
package goexec
import (
"fmt"
"golang.org/x/net/proxy"
"net"
"net/url"
)
// Dialer outlines a basic implementation for establishing network connections
type Dialer interface {
// Dial establishes a network connection (net.Conn) using the provided parameters
Dial(network string, address string) (connection net.Conn, err error)
}
// ParseProxyURI parses the provided proxy URI spec to a Dialer
func ParseProxyURI(uri string) (dialer Dialer, err error) {
// Parse proxy spec as URL
u, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("parse proxy URI: %w", err)
}
// Create dialer from URL
dialer, err = proxy.FromURL(u, nil)
if err != nil {
return nil, fmt.Errorf("init proxy: %w", err)
}
return
}
+144
View File
@@ -0,0 +1,144 @@
package scmrexec
import (
"context"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/windows"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
"github.com/rs/zerolog"
)
const (
MethodChange = "Change"
)
type ScmrChange struct {
Scmr
goexec.Cleaner
goexec.Executor
NoStart bool
ServiceName string
}
func (m *ScmrChange) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).
Str("method", MethodChange).
Str("service", m.ServiceName).
Logger()
svc := &service{name: m.ServiceName}
openResponse, err := m.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{
ServiceManager: m.scm,
ServiceName: svc.name,
DesiredAccess: ServiceAllAccess,
})
if err != nil {
log.Error().Err(err).Msg("Failed to open service handle")
return fmt.Errorf("open service request: %w", err)
}
if openResponse.Return != 0 {
log.Error().Err(err).Msg("Failed to open service handle")
return fmt.Errorf("create service: %w", err)
}
svc.handle = openResponse.Service
log.Info().Msg("Opened service handle")
defer m.AddCleaner(func(ctxInner context.Context) error {
r, errInner := m.ctl.CloseService(ctxInner, &svcctl.CloseServiceRequest{
ServiceObject: svc.handle,
})
if errInner != nil {
return fmt.Errorf("close service: %w", errInner)
}
if r.Return != 0 {
return fmt.Errorf("close service returned non-zero exit code: %02x", r.Return)
}
log.Info().Msg("Closed service handle")
return nil
})
// Note original service configuration
queryResponse, err := m.ctl.QueryServiceConfigW(ctx, &svcctl.QueryServiceConfigWRequest{
Service: svc.handle,
BufferLength: 8 * 1024,
})
if err != nil {
log.Error().Err(err).Msg("Failed to fetch service configuration")
return fmt.Errorf("get service config: %w", err)
}
if queryResponse.Return != 0 {
log.Error().Err(err).Msg("Failed to query service configuration")
return fmt.Errorf("query service config: %w", err)
}
log.Info().Str("binaryPath", queryResponse.ServiceConfig.BinaryPathName).Msg("Fetched original service configuration")
svc.originalConfig = queryResponse.ServiceConfig
stopResponse, err := m.ctl.ControlService(ctx, &svcctl.ControlServiceRequest{
Service: svc.handle,
Control: windows.SERVICE_CONTROL_STOP,
})
if err != nil {
if stopResponse == nil || stopResponse.Return != windows.ERROR_SERVICE_NOT_ACTIVE {
log.Error().Err(err).Msg("Failed to stop existing service")
return fmt.Errorf("stop service: %w", err)
}
log.Debug().Msg("Service is not running")
// TODO: restore state
/*
defer m.AddCleaner(func(ctxInner context.Context) error {
// ...
return nil
})
*/
} else {
log.Info().Msg("Stopped existing service")
}
changeResponse, err := m.ctl.ChangeServiceConfigW(ctx, &svcctl.ChangeServiceConfigWRequest{
Service: svc.handle,
BinaryPathName: in.String(),
DisplayName: svc.originalConfig.DisplayName,
ServiceType: svc.originalConfig.ServiceType,
StartType: windows.SERVICE_DEMAND_START,
ErrorControl: svc.originalConfig.ErrorControl,
LoadOrderGroup: svc.originalConfig.LoadOrderGroup,
ServiceStartName: svc.originalConfig.ServiceStartName,
TagID: svc.originalConfig.TagID,
//Dependencies: []byte(svc.originalConfig.Dependencies), // TODO
})
if err != nil {
log.Error().Err(err).Msg("Failed to request service configuration change")
return fmt.Errorf("change service config request: %w", err)
}
if changeResponse.Return != 0 {
log.Error().Err(err).Msg("Failed to change service configuration")
return fmt.Errorf("change service config: %w", err)
}
if !m.NoStart {
err = m.startService(ctx, svc)
if err != nil {
log.Error().Err(err).Msg("Failed to start service")
}
}
return
}
+125
View File
@@ -0,0 +1,125 @@
package scmrexec
import (
"context"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/util"
"github.com/FalconOpsLLC/goexec/internal/windows"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
"github.com/rs/zerolog"
)
const (
MethodCreate = "Create"
)
type ScmrCreate struct {
Scmr
goexec.Cleaner
goexec.Executor
NoDelete bool
NoStart bool
ServiceName string
DisplayName string
}
func (m *ScmrCreate) ensure() {
if m.ServiceName == "" {
m.ServiceName = util.RandomString()
}
if m.DisplayName == "" {
m.DisplayName = m.ServiceName
}
}
func (m *ScmrCreate) Execute(ctx context.Context, in *goexec.ExecutionInput) (err error) {
m.ensure()
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).
Str("method", MethodCreate).
Str("service", m.ServiceName).
Logger()
svc := &service{name: m.ServiceName}
resp, err := m.ctl.CreateServiceW(ctx, &svcctl.CreateServiceWRequest{
ServiceManager: m.scm,
ServiceName: m.ServiceName,
DisplayName: m.DisplayName,
BinaryPathName: in.String(),
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
StartType: windows.SERVICE_DEMAND_START,
DesiredAccess: ServiceAllAccess, // TODO: Replace
})
if err != nil {
log.Error().Err(err).Msg("Create service request failed")
return fmt.Errorf("create service request: %w", err)
}
if resp.Return != 0 {
log.Error().Err(err).Msg("Failed to create service")
return fmt.Errorf("create service returned non-zero exit code: %02x", resp.Return)
}
if !m.NoDelete {
m.AddCleaner(func(ctxInner context.Context) error {
r, errInner := m.ctl.DeleteService(ctxInner, &svcctl.DeleteServiceRequest{
Service: svc.handle,
})
if errInner != nil {
return fmt.Errorf("delete service: %w", errInner)
}
if r.Return != 0 {
return fmt.Errorf("delete service returned non-zero exit code: %02x", r.Return)
}
log.Info().Msg("Deleted service")
return nil
})
}
m.AddCleaner(func(ctxInner context.Context) error {
r, errInner := m.ctl.CloseService(ctxInner, &svcctl.CloseServiceRequest{
ServiceObject: svc.handle,
})
if errInner != nil {
return fmt.Errorf("close service: %w", errInner)
}
if r.Return != 0 {
return fmt.Errorf("close service returned non-zero exit code: %02x", r.Return)
}
log.Info().Msg("Closed service handle")
return nil
})
log.Info().Msg("Created service")
svc.handle = resp.Service
if !m.NoStart {
err = m.startService(ctx, svc)
if err != nil {
log.Error().Err(err).Msg("Failed to start service")
}
}
if svc.handle == nil {
if err = m.Reconnect(ctx); err != nil {
return err
}
svc, err = m.openService(ctx, svc.name)
if err != nil {
return fmt.Errorf("reopen service: %w", err)
}
}
return
}
+46
View File
@@ -0,0 +1,46 @@
package scmrexec
import (
"context"
"fmt"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
"github.com/rs/zerolog"
)
const (
MethodDelete = "Delete"
)
type ScmrDelete struct {
Scmr
goexec.Cleaner
ServiceName string
}
func (m *ScmrDelete) Clean(ctx context.Context) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).
Str("method", MethodDelete).
Str("service", m.ServiceName).
Logger()
svc, err := m.openService(ctx, m.ServiceName)
if err != nil {
return err
}
deleteResponse, err := m.ctl.DeleteService(ctx, &svcctl.DeleteServiceRequest{
Service: svc.handle,
})
if err != nil {
return fmt.Errorf("delete service: %w", err)
}
if deleteResponse.Return != 0 {
return fmt.Errorf("delete service returned non-zero exit code: %02x", deleteResponse.Return)
}
log.Info().Msg("Deleted service")
return
}
+131
View File
@@ -0,0 +1,131 @@
package scmrexec
import (
"context"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/internal/util"
"github.com/FalconOpsLLC/goexec/internal/windows"
"github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/midl/uuid"
"github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
"github.com/rs/zerolog"
)
const (
ModuleName = "SCMR"
DefaultEndpoint = "ncacn_np:[svcctl]"
ScmrUuid = "367ABB81-9844-35F1-AD32-98F038001003"
)
type Scmr struct {
client *dce.Client
ctl svcctl.SvcctlClient
scm *svcctl.Handle
hostname string
}
func (m *Scmr) Init(ctx context.Context, c *dce.Client) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).Logger()
m.client = c
if m.client.Dce() == nil {
return errors.New("DCE connection not initialized")
}
m.hostname, err = c.Target.Hostname(ctx)
if err != nil {
log.Debug().Err(err).Msg("Failed to determine target hostname")
}
if m.hostname == "" {
m.hostname = util.RandomHostname()
}
m.ctl, err = svcctl.NewSvcctlClient(ctx, m.client.Dce(), dcerpc.WithObjectUUID(uuid.MustParse(ScmrUuid)))
if err != nil {
log.Error().Err(err).Msg("Failed to initialize SVCCTL client")
return fmt.Errorf("create SVCCTL client: %w", err)
}
log.Info().Msg("Created SVCCTL client")
resp, err := m.ctl.OpenSCMW(ctx, &svcctl.OpenSCMWRequest{
MachineName: m.hostname,
DatabaseName: "ServicesActive\x00",
DesiredAccess: ServiceAllAccess, // TODO: Replace
})
if err != nil {
log.Debug().Err(err).Msg("Failed to open SCM handle")
return fmt.Errorf("open SCM handle: %w", err)
}
log.Info().Msg("Opened SCM handle")
m.scm = resp.SCM
return
}
func (m *Scmr) Reconnect(ctx context.Context) (err error) {
if err = m.client.Reconnect(ctx); err != nil {
return fmt.Errorf("reconnect: %w", err)
}
if err = m.Init(ctx, m.client); err != nil {
return fmt.Errorf("reconnect SCMR: %w", err)
}
return
}
// openService will a handle to the desired service
func (m *Scmr) openService(ctx context.Context, name string) (svc *service, err error) {
log := zerolog.Ctx(ctx)
resp, err := m.ctl.OpenServiceW(ctx, &svcctl.OpenServiceWRequest{
ServiceManager: m.scm,
ServiceName: name,
DesiredAccess: ServiceAllAccess, // TODO: dynamic
})
if err != nil {
log.Error().Err(err).Msg("Failed to open service handle")
return nil, fmt.Errorf("open service: %w", err)
}
log.Info().Msg("Opened service handle")
svc = new(service)
svc.name = name
svc.handle = resp.Service
return
}
func (m *Scmr) startService(ctx context.Context, svc *service) error {
log := zerolog.Ctx(ctx)
sr, err := m.ctl.StartServiceW(ctx, &svcctl.StartServiceWRequest{Service: svc.handle})
if err != nil {
if errors.Is(err, context.DeadlineExceeded) { // Check if execution timed out (execute "cmd.exe /c notepad" for test case)
log.Warn().Msg("Service execution deadline exceeded")
svc.handle = nil
return nil
} else if sr.Return == windows.ERROR_SERVICE_REQUEST_TIMEOUT {
log.Info().Msg("Received request timeout. Execution was likely successful")
return nil
}
log.Error().Err(err).Msg("Failed to start service")
return fmt.Errorf("start service: %w", err)
}
log.Info().Msg("Service started successfully")
return nil
}
+19
View File
@@ -0,0 +1,19 @@
package scmrexec
import (
"github.com/FalconOpsLLC/goexec/internal/windows"
"github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2"
)
const (
ServiceDeleteAccess uint32 = windows.SERVICE_DELETE
ServiceModifyAccess uint32 = windows.SERVICE_QUERY_CONFIG | windows.SERVICE_CHANGE_CONFIG | windows.SERVICE_STOP | windows.SERVICE_START | windows.SERVICE_DELETE
ServiceCreateAccess uint32 = windows.SC_MANAGER_CREATE_SERVICE | windows.SERVICE_START | windows.SERVICE_STOP | windows.SERVICE_DELETE
ServiceAllAccess uint32 = ServiceCreateAccess | ServiceModifyAccess
)
type service struct {
name string
handle *svcctl.Handle
originalConfig *svcctl.QueryServiceConfigW
}
+111
View File
@@ -0,0 +1,111 @@
package smb
import (
"context"
"errors"
"fmt"
"github.com/oiweiwei/go-smb2.fork"
"github.com/rs/zerolog"
"net"
)
type Client struct {
ClientOptions
conn net.Conn
sess *smb2.Session
mount *smb2.Share
}
func (c *Client) Session() (sess *smb2.Session) {
return c.sess
}
func (c *Client) String() string {
return ClientName
}
func (c *Client) Logger(ctx context.Context) zerolog.Logger {
return zerolog.Ctx(ctx).With().Str("client", c.String()).Logger()
}
func (c *Client) Mount(_ context.Context, share string) (err error) {
if c.sess == nil {
return errors.New("SMB session not initialized")
}
c.mount, err = c.sess.Mount(share)
return
}
func (c *Client) Connect(ctx context.Context) (err error) {
log := c.Logger(ctx)
{
if c.netDialer == nil {
panic(fmt.Errorf("TCP dialer not initialized"))
}
if c.dialer == nil {
panic(fmt.Errorf("%s dialer not initialized", c.String()))
}
}
// Establish TCP connection
c.conn, err = c.netDialer.Dial("tcp", net.JoinHostPort(c.Host, fmt.Sprintf("%d", c.Port)))
if err != nil {
return err
}
log = log.With().Str("address", c.conn.RemoteAddr().String()).Logger()
log.Debug().Msgf("Connected to %s server", c.String())
// Open SMB session
c.sess, err = c.dialer.DialContext(ctx, c.conn)
if err != nil {
log.Error().Err(err).Msgf("Failed to open %s session", c.String())
return fmt.Errorf("dial %s: %w", c.String(), err)
}
log.Debug().Msgf("Opened %s session", c.String())
return
}
func (c *Client) Close(ctx context.Context) (err error) {
log := c.Logger(ctx)
// Close TCP connection
if c.conn != nil {
defer func() {
if err = c.conn.Close(); err != nil {
log.Error().Err(err).Msgf("Failed to close %s connection", c.String())
}
log.Debug().Msgf("Closed %s connection", c.String())
}()
}
// Close SMB session
if c.sess != nil {
defer func() {
if err = c.sess.Logoff(); err != nil {
log.Error().Err(err).Msgf("Failed to discard %s session", c.String())
}
log.Debug().Msgf("Discarded %s session", c.String())
}()
}
// Unmount SMB share
if c.mount != nil {
defer func() {
if err = c.mount.Umount(); err != nil {
log.Error().Err(err).Msg("Failed to unmount share")
}
log.Debug().Msg("Unmounted file share")
}()
}
return
}
+10
View File
@@ -0,0 +1,10 @@
package smb
import "github.com/oiweiwei/go-msrpc/smb2"
const (
ClientName = "SMB"
DefaultPort = 445
DefaultDialect = smb2.SMB311
)
+98
View File
@@ -0,0 +1,98 @@
package smb
import (
"context"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/RedTeamPentesting/adauth/smbauth"
msrpcSMB2 "github.com/oiweiwei/go-msrpc/smb2"
"github.com/oiweiwei/go-smb2.fork"
"net"
)
var supportedDialects = map[msrpcSMB2.Dialect]msrpcSMB2.Dialect{
2_0_2: msrpcSMB2.SMB202,
2_1_0: msrpcSMB2.SMB210,
3_0_0: msrpcSMB2.SMB300,
3_0_2: msrpcSMB2.SMB302,
3_1_1: msrpcSMB2.SMB311,
0x202: msrpcSMB2.SMB202,
0x210: msrpcSMB2.SMB210,
0x300: msrpcSMB2.SMB300,
0x302: msrpcSMB2.SMB302,
0x311: msrpcSMB2.SMB311,
}
// ClientOptions holds configuration settings for an SMB client
type ClientOptions struct {
goexec.ClientOptions
goexec.AuthOptions
// NoSign disables packet signing
NoSign bool `json:"no_sign" yaml:"no_sign"`
// NoSeal disables packet encryption
NoSeal bool `json:"no_seal" yaml:"no_seal"`
// Dialect sets the SMB dialect to be passed to smb2.WithDialect()
Dialect msrpcSMB2.Dialect `json:"dialect" yaml:"dialect"`
netDialer goexec.Dialer
dialer *smb2.Dialer
}
func (c *Client) Parse(ctx context.Context) (err error) {
var do []msrpcSMB2.DialerOption
if c.Port == 0 {
c.Port = DefaultPort
}
if c.Dialect == 0 {
c.Dialect = DefaultDialect
}
// Validate SMB dialect/version
if d, ok := supportedDialects[c.Dialect]; ok {
do = append(do, msrpcSMB2.WithDialect(d))
} else {
return errors.New("unsupported SMB version")
}
if c.Proxy == "" {
c.netDialer = &net.Dialer{} // FUTURE: additional dial c
} else {
// Parse proxy URL
c.netDialer, err = goexec.ParseProxyURI(c.Proxy)
if err != nil {
return err
}
}
if !c.NoSeal {
// Enable encryption
do = append(do, msrpcSMB2.WithSeal())
}
if !c.NoSign {
// Enable signing
do = append(do, msrpcSMB2.WithSign())
}
// Validate authentication parameters
c.dialer, err = smbauth.Dialer(ctx, c.Credential, c.Target,
&smbauth.Options{
SMBOptions: do,
})
if err != nil {
return fmt.Errorf("set %s auth: %w", ClientName, err)
}
c.Host = c.Target.AddressWithoutPort()
return nil
}
+63
View File
@@ -0,0 +1,63 @@
package smb
import (
"context"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"io"
"os"
"regexp"
"time"
)
var (
DefaultOutputPollInterval = 1 * time.Second
DefaultOutputPollTimeout = 60 * time.Second
pathPrefix = regexp.MustCompile(`^([a-zA-Z]:)?\\*`)
)
type OutputFileFetcher struct {
goexec.Cleaner
Client *Client
Share string
File string
PollInterval time.Duration
PollTimeout time.Duration
relativePath string
}
func (o *OutputFileFetcher) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) {
if o.PollInterval == 0 {
o.PollInterval = DefaultOutputPollInterval
}
if o.PollTimeout == 0 {
o.PollTimeout = DefaultOutputPollTimeout
}
o.relativePath = pathPrefix.ReplaceAllString(o.File, "")
err = o.Client.Connect(ctx)
if err != nil {
return
}
err = o.Client.Mount(ctx, o.Share)
if err != nil {
return
}
stopAt := time.Now().Add(o.PollTimeout)
for {
if time.Now().After(stopAt) {
return
}
if reader, err = o.Client.mount.OpenFile(o.relativePath, os.O_RDONLY, 0); err == nil {
return
}
time.Sleep(o.PollInterval)
}
return
}
+113
View File
@@ -0,0 +1,113 @@
package tschexec
import (
"context"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/FalconOpsLLC/goexec/pkg/goexec/smb"
"github.com/rs/zerolog"
"time"
)
const (
MethodCreate = "Create"
)
type TschCreate struct {
Tsch
goexec.Executor
goexec.Cleaner
smb.OutputFileFetcher
IO goexec.ExecutionIO
NoDelete bool
CallDelete bool
StartDelay time.Duration
StopDelay time.Duration
DeleteDelay time.Duration
TimeOffset time.Duration
// FEATURE: more opts
}
func (m *TschCreate) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).
Str("method", MethodCreate).
Str("task", m.TaskName).
Logger()
startTime := time.Now().UTC().Add(m.StartDelay)
stopTime := startTime.Add(m.StopDelay)
trigger := taskTimeTrigger{
StartBoundary: startTime.Format(TaskXmlDurationFormat),
Enabled: true,
}
var deleteAfter string
if !m.NoDelete && !m.CallDelete {
if m.StopDelay == 0 {
m.StopDelay = time.Second // value is required, 1 second by default
}
trigger.EndBoundary = stopTime.Format(TaskXmlDurationFormat)
deleteAfter = xmlDuration(m.DeleteDelay)
}
path, err := m.registerTask(ctx,
&registerOptions{
AllowStartOnDemand: true,
AllowHardTerminate: true,
Hidden: !m.NotHidden,
triggers: taskTriggers{
TimeTriggers: []taskTimeTrigger{trigger},
},
DeleteAfter: deleteAfter,
},
execIO,
)
if err != nil {
return err
}
if !m.NoDelete {
if m.CallDelete {
m.AddCleaner(func(ctxInner context.Context) error {
log.Info().Msg("Waiting for task to start...")
select {
case <-ctxInner.Done():
log.Warn().Msg("Task deletion cancelled")
case <-time.After(m.StartDelay + (5 * time.Second)): // 5 second buffer
/*
for {
stat, err := m.tsch.GetLastRunInfo(ctx, &itaskschedulerservice.GetLastRunInfoRequest{
Path: path,
})
if err != nil {
log.Warn().Err(err).Msg("Failed to get last run info. Assuming task was executed")
} else if stat.LastRuntime.AsTime().IsZero() {
log.Warn().Msg("Task was not yet executed. Waiting 5 additional seconds")
time.Sleep(5 * time.Second)
continue
}
break
}
*/
}
return m.deleteTask(ctxInner, path)
})
} else {
log.Info().Time("when", stopTime).Msg("Task is scheduled to delete")
}
}
return
}
+91
View File
@@ -0,0 +1,91 @@
package tschexec
import (
"context"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
"github.com/rs/zerolog"
"io"
)
const (
MethodDemand = "Demand"
)
type TschDemand struct {
Tsch
goexec.Executor
goexec.Cleaner
IO goexec.ExecutionIO
NoDelete bool
NoStart bool
SessionId uint32
}
func (m *TschDemand) Execute(ctx context.Context, in *goexec.ExecutionIO) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).
Str("method", MethodDemand).
Str("task", m.TaskName).
Logger()
path, err := m.registerTask(ctx,
&registerOptions{
AllowStartOnDemand: true,
AllowHardTerminate: true,
Hidden: !m.NotHidden,
triggers: taskTriggers{},
},
in,
)
if err != nil {
return err
}
log.Info().Msg("Task registered")
if !m.NoDelete {
m.AddCleaner(func(ctxInner context.Context) error {
return m.deleteTask(ctxInner, path)
})
}
if !m.NoStart {
var flags uint32
if m.SessionId != 0 {
flags |= 4
}
runResponse, err := m.tsch.Run(ctx, &itaskschedulerservice.RunRequest{
Path: path,
Flags: flags,
SessionID: m.SessionId,
})
if err != nil {
log.Error().Err(err).Msg("Failed to run task")
return fmt.Errorf("run task: %w", err)
}
if ret := uint32(runResponse.Return); ret != 0 {
log.Error().Str("code", fmt.Sprintf("0x%08x", ret)).Msg("Task returned non-zero exit code")
return fmt.Errorf("task returned non-zero exit code: 0x%08x", ret)
}
log.Info().Msg("Task started successfully")
}
return
}
func (m *TschDemand) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) {
if m.IO.Output != nil {
return m.IO.GetOutput(ctx)
}
return nil, errors.New("no available output provider")
}
+173
View File
@@ -0,0 +1,173 @@
package tschexec
import (
"context"
"encoding/xml"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
"github.com/oiweiwei/go-msrpc/msrpc/tsch/itaskschedulerservice/v1"
"github.com/rs/zerolog"
"strings"
)
const (
ModuleName = "TSCH"
)
type Tsch struct {
goexec.Cleaner
Client *dce.Client
tsch itaskschedulerservice.TaskSchedulerServiceClient
TaskName string
TaskPath string
UserSid string
NotHidden bool
}
type registerOptions struct {
AllowStartOnDemand bool
AllowHardTerminate bool
StartWhenAvailable bool
Hidden bool
DeleteAfter string
triggers taskTriggers
}
func (m *Tsch) Connect(ctx context.Context) (err error) {
if err = m.Client.Connect(ctx); err == nil {
m.AddCleaner(m.Client.Close)
}
return
}
func (m *Tsch) Init(ctx context.Context) (err error) {
if m.Client.Dce() == nil {
return errors.New("DCE connection not initialized")
}
// Create ITaskSchedulerService Client
m.tsch, err = itaskschedulerservice.NewTaskSchedulerServiceClient(ctx, m.Client.Dce())
return
}
func (m *Tsch) taskPath() string {
if m.TaskPath == "" {
m.TaskPath = `\` + m.TaskName
}
return m.TaskPath
}
func (m *Tsch) registerTask(ctx context.Context, opts *registerOptions, in *goexec.ExecutionIO) (path string, err error) {
log := zerolog.Ctx(ctx).With().
Str("task", m.TaskName).
Logger()
ctx = log.WithContext(ctx)
principalId := "1"
settings := taskSettings{
MultipleInstancesPolicy: "IgnoreNew",
IdleSettings: taskIdleSettings{
StopOnIdleEnd: true,
RestartOnIdle: false,
},
Enabled: true,
Priority: 7, // a pretty standard value for scheduled tasks
AllowHardTerminate: opts.AllowHardTerminate,
AllowStartOnDemand: opts.AllowStartOnDemand,
Hidden: opts.Hidden,
StartWhenAvailable: opts.StartWhenAvailable,
DeleteExpiredTaskAfter: opts.DeleteAfter,
}
principals := taskPrincipals{
Principals: []taskPrincipal{
{
ID: principalId, // TODO: dynamic
UserID: m.UserSid,
RunLevel: "HighestAvailable",
},
}}
e := taskActionExec{}
if ea := strings.SplitN(in.String(), " ", 2); len(ea) == 1 {
e.Command = ea[0]
} else {
e.Command = ea[0]
e.Arguments = ea[1]
}
actions := taskActions{
Context: principalId,
Exec: []taskActionExec{e},
}
def := task{
TaskVersion: TaskXmlVersion,
TaskNamespace: TaskXmlNamespace,
Triggers: opts.triggers,
Actions: actions,
Principals: principals,
Settings: settings,
}
// Generate task XML content. See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f
doc, err := xml.Marshal(def)
if err != nil {
log.Error().Err(err).Msg("failed to marshal task XML")
return "", fmt.Errorf("marshal task: %w", err)
}
taskXml := TaskXmlHeader + string(doc)
log.Debug().Str("content", taskXml).Msg("Generated task XML")
registerResponse, err := m.tsch.RegisterTask(ctx, &itaskschedulerservice.RegisterTaskRequest{
Path: m.taskPath(),
XML: taskXml,
Flags: 0, // FEATURE: dynamic
SDDL: "",
LogonType: 0, // FEATURE: dynamic
CredsCount: 0,
Creds: nil,
})
if err != nil {
log.Error().Err(err).Msg("Failed to register task")
return "", fmt.Errorf("register task: %w", err)
}
return registerResponse.ActualPath, nil
}
func (m *Tsch) deleteTask(ctx context.Context, taskPath string) (err error) {
log := zerolog.Ctx(ctx).With().
Str("path", taskPath).Logger()
_, err = m.tsch.Delete(ctx, &itaskschedulerservice.DeleteRequest{
Path: taskPath,
})
if err != nil {
log.Error().Err(err).Msg("Failed to delete task")
return fmt.Errorf("delete task: %w", err)
}
log.Info().Msg("Task deleted")
return
}
+166
View File
@@ -0,0 +1,166 @@
package tschexec
import (
"encoding/xml"
"fmt"
"regexp"
"time"
)
const (
TaskXmlHeader = `<?xml version="1.0" encoding="UTF-16"?>`
TaskXmlNamespace = "http://schemas.microsoft.com/windows/2004/02/mit/task"
TaskXmlVersion = "1.2"
TaskXmlDurationFormat = "2006-01-02T15:04:05.9999999Z"
)
var (
TaskPathRegex = regexp.MustCompile(`^\\[^ :/\\][^:/]*$`)
TaskNameRegex = regexp.MustCompile(`^[^ :/\\][^:/\\]*$`)
)
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f
type taskTriggers struct {
XMLName xml.Name `xml:"Triggers"`
TimeTriggers []taskTimeTrigger `xml:"TimeTrigger,omitempty"`
}
type taskTimeTrigger struct {
XMLName xml.Name `xml:"TimeTrigger"`
StartBoundary string `xml:"StartBoundary,omitempty"` // Derived from time.Time
EndBoundary string `xml:"EndBoundary,omitempty"` // Derived from time.Time; must be > StartBoundary
Enabled bool `xml:"Enabled"`
}
type taskIdleSettings struct {
XMLName xml.Name `xml:"IdleSettings"`
StopOnIdleEnd bool `xml:"StopOnIdleEnd"`
RestartOnIdle bool `xml:"RestartOnIdle"`
}
type taskSettings struct {
XMLName xml.Name `xml:"Settings"`
Enabled bool `xml:"Enabled"`
Hidden bool `xml:"Hidden"`
DisallowStartIfOnBatteries bool `xml:"DisallowStartIfOnBatteries"`
StopIfGoingOnBatteries bool `xml:"StopIfGoingOnBatteries"`
AllowHardTerminate bool `xml:"AllowHardTerminate"`
RunOnlyIfNetworkAvailable bool `xml:"RunOnlyIfNetworkAvailable"`
AllowStartOnDemand bool `xml:"AllowStartOnDemand"`
WakeToRun bool `xml:"WakeToRun"`
RunOnlyIfIdle bool `xml:"RunOnlyIfIdle"`
StartWhenAvailable bool `xml:"StartWhenAvailable"`
Priority int `xml:"Priority,omitempty"` // 1 to 10 inclusive
MultipleInstancesPolicy string `xml:"MultipleInstancesPolicy,omitempty"`
ExecutionTimeLimit string `xml:"ExecutionTimeLimit,omitempty"`
DeleteExpiredTaskAfter string `xml:"DeleteExpiredTaskAfter,omitempty"` // Derived from time.Duration
IdleSettings taskIdleSettings `xml:"IdleSettings,omitempty"`
}
type taskActionExec struct {
XMLName xml.Name `xml:"Exec"`
Command string `xml:"Command"`
Arguments string `xml:"Arguments,omitempty"`
}
type taskActions struct {
XMLName xml.Name `xml:"Actions"`
Context string `xml:"Context,attr"`
Exec []taskActionExec `xml:"Exec,omitempty"`
}
type taskPrincipals struct {
XMLName xml.Name `xml:"Principals"`
Principals []taskPrincipal `xml:"Principal,omitempty"`
}
type taskPrincipal struct {
XMLName xml.Name `xml:"Principal"`
ID string `xml:"id,attr"`
UserID string `xml:"UserId"`
RunLevel string `xml:"RunLevel"`
}
type task struct {
XMLName xml.Name `xml:"Task"`
TaskVersion string `xml:"version,attr"`
TaskNamespace string `xml:"xmlns,attr"`
Triggers taskTriggers `xml:"Triggers"`
Actions taskActions `xml:"Actions"`
Principals taskPrincipals `xml:"Principals"`
Settings taskSettings `xml:"Settings"`
}
// newSettings just creates a taskSettings instance with the necessary values + a few dynamic ones
func newSettings(terminate, onDemand, startWhenAvailable bool) *taskSettings {
return &taskSettings{
MultipleInstancesPolicy: "IgnoreNew",
AllowHardTerminate: terminate,
IdleSettings: taskIdleSettings{
StopOnIdleEnd: true,
RestartOnIdle: false,
},
AllowStartOnDemand: onDemand,
Enabled: true,
Hidden: true,
Priority: 7, // a pretty standard value for scheduled tasks
StartWhenAvailable: startWhenAvailable,
}
}
// newTask creates a task with any static values filled
func newTask(se *taskSettings, pr []taskPrincipal, tr taskTriggers, cmd, args string) *task {
if se == nil {
se = newSettings(true, true, false)
}
if pr == nil || len(pr) == 0 {
pr = []taskPrincipal{
{
ID: "1",
UserID: "S-1-5-18",
RunLevel: "HighestAvailable",
},
}
}
return &task{
TaskVersion: "1.2",
TaskNamespace: "http://schemas.microsoft.com/windows/2004/02/mit/task",
Triggers: tr,
Principals: taskPrincipals{Principals: pr},
Settings: *se,
Actions: taskActions{
Context: pr[0].ID,
Exec: []taskActionExec{
{
Command: cmd,
Arguments: args,
},
},
},
}
}
// xmlDuration is a *very* simple implementation of xs:duration - only accepts +seconds
func xmlDuration(dur time.Duration) string {
if s := int(dur.Seconds()); s >= 0 {
return fmt.Sprintf(`PT%dS`, s)
}
return `PT0S`
}
// ValidateTaskName will validate the provided task name according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1
func ValidateTaskName(taskName string) error {
if !TaskNameRegex.MatchString(taskName) {
return fmt.Errorf("invalid task name: %s", taskName)
}
return nil
}
// ValidateTaskPath will validate the provided task path according to https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/fa8809c8-4f0f-4c6d-994a-6c10308757c1
func ValidateTaskPath(taskPath string) error {
if !TaskPathRegex.MatchString(taskPath) {
return fmt.Errorf("invalid task path: %s", taskPath)
}
return nil
}
+31
View File
@@ -0,0 +1,31 @@
package wmiexec
import (
"context"
"encoding/json"
"github.com/rs/zerolog"
)
const (
MethodCall = "Call"
)
type WmiCall struct {
Wmi
Class string
Method string
Args map[string]any
}
func (m *WmiCall) Call(ctx context.Context) (out []byte, err error) {
var outMap map[string]any
if outMap, err = m.query(ctx, m.Class, m.Method, m.Args); err != nil {
return
}
zerolog.Ctx(ctx).Info().Msg("Call succeeded")
out, err = json.Marshal(outMap)
return
}
+140
View File
@@ -0,0 +1,140 @@
package wmiexec
import (
"context"
"errors"
"fmt"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/FalconOpsLLC/goexec/pkg/goexec/dce"
"github.com/oiweiwei/go-msrpc/dcerpc"
"github.com/oiweiwei/go-msrpc/msrpc/dcom"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/iactivation/v0"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemlevel1login/v0"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmi/iwbemservices/v0"
"github.com/oiweiwei/go-msrpc/msrpc/dcom/wmio/query"
"github.com/rs/zerolog"
)
const (
ModuleName = "WMI"
DefaultEndpoint = "ncacn_ip_tcp:[135]"
)
type Wmi struct {
goexec.Cleaner
Client *dce.Client
Resource string
servicesClient iwbemservices.ServicesClient
}
func (m *Wmi) Connect(ctx context.Context) (err error) {
if err = m.Client.Connect(ctx); err == nil {
m.AddCleaner(m.Client.Close)
}
return
}
func (m *Wmi) Init(ctx context.Context) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).Logger()
if m.Client.Dce() == nil {
return errors.New("DCE connection not initialized")
}
actClient, err := iactivation.NewActivationClient(ctx, m.Client.Dce())
if err != nil {
log.Error().Err(err).Msg("Failed to initialize IActivation client")
return fmt.Errorf("create IActivation client: %w", err)
}
actResponse, err := actClient.RemoteActivation(ctx, &iactivation.RemoteActivationRequest{
ORPCThis: ORPCThis,
ClassID: wmi.Level1LoginClassID.GUID(),
IIDs: []*dcom.IID{iwbemlevel1login.Level1LoginIID},
RequestedProtocolSequences: []uint16{ProtocolSequenceRPC}, // FEATURE: Named pipe support?
})
if err != nil {
log.Error().Err(err).Msg("Failed to activate remote object")
return fmt.Errorf("request remote activation: %w", err)
}
if actResponse.HResult != 0 {
return fmt.Errorf("remote activation failed with code %d", actResponse.HResult)
}
log.Info().Msg("Remote activation succeeded")
var newOpts []dcerpc.Option
for _, bind := range actResponse.OXIDBindings.GetStringBindings() {
stringBinding, err := dcerpc.ParseStringBinding("ncacn_ip_tcp:" + bind.NetworkAddr) // TODO: try bind.String()
if err != nil {
log.Error().Err(err).Msg("Failed to parse string binding")
continue
}
stringBinding.NetworkAddress = m.Client.Target.AddressWithoutPort()
newOpts = append(newOpts, dcerpc.WithEndpoint(stringBinding.String()))
}
if err = m.Client.Reconnect(ctx, newOpts...); err != nil {
log.Error().Err(err).Msg("Failed to connect to remote instance")
return fmt.Errorf("connect remote instance: %w", err)
}
log.Info().Msg("Connected to remote instance")
ipid := actResponse.InterfaceData[0].GetStandardObjectReference().Std.IPID
loginClient, err := iwbemlevel1login.NewLevel1LoginClient(ctx, m.Client.Dce(), dcom.WithIPID(ipid))
if err != nil {
log.Error().Err(err).Msg("Failed to create IWbemLevel1Login client")
return fmt.Errorf("create IWbemLevel1Login client: %w", err)
}
login, err := loginClient.NTLMLogin(ctx, &iwbemlevel1login.NTLMLoginRequest{
This: ORPCThis,
NetworkResource: m.Resource,
})
log.Info().Msg("Completed NTLMLogin operation")
if err != nil {
log.Error().Err(err).Msg("Failed to login on remote instance")
return fmt.Errorf("login: IWbemLevel1Login::NTLMLogin: %w", err)
}
ipid = login.Namespace.InterfacePointer().IPID()
m.servicesClient, err = iwbemservices.NewServicesClient(ctx, m.Client.Dce(), dcom.WithIPID(ipid))
if err != nil {
log.Error().Err(err).Msg("Failed to create services client")
return fmt.Errorf("create IWbemServices client: %w", err)
}
log.Info().Msg("Initialized services client")
return
}
func (m *Wmi) query(ctx context.Context, class, method string, values map[string]any) (outValues map[string]any, err error) {
outValues = make(map[string]any)
if m.servicesClient == nil {
return nil, errors.New("module has not been initialized")
}
if out, err := query.NewBuilder(ctx, m.servicesClient, ComVersion).
Spawn(class). // The class to instantiate (i.e. Win32_Process)
Method(method). // The method to call (i.e. Create)
Values(values). // The values to pass to method
Exec().
Object(); err == nil {
return out.Values(), err
}
return nil, fmt.Errorf("spawn WMI query: %w", err)
}
+64
View File
@@ -0,0 +1,64 @@
package wmiexec
import (
"context"
"errors"
"github.com/FalconOpsLLC/goexec/pkg/goexec"
"github.com/rs/zerolog"
"io"
)
const (
MethodProc = "Proc"
)
type WmiProc struct {
Wmi
IO goexec.ExecutionIO
WorkingDirectory string
}
func (m *WmiProc) Execute(ctx context.Context, execIO *goexec.ExecutionIO) (err error) {
log := zerolog.Ctx(ctx).With().
Str("module", ModuleName).
Str("method", MethodProc).
Logger()
ctx = log.WithContext(ctx)
if execIO == nil {
return errors.New("execution IO is nil")
}
out, err := m.query(ctx,
"Win32_Process",
"Create",
map[string]any{
"CommandLine": execIO.String(),
"WorkingDir": m.WorkingDirectory,
},
)
if err != nil {
return
}
if pid := out["ProcessId"].(uint32); pid != 0 {
log = log.With().Uint32("pid", pid).Logger()
}
log.Info().Err(err).Msg("Process created")
if ret := out["ReturnValue"].(uint32); ret != 0 {
log.Error().Err(err).Uint32("return", ret).Msg("Process returned non-zero exit code")
}
return
}
func (m *WmiProc) GetOutput(ctx context.Context) (reader io.ReadCloser, err error) {
if m.IO.Output != nil {
return m.IO.GetOutput(ctx)
}
return nil, errors.New("no available output provider")
}
+16
View File
@@ -0,0 +1,16 @@
package wmiexec
import "github.com/oiweiwei/go-msrpc/msrpc/dcom"
const (
ProtocolSequenceRPC uint16 = 7
ProtocolSequenceNP uint16 = 15
)
var (
ComVersion = &dcom.COMVersion{
MajorVersion: 5,
MinorVersion: 7,
}
ORPCThis = &dcom.ORPCThis{Version: ComVersion}
)