main
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}