Commit 5197804
2025-09-15 11:42:50
Changed files (13)
labs
pkg
mythic
cmd/callbacks.go
@@ -0,0 +1,69 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "go-mythic/pkg/mythic"
+ "os"
+ "text/tabwriter"
+ "time"
+
+ "github.com/spf13/cobra"
+)
+
+var callbacksCmd = &cobra.Command{
+ Use: "callbacks",
+ Short: "List active callbacks",
+ Long: "Display all active callbacks from the Mythic server, equivalent to the Active Callbacks view in the UI.",
+ RunE: runCallbacks,
+}
+
+func init() {
+ rootCmd.AddCommand(callbacksCmd)
+}
+
+func runCallbacks(cmd *cobra.Command, args []string) error {
+ if err := validateAuth(); err != nil {
+ return err
+ }
+
+ client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ callbacks, err := client.GetActiveCallbacks(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get active callbacks: %w", err)
+ }
+
+ if len(callbacks) == 0 {
+ fmt.Println("No active callbacks found")
+ return nil
+ }
+
+ // Create a tab writer for formatted output
+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+ fmt.Fprintln(w, "ID\tTYPE\tHOST\tUSER\tPROCESS\tPID\tLAST CHECKIN\tDESCRIPTION")
+ fmt.Fprintln(w, "--\t----\t----\t----\t-------\t---\t------------\t-----------")
+
+ for _, callback := range callbacks {
+ agentType := callback.Payload.PayloadType.Name
+ if agentType == "" {
+ agentType = "unknown"
+ }
+
+ fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%d\t%s\t%s\n",
+ callback.DisplayID,
+ agentType,
+ callback.Host,
+ callback.User,
+ callback.ProcessName,
+ callback.PID,
+ callback.LastCheckin,
+ callback.Description,
+ )
+ }
+
+ w.Flush()
+ return nil
+}
\ No newline at end of file
cmd/exec.go
@@ -0,0 +1,103 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "go-mythic/pkg/mythic"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/spf13/cobra"
+)
+
+var execCmd = &cobra.Command{
+ Use: "exec <callback_id> <command> [args...]",
+ Short: "Execute a command on a specific callback",
+ Long: "Execute a command on the specified callback and wait for the response. Supports commands like help, ps, sleep, etc.",
+ Args: cobra.MinimumNArgs(2),
+ RunE: runExec,
+}
+
+var (
+ waitTime int
+)
+
+func init() {
+ rootCmd.AddCommand(execCmd)
+ execCmd.Flags().IntVarP(&waitTime, "wait", "w", 30, "Maximum time to wait for response (seconds)")
+}
+
+func runExec(cmd *cobra.Command, args []string) error {
+ if err := validateAuth(); err != nil {
+ return err
+ }
+
+ callbackID, err := strconv.Atoi(args[0])
+ if err != nil {
+ return fmt.Errorf("invalid callback ID: %s", args[0])
+ }
+
+ command := args[1]
+ params := ""
+ if len(args) > 2 {
+ params = strings.Join(args[2:], " ")
+ }
+
+ client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+ ctx := context.Background()
+
+ // Verify callback exists and is active
+ callbacks, err := client.GetActiveCallbacks(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get callbacks: %w", err)
+ }
+
+ var targetCallback *mythic.Callback
+ for _, callback := range callbacks {
+ if callback.DisplayID == callbackID {
+ targetCallback = &callback
+ break
+ }
+ }
+
+ if targetCallback == nil {
+ return fmt.Errorf("callback %d not found or not active", callbackID)
+ }
+
+ fmt.Printf("Executing '%s %s' on callback %d (%s@%s)\n",
+ command, params, callbackID, targetCallback.User, targetCallback.Host)
+
+ // Create the task
+ task, err := client.CreateTask(ctx, targetCallback.ID, command, params)
+ if err != nil {
+ return fmt.Errorf("failed to create task: %w", err)
+ }
+
+ fmt.Printf("Task %d created, waiting for response...\n", task.DisplayID)
+
+ // Poll for response
+ for i := 0; i < waitTime; i++ {
+ time.Sleep(1 * time.Second)
+
+ updatedTask, err := client.GetTaskResponse(ctx, task.ID)
+ if err != nil {
+ return fmt.Errorf("failed to get task response: %w", err)
+ }
+
+ if updatedTask.Status == "completed" || updatedTask.Status == "error" {
+ fmt.Printf("\nTask Status: %s\n", updatedTask.Status)
+ if updatedTask.Response != "" {
+ fmt.Printf("Response:\n%s\n", updatedTask.Response)
+ }
+ return nil
+ }
+
+ if i > 0 && i%5 == 0 {
+ fmt.Printf(".")
+ }
+ }
+
+ fmt.Printf("\nTimeout waiting for response after %d seconds\n", waitTime)
+ return nil
+}
\ No newline at end of file
cmd/interact.go
@@ -0,0 +1,145 @@
+package cmd
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "go-mythic/pkg/mythic"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/spf13/cobra"
+)
+
+var interactCmd = &cobra.Command{
+ Use: "interact <callback_id>",
+ Short: "Interact with a specific callback",
+ Long: "Start an interactive session with a callback, equivalent to right-clicking and selecting 'Interact' in the UI.",
+ Args: cobra.ExactArgs(1),
+ RunE: runInteract,
+}
+
+func init() {
+ rootCmd.AddCommand(interactCmd)
+}
+
+func runInteract(cmd *cobra.Command, args []string) error {
+ if err := validateAuth(); err != nil {
+ return err
+ }
+
+ callbackID, err := strconv.Atoi(args[0])
+ if err != nil {
+ return fmt.Errorf("invalid callback ID: %s", args[0])
+ }
+
+ client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
+ ctx := context.Background()
+
+ // Verify callback exists and is active
+ callbacks, err := client.GetActiveCallbacks(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get callbacks: %w", err)
+ }
+
+ var targetCallback *mythic.Callback
+ for _, callback := range callbacks {
+ if callback.DisplayID == callbackID {
+ targetCallback = &callback
+ break
+ }
+ }
+
+ if targetCallback == nil {
+ return fmt.Errorf("callback %d not found or not active", callbackID)
+ }
+
+ agentType := targetCallback.Payload.PayloadType.Name
+ if agentType == "" {
+ agentType = "agent" // fallback if payload type is not available
+ }
+
+ fmt.Printf("Interacting with callback %d (%s@%s - %s [%s])\n",
+ targetCallback.DisplayID,
+ targetCallback.User,
+ targetCallback.Host,
+ targetCallback.ProcessName,
+ agentType)
+ fmt.Println("Type 'exit' to quit the interactive session")
+ fmt.Println("---")
+
+ scanner := bufio.NewScanner(os.Stdin)
+ for {
+ fmt.Printf("%s[%d]> ", agentType, callbackID)
+ if !scanner.Scan() {
+ break
+ }
+
+ input := strings.TrimSpace(scanner.Text())
+ if input == "" {
+ continue
+ }
+
+ if input == "exit" {
+ fmt.Println("Exiting interactive session")
+ break
+ }
+
+ if err := executeCommand(ctx, client, targetCallback.ID, input); err != nil {
+ fmt.Printf("Error: %v\n", err)
+ }
+ }
+
+ return scanner.Err()
+}
+
+func executeCommand(ctx context.Context, client *mythic.Client, callbackID int, input string) error {
+ parts := strings.Fields(input)
+ if len(parts) == 0 {
+ return nil
+ }
+
+ command := parts[0]
+ params := ""
+ if len(parts) > 1 {
+ params = strings.Join(parts[1:], " ")
+ }
+
+ // Create the task
+ task, err := client.CreateTask(ctx, callbackID, command, params)
+ if err != nil {
+ return fmt.Errorf("failed to create task: %w", err)
+ }
+
+ fmt.Printf("Task %d created\n", task.DisplayID)
+
+ // Poll for response
+ for i := 0; i < 30; i++ { // Poll for up to 30 seconds
+ time.Sleep(1 * time.Second)
+
+ updatedTask, err := client.GetTaskResponse(ctx, task.ID)
+ if err != nil {
+ return fmt.Errorf("failed to get task response: %w", err)
+ }
+
+ if updatedTask.Status == "completed" || updatedTask.Status == "error" {
+ if updatedTask.Response != "" {
+ fmt.Printf("Response:\n%s\n", updatedTask.Response)
+ } else {
+ fmt.Printf("Task completed with status: %s\n", updatedTask.Status)
+ }
+ return nil
+ }
+
+ if i == 0 {
+ fmt.Printf("Waiting for response...")
+ } else if i%5 == 0 {
+ fmt.Printf(".")
+ }
+ }
+
+ fmt.Println("\nTimeout waiting for response")
+ return nil
+}
\ No newline at end of file
cmd/root.go
@@ -0,0 +1,53 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+)
+
+var (
+ mythicURL string
+ token string
+ insecure bool
+ socksProxy string
+)
+
+var rootCmd = &cobra.Command{
+ Use: "go-mythic",
+ Short: "A CLI tool for interacting with Mythic C2 framework",
+ Long: `go-mythic is a command-line interface for interacting with the Mythic C2 framework.
+It provides programmatic access to Mythic operations, replacing the need for UI interactions.`,
+}
+
+func Execute() {
+ err := rootCmd.Execute()
+ if err != nil {
+ os.Exit(1)
+ }
+}
+
+func init() {
+ rootCmd.PersistentFlags().StringVar(&mythicURL, "url", "https://mythic.adversarytactics.local:7443/graphql/", "Mythic GraphQL API URL")
+ rootCmd.PersistentFlags().StringVar(&token, "token", "", "JWT authentication token")
+ rootCmd.PersistentFlags().BoolVar(&insecure, "insecure", true, "Skip TLS certificate verification")
+ rootCmd.PersistentFlags().StringVar(&socksProxy, "socks", "", "SOCKS5 proxy address (e.g., 127.0.0.1:9050)")
+
+ // Set default URL from environment if available
+ if urlEnv := os.Getenv("MYTHIC_API_URL"); urlEnv != "" {
+ mythicURL = urlEnv
+ }
+
+ // Set default token from environment if available
+ if tokenEnv := os.Getenv("MYTHIC_API_TOKEN"); tokenEnv != "" {
+ token = tokenEnv
+ }
+}
+
+func validateAuth() error {
+ if token == "" {
+ return fmt.Errorf("authentication token is required. Use --token flag or set MYTHIC_API_TOKEN environment variable")
+ }
+ return nil
+}
\ No newline at end of file
labs/01.md
@@ -0,0 +1,23 @@
+## Explore the User Interface
+
+Next, briefly familiarize yourself with the layout of the Mythic UI. If you’re new to Mythic, refer to the Mythic Documentation.
+
+Try tasking your active Apollo agent with a few commands:
+
+ - In the left-side menu, select Active Callbacks (telephone with arrow icon).
+ - Right-click on the agent with the most recent checkin time (< 60 seconds) and select “Interact” or Double-click to open the console.
+ - Type help and press enter to obtain a list of agent commands and their description.
+ - Type help ps and press enter to see detailed description of a command.
+ - Type ps to list the running processes on the system.
+
+Next, task your agent to increase its checkin frequency so you can act more interactively with the remote system:
+
+ - Type sleep 5 to set your Beacon’s checkin frequency to 5 seconds
+
+Try out any additional commands you found interesting from the help menu.
+
+Further information on each command can be found in the documentations provided with every agent:
+
+ - In the left-side menu, select Payloads/C2 Services (headphones icon)
+ - One the right side under Actions, select Documentation for the agent or service you want more information on (book icon)
+
pkg/mythic/client.go
@@ -0,0 +1,307 @@
+package mythic
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/base64"
+ "fmt"
+ "net"
+ "net/http"
+ "regexp"
+ "unicode/utf8"
+
+ "github.com/machinebox/graphql"
+ "golang.org/x/net/proxy"
+)
+
+type Client struct {
+ client *graphql.Client
+ token string
+}
+
+type Callback struct {
+ ID int `json:"id"`
+ DisplayID int `json:"display_id"`
+ User string `json:"user"`
+ Host string `json:"host"`
+ ProcessName string `json:"process_name"`
+ PID int `json:"pid"`
+ Description string `json:"description"`
+ LastCheckin string `json:"last_checkin"`
+ Active bool `json:"active"`
+ Payload struct {
+ PayloadType struct {
+ Name string `json:"name"`
+ } `json:"payloadtype"`
+ } `json:"payload"`
+}
+
+type Task struct {
+ ID int `json:"id"`
+ DisplayID int `json:"display_id"`
+ Command string `json:"command"`
+ Params string `json:"params"`
+ Status string `json:"status"`
+ Response string `json:"response,omitempty"`
+ CallbackID int `json:"callback_id"`
+ OperatorID int `json:"operator_id"`
+}
+
+func NewClient(url, token string, insecure bool, socksProxy string) *Client {
+ client := graphql.NewClient(url)
+
+ // Create transport with TLS config
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
+ }
+
+ // Configure SOCKS5 proxy if provided
+ if socksProxy != "" {
+ dialer, err := proxy.SOCKS5("tcp", socksProxy, nil, proxy.Direct)
+ if err == nil {
+ tr.Dial = func(network, addr string) (net.Conn, error) {
+ return dialer.Dial(network, addr)
+ }
+ }
+ }
+
+ httpClient := &http.Client{Transport: tr}
+ client = graphql.NewClient(url, graphql.WithHTTPClient(httpClient))
+
+ return &Client{
+ client: client,
+ token: token,
+ }
+}
+
+// isBase64 checks if a string is valid base64 and likely contains binary data
+func isBase64(s string) bool {
+ // Must be at least 4 characters for valid base64
+ if len(s) < 4 {
+ return false
+ }
+
+ // Check if it matches base64 pattern
+ base64Regex := regexp.MustCompile(`^[A-Za-z0-9+/]*={0,2}$`)
+ if !base64Regex.MatchString(s) {
+ return false
+ }
+
+ // Try to decode
+ decoded, err := base64.StdEncoding.DecodeString(s)
+ if err != nil {
+ return false
+ }
+
+ // If decoded content is not valid UTF-8, it's likely binary data
+ if !utf8.Valid(decoded) {
+ return true
+ }
+
+ // If it's valid UTF-8 but the original string looks like base64
+ // and is significantly longer than the decoded version, it's likely base64
+ if float64(len(s)) > float64(len(decoded))*1.2 {
+ return true
+ }
+
+ return false
+}
+
+// decodeResponseText tries to base64 decode response_text, as Mythic typically encodes all responses
+func decodeResponseText(responseText string) string {
+ // First, try to base64 decode - Mythic typically base64 encodes all response_text
+ if decoded, err := base64.StdEncoding.DecodeString(responseText); err == nil {
+ // If decoded is valid UTF-8 text, return it
+ if utf8.Valid(decoded) {
+ return string(decoded)
+ }
+ // If it's binary data, return a placeholder message
+ return fmt.Sprintf("[Binary data: %d bytes]", len(decoded))
+ }
+
+ // If base64 decode fails, return original text
+ return responseText
+}
+
+func (c *Client) GetCallbacks(ctx context.Context) ([]Callback, error) {
+ req := graphql.NewRequest(`
+ query GetCallbacks {
+ callback {
+ id
+ display_id
+ user
+ host
+ process_name
+ pid
+ description
+ last_checkin
+ active
+ payload {
+ payloadtype {
+ name
+ }
+ }
+ }
+ }
+ `)
+
+ req.Header.Set("Authorization", "Bearer "+c.token)
+
+ var resp struct {
+ Callback []Callback `json:"callback"`
+ }
+
+ if err := c.client.Run(ctx, req, &resp); err != nil {
+ return nil, fmt.Errorf("failed to get callbacks: %w", err)
+ }
+
+ return resp.Callback, nil
+}
+
+func (c *Client) GetActiveCallbacks(ctx context.Context) ([]Callback, error) {
+ callbacks, err := c.GetCallbacks(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ var active []Callback
+ for _, callback := range callbacks {
+ if callback.Active {
+ active = append(active, callback)
+ }
+ }
+
+ return active, nil
+}
+
+func (c *Client) CreateTask(ctx context.Context, callbackID int, command, params string) (*Task, error) {
+ req := graphql.NewRequest(`
+ mutation createTasking($callback_id: Int!, $command: String!, $params: String!, $tasking_location: String) {
+ createTask(callback_id: $callback_id, command: $command, params: $params, tasking_location: $tasking_location) {
+ status
+ id
+ display_id
+ error
+ }
+ }
+ `)
+
+ req.Var("callback_id", callbackID)
+ req.Var("command", command)
+ req.Var("params", params)
+ req.Var("tasking_location", "command_line")
+ req.Header.Set("Authorization", "Bearer "+c.token)
+
+ var resp struct {
+ CreateTask struct {
+ Status string `json:"status"`
+ ID int `json:"id"`
+ DisplayID int `json:"display_id"`
+ Error string `json:"error"`
+ } `json:"createTask"`
+ }
+
+ if err := c.client.Run(ctx, req, &resp); err != nil {
+ return nil, fmt.Errorf("failed to create task: %w", err)
+ }
+
+ if resp.CreateTask.Status != "success" {
+ return nil, fmt.Errorf("task creation failed: %s", resp.CreateTask.Error)
+ }
+
+ return &Task{
+ ID: resp.CreateTask.ID,
+ DisplayID: resp.CreateTask.DisplayID,
+ Command: command,
+ Params: params,
+ Status: "submitted",
+ CallbackID: callbackID,
+ }, nil
+}
+
+func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error) {
+ req := graphql.NewRequest(`
+ query GetTask($id: Int!) {
+ task_by_pk(id: $id) {
+ id
+ display_id
+ command_name
+ original_params
+ display_params
+ status
+ completed
+ callback {
+ id
+ display_id
+ }
+ }
+ }
+ `)
+
+ req.Var("id", taskID)
+ req.Header.Set("Authorization", "Bearer "+c.token)
+
+ var resp struct {
+ Task struct {
+ ID int `json:"id"`
+ DisplayID int `json:"display_id"`
+ CommandName string `json:"command_name"`
+ OriginalParams string `json:"original_params"`
+ DisplayParams string `json:"display_params"`
+ Status string `json:"status"`
+ Completed bool `json:"completed"`
+ Callback struct {
+ ID int `json:"id"`
+ DisplayID int `json:"display_id"`
+ } `json:"callback"`
+ } `json:"task_by_pk"`
+ }
+
+ if err := c.client.Run(ctx, req, &resp); err != nil {
+ return nil, fmt.Errorf("failed to get task: %w", err)
+ }
+
+ // Get task responses
+ responseReq := graphql.NewRequest(`
+ query GetTaskResponses($task_display_id: Int!) {
+ response(where: {task: {display_id: {_eq: $task_display_id}}}, order_by: {id: desc}, limit: 1) {
+ response_text
+ timestamp
+ }
+ }
+ `)
+
+ responseReq.Var("task_display_id", resp.Task.DisplayID)
+ responseReq.Header.Set("Authorization", "Bearer "+c.token)
+
+ var responseResp struct {
+ Response []struct {
+ ResponseText string `json:"response_text"`
+ Timestamp string `json:"timestamp"`
+ } `json:"response"`
+ }
+
+ if err := c.client.Run(ctx, responseReq, &responseResp); err != nil {
+ return nil, fmt.Errorf("failed to get task response: %w", err)
+ }
+
+ response := ""
+ if len(responseResp.Response) > 0 {
+ response = decodeResponseText(responseResp.Response[0].ResponseText)
+ }
+
+ status := resp.Task.Status
+ if resp.Task.Completed {
+ status = "completed"
+ }
+
+ return &Task{
+ ID: resp.Task.ID,
+ DisplayID: resp.Task.DisplayID,
+ Command: resp.Task.CommandName,
+ Params: resp.Task.DisplayParams,
+ Status: status,
+ Response: response,
+ CallbackID: resp.Task.Callback.ID,
+ }, nil
+}
\ No newline at end of file
.gitignore
@@ -0,0 +1,3 @@
+bin/*
+mythic.sh
+contrib/Mythic_Scripting
go.mod
@@ -0,0 +1,16 @@
+module go-mythic
+
+go 1.24.2
+
+require (
+ github.com/machinebox/graphql v0.2.2
+ github.com/spf13/cobra v1.10.1
+)
+
+require (
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/matryer/is v1.4.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/spf13/pflag v1.0.9 // indirect
+ golang.org/x/net v0.44.0 // indirect
+)
go.sum
@@ -0,0 +1,18 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
+github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
+golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
main.go
@@ -0,0 +1,7 @@
+package main
+
+import "go-mythic/cmd"
+
+func main() {
+ cmd.Execute()
+}
\ No newline at end of file
Makefile
@@ -0,0 +1,163 @@
+# Project variables
+BINARY_NAME=go-mythic
+PACKAGE_PATH=.
+BIN_DIR=./bin
+VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
+BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
+COMMIT_HASH=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+
+# Go variables
+GOCMD=go
+GOBUILD=$(GOCMD) build
+GOCLEAN=$(GOCMD) clean
+GOTEST=$(GOCMD) test
+GOGET=$(GOCMD) get
+GOMOD=$(GOCMD) mod
+GOFMT=$(GOCMD) fmt
+
+# Build flags
+LDFLAGS=-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.CommitHash=$(COMMIT_HASH)"
+
+# Default target
+.PHONY: all
+all: clean build
+
+# Create bin directory
+$(BIN_DIR):
+ @mkdir -p $(BIN_DIR)
+
+# Build for current platform
+.PHONY: build
+build: $(BIN_DIR)
+ @echo "Building $(BINARY_NAME) for current platform..."
+ $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME) $(PACKAGE_PATH)
+ @echo "Binary built: $(BIN_DIR)/$(BINARY_NAME)"
+
+# Build for Linux AMD64
+.PHONY: build-linux
+build-linux: $(BIN_DIR)
+ @echo "Building $(BINARY_NAME) for Linux AMD64..."
+ GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-linux-amd64 $(PACKAGE_PATH)
+
+# Build for Linux ARM64
+.PHONY: build-linux-arm64
+build-linux-arm64: $(BIN_DIR)
+ @echo "Building $(BINARY_NAME) for Linux ARM64..."
+ GOOS=linux GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-linux-arm64 $(PACKAGE_PATH)
+
+# Build for macOS AMD64
+.PHONY: build-darwin
+build-darwin: $(BIN_DIR)
+ @echo "Building $(BINARY_NAME) for macOS AMD64..."
+ GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-darwin-amd64 $(PACKAGE_PATH)
+
+# Build for macOS ARM64 (Apple Silicon)
+.PHONY: build-darwin-arm64
+build-darwin-arm64: $(BIN_DIR)
+ @echo "Building $(BINARY_NAME) for macOS ARM64..."
+ GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-darwin-arm64 $(PACKAGE_PATH)
+
+# Build for Windows AMD64
+.PHONY: build-windows
+build-windows: $(BIN_DIR)
+ @echo "Building $(BINARY_NAME) for Windows AMD64..."
+ GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BIN_DIR)/$(BINARY_NAME)-windows-amd64.exe $(PACKAGE_PATH)
+
+# Build for all platforms
+.PHONY: build-all
+build-all: build-linux build-linux-arm64 build-darwin build-darwin-arm64 build-windows
+ @echo "All platform builds completed"
+
+# Run tests
+.PHONY: test
+test:
+ @echo "Running tests..."
+ $(GOTEST) -v ./...
+
+# Run tests with coverage
+.PHONY: test-coverage
+test-coverage:
+ @echo "Running tests with coverage..."
+ $(GOTEST) -v -coverprofile=coverage.out ./...
+ $(GOCMD) tool cover -html=coverage.out -o coverage.html
+ @echo "Coverage report generated: coverage.html"
+
+# Clean build artifacts
+.PHONY: clean
+clean:
+ @echo "Cleaning..."
+ $(GOCLEAN)
+ @rm -rf $(BIN_DIR)
+ @rm -f coverage.out coverage.html
+ @echo "Clean completed"
+
+# Install dependencies
+.PHONY: deps
+deps:
+ @echo "Installing dependencies..."
+ $(GOMOD) download
+ $(GOMOD) tidy
+
+# Format code
+.PHONY: fmt
+fmt:
+ @echo "Formatting code..."
+ $(GOFMT) ./...
+
+# Run linter (if golangci-lint is installed)
+.PHONY: lint
+lint:
+ @if command -v golangci-lint >/dev/null 2>&1; then \
+ echo "Running linter..."; \
+ golangci-lint run; \
+ else \
+ echo "golangci-lint not installed, skipping..."; \
+ fi
+
+# Install binary to system
+.PHONY: install
+install: build
+ @echo "Installing $(BINARY_NAME) to /usr/local/bin..."
+ @sudo cp $(BIN_DIR)/$(BINARY_NAME) /usr/local/bin/
+ @echo "Installation completed"
+
+# Uninstall binary from system
+.PHONY: uninstall
+uninstall:
+ @echo "Removing $(BINARY_NAME) from /usr/local/bin..."
+ @sudo rm -f /usr/local/bin/$(BINARY_NAME)
+ @echo "Uninstall completed"
+
+# Development build (fast, no optimizations)
+.PHONY: dev
+dev: $(BIN_DIR)
+ @echo "Building development version..."
+ $(GOBUILD) -o $(BIN_DIR)/$(BINARY_NAME) $(PACKAGE_PATH)
+
+# Release build (optimized)
+.PHONY: release
+release: clean test build-all
+ @echo "Release build completed"
+
+# Show help
+.PHONY: help
+help:
+ @echo "Available targets:"
+ @echo " build - Build for current platform"
+ @echo " build-linux - Build for Linux AMD64"
+ @echo " build-linux-arm64 - Build for Linux ARM64"
+ @echo " build-darwin - Build for macOS AMD64"
+ @echo " build-darwin-arm64 - Build for macOS ARM64"
+ @echo " build-windows - Build for Windows AMD64"
+ @echo " build-all - Build for all platforms"
+ @echo " test - Run tests"
+ @echo " test-coverage - Run tests with coverage report"
+ @echo " clean - Clean build artifacts"
+ @echo " deps - Install dependencies"
+ @echo " fmt - Format code"
+ @echo " lint - Run linter"
+ @echo " install - Install binary to system"
+ @echo " uninstall - Remove binary from system"
+ @echo " dev - Fast development build"
+ @echo " release - Full release build (all platforms)"
+ @echo " help - Show this help"
\ No newline at end of file
README.md
@@ -0,0 +1,28 @@
+# `go-mythic`
+
+## project goals
+
+For each lab we're going to be instructed to click on the mythic user interface - yuck.
+This project is focused on creating a golang cli application to accomplish the same tasks.
+
+## resources
+
+### the mythic server and `mythic.sh`
+
+This mythic system is setup in a test range with full athorized red team testing (it's a training course).
+We have a user-level access to this server.
+
+`mythic.sh` provides an example curl interface to a test mythic api.
+The `TOKEN` provided in this file should be used for testing all developed functions.
+
+### `Mythic_Scripting`
+
+The `contrib/Mythic_Scripting` directory contains a python implementation of accessing and interacting with the mythic api.
+It is a clone of the `https://github.com/MythicMeta/Mythic_Scripting.git`.
+
+## labs
+
+The `labs` directory will contain markdown files that will contain the steps that are desired to be accomplished/
+They will always be instructing the user to click and use the UI.
+We will be translating these instructions into functions and cli subcommands.
+
USAGE.md
@@ -0,0 +1,137 @@
+# go-mythic Usage Examples
+
+## Building
+
+### Using Makefile (Recommended)
+```bash
+# Build for current platform
+make build
+
+# Build for all platforms
+make build-all
+
+# Development build (faster)
+make dev
+
+# Clean build artifacts
+make clean
+
+# Show all available targets
+make help
+```
+
+### Manual Build
+```bash
+go build -o bin/go-mythic .
+```
+
+## Basic Commands
+
+### List Active Callbacks
+```bash
+./go-mythic callbacks
+```
+
+### Execute Commands
+```bash
+# Get help for all commands
+./go-mythic exec 1 help
+
+# Get detailed help for a specific command
+./go-mythic exec 1 help ps
+
+# List processes
+./go-mythic exec 1 ps
+
+# Set checkin frequency to 5 seconds
+./go-mythic exec 1 sleep 5
+```
+
+### Interactive Session
+```bash
+./go-mythic interact 1
+```
+
+## Using SOCKS5 Proxy
+
+The CLI supports SOCKS5 proxy routing for all network traffic, replacing the need for proxychains:
+
+```bash
+# Use SOCKS5 proxy (equivalent to proxychains)
+./go-mythic --socks 127.0.0.1:9050 callbacks
+
+# Execute command through proxy
+./go-mythic --socks 127.0.0.1:9050 exec 1 ps
+
+# Interactive session through proxy
+./go-mythic --socks 127.0.0.1:9050 interact 1
+```
+
+## Configuration
+
+### Authentication Token
+Authentication is required via JWT token. Set it using one of these methods:
+
+```bash
+# Set via environment variable (recommended)
+export MYTHIC_API_TOKEN="your-jwt-token-here"
+./go-mythic callbacks
+
+# Set via command line flag
+./go-mythic --token "your-jwt-token-here" callbacks
+
+# Get token from mythic.sh for testing
+export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
+```
+
+### Custom Mythic Server
+```bash
+# Set via environment variable
+export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
+./go-mythic callbacks
+
+# Set via command line flag
+./go-mythic --url "https://your-mythic-server:7443/graphql/" callbacks
+```
+
+### Complete Environment Setup
+```bash
+# Set both URL and token via environment variables
+export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
+export MYTHIC_API_TOKEN="your-jwt-token-here"
+./go-mythic callbacks
+```
+
+## Lab 01 Equivalents
+
+These commands accomplish the goals from labs/01.md:
+
+1. **View Active Callbacks** (equivalent to clicking Active Callbacks in UI):
+ ```bash
+ ./go-mythic callbacks
+ ```
+
+2. **Interact with Agent** (equivalent to right-click → Interact):
+ ```bash
+ ./go-mythic interact <callback_id>
+ ```
+
+3. **Get Help** (equivalent to typing "help" in UI console):
+ ```bash
+ ./go-mythic exec <callback_id> help
+ ```
+
+4. **Get Command Details** (equivalent to typing "help ps"):
+ ```bash
+ ./go-mythic exec <callback_id> help ps
+ ```
+
+5. **List Processes** (equivalent to typing "ps"):
+ ```bash
+ ./go-mythic exec <callback_id> ps
+ ```
+
+6. **Set Checkin Frequency** (equivalent to typing "sleep 5"):
+ ```bash
+ ./go-mythic exec <callback_id> sleep 5
+ ```