问题
通过 MCP 调用 execute_command("bof --help")(以及任何 --help/-h/help 形态)始终返回:
Command executed successfully (no output)
而终端里直接敲 --help 是有完整输出的。
原因
两个原因,都在 malice-network-dev/client/core/:
-
execute_command 捕获不到 Cobra 的 help 文本
execute_command → executeCommandWithTaskWait(utils.go:204)用 client.Stdout.Range(start, now) 截取输出。
client.Stdout 是 NewStdoutWrapper(os.Stdout)(external/IoM-go/client/log.go:16)——一个环形缓冲 wrapper,只有「写到这个 wrapper」的内容才会被 .Range() 截到。
- Cobra 的
--help 走 cmd.OutOrStdout(),全仓库没有任何 cmd.SetOut(client.Stdout)(grep 无命中),所以 help 直接打到原始 os.Stdout,绕过了 wrapper 缓冲 → .Range() 截到空串 → MCP 返回 "no output"。
- 对照:
module list / whoami 的输出走 console 日志路径(logs.Log.SetOutput(client.Stdout),console.go:247),所以能被截到。
-
MCP server 自己埋了个误导 Tip
修复思路
既然 Cobra 的 help 走 stdout wrapper 捕获不到,就在 execute_command handler 里短路 help 请求:识别出 --help/-h/help,用 cmd.SetOut(&buf) + cmd.Help() 把 help 文本抓进缓冲再返回,绕开 stdout 捕获通道。
问题二:MCP 执行 session newbind 报 not found pipeline
现象
通过 MCP 客户端执行 session newbind --pipeline <bind_pipeline> --target <ip:port> 时,报错 rpc error: code = Unknown desc = not found pipeline,bind 会话无法建立;同一操作在 tui/webui 客户端正常。
根因
1. 触发链路与失败点
session newbind 在客户端的执行流程为两步 RPC(client/command/sessions/new.go):
// 第一步:注册会话
con.Rpc.Register(con.Context(), &clientpb.RegisterSession{...})
// 第二步:下发 init spite,驱动 listener 去 dial 目标 bind 马
con.Rpc.InitBindSession(sess.Context(), &implantpb.Init{Data: sess.Raw()})
失败发生在第二步。服务端 InitBindSession(server/rpc/rpc-implant.go:222)经 GenericHandler(server/rpc/generic.go:337)调用 loadPipelineStreamForSession(server/rpc/generic.go:324)查找 listener 为该 pipeline 注册的流:
func loadPipelineStreamForSession(sess *core.Session) (interface{}, bool) {
if sess == nil || sess.PipelineID == "" {
return nil, false
}
// ① 以 "listenerID/pipelineID" 复合键查找
if streamVal, ok := pipelinesCh.Load(core.PipelineRuntimeKey(sess.ListenerID, sess.PipelineID)); ok {
return streamVal, true
}
// ② 退化到裸 pipelineID 查找,但前提是 ListenerID 非空
if sess.ListenerID != "" && runtimePipelineNameCount(sess.PipelineID) <= 1 {
return pipelinesCh.Load(sess.PipelineID)
}
return nil, false // ← 在这里返回 false,上层转为 ErrNotFoundPipeline
}
而 pipelinesCh 的写入方(server/rpc/rpc-listener.go:86,listener 的 SpiteStream 建立时)只写复合键:
pipelineKey := core.PipelineRuntimeKey(listenerID, pipelineID) // 例如 "listener/test"
pipelinesCh.Store(pipelineKey, stream)
因此查找命中的唯一现实路径是 ①,它要求 sess.ListenerID 非空且与 listener 注册流时使用的 ID 一致。
2. 为什么 MCP 客户端会失败
会话的 ListenerID 来自客户端注册时提交的 RegisterSession(server/internal/core/session.go:112:ListenerID: req.ListenerId)。而修复前的客户端代码根本没有设置这个字段(client/command/sessions/new.go:32-43):
// 修复前:没有 ListenerId 字段
_, err := con.Rpc.Register(con.Context(), &clientpb.RegisterSession{
PipelineId: PipelineID,
RawId: encoders.BytesToUint32(rid),
SessionId: sid,
Target: target,
Type: consts.ImplantMaleficBind,
...
})
于是 sess.ListenerID == "":
- 路径 ①:复合键变成
"<空>/test",与 "listener/test" 不匹配;
- 路径 ②:
sess.ListenerID != "" 条件不成立,直接跳过;
- 结果:
loadPipelineStreamForSession 返回 false → GenericHandler 回滚任务并返回 types.ErrNotFoundPipeline(server/rpc/generic.go:346)。
tui/webui 客户端构建的 RegisterSession 携带了 ListenerId(如 "listener"),所以同样的 pipeline 在那些客户端上工作正常。这也是"运行时新建的 bind pipeline 报 not found pipeline,而服务端启动时恢复的 pipeline 正常"这一表象的来源——早期排查时曾误判为"运行时创建的 pipeline 没进注册表",实际是客户端没有提交 listener 归属。
修改思路
- 修客户端,而不是修服务端
• RegisterSession.ListenerId 字段在 proto 契约中早已定义(第 3 字段),服务端会原样落库,tui/webui
一直依赖它正常工作。
• 是 MCP 客户端没有履行契约,所以只需补齐客户端的字段提交;若改服务端去兼容空 ListenerId,属于放大改动面。
2. 从本地缓存自动解析,而不是要求用户传参
• --pipeline 参数本身就是 pipeline 名,客户端缓存(con.ServerState.FindCachedPipeline)中已有完整的 pipeline →
listener 映射。
• 新增 resolvePipelineListenerID() 辅助函数,注册时自动查出 ListenerId 填入 RegisterSession,对用户完全透明,CLI
用法不变。
3. 查不到时降级返回空串,而不是报错
• 保持与旧行为一致的降级路径(服务端 validateReRegisterPipeline 对空 ListenerId 有 core.Listeners.Find
兜底),不引入新的失败模式。
问题
通过 MCP 调用
execute_command("bof --help")(以及任何--help/-h/help形态)始终返回:而终端里直接敲
--help是有完整输出的。原因
两个原因,都在
malice-network-dev/client/core/:execute_command捕获不到 Cobra 的 help 文本execute_command→executeCommandWithTaskWait(utils.go:204)用client.Stdout.Range(start, now)截取输出。client.Stdout是NewStdoutWrapper(os.Stdout)(external/IoM-go/client/log.go:16)——一个环形缓冲 wrapper,只有「写到这个 wrapper」的内容才会被.Range()截到。--help走cmd.OutOrStdout(),全仓库没有任何cmd.SetOut(client.Stdout)(grep 无命中),所以 help 直接打到原始os.Stdout,绕过了 wrapper 缓冲 →.Range()截到空串 → MCP 返回 "no output"。module list/whoami的输出走 console 日志路径(logs.Log.SetOutput(client.Stdout),console.go:247),所以能被截到。MCP server 自己埋了个误导 Tip
mcp.go:448在每次search_commands返回末尾硬编码:修复思路
既然 Cobra 的 help 走 stdout wrapper 捕获不到,就在
execute_commandhandler 里短路 help 请求:识别出--help/-h/help,用cmd.SetOut(&buf)+cmd.Help()把 help 文本抓进缓冲再返回,绕开 stdout 捕获通道。问题二:MCP 执行
session newbind报not found pipeline现象
通过 MCP 客户端执行
session newbind --pipeline <bind_pipeline> --target <ip:port>时,报错rpc error: code = Unknown desc = not found pipeline,bind 会话无法建立;同一操作在 tui/webui 客户端正常。根因
1. 触发链路与失败点
session newbind在客户端的执行流程为两步 RPC(client/command/sessions/new.go):失败发生在第二步。服务端
InitBindSession(server/rpc/rpc-implant.go:222)经GenericHandler(server/rpc/generic.go:337)调用loadPipelineStreamForSession(server/rpc/generic.go:324)查找 listener 为该 pipeline 注册的流:而
pipelinesCh的写入方(server/rpc/rpc-listener.go:86,listener 的SpiteStream建立时)只写复合键:因此查找命中的唯一现实路径是 ①,它要求
sess.ListenerID非空且与 listener 注册流时使用的 ID 一致。2. 为什么 MCP 客户端会失败
会话的
ListenerID来自客户端注册时提交的RegisterSession(server/internal/core/session.go:112:ListenerID: req.ListenerId)。而修复前的客户端代码根本没有设置这个字段(client/command/sessions/new.go:32-43):于是
sess.ListenerID == "":"<空>/test",与"listener/test"不匹配;sess.ListenerID != ""条件不成立,直接跳过;loadPipelineStreamForSession返回 false →GenericHandler回滚任务并返回types.ErrNotFoundPipeline(server/rpc/generic.go:346)。tui/webui 客户端构建的
RegisterSession携带了ListenerId(如"listener"),所以同样的 pipeline 在那些客户端上工作正常。这也是"运行时新建的 bind pipeline 报 not found pipeline,而服务端启动时恢复的 pipeline 正常"这一表象的来源——早期排查时曾误判为"运行时创建的 pipeline 没进注册表",实际是客户端没有提交 listener 归属。修改思路
• RegisterSession.ListenerId 字段在 proto 契约中早已定义(第 3 字段),服务端会原样落库,tui/webui
一直依赖它正常工作。
• 是 MCP 客户端没有履行契约,所以只需补齐客户端的字段提交;若改服务端去兼容空 ListenerId,属于放大改动面。
2. 从本地缓存自动解析,而不是要求用户传参
• --pipeline 参数本身就是 pipeline 名,客户端缓存(con.ServerState.FindCachedPipeline)中已有完整的 pipeline →
listener 映射。
• 新增 resolvePipelineListenerID() 辅助函数,注册时自动查出 ListenerId 填入 RegisterSession,对用户完全透明,CLI
用法不变。
3. 查不到时降级返回空串,而不是报错
• 保持与旧行为一致的降级路径(服务端 validateReRegisterPipeline 对空 ListenerId 有 core.Listeners.Find
兜底),不引入新的失败模式。