Commit 750a447
Changed files (26)
cmd
pkg
cmd/callbacks.go
@@ -3,7 +3,7 @@ package cmd
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"os"
"text/tabwriter"
"time"
cmd/files.go
@@ -3,8 +3,8 @@ package cmd
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
- "go-mythic/pkg/mythic/api"
+ "mysh/pkg/mythic"
+ "mysh/pkg/mythic/api"
"os"
"path/filepath"
"regexp"
@@ -162,8 +162,8 @@ func runFilesList(cmd *cobra.Command, args []string) error {
w.Flush()
- fmt.Println("\nTo download a file: go-mythic files download <uuid>")
- fmt.Println("To view full UUIDs: go-mythic files | grep -v UUID")
+ fmt.Println("\nTo download a file: mysh files download <uuid>")
+ fmt.Println("To view full UUIDs: mysh files | grep -v UUID")
return nil
}
cmd/grep.go
@@ -3,11 +3,10 @@ package cmd
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"os"
"regexp"
"strings"
- "sync"
"text/tabwriter"
"time"
@@ -20,15 +19,18 @@ var (
grepLimit int
grepRegex bool
grepInvert bool
- grepWorkers int
+ grepSearchCommands bool
+ grepSearchOutput bool
)
var grepCmd = &cobra.Command{
Use: "grep <pattern>",
- Short: "Search through task outputs",
- Long: `Search through task response outputs for a specific pattern.
+ Short: "Search through task outputs and commands",
+ Long: `Search through task response outputs and/or command execution strings for a specific pattern.
Shows a table of matching tasks with their commands instead of full output.
-This is like grep for Mythic task results - find specific strings, commands, or data across all your task history.`,
+This is like grep for Mythic task results - find specific strings, commands, or data across all your task history.
+
+By default, searches both command strings and output. Use --commands-only or --output-only to restrict search scope.`,
Args: cobra.ExactArgs(1),
RunE: runGrep,
}
@@ -40,20 +42,30 @@ func init() {
grepCmd.Flags().IntVarP(&grepLimit, "limit", "l", 0, "Maximum number of tasks to search (0 = no limit)")
grepCmd.Flags().BoolVarP(&grepRegex, "regexp", "E", false, "Treat pattern as regular expression")
grepCmd.Flags().BoolVarP(&grepInvert, "invert-match", "v", false, "Show tasks that don't match")
- grepCmd.Flags().IntVarP(&grepWorkers, "workers", "w", 10, "Number of parallel workers for searching")
+ grepCmd.Flags().BoolVar(&grepSearchCommands, "commands-only", false, "Search only in command execution strings (command + params)")
+ grepCmd.Flags().BoolVar(&grepSearchOutput, "output-only", false, "Search only in task response outputs")
}
-type taskResult struct {
- task mythic.Task
- match bool
- err error
-}
func runGrep(cmd *cobra.Command, args []string) error {
if err := validateConfig(); err != nil {
return err
}
+ // Validate search flags
+ if grepSearchCommands && grepSearchOutput {
+ return fmt.Errorf("cannot use both --commands-only and --output-only flags together")
+ }
+
+ // Set defaults: if neither flag is specified, search both
+ searchCommands := true
+ searchOutput := true
+ if grepSearchCommands {
+ searchOutput = false
+ } else if grepSearchOutput {
+ searchCommands = false
+ }
+
pattern := args[0]
client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
@@ -64,30 +76,30 @@ func runGrep(cmd *cobra.Command, args []string) error {
var tasks []mythic.Task
var err error
+ // Determine search scope description
+ var searchScope string
+ if searchCommands && searchOutput {
+ searchScope = "commands and outputs"
+ } else if searchCommands {
+ searchScope = "command strings"
+ } else {
+ searchScope = "outputs"
+ }
+
if grepCallbackID > 0 {
- // Search tasks for specific callback
- tasks, err = client.GetCallbackTasks(listCtx, grepCallbackID)
+ // Search tasks for specific callback - use efficient query that includes responses
+ tasks, err = client.GetTasksWithResponses(listCtx, grepCallbackID, grepLimit)
if err != nil {
return fmt.Errorf("failed to get tasks for callback %d: %w", grepCallbackID, err)
}
- fmt.Printf("Searching tasks from callback %d for pattern: %s\n", grepCallbackID, pattern)
+ fmt.Printf("Searching %s from callback %d for pattern: %s\n", searchScope, grepCallbackID, pattern)
} else {
- // Get all tasks from all callbacks
- callbacks, err := client.GetActiveCallbacks(listCtx)
+ // Get all tasks from all callbacks with responses in a single query
+ tasks, err = client.GetAllTasksWithResponses(listCtx, grepLimit)
if err != nil {
- return fmt.Errorf("failed to get callbacks: %w", err)
- }
-
- fmt.Printf("Searching all tasks for pattern: %s\n", pattern)
-
- for _, callback := range callbacks {
- callbackTasks, err := client.GetCallbackTasks(listCtx, callback.ID)
- if err != nil {
- fmt.Printf("Warning: Failed to get tasks for callback %d: %v\n", callback.DisplayID, err)
- continue
- }
- tasks = append(tasks, callbackTasks...)
+ return fmt.Errorf("failed to get all tasks with responses: %w", err)
}
+ fmt.Printf("Searching %s in all tasks for pattern: %s\n", searchScope, pattern)
}
if len(tasks) == 0 {
@@ -95,13 +107,7 @@ func runGrep(cmd *cobra.Command, args []string) error {
return nil
}
- // Apply limit if specified
- if grepLimit > 0 && len(tasks) > grepLimit {
- // Take the most recent tasks
- tasks = tasks[len(tasks)-grepLimit:]
- }
-
- fmt.Printf("Searching through %d tasks with %d workers...\n\n", len(tasks), grepWorkers)
+ fmt.Printf("Searching through %d tasks...\n\n", len(tasks))
// Prepare pattern for matching
var matcher func(string) bool
@@ -129,38 +135,38 @@ func runGrep(cmd *cobra.Command, args []string) error {
}
}
- // Use worker pool to process tasks in parallel
- results := processTasksWithWorkerPool(client, tasks, matcher, grepWorkers)
-
+ // Search through tasks in memory (much faster since we have all data)
var matchingTasks []mythic.Task
processedTasks := 0
- errorCount := 0
- for result := range results {
+ for _, task := range tasks {
processedTasks++
- if result.err != nil {
- errorCount++
- if errorCount <= 5 { // Only show first 5 errors
- fmt.Printf("Warning: Failed to get response for task %d: %v\n", result.task.DisplayID, result.err)
+ var matches bool
+
+ // Check command string if enabled
+ if searchCommands {
+ commandString := task.Command
+ if task.Params != "" {
+ commandString += " " + task.Params
}
- continue
+ matches = matcher(commandString)
+ }
+
+ // Check response output if enabled and not already matched
+ if !matches && searchOutput && task.Response != "" {
+ matches = matcher(task.Response)
}
// Apply invert logic
- matches := result.match
if grepInvert {
matches = !matches
}
if matches {
- matchingTasks = append(matchingTasks, result.task)
+ matchingTasks = append(matchingTasks, task)
}
}
- if errorCount > 5 {
- fmt.Printf("Warning: Suppressed %d additional errors\n", errorCount-5)
- }
-
if len(matchingTasks) == 0 {
fmt.Printf("No matches found in %d tasks searched\n", processedTasks)
return nil
@@ -169,67 +175,10 @@ func runGrep(cmd *cobra.Command, args []string) error {
// Show results in table format
showMatchingTasksTable(matchingTasks)
- fmt.Printf("\nFound %d matches in %d tasks searched", len(matchingTasks), processedTasks)
- if errorCount > 0 {
- fmt.Printf(" (%d errors)", errorCount)
- }
- fmt.Println()
+ fmt.Printf("\nFound %d matches in %d tasks searched\n", len(matchingTasks), processedTasks)
return nil
}
-func processTasksWithWorkerPool(client *mythic.Client, tasks []mythic.Task, matcher func(string) bool, numWorkers int) <-chan taskResult {
- taskChan := make(chan mythic.Task, len(tasks))
- resultChan := make(chan taskResult, len(tasks))
-
- // Start workers
- var wg sync.WaitGroup
- for i := 0; i < numWorkers; i++ {
- wg.Add(1)
- go worker(&wg, client, taskChan, resultChan, matcher)
- }
-
- // Send tasks to workers
- go func() {
- for _, task := range tasks {
- taskChan <- task
- }
- close(taskChan)
- }()
-
- // Close results channel when all workers are done
- go func() {
- wg.Wait()
- close(resultChan)
- }()
-
- return resultChan
-}
-
-func worker(wg *sync.WaitGroup, client *mythic.Client, taskChan <-chan mythic.Task, resultChan chan<- taskResult, matcher func(string) bool) {
- defer wg.Done()
-
- for task := range taskChan {
- // Use per-request timeout to avoid global timeouts
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-
- fullTask, err := client.GetTaskResponse(ctx, task.ID)
- cancel()
-
- if err != nil {
- resultChan <- taskResult{task: task, match: false, err: err}
- continue
- }
-
- if fullTask.Response == "" {
- resultChan <- taskResult{task: task, match: false, err: nil}
- continue
- }
-
- // Check if response matches pattern
- matches := matcher(fullTask.Response)
- resultChan <- taskResult{task: *fullTask, match: matches, err: nil}
- }
-}
func showMatchingTasksTable(tasks []mythic.Task) {
// Create a tab writer for formatted output
cmd/grep_test.go
@@ -0,0 +1,144 @@
+package cmd
+
+import (
+ "regexp"
+ "strings"
+ "testing"
+)
+
+func TestGrepMatcher(t *testing.T) {
+ tests := []struct {
+ name string
+ pattern string
+ text string
+ caseInsensitive bool
+ regex bool
+ expected bool
+ }{
+ {
+ name: "simple string match",
+ pattern: "hello",
+ text: "hello world",
+ expected: true,
+ },
+ {
+ name: "simple string no match",
+ pattern: "hello",
+ text: "goodbye world",
+ expected: false,
+ },
+ {
+ name: "case insensitive match",
+ pattern: "HELLO",
+ text: "hello world",
+ caseInsensitive: true,
+ expected: true,
+ },
+ {
+ name: "case sensitive no match",
+ pattern: "HELLO",
+ text: "hello world",
+ caseInsensitive: false,
+ expected: false,
+ },
+ {
+ name: "regex match",
+ pattern: "hel+o",
+ text: "hello world",
+ regex: true,
+ expected: true,
+ },
+ {
+ name: "regex no match",
+ pattern: "hel{2,}o",
+ text: "helo world",
+ regex: true,
+ expected: false,
+ },
+ {
+ name: "regex case insensitive",
+ pattern: "HEL+O",
+ text: "hello world",
+ regex: true,
+ caseInsensitive: true,
+ expected: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Simulate the matcher creation logic from runGrep
+ var matcher func(string) bool
+ if tt.regex {
+ regexPattern := tt.pattern
+ if tt.caseInsensitive {
+ regexPattern = "(?i)" + tt.pattern
+ }
+ re, err := regexp.Compile(regexPattern)
+ if err != nil {
+ t.Fatalf("Failed to compile regex: %v", err)
+ }
+ matcher = re.MatchString
+ } else {
+ searchPattern := tt.pattern
+ if tt.caseInsensitive {
+ searchPattern = strings.ToLower(tt.pattern)
+ }
+ matcher = func(text string) bool {
+ searchText := text
+ if tt.caseInsensitive {
+ searchText = strings.ToLower(text)
+ }
+ return strings.Contains(searchText, searchPattern)
+ }
+ }
+
+ result := matcher(tt.text)
+ if result != tt.expected {
+ t.Errorf("Expected %v, got %v for pattern %q against text %q", tt.expected, result, tt.pattern, tt.text)
+ }
+ })
+ }
+}
+
+func TestGrepCommandStringConstruction(t *testing.T) {
+ tests := []struct {
+ name string
+ command string
+ params string
+ expected string
+ }{
+ {
+ name: "command with params",
+ command: "ls",
+ params: "-la /tmp",
+ expected: "ls -la /tmp",
+ },
+ {
+ name: "command without params",
+ command: "whoami",
+ params: "",
+ expected: "whoami",
+ },
+ {
+ name: "command with complex params",
+ command: "powershell",
+ params: "-ExecutionPolicy Bypass -Command \"Get-Process\"",
+ expected: "powershell -ExecutionPolicy Bypass -Command \"Get-Process\"",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Simulate the command string construction from worker function
+ commandString := tt.command
+ if tt.params != "" {
+ commandString += " " + tt.params
+ }
+
+ if commandString != tt.expected {
+ t.Errorf("Expected %q, got %q", tt.expected, commandString)
+ }
+ })
+ }
+}
\ No newline at end of file
cmd/interact.go
@@ -4,8 +4,8 @@ import (
"bufio"
"context"
"fmt"
- "go-mythic/pkg/mythic"
- "go-mythic/pkg/mythic/api"
+ "mysh/pkg/mythic"
+ "mysh/pkg/mythic/api"
"os"
"strconv"
"strings"
cmd/payload.go
@@ -3,7 +3,7 @@ package cmd
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"os"
"strconv"
"strings"
@@ -129,8 +129,8 @@ func showPayloadList(ctx context.Context, client *mythic.Client) error {
w.Flush()
fmt.Println("\n* = Auto-generated payload")
- fmt.Println("\nTo view payload details: go-mythic payload <id>")
- fmt.Println("To download a payload file: go-mythic files download <agent_file_id>")
+ fmt.Println("\nTo view payload details: mysh payload <id>")
+ fmt.Println("To download a payload file: mysh files download <agent_file_id>")
return nil
}
@@ -212,7 +212,7 @@ func showPayloadDetails(ctx context.Context, client *mythic.Client, payloadID in
// Download instructions
if targetPayload.AgentFileID != "" {
fmt.Printf("\nTo download this payload:\n")
- fmt.Printf(" go-mythic files download %s\n", targetPayload.AgentFileID)
+ fmt.Printf(" mysh files download %s\n", targetPayload.AgentFileID)
}
return nil
cmd/root.go
@@ -15,9 +15,9 @@ var (
)
var rootCmd = &cobra.Command{
- Use: "go-mythic",
+ Use: "mysh",
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.
+ Long: `mysh (pronounced mɪʃ) 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.`,
}
cmd/task.go
@@ -3,8 +3,8 @@ package cmd
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
- "go-mythic/pkg/mythic/api"
+ "mysh/pkg/mythic"
+ "mysh/pkg/mythic/api"
"strconv"
"strings"
cmd/task_list.go
@@ -3,7 +3,7 @@ package cmd
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"os"
"strconv"
"strings"
cmd/task_view.go
@@ -3,7 +3,7 @@ package cmd
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"strconv"
"strings"
"time"
cmd/upload.go
@@ -5,7 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"os"
"path/filepath"
"strings"
pkg/mythic/api/callbacks.go
@@ -3,7 +3,7 @@ package api
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
)
// FindActiveCallback looks up an active callback by its display ID
pkg/mythic/api/callbacks_test.go
@@ -2,7 +2,7 @@ package api
import (
"context"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"testing"
)
@@ -27,6 +27,14 @@ func (m *MockClient) GetTaskResponse(ctx context.Context, taskID int) (*mythic.T
return nil, nil // Not implemented for this test
}
+func (m *MockClient) GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]mythic.Task, error) {
+ return nil, nil // Not implemented for this test
+}
+
+func (m *MockClient) GetAllTasksWithResponses(ctx context.Context, limit int) ([]mythic.Task, error) {
+ return nil, nil // Not implemented for this test
+}
+
func TestFindActiveCallback(t *testing.T) {
tests := []struct {
name string
pkg/mythic/api/interfaces.go
@@ -2,7 +2,7 @@ package api
import (
"context"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
)
// MythicClient defines the interface for Mythic operations needed by the api package
@@ -10,6 +10,8 @@ type MythicClient interface {
GetActiveCallbacks(ctx context.Context) ([]mythic.Callback, error)
CreateTask(ctx context.Context, callbackID int, command, params string) (*mythic.Task, error)
GetTaskResponse(ctx context.Context, taskID int) (*mythic.Task, error)
+ GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]mythic.Task, error)
+ GetAllTasksWithResponses(ctx context.Context, limit int) ([]mythic.Task, error)
}
// Ensure that the real Client implements the interface
pkg/mythic/api/tasks.go
@@ -3,7 +3,7 @@ package api
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"time"
)
pkg/mythic/api/tasks_test.go
@@ -3,7 +3,7 @@ package api
import (
"context"
"fmt"
- "go-mythic/pkg/mythic"
+ "mysh/pkg/mythic"
"testing"
"time"
)
@@ -72,6 +72,14 @@ func (m *MockTaskClient) SetTaskCompleted(taskID int, response string) {
}
}
+func (m *MockTaskClient) GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]mythic.Task, error) {
+ return nil, nil // Not implemented for this test
+}
+
+func (m *MockTaskClient) GetAllTasksWithResponses(ctx context.Context, limit int) ([]mythic.Task, error) {
+ return nil, nil // Not implemented for this test
+}
+
func TestDefaultTaskPollConfig(t *testing.T) {
config := DefaultTaskPollConfig()
pkg/mythic/client.go
@@ -533,6 +533,141 @@ func (c *Client) DownloadFile(ctx context.Context, fileUUID string) ([]byte, err
return data, nil
}
+func (c *Client) GetTasksWithResponses(ctx context.Context, callbackID int, limit int) ([]Task, error) {
+ req := graphql.NewRequest(GetTasksWithResponses)
+
+ if callbackID > 0 {
+ req.Var("callback_id", callbackID)
+ }
+ if limit > 0 {
+ req.Var("limit", limit)
+ }
+ 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"`
+ Timestamp string `json:"timestamp"`
+ Callback struct {
+ ID int `json:"id"`
+ DisplayID int `json:"display_id"`
+ Host string `json:"host"`
+ User string `json:"user"`
+ } `json:"callback"`
+ Responses []struct {
+ ResponseText string `json:"response_text"`
+ Timestamp string `json:"timestamp"`
+ } `json:"responses"`
+ } `json:"task"`
+ }
+
+ if err := c.client.Run(ctx, req, &resp); err != nil {
+ return nil, fmt.Errorf("failed to get tasks with responses: %w", err)
+ }
+
+ var tasks []Task
+ for _, t := range resp.Task {
+ // Concatenate all response chunks in chronological order
+ var responseBuilder strings.Builder
+ for _, resp := range t.Responses {
+ decodedText := decodeResponseText(resp.ResponseText)
+ responseBuilder.WriteString(decodedText)
+ }
+ response := responseBuilder.String()
+
+ status := t.Status
+ if t.Completed {
+ status = "completed"
+ }
+
+ tasks = append(tasks, Task{
+ ID: t.ID,
+ DisplayID: t.DisplayID,
+ Command: t.CommandName,
+ Params: t.DisplayParams,
+ Status: status,
+ Response: response,
+ CallbackID: t.Callback.ID,
+ Timestamp: t.Timestamp,
+ Completed: t.Completed,
+ })
+ }
+
+ return tasks, nil
+}
+
+func (c *Client) GetAllTasksWithResponses(ctx context.Context, limit int) ([]Task, error) {
+ req := graphql.NewRequest(GetAllTasksWithResponsesFromAllCallbacks)
+
+ if limit > 0 {
+ req.Var("limit", limit)
+ }
+ 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"`
+ Timestamp string `json:"timestamp"`
+ Callback struct {
+ ID int `json:"id"`
+ DisplayID int `json:"display_id"`
+ Host string `json:"host"`
+ User string `json:"user"`
+ } `json:"callback"`
+ Responses []struct {
+ ResponseText string `json:"response_text"`
+ Timestamp string `json:"timestamp"`
+ } `json:"responses"`
+ } `json:"task"`
+ }
+
+ if err := c.client.Run(ctx, req, &resp); err != nil {
+ return nil, fmt.Errorf("failed to get all tasks with responses: %w", err)
+ }
+
+ var tasks []Task
+ for _, t := range resp.Task {
+ // Concatenate all response chunks in chronological order
+ var responseBuilder strings.Builder
+ for _, resp := range t.Responses {
+ decodedText := decodeResponseText(resp.ResponseText)
+ responseBuilder.WriteString(decodedText)
+ }
+ response := responseBuilder.String()
+
+ status := t.Status
+ if t.Completed {
+ status = "completed"
+ }
+
+ tasks = append(tasks, Task{
+ ID: t.ID,
+ DisplayID: t.DisplayID,
+ Command: t.CommandName,
+ Params: t.DisplayParams,
+ Status: status,
+ Response: response,
+ CallbackID: t.Callback.ID,
+ Timestamp: t.Timestamp,
+ Completed: t.Completed,
+ })
+ }
+
+ return tasks, nil
+}
+
func (c *Client) GetPayloads(ctx context.Context) ([]Payload, error) {
req := graphql.NewRequest(GetPayloads)
pkg/mythic/client_test.go
@@ -1,6 +1,7 @@
package mythic
import (
+ "context"
"testing"
)
@@ -128,4 +129,31 @@ func TestNewClientWithSocksProxy(t *testing.T) {
if client.token != token {
t.Errorf("Expected token %q, got %q", token, client.token)
}
+}
+
+func TestClientMethodsExist(t *testing.T) {
+ apiURL := "https://example.com/graphql"
+ token := "test-token"
+ client := NewClient(apiURL, token, true, "")
+
+ // Test that the new methods exist and can be called (they'll fail due to no server, but should not panic)
+ // This ensures the GraphQL queries compile and the methods are properly defined
+ if client == nil {
+ t.Fatal("NewClient returned nil")
+ }
+
+ // We can't make actual calls without a server, but we can verify the methods exist
+ // by checking they don't panic when called with a canceled context
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel() // Cancel immediately to avoid network calls
+
+ _, err := client.GetTasksWithResponses(ctx, 1, 10)
+ if err == nil {
+ t.Error("Expected error due to canceled context")
+ }
+
+ _, err = client.GetAllTasksWithResponses(ctx, 10)
+ if err == nil {
+ t.Error("Expected error due to canceled context")
+ }
}
\ No newline at end of file
pkg/mythic/queries.go
@@ -524,4 +524,52 @@ query GetAllFiles {
}
}
}
+}`
+
+const GetTasksWithResponses = `
+query GetTasksWithResponses($callback_id: Int, $limit: Int) {
+ task(where: {callback_id: {_eq: $callback_id}}, order_by: {id: desc}, limit: $limit) {
+ id
+ display_id
+ command_name
+ original_params
+ display_params
+ status
+ completed
+ timestamp
+ callback {
+ id
+ display_id
+ host
+ user
+ }
+ responses(order_by: {id: asc}) {
+ response_text
+ timestamp
+ }
+ }
+}`
+
+const GetAllTasksWithResponsesFromAllCallbacks = `
+query GetAllTasksWithResponsesFromAllCallbacks($limit: Int) {
+ task(order_by: {id: desc}, limit: $limit) {
+ id
+ display_id
+ command_name
+ original_params
+ display_params
+ status
+ completed
+ timestamp
+ callback {
+ id
+ display_id
+ host
+ user
+ }
+ responses(order_by: {id: asc}) {
+ response_text
+ timestamp
+ }
+ }
}`
\ No newline at end of file
go.mod
@@ -1,4 +1,4 @@
-module go-mythic
+module mysh
go 1.24.2
investigate_forge.py
@@ -1,68 +0,0 @@
-#!/usr/bin/env python3
-
-import asyncio
-import sys
-import os
-
-# Add the contrib directory to the path
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'contrib', 'Mythic_Scripting'))
-
-from mythic import mythic
-
-async def investigate_forge_tasks():
- try:
- # Connect to Mythic using environment variables
- server_ip = "mythic.adversarytactics.local"
- apitoken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTgwNTc3ODUsImlhdCI6MTc1ODA0MzM4NSwidXNlcl9pZCI6NCwiYXV0aCI6ImFwaSIsImV2ZW50c3RlcGluc3RhbmNlX2lkIjowLCJhcGl0b2tlbnNfaWQiOjI2LCJvcGVyYXRpb25faWQiOjB9.dNx93CuQLMMhvbJEgVs1Rg_IQYDfhtTRjEiupXlEpYc"
-
- mythic_conn = await mythic.login(
- server_ip=server_ip,
- apitoken=apitoken,
- server_port=7443,
- ssl=True
- )
-
- print("Connected to Mythic successfully")
-
- # Get all tasks
- tasks = await mythic_conn.get_all_tasks()
-
- # Filter for forge-related tasks
- forge_tasks = []
- for task in tasks:
- if task.command and 'forge' in task.command.lower():
- forge_tasks.append(task)
-
- if not forge_tasks:
- print("No forge-related tasks found")
- return
-
- print(f"Found {len(forge_tasks)} forge-related tasks")
- print("=" * 60)
-
- for task in forge_tasks:
- print(f"Task ID: {task.display_id}")
- print(f"Command: {task.command}")
- print(f"Status: {task.status}")
- print(f"Completed: {task.completed}")
- print(f"Callback: {task.callback.display_id if task.callback else 'Unknown'}")
- print(f"Parameters: {task.original_params if task.original_params else 'None'}")
-
- # Get task responses
- responses = await mythic_conn.get_all_responses_for_task(task.id)
- if responses:
- print("Responses:")
- for resp in responses:
- print(f" - {resp.response[:200]}{'...' if len(resp.response) > 200 else ''}")
- else:
- print("No responses found")
-
- print("-" * 40)
-
- except Exception as e:
- print(f"Error: {e}")
- import traceback
- traceback.print_exc()
-
-if __name__ == "__main__":
- asyncio.run(investigate_forge_tasks())
\ No newline at end of file
main.go
@@ -1,6 +1,6 @@
package main
-import "go-mythic/cmd"
+import "mysh/cmd"
func main() {
cmd.Execute()
Makefile
@@ -1,5 +1,5 @@
# Project variables
-BINARY_NAME=go-mythic
+BINARY_NAME=mysh
PACKAGE_PATH=.
BIN_DIR=./bin
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
README.md
@@ -1,9 +1,9 @@
-# `go-mythic`
+# `mysh` (pronounced mɪʃ)
## 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.
+This project is focused on creating a golang CLI application called `mysh` (pronounced mɪʃ) to accomplish the same tasks.
## resources
TODO.md
@@ -1,4 +1,5 @@
-- grep should also look at the command execution strings too, not just output
+- ✅ grep should also look at the command execution strings too, not just output
+- ✅ Renamed project from go-mythic to mysh (pronounced mɪʃ)
- forge equivalency with execute_assembly and inline_assembly
- task-list --all (same as --limit 0)
USAGE.md
@@ -1,4 +1,4 @@
-# go-mythic Usage Examples
+# mysh (pronounced mɪʃ) Usage Examples
## Building
@@ -22,7 +22,7 @@ make help
### Manual Build
```bash
-go build -o bin/go-mythic .
+go build -o bin/mysh .
```
## Project Structure
@@ -42,27 +42,27 @@ To sync with upstream updates:
### List Active Callbacks
```bash
-./go-mythic callbacks
+./mysh callbacks
```
### Execute Commands
```bash
# Get help for all commands
-./go-mythic exec 1 help
+./mysh exec 1 help
# Get detailed help for a specific command
-./go-mythic exec 1 help ps
+./mysh exec 1 help ps
# List processes
-./go-mythic exec 1 ps
+./mysh exec 1 ps
# Set checkin frequency to 5 seconds
-./go-mythic exec 1 sleep 5
+./mysh exec 1 sleep 5
```
### Interactive Session
```bash
-./go-mythic interact 1
+./mysh interact 1
```
## Using SOCKS5 Proxy
@@ -71,13 +71,13 @@ The CLI supports SOCKS5 proxy routing for all network traffic, replacing the nee
```bash
# Use SOCKS5 proxy (equivalent to proxychains)
-./go-mythic --socks 127.0.0.1:9050 callbacks
+./mysh --socks 127.0.0.1:9050 callbacks
# Execute command through proxy
-./go-mythic --socks 127.0.0.1:9050 exec 1 ps
+./mysh --socks 127.0.0.1:9050 exec 1 ps
# Interactive session through proxy
-./go-mythic --socks 127.0.0.1:9050 interact 1
+./mysh --socks 127.0.0.1:9050 interact 1
```
## Configuration
@@ -89,12 +89,12 @@ The CLI supports SOCKS5 proxy routing for all network traffic, replacing the nee
# Set via environment variables (recommended)
export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
export MYTHIC_API_TOKEN="your-jwt-token-here"
-./go-mythic callbacks
+./mysh callbacks
# For testing with mythic.sh
export MYTHIC_API_URL="https://mythic.adversarytactics.local:7443/graphql/"
export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
-./go-mythic callbacks
+./mysh callbacks
```
### Individual Configuration Options
@@ -105,7 +105,7 @@ export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
# Set via command line flag
-./go-mythic --url "https://your-mythic-server:7443/graphql/" callbacks
+./mysh --url "https://your-mythic-server:7443/graphql/" callbacks
```
#### Authentication Token
@@ -114,7 +114,7 @@ export MYTHIC_API_URL="https://your-mythic-server:7443/graphql/"
export MYTHIC_API_TOKEN="your-jwt-token-here"
# Set via command line flag
-./go-mythic --token "your-jwt-token-here" callbacks
+./mysh --token "your-jwt-token-here" callbacks
# Get token from mythic.sh for testing
export MYTHIC_API_TOKEN="$(grep TOKEN mythic.sh | cut -d'=' -f2)"
@@ -126,30 +126,30 @@ These commands accomplish the goals from labs/01.md:
1. **View Active Callbacks** (equivalent to clicking Active Callbacks in UI):
```bash
- ./go-mythic callbacks
+ ./mysh callbacks
```
2. **Interact with Agent** (equivalent to right-click → Interact):
```bash
- ./go-mythic interact <callback_id>
+ ./mysh interact <callback_id>
```
3. **Get Help** (equivalent to typing "help" in UI console):
```bash
- ./go-mythic exec <callback_id> help
+ ./mysh exec <callback_id> help
```
4. **Get Command Details** (equivalent to typing "help ps"):
```bash
- ./go-mythic exec <callback_id> help ps
+ ./mysh exec <callback_id> help ps
```
5. **List Processes** (equivalent to typing "ps"):
```bash
- ./go-mythic exec <callback_id> ps
+ ./mysh exec <callback_id> ps
```
6. **Set Checkin Frequency** (equivalent to typing "sleep 5"):
```bash
- ./go-mythic exec <callback_id> sleep 5
+ ./mysh exec <callback_id> sleep 5
```