main
Raw Download raw file
  1package cmd
  2
  3import (
  4	"context"
  5	"fmt"
  6	"mysh/pkg/mythic"
  7	"os"
  8	"strconv"
  9	"strings"
 10	"time"
 11
 12	"github.com/jedib0t/go-pretty/v6/table"
 13	"github.com/spf13/cobra"
 14)
 15
 16var payloadCmd = &cobra.Command{
 17	Use:     "payload [payload_id]",
 18	Aliases: []string{"payloads"},
 19	Short:   "List payloads or show payload details",
 20	Long:    "Display all payloads created in Mythic, or show detailed information for a specific payload by ID.",
 21	Args:    cobra.RangeArgs(0, 1),
 22	RunE:    runPayload,
 23}
 24
 25func init() {
 26	rootCmd.AddCommand(payloadCmd)
 27}
 28
 29func runPayload(cmd *cobra.Command, args []string) error {
 30	if err := validateConfig(); err != nil {
 31		return err
 32	}
 33
 34	client := mythic.NewClient(mythicURL, token, insecure, socksProxy)
 35	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 36	defer cancel()
 37
 38	// If payload ID is provided, show detailed view
 39	if len(args) > 0 {
 40		payloadID, err := strconv.Atoi(args[0])
 41		if err != nil {
 42			return fmt.Errorf("invalid payload ID: %s", args[0])
 43		}
 44		return showPayloadDetails(ctx, client, payloadID)
 45	}
 46
 47	// Otherwise, show list view
 48	return showPayloadList(ctx, client)
 49}
 50
 51func showPayloadList(ctx context.Context, client *mythic.Client) error {
 52	payloads, err := client.GetPayloads(ctx)
 53	if err != nil {
 54		return fmt.Errorf("failed to get payloads: %w", err)
 55	}
 56
 57	if len(payloads) == 0 {
 58		fmt.Println("No payloads found")
 59		return nil
 60	}
 61
 62	fmt.Printf("Payloads (%d total):\n\n", len(payloads))
 63
 64	// Create a table for formatted output
 65	t := table.NewWriter()
 66	t.SetOutputMirror(os.Stdout)
 67
 68	// Configure table styling - header borders only
 69	t.SetStyle(table.Style{
 70		Name: "HeaderOnlyBorders",
 71		Box:  table.StyleBoxDefault,
 72		Options: table.Options{
 73			DrawBorder:      false, // No outer border
 74			SeparateColumns: false, // No column separators
 75			SeparateFooter:  false, // No footer separator
 76			SeparateHeader:  true,  // Keep header separator
 77			SeparateRows:    false, // No row separators
 78		},
 79	})
 80
 81	// Define columns with priorities (1=highest, higher numbers can be dropped)
 82	columns := []ColumnInfo{
 83		{Header: "ID", Number: 1, MinWidth: 3, MaxWidth: 6, Priority: 1},            // Always show
 84		{Header: "TYPE", Number: 2, MinWidth: 6, MaxWidth: 10, Priority: 2},         // High priority
 85		{Header: "DESCRIPTION", Number: 3, MinWidth: 10, MaxWidth: 20, Priority: 5}, // Can drop
 86		{Header: "FILENAME", Number: 4, MinWidth: 8, MaxWidth: 16, Priority: 3},     // Important
 87		{Header: "BUILD STATUS", Number: 5, MinWidth: 6, MaxWidth: 10, Priority: 4}, // Important
 88		{Header: "C2 PROFILES", Number: 6, MinWidth: 8, MaxWidth: 12, Priority: 6},  // Can drop
 89		{Header: "OPERATOR", Number: 7, MinWidth: 6, MaxWidth: 10, Priority: 7},     // Can drop
 90		{Header: "CREATED", Number: 8, MinWidth: 10, MaxWidth: 14, Priority: 8},     // First to drop
 91	}
 92
 93	// Configure table for current terminal width
 94	dt := configureTableForTerminal(t, columns)
 95
 96	for _, payload := range payloads {
 97		// Use full values - table library will handle truncation
 98		description := payload.Description
 99		if description == "" {
100			description = "-"
101		}
102
103		filename := payload.Filename
104		if filename == "" {
105			filename = "-"
106		}
107
108		// Format build status
109		buildStatus := payload.BuildPhase
110		if buildStatus == "" {
111			buildStatus = "unknown"
112		}
113
114		// Format C2 profiles
115		c2ProfilesStr := strings.Join(payload.C2Profiles, ",")
116		if c2ProfilesStr == "" {
117			c2ProfilesStr = "-"
118		}
119
120		// Format creation time
121		created := payload.CreationTime
122		if created != "" {
123			if t, err := time.Parse(time.RFC3339, created); err == nil {
124				created = t.Format("2006-01-02 15:04")
125			}
126		}
127
128		// Show auto-generated indicator
129		typeStr := payload.PayloadTypeName
130		if payload.AutoGenerated {
131			typeStr += "*"
132		}
133
134		// Prepare all column data
135		allData := []interface{}{
136			payload.ID,
137			typeStr,
138			description,
139			filename,
140			buildStatus,
141			c2ProfilesStr,
142			payload.OperatorUsername,
143			created,
144		}
145
146		// Add row with only visible columns
147		dt.AppendRowForColumns(allData)
148	}
149
150	t.Render()
151
152	fmt.Println("\n* = Auto-generated payload")
153	fmt.Println("\nTo view payload details: mysh payload <id>")
154	fmt.Println("To download a payload file: mysh files download <agent_file_id>")
155
156	return nil
157}
158
159func showPayloadDetails(ctx context.Context, client *mythic.Client, payloadID int) error {
160	payloads, err := client.GetPayloads(ctx)
161	if err != nil {
162		return fmt.Errorf("failed to get payloads: %w", err)
163	}
164
165	var targetPayload *mythic.Payload
166	for _, payload := range payloads {
167		if payload.ID == payloadID {
168			targetPayload = &payload
169			break
170		}
171	}
172
173	if targetPayload == nil {
174		return fmt.Errorf("payload with ID %d not found", payloadID)
175	}
176
177	fmt.Printf("Payload Details (ID: %d)\n", targetPayload.ID)
178	fmt.Println(strings.Repeat("=", 50))
179
180	// Basic information
181	fmt.Printf("UUID: %s\n", targetPayload.UUID)
182	fmt.Printf("Type: %s", targetPayload.PayloadTypeName)
183	if targetPayload.AutoGenerated {
184		fmt.Printf(" (auto-generated)")
185	}
186	fmt.Println()
187
188	fmt.Printf("Description: %s\n", getValueOrDefault(targetPayload.Description, "No description"))
189	fmt.Printf("Filename: %s\n", getValueOrDefault(targetPayload.Filename, "No filename"))
190
191	// Build information
192	fmt.Println("\nBuild Information:")
193	fmt.Printf("  Status: %s\n", getValueOrDefault(targetPayload.BuildPhase, "unknown"))
194
195	if targetPayload.BuildMessage != "" {
196		fmt.Printf("  Message: %s\n", targetPayload.BuildMessage)
197	}
198
199	if targetPayload.BuildStderr != "" {
200		fmt.Printf("  Errors: %s\n", targetPayload.BuildStderr)
201	}
202
203	// C2 Profiles
204	fmt.Println("\nC2 Profiles:")
205	if len(targetPayload.C2Profiles) > 0 {
206		for _, profile := range targetPayload.C2Profiles {
207			fmt.Printf("  - %s\n", profile)
208		}
209	} else {
210		fmt.Println("  No C2 profiles configured")
211	}
212
213	// File information
214	if targetPayload.AgentFileID != "" {
215		fmt.Println("\nFile Information:")
216		fmt.Printf("  Agent File ID: %s\n", targetPayload.AgentFileID)
217		fmt.Printf("  File ID: %d\n", targetPayload.FileID)
218	}
219
220	// Metadata
221	fmt.Println("\nMetadata:")
222	fmt.Printf("  Operator: %s (ID: %d)\n", targetPayload.OperatorUsername, targetPayload.OperatorID)
223	fmt.Printf("  Callback Alert: %t\n", targetPayload.CallbackAlert)
224
225	if targetPayload.CreationTime != "" {
226		if t, err := time.Parse(time.RFC3339, targetPayload.CreationTime); err == nil {
227			fmt.Printf("  Created: %s\n", t.Format("2006-01-02 15:04:05"))
228		} else {
229			fmt.Printf("  Created: %s\n", targetPayload.CreationTime)
230		}
231	}
232
233	// Download instructions
234	if targetPayload.AgentFileID != "" {
235		fmt.Printf("\nTo download this payload:\n")
236		fmt.Printf("  mysh files download %s\n", targetPayload.AgentFileID)
237	}
238
239	return nil
240}
241
242func getValueOrDefault(value, defaultValue string) string {
243	if value == "" {
244		return defaultValue
245	}
246	return value
247}