Commit de61f63
Changed files (6)
cmd/callbacks.go
@@ -6,6 +6,7 @@ import (
"mysh/pkg/mythic"
"os"
"sort"
+ "strconv"
"strings"
"time"
@@ -22,13 +23,15 @@ var (
sortByLast bool
sortByDescription bool
sortReverse bool
+ showCommands bool
)
var callbacksCmd = &cobra.Command{
- Use: "callbacks",
+ Use: "callbacks [callback_id]",
Aliases: []string{"cb", "callback"},
- Short: "List active callbacks",
- Long: "Display all active callbacks from the Mythic server, equivalent to the Active Callbacks view in the UI.",
+ Short: "List active callbacks or show detailed callback information",
+ Long: "Display all active callbacks from the Mythic server, or show detailed information about a specific callback including available commands.",
+ Args: cobra.MaximumNArgs(1),
RunE: runCallbacks,
}
@@ -46,6 +49,9 @@ func init() {
// Reverse sort flag
callbacksCmd.Flags().BoolVar(&sortReverse, "rev", false, "Reverse sort order")
+
+ // Commands flag for detailed view
+ callbacksCmd.Flags().BoolVar(&showCommands, "commands", false, "Show available commands in detailed callback view")
}
// formatTimeSince formats a timestamp into a human-readable "time ago" format
@@ -182,6 +188,16 @@ func runCallbacks(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
+ // Check if a specific callback ID was provided
+ if len(args) == 1 {
+ callbackID, err := strconv.Atoi(args[0])
+ if err != nil {
+ return fmt.Errorf("invalid callback ID: %s", args[0])
+ }
+ return showDetailedCallback(ctx, client, callbackID)
+ }
+
+ // Show table of all active callbacks
callbacks, err := client.GetActiveCallbacks(ctx)
if err != nil {
return fmt.Errorf("failed to get active callbacks: %w", err)
@@ -194,12 +210,10 @@ func runCallbacks(cmd *cobra.Command, args []string) error {
// Sort callbacks based on flags
sortField := getSortField()
- // For ID sort, default to newest (highest) last, so reverse=true by default
+ // For ID sort, default to ascending order with highest ID last (reverse=false)
// For other sorts, use the --rev flag
shouldReverse := sortReverse
- if sortField == "id" && !sortReverse {
- shouldReverse = true // Default for ID is newest last (reverse order)
- }
+ // Note: No special logic needed for ID sort - ascending is the natural order
sortCallbacks(callbacks, sortField, shouldReverse)
@@ -262,3 +276,219 @@ func runCallbacks(cmd *cobra.Command, args []string) error {
t.Render()
return nil
}
+
+// wrapDescription wraps text to fit within the specified width, adding proper indentation for continuation lines
+func wrapDescription(text string, maxWidth int, indent string) string {
+ if text == "" {
+ return ""
+ }
+
+ // If the text fits on one line, return as-is
+ if len(text) <= maxWidth {
+ return text
+ }
+
+ words := strings.Fields(text)
+ if len(words) == 0 {
+ return text
+ }
+
+ var lines []string
+ var currentLine []string
+ currentLength := 0
+
+ for _, word := range words {
+ // Check if adding this word would exceed the line width
+ wordLen := len(word)
+ if currentLength > 0 {
+ wordLen += 1 // space before word
+ }
+
+ if currentLength == 0 || currentLength + wordLen <= maxWidth {
+ // Word fits on current line
+ currentLine = append(currentLine, word)
+ if currentLength == 0 {
+ currentLength = len(word)
+ } else {
+ currentLength += 1 + len(word) // space + word
+ }
+ } else {
+ // Word doesn't fit, finish current line and start new one
+ if len(currentLine) > 0 {
+ lines = append(lines, strings.Join(currentLine, " "))
+ }
+
+ // Handle very long single words that exceed maxWidth
+ if len(word) > maxWidth {
+ // Split the word if it's longer than maxWidth
+ for len(word) > maxWidth {
+ lines = append(lines, word[:maxWidth])
+ word = word[maxWidth:]
+ }
+ if len(word) > 0 {
+ currentLine = []string{word}
+ currentLength = len(word)
+ } else {
+ currentLine = []string{}
+ currentLength = 0
+ }
+ } else {
+ // Start new line with this word
+ currentLine = []string{word}
+ currentLength = len(word)
+ }
+ }
+ }
+
+ // Add the final line
+ if len(currentLine) > 0 {
+ lines = append(lines, strings.Join(currentLine, " "))
+ }
+
+ // Join lines with appropriate indentation
+ result := lines[0]
+ for i := 1; i < len(lines); i++ {
+ result += "\n" + indent + lines[i]
+ }
+
+ return result
+}
+
+func showDetailedCallback(ctx context.Context, client *mythic.Client, callbackID int) error {
+ callback, err := client.GetDetailedCallback(ctx, callbackID)
+ if err != nil {
+ return err
+ }
+
+ // Display detailed callback information
+ fmt.Printf("Callback %d\n", callback.DisplayID)
+ fmt.Printf("\n")
+
+ // Agent Overview Section
+ fmt.Printf("Agent Overview\n")
+ fmt.Printf(" ID: %d\n", callback.DisplayID)
+ fmt.Printf(" Status: %s\n", getStatusString(callback.Active))
+ fmt.Printf(" Agent Type: %s\n", callback.Payload.PayloadType.Name)
+ fmt.Printf(" Operator: %s\n", callback.Operator.Username)
+ fmt.Printf(" Last Checkin: %s\n", formatTimeSince(callback.LastCheckin))
+ if callback.Description != "" {
+ fmt.Printf(" Description: %s\n", callback.Description)
+ }
+ fmt.Printf("\n")
+
+ // System Information Section
+ fmt.Printf("System Information\n")
+ fmt.Printf(" Hostname: %s\n", callback.Host)
+ fmt.Printf(" Username: %s\n", callback.User)
+ fmt.Printf(" Process: %s (PID: %d)\n", callback.ProcessName, callback.PID)
+ if callback.OS != "" {
+ fmt.Printf(" Operating System: %s\n", callback.OS)
+ }
+ if callback.Architecture != "" {
+ fmt.Printf(" Architecture: %s\n", callback.Architecture)
+ }
+ if callback.Domain != "" {
+ fmt.Printf(" Domain: %s\n", callback.Domain)
+ }
+ if callback.IntegrityLevel > 0 {
+ fmt.Printf(" Integrity Level: %d\n", callback.IntegrityLevel)
+ }
+ fmt.Printf("\n")
+
+ // Network Context Section
+ fmt.Printf("Network Context\n")
+ if callback.IP != "" {
+ fmt.Printf(" Internal IP: %s\n", callback.IP)
+ }
+ if callback.ExternalIP != "" {
+ fmt.Printf(" External IP: %s\n", callback.ExternalIP)
+ }
+ if callback.SleepInfo != "" {
+ fmt.Printf(" Sleep Config: %s\n", callback.SleepInfo)
+ }
+ if callback.CryptoType != "" {
+ fmt.Printf(" Crypto Type: %s\n", callback.CryptoType)
+ }
+ fmt.Printf("\n")
+
+ // Payload Information Section
+ fmt.Printf("Payload Information\n")
+ fmt.Printf(" UUID: %s\n", callback.Payload.UUID)
+ fmt.Printf(" Type: %s\n", callback.Payload.PayloadType.Name)
+ if callback.Payload.PayloadType.Author != "" {
+ fmt.Printf(" Author: %s\n", callback.Payload.PayloadType.Author)
+ }
+ if callback.Payload.PayloadType.Note != "" {
+ fmt.Printf(" Type Note: %s\n", callback.Payload.PayloadType.Note)
+ }
+ if callback.Payload.Description != "" {
+ fmt.Printf(" Description: %s\n", callback.Payload.Description)
+ }
+ if callback.AgentCallbackID != "" {
+ fmt.Printf(" Agent ID: %s\n", callback.AgentCallbackID)
+ }
+ // Available Commands Section - only show if --commands flag is used
+ if showCommands {
+ fmt.Printf("\n")
+ fmt.Printf("Available Commands (%d loaded)\n", len(callback.LoadedCommands))
+ if len(callback.LoadedCommands) > 0 {
+ // Sort commands alphabetically
+ commands := make([]mythic.LoadedCommand, len(callback.LoadedCommands))
+ copy(commands, callback.LoadedCommands)
+ sort.Slice(commands, func(i, j int) bool {
+ return commands[i].Command.Cmd < commands[j].Command.Cmd
+ })
+
+ // Find the longest command name for consistent alignment
+ maxCmdLen := 0
+ for _, cmd := range commands {
+ if len(cmd.Command.Cmd) > maxCmdLen {
+ maxCmdLen = len(cmd.Command.Cmd)
+ }
+ }
+
+ // Calculate available width for descriptions
+ termWidth := getTerminalWidth()
+ prefixWidth := 2 + maxCmdLen + 1 // " longest_command_name "
+ adminFlagMaxWidth := 10 // " [ADMIN]" plus some buffer
+ availableWidth := termWidth - prefixWidth - adminFlagMaxWidth
+
+ // Ensure minimum width for readability
+ if availableWidth < 30 {
+ availableWidth = 30
+ }
+
+ // Create indent string to match the command name alignment
+ indent := " " + strings.Repeat(" ", maxCmdLen) + " "
+
+ for _, cmd := range commands {
+ adminFlag := ""
+ if cmd.Command.NeedsAdmin {
+ adminFlag = " [ADMIN]"
+ }
+
+ // Wrap the description
+ wrappedDesc := wrapDescription(cmd.Command.Description, availableWidth, indent)
+
+ // Print the command with wrapped description using dynamic width
+ fmt.Printf(" %-*s %s%s\n", maxCmdLen, cmd.Command.Cmd, wrappedDesc, adminFlag)
+ }
+ } else {
+ fmt.Printf(" No commands loaded\n")
+ }
+ }
+
+ // Show extra info if available
+ if callback.ExtraInfo != "" {
+ fmt.Printf("\nExtra Information:\n%s\n", callback.ExtraInfo)
+ }
+
+ return nil
+}
+
+func getStatusString(active bool) string {
+ if active {
+ return "Active"
+ }
+ return "Inactive"
+}
cmd/task.go
@@ -3,11 +3,13 @@ package cmd
import (
"context"
"fmt"
+ "mysh/pkg/cache"
"mysh/pkg/mythic"
"mysh/pkg/mythic/api"
"sort"
"strconv"
"strings"
+ "time"
"github.com/spf13/cobra"
)
@@ -16,14 +18,15 @@ var (
taskWaitTime int
taskRawOutput bool
taskNoWait bool
+ taskCopyID int
)
var taskCmd = &cobra.Command{
- Use: "task <callback_id(s)> <command> [args...]",
+ Use: "task <callback_id(s)> [command] [args...]",
Aliases: []string{"t", "exec"},
Short: "Send a command to one or more agents",
- Long: "Send a command to specified agent callback(s) and wait for response. Supports single IDs, comma-separated lists, and ranges (e.g., '1,3-5,10').",
- Args: cobra.MinimumNArgs(2),
+ Long: "Send a command to specified agent callback(s) and wait for response. Supports single IDs, comma-separated lists, and ranges (e.g., '1,3-5,10'). Use --copy-task to copy parameters from an existing task.",
+ Args: cobra.MinimumNArgs(1),
RunE: runTask,
}
@@ -32,6 +35,7 @@ func init() {
taskCmd.Flags().IntVarP(&taskWaitTime, "wait", "w", 30, "Maximum time to wait for response (seconds)")
taskCmd.Flags().BoolVar(&taskRawOutput, "raw", false, "Output only raw response bytes")
taskCmd.Flags().BoolVar(&taskNoWait, "no-wait", false, "Create task but don't wait for response")
+ taskCmd.Flags().IntVar(&taskCopyID, "copy-task", 0, "Copy command and parameters from existing task ID")
}
// parseCallbackIDs parses a callback ID string that can contain:
@@ -112,15 +116,68 @@ func runTask(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid callback ID(s): %w", err)
}
- command := args[1]
- params := ""
- if len(args) > 2 {
- params = strings.Join(args[2:], " ")
- }
+ var command, params string
client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
ctx := context.Background()
+ // Handle task copying
+ if taskCopyID > 0 {
+ // Copy task parameters from existing task
+ copyCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+
+ // Initialize cache for task lookup
+ taskCache, err := cache.New(mythicURL)
+ if err != nil {
+ taskCache = nil
+ }
+
+ // Try to get task from cache first
+ var sourceTask *mythic.Task
+ if taskCache != nil {
+ if cachedTask, found := taskCache.GetCachedTask(taskCopyID, mythicURL); found {
+ sourceTask = cachedTask
+ }
+ }
+
+ // If not in cache, fetch from server
+ if sourceTask == nil {
+ sourceTask, err = client.GetTaskResponse(copyCtx, taskCopyID)
+ if err != nil {
+ return fmt.Errorf("failed to get source task %d: %w", taskCopyID, err)
+ }
+
+ // Cache the result if task is completed and cache is available
+ if taskCache != nil {
+ taskCache.CacheTask(sourceTask, mythicURL)
+ }
+ }
+
+ // Use the source task's command and original parameters
+ command = sourceTask.Command
+ if sourceTask.OriginalParams != "" {
+ params = sourceTask.OriginalParams
+ } else if sourceTask.Params != "" {
+ params = sourceTask.Params
+ }
+
+ fmt.Printf("Copying task %d: %s\n", taskCopyID, command)
+ if params != "" {
+ fmt.Printf("Parameters: %s\n", params)
+ }
+ fmt.Println()
+ } else {
+ // Normal mode: use command line arguments
+ if len(args) < 2 {
+ return fmt.Errorf("command is required when not using --copy-task")
+ }
+ command = args[1]
+ if len(args) > 2 {
+ params = strings.Join(args[2:], " ")
+ }
+ }
+
// Track results for multiple callbacks
type TaskResult struct {
CallbackID int
cmd/task_view.go
@@ -2,11 +2,11 @@ package cmd
import (
"context"
+ "encoding/json"
"fmt"
"mysh/pkg/cache"
"mysh/pkg/mythic"
"mysh/pkg/mythic/api"
- "os"
"strconv"
"strings"
"time"
@@ -34,6 +34,29 @@ func init() {
taskViewCmd.Flags().BoolVarP(&taskViewDetails, "details", "d", false, "Show only task details (no response)")
}
+// formatJSONForDisplay formats JSON with proper indentation for display
+func formatJSONForDisplay(jsonStr, indent string) string {
+ if jsonStr == "" {
+ return ""
+ }
+
+ // Try to parse and format as JSON
+ var parsed interface{}
+ if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
+ // If it's not valid JSON, return as-is with indent
+ return indent + jsonStr
+ }
+
+ // Format with indentation
+ formatted, err := json.MarshalIndent(parsed, indent, " ")
+ if err != nil {
+ // If formatting fails, return original with indent
+ return indent + jsonStr
+ }
+
+ return string(formatted)
+}
+
func runTaskView(cmd *cobra.Command, args []string) error {
if err := validateConfig(); err != nil {
return err
@@ -57,7 +80,7 @@ func runTaskView(cmd *cobra.Command, args []string) error {
var task *mythic.Task
- // Try to get from cache first for completed tasks
+ // Try to get from cache first if available
if taskCache != nil {
if cachedTask, found := taskCache.GetCachedTask(taskID, mythicURL); found {
task = cachedTask
@@ -71,12 +94,9 @@ func runTaskView(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to get task %d: %w", taskID, err)
}
- // Cache the result if it's completed and cache is available
+ // Cache the result if task is completed and cache is available
if taskCache != nil {
- if err := taskCache.CacheTask(task, mythicURL); err != nil {
- // Log error but don't fail the command
- fmt.Fprintf(os.Stderr, "Warning: failed to cache task result: %v\n", err)
- }
+ taskCache.CacheTask(task, mythicURL)
}
}
@@ -103,37 +123,101 @@ func runTaskView(cmd *cobra.Command, args []string) error {
fmt.Println(strings.Repeat("=", 60))
// Basic task info
- fmt.Printf("Command: %s\n", task.Command)
- fmt.Printf("Parameters: %s\n", task.Params)
+ if task.Command != "" {
+ fmt.Printf("Command: %s\n", task.Command)
+ } else {
+ fmt.Printf("Command: (not available)\n")
+ }
fmt.Printf("Status: %s\n", task.Status)
fmt.Printf("Completed: %t\n", task.Completed)
+ // Operator and agent info
+ if task.Operator.Username != "" {
+ fmt.Printf("Operator: %s\n", task.Operator.Username)
+ }
+ if task.AgentTaskID != "" {
+ fmt.Printf("Agent Task ID: %s\n", task.AgentTaskID)
+ }
+
+ // Parameters section with proper JSON formatting
+ hasParams := task.OriginalParams != "" || task.DisplayParams != "" || task.Params != ""
+ if hasParams {
+ fmt.Printf("\nAgent Parameters:\n")
+
+ // Show original parameters (JSON) if available
+ if task.OriginalParams != "" {
+ fmt.Printf(" Original (JSON):\n %s\n", task.OriginalParams)
+ }
+
+ // Show display parameters if different from original
+ if task.DisplayParams != "" && task.DisplayParams != task.OriginalParams {
+ fmt.Printf(" Display Format:\n %s\n", task.DisplayParams)
+ }
+
+ // Show processed parameters if different from both above
+ if task.Params != "" && task.Params != task.DisplayParams && task.Params != task.OriginalParams {
+ fmt.Printf(" Processed:\n %s\n", task.Params)
+ }
+
+ // If only basic params available, show those
+ if task.OriginalParams == "" && task.DisplayParams == "" && task.Params != "" {
+ fmt.Printf(" Parameters:\n %s\n", task.Params)
+ }
+
+ // If no parameters at all
+ if !hasParams {
+ fmt.Printf(" (No parameters)\n")
+ }
+ }
+
+ // Task metadata
+ fmt.Printf("\nTask Metadata:\n")
+ if task.ParameterGroupName != "" {
+ fmt.Printf(" Parameter Group: %s\n", task.ParameterGroupName)
+ }
+ if task.TaskingLocation != "" {
+ fmt.Printf(" Tasking Location: %s\n", task.TaskingLocation)
+ }
+ if task.IsInteractiveTask {
+ fmt.Printf(" Interactive Task: Yes (Type: %d)\n", task.InteractiveTaskType)
+ }
+ if task.ParentTaskID > 0 {
+ fmt.Printf(" Parent Task: %d\n", task.ParentTaskID)
+ }
+ if task.Token.TokenID > 0 {
+ fmt.Printf(" Token ID: %d\n", task.Token.TokenID)
+ }
+
// Callback information
+ fmt.Printf("\nCallback Information:\n")
if callback != nil {
- fmt.Printf("Callback: %d (%s@%s)\n", task.CallbackID, callback.User, callback.Host)
- fmt.Printf("Process: %s (PID: %d)\n", callback.ProcessName, callback.PID)
+ fmt.Printf(" Callback ID: %d\n", task.CallbackID)
+ fmt.Printf(" Host/User: %s@%s\n", callback.User, callback.Host)
+ fmt.Printf(" Process: %s (PID: %d)\n", callback.ProcessName, callback.PID)
if callback.Description != "" {
- fmt.Printf("Description: %s\n", callback.Description)
+ fmt.Printf(" Description: %s\n", callback.Description)
}
agentType := callback.Payload.PayloadType.Name
if agentType != "" {
- fmt.Printf("Agent Type: %s\n", agentType)
+ fmt.Printf(" Agent Type: %s\n", agentType)
}
} else {
- fmt.Printf("Callback: %d\n", task.CallbackID)
+ fmt.Printf(" Callback ID: %d\n", task.CallbackID)
}
// Timing information
+ fmt.Printf("\nTiming:\n")
if task.Timestamp != "" {
if t, err := time.Parse(time.RFC3339, task.Timestamp); err == nil {
- fmt.Printf("Created: %s\n", t.Format("2006-01-02 15:04:05 MST"))
+ fmt.Printf(" Created: %s\n", t.Format("2006-01-02 15:04:05 MST"))
} else {
- fmt.Printf("Created: %s\n", task.Timestamp)
+ fmt.Printf(" Created: %s\n", task.Timestamp)
}
}
- // Additional technical details
- fmt.Printf("Internal ID: %d\n", task.ID)
+ // Technical details
+ fmt.Printf("\nTechnical Details:\n")
+ fmt.Printf(" Internal ID: %d\n", task.ID)
// If --details flag is set, only show the header
if taskViewDetails {
pkg/mythic/client.go
@@ -42,17 +42,96 @@ type Callback struct {
} `json:"payload"`
}
+type DetailedCallback struct {
+ // Basic info
+ 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"`
+
+ // Extended system info
+ Architecture string `json:"architecture"`
+ OS string `json:"os"`
+ Domain string `json:"domain"`
+ IntegrityLevel int `json:"integrity_level"`
+ IP string `json:"ip"`
+ ExternalIP string `json:"external_ip"`
+ SleepInfo string `json:"sleep_info"`
+ ExtraInfo string `json:"extra_info"`
+
+ // Payload info
+ RegisteredPayloadID int `json:"registered_payload_id"`
+ AgentCallbackID string `json:"agent_callback_id"`
+ CryptoType string `json:"crypto_type"`
+
+ // Timestamps
+ InitCallback string `json:"init_callback"`
+
+ // Relationships
+ Payload struct {
+ ID int `json:"id"`
+ UUID string `json:"uuid"`
+ Description string `json:"description"`
+ PayloadType struct {
+ Name string `json:"name"`
+ Author string `json:"author"`
+ Note string `json:"note"`
+ } `json:"payloadtype"`
+ } `json:"payload"`
+
+ // Operator info
+ Operator struct {
+ Username string `json:"username"`
+ } `json:"operator"`
+
+ // Commands
+ LoadedCommands []LoadedCommand `json:"loadedcommands"`
+}
+
+type LoadedCommand struct {
+ ID int `json:"id"`
+ Version int `json:"version"`
+ Timestamp string `json:"timestamp"`
+ Command struct {
+ Cmd string `json:"cmd"`
+ Description string `json:"description"`
+ HelpCmd string `json:"help_cmd"`
+ Author string `json:"author"`
+ NeedsAdmin bool `json:"needs_admin"`
+ Attributes interface{} `json:"attributes"`
+ } `json:"command"`
+}
+
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"`
- Timestamp string `json:"timestamp,omitempty"`
- Completed bool `json:"completed"`
+ ID int `json:"id"`
+ DisplayID int `json:"display_id"`
+ Command string `json:"command_name"`
+ Params string `json:"params"`
+ OriginalParams string `json:"original_params"`
+ DisplayParams string `json:"display_params"`
+ Status string `json:"status"`
+ Response string `json:"response,omitempty"`
+ CallbackID int `json:"callback_id"`
+ OperatorID int `json:"operator_id"`
+ Timestamp string `json:"timestamp,omitempty"`
+ Completed bool `json:"completed"`
+ AgentTaskID string `json:"agent_task_id"`
+ ParameterGroupName string `json:"parameter_group_name"`
+ TaskingLocation string `json:"tasking_location"`
+ IsInteractiveTask bool `json:"is_interactive_task"`
+ InteractiveTaskType int `json:"interactive_task_type"`
+ ParentTaskID int `json:"parent_task_id"`
+ Operator struct {
+ Username string `json:"username"`
+ } `json:"operator"`
+ Token struct {
+ TokenID int `json:"token_id"`
+ } `json:"token"`
}
type File struct {
@@ -219,6 +298,27 @@ func (c *Client) GetActiveCallbacks(ctx context.Context) ([]Callback, error) {
return active, nil
}
+func (c *Client) GetDetailedCallback(ctx context.Context, callbackID int) (*DetailedCallback, error) {
+ req := graphql.NewRequest(GetDetailedCallback)
+
+ req.Var("callback_id", callbackID)
+ req.Header.Set("apitoken", c.token)
+
+ var resp struct {
+ Callback *DetailedCallback `json:"callback_by_pk"`
+ }
+
+ if err := c.client.Run(ctx, req, &resp); err != nil {
+ return nil, fmt.Errorf("failed to get detailed callback: %w", err)
+ }
+
+ if resp.Callback == nil {
+ return nil, fmt.Errorf("callback %d not found", callbackID)
+ }
+
+ return resp.Callback, nil
+}
+
func (c *Client) CreateTask(ctx context.Context, callbackID int, command, params string) (*Task, error) {
req := graphql.NewRequest(CreateTask)
@@ -263,14 +363,28 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
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"`
+ CommandName string `json:"command_name"`
+ Params string `json:"params"`
+ OriginalParams string `json:"original_params"`
+ DisplayParams string `json:"display_params"`
+ Status string `json:"status"`
+ Completed bool `json:"completed"`
+ Timestamp string `json:"timestamp"`
+ AgentTaskID string `json:"agent_task_id"`
+ ParameterGroupName string `json:"parameter_group_name"`
+ TaskingLocation string `json:"tasking_location"`
+ IsInteractiveTask bool `json:"is_interactive_task"`
+ InteractiveTaskType int `json:"interactive_task_type"`
+ ParentTaskID int `json:"parent_task_id"`
+ Operator struct {
+ Username string `json:"username"`
+ } `json:"operator"`
+ Token struct {
+ TokenID int `json:"token_id"`
+ } `json:"token"`
+ Callback struct {
ID int `json:"id"`
DisplayID int `json:"display_id"`
} `json:"callback"`
@@ -281,6 +395,8 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
return nil, fmt.Errorf("failed to get task: %w", err)
}
+
+
// Get task responses
responseReq := graphql.NewRequest(GetTaskResponses)
@@ -312,14 +428,33 @@ func (c *Client) GetTaskResponse(ctx context.Context, taskID int) (*Task, error)
}
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,
- Completed: resp.Task.Completed,
+ ID: resp.Task.ID,
+ DisplayID: resp.Task.DisplayID,
+ Command: resp.Task.CommandName,
+ Params: resp.Task.Params,
+ OriginalParams: resp.Task.OriginalParams,
+ DisplayParams: resp.Task.DisplayParams,
+ Status: status,
+ Response: response,
+ CallbackID: resp.Task.Callback.ID,
+ Completed: resp.Task.Completed,
+ Timestamp: resp.Task.Timestamp,
+ AgentTaskID: resp.Task.AgentTaskID,
+ ParameterGroupName: resp.Task.ParameterGroupName,
+ TaskingLocation: resp.Task.TaskingLocation,
+ IsInteractiveTask: resp.Task.IsInteractiveTask,
+ InteractiveTaskType: resp.Task.InteractiveTaskType,
+ ParentTaskID: resp.Task.ParentTaskID,
+ Operator: struct {
+ Username string `json:"username"`
+ }{
+ Username: resp.Task.Operator.Username,
+ },
+ Token: struct {
+ TokenID int `json:"token_id"`
+ }{
+ TokenID: resp.Task.Token.TokenID,
+ },
}, nil
}
pkg/mythic/queries.go
@@ -363,16 +363,83 @@ query GetCallbacks {
}
}`
+const GetDetailedCallback = `
+query GetDetailedCallback($callback_id: Int!) {
+ callback_by_pk(id: $callback_id) {
+ id
+ display_id
+ user
+ host
+ process_name
+ pid
+ description
+ last_checkin
+ active
+ architecture
+ os
+ domain
+ integrity_level
+ ip
+ external_ip
+ sleep_info
+ extra_info
+ registered_payload_id
+ agent_callback_id
+ crypto_type
+ init_callback
+ payload {
+ id
+ uuid
+ description
+ payloadtype {
+ name
+ author
+ note
+ }
+ }
+ operator {
+ username
+ }
+ loadedcommands {
+ id
+ version
+ timestamp
+ command {
+ cmd
+ description
+ help_cmd
+ author
+ needs_admin
+ attributes
+ }
+ }
+ }
+}`
+
const GetTask = `
query GetTask($id: Int!) {
task_by_pk(id: $id) {
id
display_id
command_name
+ params
original_params
display_params
status
completed
+ timestamp
+ agent_task_id
+ parameter_group_name
+ tasking_location
+ is_interactive_task
+ interactive_task_type
+ parent_task_id
+ operator {
+ username
+ }
+ token {
+ token_id
+ }
callback {
id
display_id
TODO.md
@@ -1,25 +1,37 @@
-- grep --delete - flag that deletes the tasking from the server that match the grep - require confirmation
-- forge payloads - equivalency with execute_assembly and inline_assembly
+make a cli file browser that tasks apollo ls commands to cd and ls into remote directories
+`get` should download the file
+make it tasks the next ls 1 depth lower than what the user sees to make it seem quick
+search cached tasks with path ls results before retasking (uses the pre-fetch data)
+keep polling the tasks for that cb to make sure results are cached
+show the current callback number and current working directory in the prompt
+stretch goal: tab complete with the current directories contents
-- MYTHIC_API_INSECURE= boolean --insecure flag - and invert default = false
-- update output table formats to respect terminal width
-- invert cobra-cli nto functions (no global vars)
+- [ ] copy an existing task verbatium into a new task `mysh task [cb-list] --copy-task [task-id]
-- ✅ grep --raw - only diplay of all matching items raw output follow grep -R style single-line file dilimeters in the output
-- ✅ use per-server cache dirs
-- ✅ make sure raw output has a newline at the end
-- ✅ make mysh cache default to info output
-- ✅ create a .cache/mysh/<server> to store task results
-- ✅ make callbacks sortable by columns
-- ✅ make tv have a -d --details to only show the header
-- ✅ make tv have more task details in the header
-- ✅ make tv --raw have short -r
-- ✅ update callbacks to show last-checkin as 0s or 0m00s ago
-- ✅ make cb and callback aliases for callbacks
-- ✅ task [callback] arg make it accept lists, comma seperated, or ranges e.g. 1,2,3-5,10
-- ✅ task with --no-wait, doesn't wait for output
-- ✅ task-list without callback lists all tasks and shows callback column
-- ✅ task-list --all (same as --limit 0), and -a
-- ✅ Renamed project from go-mythic to mysh (pronounced mɪʃh)
-- ✅ grep should also look at the command execution strings too, not just output
+- [ ] forge payloads - equivalency with execute_assembly and inline_assembly
+
+- [ ] MYTHIC_API_INSECURE= boolean --insecure flag - and invert default = false
+- [ ] update output table formats to respect terminal width
+- [ ] invert cobra-cli nto functions (no global vars)
+
+- [ ] credentials listing
+
+- [x] task view needs to show Agent Parameters:
+- [x] grep --raw - only diplay of all matching items raw output follow grep -R style single-line file dilimeters in the output
+- [x] use per-server cache dirs
+- [x] make sure raw output has a newline at the end
+- [x] make mysh cache default to info output
+- [x] create a .cache/mysh/<server> to store task results
+- [x] make callbacks sortable by columns
+- [x] make tv have a -d --details to only show the header
+- [x] make tv have more task details in the header
+- [x] make tv --raw have short -r
+- [x] update callbacks to show last-checkin as 0s or 0m00s ago
+- [x] make cb and callback aliases for callbacks
+- [x] task [callback] arg make it accept lists, comma seperated, or ranges e.g. 1,2,3-5,10
+- [x] task with --no-wait, doesn't wait for output
+- [x] task-list without callback lists all tasks and shows callback column
+- [x] task-list --all (same as --limit 0), and -a
+- [x] Renamed project from go-mythic to mysh (pronounced mɪʃh)
+- [x] grep should also look at the command execution strings too, not just output