Skip to content

Commit 40f4dbc

Browse files
committed
fix: misc
1 parent b048f07 commit 40f4dbc

20 files changed

Lines changed: 261 additions & 63 deletions

client/command/common/completer.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,25 @@ func SessionModuleCompleter(con *core.Console) carapace.Action {
360360
return carapace.ActionCallback(callback)
361361
}
362362

363+
func SessionBundleCompleter(con *core.Console) carapace.Action {
364+
callback := func(c carapace.Context) carapace.Action {
365+
results := make([]string, 0)
366+
sess := con.GetInteractive()
367+
if sess == nil || sess.Data == nil || sess.Data.BundleMap == nil {
368+
return carapace.ActionValuesDescribed(results...).Tag("session bundles")
369+
}
370+
seen := make(map[string]bool)
371+
for _, bundle := range sess.Data.BundleMap {
372+
if bundle != "" && !seen[bundle] {
373+
seen[bundle] = true
374+
results = append(results, bundle, "")
375+
}
376+
}
377+
return carapace.ActionValuesDescribed(results...).Tag("session bundles")
378+
}
379+
return carapace.ActionCallback(callback)
380+
}
381+
363382
func ModulesCompleter() carapace.Action {
364383
callback := func(c carapace.Context) carapace.Action {
365384
results := make([]string, 0)

client/command/common/output.go

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,47 @@ import (
1010
"github.com/spf13/pflag"
1111
)
1212

13-
// BindOutputFlags adds --file and --output flags to a command
13+
// BindOutputFlags adds -f/--file flag to a command for saving output to a file.
1414
func BindOutputFlags(cmd *cobra.Command) {
1515
Bind("output", false, cmd, func(f *pflag.FlagSet) {
16-
f.BoolP("file", "f", false, "save output to file")
17-
f.StringP("output", "o", "", "output file path")
16+
f.StringP("file", "f", "", "save output to file path")
1817
})
1918
}
2019

21-
// HandleTaskOutput waits for task completion and optionally saves to file
20+
// HandleTaskOutput optionally saves task output to file based on -f flag,
21+
// then always displays output to console.
2222
func HandleTaskOutput(cmd *cobra.Command, con *core.Console, task *clientpb.Task) error {
23-
toFile, _ := cmd.Flags().GetBool("file")
24-
outputPath, _ := cmd.Flags().GetString("output")
23+
outputPath, _ := cmd.Flags().GetString("file")
2524

26-
if !toFile && outputPath == "" {
27-
// 正常流程:异步处理
25+
if outputPath == "" {
2826
con.GetInteractive().Console(task, string(*con.App.Shell().Line()))
2927
return nil
3028
}
3129

32-
// 需要保存文件:同步等待结果
33-
tasksContext, err := con.Rpc.GetAllTaskContent(con.GetInteractive().Context(), &clientpb.Task{
30+
if task == nil {
31+
return fmt.Errorf("task is nil")
32+
}
33+
34+
session := con.GetInteractive()
35+
if session == nil {
36+
return fmt.Errorf("no active session")
37+
}
38+
39+
taskReq := &clientpb.Task{
3440
SessionId: task.SessionId,
3541
TaskId: task.TaskId,
3642
Need: -1,
37-
})
38-
if err != nil {
39-
return fmt.Errorf("failed to get task content: %w", err)
4043
}
4144

42-
if outputPath == "" {
43-
outputPath = fmt.Sprintf("task_%d.txt", task.TaskId)
45+
// Task content may arrive asynchronously. Wait for completion first so the
46+
// server-side cache/disk state is ready before collecting all chunks.
47+
if _, err := con.Rpc.WaitTaskFinish(session.Context(), taskReq); err != nil {
48+
return fmt.Errorf("failed to wait task finish: %w", err)
49+
}
50+
51+
tasksContext, err := con.Rpc.GetAllTaskContent(session.Context(), taskReq)
52+
if err != nil {
53+
return fmt.Errorf("failed to get task content: %w", err)
4454
}
4555

4656
var rendered []byte
@@ -54,6 +64,9 @@ func HandleTaskOutput(cmd *cobra.Command, con *core.Console, task *clientpb.Task
5464
if err != nil {
5565
return fmt.Errorf("failed to render task output: %w", err)
5666
}
67+
if text == "" {
68+
continue
69+
}
5770
rendered = append(rendered, []byte(text+"\n")...)
5871
}
5972

@@ -63,7 +76,13 @@ func HandleTaskOutput(cmd *cobra.Command, con *core.Console, task *clientpb.Task
6376

6477
con.Log.Infof("Task output saved to: %s\n", outputPath)
6578

66-
// 同时也显示到控制台
67-
con.GetInteractive().Console(task, string(*con.App.Shell().Line()))
79+
// File output already waited for completion, so a user-supplied --wait would
80+
// otherwise trigger a second wait/render pass in PersistentPostRunE.
81+
if waitFlag := cmd.Flags().Lookup("wait"); waitFlag != nil {
82+
_ = cmd.Flags().Set("wait", "false")
83+
}
84+
85+
// Also display to console
86+
session.Console(task, string(*con.App.Shell().Line()))
6887
return nil
6988
}

client/command/exec/commands.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ run gogo.exe -- -i 127.0.0.1 -p http
3535
carapace.ActionValues().Usage("command to execute"),
3636
carapace.ActionValues().Usage("arguments to the command"),
3737
)
38+
common.BindOutputFlags(runCmd)
3839

3940
executeCmd := &cobra.Command{
4041
Use: consts.ModuleAliasExecute + " [cmdline]",
@@ -60,6 +61,7 @@ execute gogo.exe -- -i 127.0.0.1 -p http
6061
common.BindArgCompletions(executeCmd, nil,
6162
carapace.ActionValues().Usage("command to execute"),
6263
)
64+
common.BindOutputFlags(executeCmd)
6365

6466
shellCmd := &cobra.Command{
6567
Use: consts.ModuleAliasShell + " [cmdline]",
@@ -83,6 +85,7 @@ execute gogo.exe -- -i 127.0.0.1 -p http
8385
common.BindFlag(shellCmd, func(f *pflag.FlagSet) {
8486
f.BoolP("quiet", "q", false, "disable output")
8587
})
88+
common.BindOutputFlags(shellCmd)
8689

8790
execLocalCmd := &cobra.Command{
8891
Use: consts.ModuleExecuteLocal + " [local_exe]",
@@ -108,6 +111,7 @@ execute_local local_exe --ppid 1234 --block_dll --etw --argue "argue"
108111
f.StringP("process", "n", "", "custom process path")
109112
f.BoolP("output", "o", false, "disable output")
110113
})
114+
common.BindOutputFlags(execLocalCmd)
111115

112116
inlineLocalCmd := &cobra.Command{
113117
Use: consts.ModuleInlineLocal + " [local_exe]",
@@ -133,6 +137,7 @@ inline_local whoami
133137
f.StringP("process", "n", "", "custom process path")
134138
f.BoolP("output", "o", false, "disable output")
135139
})
140+
common.BindOutputFlags(inlineLocalCmd)
136141

137142
powershellCmd := &cobra.Command{
138143
Use: consts.ModuleAliasPowershell + " [cmdline]",
@@ -160,6 +165,7 @@ powershell dir
160165
common.BindFlag(powershellCmd, func(f *pflag.FlagSet) {
161166
f.BoolP("quiet", "q", false, "disable output")
162167
})
168+
common.BindOutputFlags(powershellCmd)
163169

164170
execAssemblyCmd := &cobra.Command{
165171
Use: consts.ModuleExecuteAssembly + " [file]",
@@ -185,6 +191,7 @@ execute_assembly potato.exe "whoami"
185191
carapace.ActionValues().Usage("arguments to pass to the assembly args"))
186192

187193
common.BindFlag(execAssemblyCmd, common.SacrificeFlagSet, common.CLRFlagSet)
194+
common.BindOutputFlags(execAssemblyCmd)
188195

189196
inlineAssemblyCmd := &cobra.Command{
190197
Use: consts.ModuleInlineAssembly + " [file]",
@@ -216,6 +223,7 @@ inline_assembly --amsi potato.exe -- cmd /c whoami
216223
carapace.ActionValues().Usage("arguments to pass to the assembly args"))
217224

218225
common.BindFlag(inlineAssemblyCmd, common.CLRFlagSet)
226+
common.BindOutputFlags(inlineAssemblyCmd)
219227

220228
execShellcodeCmd := &cobra.Command{
221229
Use: consts.ModuleExecuteShellcode + " [shellcode_file]",
@@ -242,6 +250,7 @@ execute_shellcode example.bin
242250
carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint"))
243251

244252
common.BindFlag(execShellcodeCmd, common.ExecuteFlagSet, common.SacrificeFlagSet)
253+
common.BindOutputFlags(execShellcodeCmd)
245254

246255
inlineShellcodeCmd := &cobra.Command{
247256
Use: consts.ModuleAliasInlineShellcode + " [shellcode_file]",
@@ -269,6 +278,7 @@ inline_shellcode example.bin
269278
common.BindArgCompletions(inlineShellcodeCmd, nil,
270279
carapace.ActionFiles().Usage("path the shellcode file"))
271280
common.BindFlag(inlineShellcodeCmd, common.ExecuteFlagSet)
281+
common.BindOutputFlags(inlineShellcodeCmd)
272282

273283
execDLLCmd := &cobra.Command{
274284
Use: consts.ModuleExecuteDll + " [dll]",
@@ -304,6 +314,7 @@ execute_dll example.dll -e entrypoint -- arg1 arg2
304314
f.StringP("entrypoint", "e", "", "custom entrypoint")
305315
f.StringP("binPath", "", "", "custom process path")
306316
})
317+
common.BindOutputFlags(execDLLCmd)
307318

308319
common.BindFlagCompletions(execDLLCmd, func(comp carapace.ActionMap) {
309320
comp["binPath"] = carapace.ActionFiles()
@@ -334,6 +345,7 @@ dllspawn example.dll
334345
f.StringP("entrypoint", "e", "", "custom entrypoint")
335346
f.StringP("binPath", "", "", "custom process path")
336347
})
348+
common.BindOutputFlags(execDLLSpawnCmd)
337349

338350
common.BindFlagCompletions(execDLLSpawnCmd, func(comp carapace.ActionMap) {
339351
comp["binPath"] = carapace.ActionFiles()
@@ -371,6 +383,7 @@ inline_dll example.dll -e RunFunction -- arg1 arg2
371383
common.BindFlag(inlineDLLCmd, common.ExecuteFlagSet, func(f *pflag.FlagSet) {
372384
f.StringP("entrypoint", "e", "", "entrypoint")
373385
})
386+
common.BindOutputFlags(inlineDLLCmd)
374387

375388
execExeCmd := &cobra.Command{
376389
Use: consts.ModuleExecuteExe + " [exe]",
@@ -395,6 +408,7 @@ execute_exe gogo.exe -- -i 123.123.123.123 -p top2
395408
carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint"))
396409

397410
common.BindFlag(execExeCmd, common.ExecuteFlagSet, common.SacrificeFlagSet)
411+
common.BindOutputFlags(execExeCmd)
398412

399413
inlinePECmd := &cobra.Command{
400414
Use: consts.ModuleAliasInlineExe + " [exe]",
@@ -421,6 +435,7 @@ inline_exe hackbrowserdata.exe -- -h
421435
`,
422436
}
423437
common.BindFlag(inlinePECmd, common.ExecuteFlagSet)
438+
common.BindOutputFlags(inlinePECmd)
424439
common.BindArgCompletions(inlinePECmd, nil,
425440
carapace.ActionFiles().Usage("path the PE file"))
426441

@@ -456,6 +471,7 @@ bof dir.x64.o -- wstr:"C:\\Windows\\System32"
456471
common.BindArgCompletions(execBofCmd, nil,
457472
carapace.ActionFiles().Usage("path the BOF file"),
458473
carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint"))
474+
common.BindOutputFlags(execBofCmd)
459475

460476
powerpickCmd := &cobra.Command{
461477
Use: consts.ModulePowerpick + " [args]",
@@ -483,6 +499,7 @@ powerpick -s powerview.ps1 -- Get-NetUser
483499
common.BindFlagCompletions(powerpickCmd, func(comp carapace.ActionMap) {
484500
comp["script"] = carapace.ActionFiles()
485501
})
502+
common.BindOutputFlags(powerpickCmd)
486503

487504
return []*cobra.Command{
488505
runCmd,

client/command/exec/dllspwan.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ func ExecuteDLLSpawnCmd(cmd *cobra.Command, con *core.Console) error {
2929
if err != nil {
3030
return err
3131
}
32-
session.Console(task, string(*con.App.Shell().Line()))
33-
return nil
32+
return common.HandleTaskOutput(cmd, con, task)
3433
}
3534

3635
func ExecuteDLLSpawn(rpc clientrpc.MaliceRPCClient, sess *client.Session, dllPath string, entrypoint string, data string, binPath string, out bool, timeout uint32, arch string, process string, sac *implantpb.SacrificeProcess) (*clientpb.Task, error) {

client/command/exec/exec_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package exec_test
22

33
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
410
"testing"
511

612
"github.com/chainreactors/IoM-go/consts"
13+
"github.com/chainreactors/IoM-go/proto/client/clientpb"
714
implantpb "github.com/chainreactors/IoM-go/proto/implant/implantpb"
815
"github.com/chainreactors/malice-network/client/command/testsupport"
916
"google.golang.org/grpc/metadata"
@@ -105,6 +112,87 @@ func TestExecCommandConformance(t *testing.T) {
105112
})
106113
}
107114

115+
func TestShellFileOutputWaitsForAsyncTask(t *testing.T) {
116+
h := testsupport.NewHarness(t)
117+
outputPath := filepath.Join(t.TempDir(), "shell-output.txt")
118+
119+
waitCalled := false
120+
h.Recorder.OnTaskContext("WaitTaskFinish", func(_ context.Context, request any) (*clientpb.TaskContext, error) {
121+
waitCalled = true
122+
task, ok := request.(*clientpb.Task)
123+
if !ok {
124+
return nil, fmt.Errorf("wait request type = %T, want *clientpb.Task", request)
125+
}
126+
return &clientpb.TaskContext{
127+
Task: &clientpb.Task{
128+
TaskId: task.GetTaskId(),
129+
SessionId: task.GetSessionId(),
130+
Type: consts.ModuleExecute,
131+
Cur: 2,
132+
Total: 2,
133+
Finished: true,
134+
},
135+
Session: h.Session.Session,
136+
Spite: &implantpb.Spite{
137+
Error: 6,
138+
},
139+
}, nil
140+
})
141+
h.Recorder.OnTaskContexts("GetAllTaskContent", func(_ context.Context, request any) (*clientpb.TaskContexts, error) {
142+
if !waitCalled {
143+
return nil, errors.New("GetAllTaskContent called before WaitTaskFinish")
144+
}
145+
task, ok := request.(*clientpb.Task)
146+
if !ok {
147+
return nil, fmt.Errorf("content request type = %T, want *clientpb.Task", request)
148+
}
149+
return &clientpb.TaskContexts{
150+
Task: &clientpb.Task{
151+
TaskId: task.GetTaskId(),
152+
SessionId: task.GetSessionId(),
153+
Type: consts.ModuleExecute,
154+
Cur: 2,
155+
Total: 2,
156+
Finished: true,
157+
},
158+
Session: h.Session.Session,
159+
Spites: []*implantpb.Spite{
160+
{Error: 0},
161+
{Error: 6},
162+
},
163+
}, nil
164+
})
165+
166+
if err := h.Execute(consts.ModuleAliasShell, "-f", outputPath, "whoami"); err != nil {
167+
t.Fatalf("execute shell with file output failed: %v", err)
168+
}
169+
170+
calls := h.Recorder.Calls()
171+
if len(calls) != 3 {
172+
t.Fatalf("primary call count = %d, want 3 (Execute, WaitTaskFinish, GetAllTaskContent)", len(calls))
173+
}
174+
if calls[0].Method != "Execute" || calls[1].Method != "WaitTaskFinish" || calls[2].Method != "GetAllTaskContent" {
175+
t.Fatalf("primary call order = %#v, want [Execute WaitTaskFinish GetAllTaskContent]", []string{calls[0].Method, calls[1].Method, calls[2].Method})
176+
}
177+
178+
taskReq, ok := calls[1].Request.(*clientpb.Task)
179+
if !ok {
180+
t.Fatalf("wait request type = %T, want *clientpb.Task", calls[1].Request)
181+
}
182+
183+
data, err := os.ReadFile(outputPath)
184+
if err != nil {
185+
t.Fatalf("read output file failed: %v", err)
186+
}
187+
text := string(data)
188+
if !strings.Contains(text, fmt.Sprintf("task: %d true", taskReq.GetTaskId())) {
189+
t.Fatalf("output file = %q, want first aggregated chunk", text)
190+
}
191+
if !strings.Contains(text, fmt.Sprintf("task: %d false", taskReq.GetTaskId())) {
192+
t.Fatalf("output file = %q, want second aggregated chunk", text)
193+
}
194+
}
195+
108196
func assertExecTaskEvent(t testing.TB, h *testsupport.Harness, md metadata.MD, wantType string) {
109197
t.Helper()
110198

client/command/exec/execute-assembly.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ func ExecuteAssemblyCmd(cmd *cobra.Command, con *core.Console) error {
2121
if err != nil {
2222
return err
2323
}
24-
session.Console(task, string(*con.App.Shell().Line()))
25-
return nil
24+
return common.HandleTaskOutput(cmd, con, task)
2625
}
2726

2827
func ExecuteAssembly(rpc clientrpc.MaliceRPCClient, sess *client.Session, path string, args []string, out bool, param map[string]string, sac *implantpb.SacrificeProcess) (*clientpb.Task, error) {
@@ -46,8 +45,7 @@ func InlineAssemblyCmd(cmd *cobra.Command, con *core.Console) error {
4645
if err != nil {
4746
return err
4847
}
49-
session.Console(task, string(*con.App.Shell().Line()))
50-
return nil
48+
return common.HandleTaskOutput(cmd, con, task)
5149
}
5250

5351
func InlineAssembly(rpc clientrpc.MaliceRPCClient, sess *client.Session, path string, args []string, out bool, param map[string]string) (*clientpb.Task, error) {

client/command/exec/execute-bof.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ func ExecuteBofCmd(cmd *cobra.Command, con *core.Console) error {
2121
if err != nil {
2222
return err
2323
}
24-
con.GetInteractive().Console(task, string(*con.App.Shell().Line()))
25-
return nil
24+
return common.HandleTaskOutput(cmd, con, task)
2625
}
2726

2827
func ExecBof(rpc clientrpc.MaliceRPCClient, sess *client.Session, bofPath string, args []string, out bool) (*clientpb.Task, error) {

0 commit comments

Comments
 (0)