mirror of
https://github.com/FalconOpsLLC/goexec
synced 2026-06-06 15:44:27 +00:00
rewrote everything lol
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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...)...)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package dce
|
||||
|
||||
const (
|
||||
ClientName = "DCE"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package smb
|
||||
|
||||
import "github.com/oiweiwei/go-msrpc/smb2"
|
||||
|
||||
const (
|
||||
ClientName = "SMB"
|
||||
|
||||
DefaultPort = 445
|
||||
DefaultDialect = smb2.SMB311
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
®isterOptions{
|
||||
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
|
||||
}
|
||||
@@ -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,
|
||||
®isterOptions{
|
||||
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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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}
|
||||
)
|
||||
Reference in New Issue
Block a user