diff --git a/client/command/beacons/beacons.go b/client/command/beacons/beacons.go index 36214fe606..6945ec1e8c 100644 --- a/client/command/beacons/beacons.go +++ b/client/command/beacons/beacons.go @@ -29,12 +29,39 @@ import ( "golang.org/x/term" "github.com/bishopfox/sliver/client/command/kill" + "github.com/bishopfox/sliver/client/command/output" "github.com/bishopfox/sliver/client/command/settings" "github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/protobuf/clientpb" "github.com/bishopfox/sliver/protobuf/commonpb" ) +// BeaconResult represents a single beacon in structured output. +type BeaconResult struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Transport string `json:"transport" yaml:"transport"` + RemoteAddress string `json:"remoteAddress" yaml:"remoteAddress"` + Hostname string `json:"hostname" yaml:"hostname"` + Username string `json:"username" yaml:"username"` + Process string `json:"process" yaml:"process"` + PID uint32 `json:"pid" yaml:"pid"` + OS string `json:"os" yaml:"os"` + Arch string `json:"arch" yaml:"arch"` + Locale string `json:"locale" yaml:"locale"` + Integrity string `json:"integrity,omitempty" yaml:"integrity,omitempty"` + LastCheckin int64 `json:"lastCheckin" yaml:"lastCheckin"` + NextCheckin int64 `json:"nextCheckin" yaml:"nextCheckin"` + TasksCount uint32 `json:"tasksCount" yaml:"tasksCount"` + TasksCountCompleted uint32 `json:"tasksCountCompleted" yaml:"tasksCountCompleted"` + Active bool `json:"active" yaml:"active"` +} + +// BeaconListResult represents the beacons list in structured output. +type BeaconListResult struct { + Beacons []BeaconResult `json:"beacons" yaml:"beacons"` +} + // BeaconsCmd - Display/interact with beacons func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { killFlag, _ := cmd.Flags().GetString("kill") @@ -103,7 +130,13 @@ func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { con.PrintErrorf("%s\n", err) return } - PrintBeacons(beacons.Beacons, filter, filterRegex, con) + + format := output.GetOutputFormat(cmd) + if format != output.FormatText { + PrintBeaconsStructured(beacons.Beacons, filter, filterRegex, con, format) + } else { + PrintBeacons(beacons.Beacons, filter, filterRegex, con) + } } // PrintBeacons - Display a list of beacons @@ -116,6 +149,71 @@ func PrintBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regexp con.Printf("%s\n", tw.Render()) } +// PrintBeaconsStructured prints beacons in JSON or YAML format. +func PrintBeaconsStructured(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, format output.OutputFormat) { + result := BeaconListResult{ + Beacons: make([]BeaconResult, 0), + } + + for _, beacon := range beacons { + username := strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\") + + // Apply filters + if filter != "" || filterRegex != nil { + match := false + fields := []string{ + beacon.ID, beacon.Name, beacon.Transport, beacon.RemoteAddress, + beacon.Hostname, username, beacon.Filename, beacon.OS, beacon.Arch, + } + if filter != "" { + for _, field := range fields { + if strings.Contains(field, filter) { + match = true + break + } + } + } + if !match && filterRegex != nil { + for _, field := range fields { + if filterRegex.MatchString(field) { + match = true + break + } + } + } + if !match { + continue + } + } + + br := BeaconResult{ + ID: beacon.ID, + Name: beacon.Name, + Transport: beacon.Transport, + RemoteAddress: beacon.RemoteAddress, + Hostname: beacon.Hostname, + Username: username, + Process: beacon.Filename, + PID: beacon.PID, + OS: beacon.OS, + Arch: beacon.Arch, + Locale: beacon.Locale, + Integrity: beacon.Integrity, + LastCheckin: beacon.LastCheckin, + NextCheckin: beacon.NextCheckin, + TasksCount: beacon.TasksCount, + TasksCountCompleted: beacon.TasksCountCompleted, + Active: con.ActiveTarget.GetBeacon() != nil && con.ActiveTarget.GetBeacon().ID == beacon.ID, + } + + result.Beacons = append(result.Beacons, br) + } + + if err := output.PrintStructured(result, format); err != nil { + con.PrintErrorf("Failed to format output: %s\n", err) + } +} + func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient) table.Writer { width, _, err := term.GetSize(0) if err != nil { diff --git a/client/command/beacons/commands.go b/client/command/beacons/commands.go index fffaca1f18..64800c337c 100644 --- a/client/command/beacons/commands.go +++ b/client/command/beacons/commands.go @@ -11,6 +11,7 @@ import ( "github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/help" + "github.com/bishopfox/sliver/client/command/output" "github.com/bishopfox/sliver/client/console" consts "github.com/bishopfox/sliver/client/constants" "github.com/bishopfox/sliver/protobuf/commonpb" @@ -39,6 +40,7 @@ func Commands(con *console.SliverClient) []*cobra.Command { f.StringP("filter", "f", "", "filter beacons by substring") f.StringP("filter-re", "e", "", "filter beacons by regular expression") }) + output.BindOutputFlags(beaconsCmd) flags.BindFlagCompletions(beaconsCmd, func(comp *carapace.ActionMap) { (*comp)["kill"] = BeaconIDCompleter(con) (*comp)["interact"] = BeaconIDCompleter(con) diff --git a/client/command/creds/commands.go b/client/command/creds/commands.go index cc567e70fc..26a1cd78ff 100644 --- a/client/command/creds/commands.go +++ b/client/command/creds/commands.go @@ -4,6 +4,7 @@ import ( "github.com/bishopfox/sliver/client/command/completers" "github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/help" + "github.com/bishopfox/sliver/client/command/output" "github.com/bishopfox/sliver/client/console" consts "github.com/bishopfox/sliver/client/constants" "github.com/rsteube/carapace" @@ -25,6 +26,7 @@ func Commands(con *console.SliverClient) []*cobra.Command { flags.Bind("creds", true, credsCmd, func(f *pflag.FlagSet) { f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds") }) + output.BindOutputFlags(credsCmd) credsAddCmd := &cobra.Command{ Use: consts.AddStr, diff --git a/client/command/creds/creds.go b/client/command/creds/creds.go index fab6114574..6389523bd7 100644 --- a/client/command/creds/creds.go +++ b/client/command/creds/creds.go @@ -23,6 +23,7 @@ import ( "fmt" "strings" + "github.com/bishopfox/sliver/client/command/output" "github.com/bishopfox/sliver/client/command/settings" "github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/protobuf/clientpb" @@ -32,6 +33,23 @@ import ( "github.com/spf13/cobra" ) +// CredentialResult represents a single credential in structured output. +type CredentialResult struct { + ID string `json:"id" yaml:"id"` + Collection string `json:"collection" yaml:"collection"` + Username string `json:"username" yaml:"username"` + Plaintext string `json:"plaintext,omitempty" yaml:"plaintext,omitempty"` + Hash string `json:"hash,omitempty" yaml:"hash,omitempty"` + HashType string `json:"hashType,omitempty" yaml:"hashType,omitempty"` + IsCracked bool `json:"isCracked" yaml:"isCracked"` + OriginHostUUID string `json:"originHostUuid,omitempty" yaml:"originHostUuid,omitempty"` +} + +// CredentialListResult represents the credentials list in structured output. +type CredentialListResult struct { + Credentials []CredentialResult `json:"credentials" yaml:"credentials"` +} + // CredsCmd - Manage credentials. func CredsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { creds, err := con.Rpc.Creds(context.Background(), &commonpb.Empty{}) @@ -43,7 +61,13 @@ func CredsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { con.PrintInfof("No credentials 🙁\n") return } - PrintCreds(creds.Credentials, con) + + format := output.GetOutputFormat(cmd) + if format != output.FormatText { + PrintCredsStructured(creds.Credentials, con, format) + } else { + PrintCreds(creds.Credentials, con) + } } func PrintCreds(creds []*clientpb.Credential, con *console.SliverClient) { @@ -132,6 +156,31 @@ func CredsCollectionCompleter(con *console.SliverClient) carapace.Action { }) } +// PrintCredsStructured prints credentials in JSON or YAML format. +func PrintCredsStructured(creds []*clientpb.Credential, con *console.SliverClient, format output.OutputFormat) { + result := CredentialListResult{ + Credentials: make([]CredentialResult, 0), + } + + for _, cred := range creds { + cr := CredentialResult{ + ID: cred.ID, + Collection: cred.Collection, + Username: cred.Username, + Plaintext: cred.Plaintext, + Hash: cred.Hash, + HashType: cred.HashType, + IsCracked: cred.IsCracked, + OriginHostUUID: cred.OriginHostUUID, + } + result.Credentials = append(result.Credentials, cr) + } + + if err := output.PrintStructured(result, format); err != nil { + con.PrintErrorf("Failed to format output: %s\n", err) + } +} + // CredsCredentialIDCompleter completes credential IDs. func CredsCredentialIDCompleter(con *console.SliverClient) carapace.Action { return carapace.ActionCallback(func(c carapace.Context) carapace.Action { diff --git a/client/command/loot/commands.go b/client/command/loot/commands.go index b7fbdebed3..b129350565 100644 --- a/client/command/loot/commands.go +++ b/client/command/loot/commands.go @@ -2,6 +2,7 @@ package loot import ( "github.com/bishopfox/sliver/client/command/completers" + "github.com/bishopfox/sliver/client/command/output" "github.com/rsteube/carapace" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -29,6 +30,7 @@ func Commands(con *console.SliverClient) []*cobra.Command { flags.Bind("loot", false, lootCmd, func(f *pflag.FlagSet) { f.StringP("filter", "f", "", "filter based on loot type") }) + output.BindOutputFlags(lootCmd) lootAddCmd := &cobra.Command{ Use: consts.LootLocalStr, diff --git a/client/command/loot/loot.go b/client/command/loot/loot.go index 59bcfb58e8..15c1a6899f 100644 --- a/client/command/loot/loot.go +++ b/client/command/loot/loot.go @@ -29,6 +29,7 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" + "github.com/bishopfox/sliver/client/command/output" "github.com/bishopfox/sliver/client/command/settings" "github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/forms" @@ -37,6 +38,21 @@ import ( "github.com/bishopfox/sliver/util" ) +// LootItemResult represents a single loot item in structured output. +type LootItemResult struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + FileName string `json:"fileName" yaml:"fileName"` + FileType string `json:"fileType" yaml:"fileType"` + Size int64 `json:"size" yaml:"size"` + SizeH string `json:"sizeHuman" yaml:"sizeHuman"` +} + +// LootListResult represents the loot list in structured output. +type LootListResult struct { + Items []LootItemResult `json:"items" yaml:"items"` +} + // LootCmd - The loot root command func LootCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { allLoot, err := con.Rpc.LootAll(context.Background(), &commonpb.Empty{}) @@ -44,7 +60,13 @@ func LootCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { con.PrintErrorf("Failed to fetch loot %s\n", err) return } - PrintAllFileLootTable(allLoot, con) + + format := output.GetOutputFormat(cmd) + if format != output.FormatText { + PrintLootStructured(allLoot, con, format) + } else { + PrintAllFileLootTable(allLoot, con) + } } // PrintAllFileLootTable - Displays a table of all file loot @@ -212,3 +234,35 @@ func isText(sample []byte) bool { } return true } + +// PrintLootStructured prints loot in JSON or YAML format. +func PrintLootStructured(allLoot *clientpb.AllLoot, con *console.SliverClient, format output.OutputFormat) { + result := LootListResult{ + Items: make([]LootItemResult, 0), + } + + if allLoot == nil || len(allLoot.Loot) == 0 { + if err := output.PrintStructured(result, format); err != nil { + con.PrintErrorf("Failed to format output: %s\n", err) + } + return + } + + for _, loot := range allLoot.Loot { + if loot.File != nil { + item := LootItemResult{ + ID: loot.ID, + Name: loot.Name, + FileName: loot.File.Name, + FileType: fileTypeToStr(loot.FileType), + Size: loot.Size, + SizeH: util.ByteCountBinary(loot.Size), + } + result.Items = append(result.Items, item) + } + } + + if err := output.PrintStructured(result, format); err != nil { + con.PrintErrorf("Failed to format output: %s\n", err) + } +} diff --git a/client/command/output/output.go b/client/command/output/output.go new file mode 100644 index 0000000000..8b007166cd --- /dev/null +++ b/client/command/output/output.go @@ -0,0 +1,89 @@ +package output + +/* + Sliver Implant Framework + Copyright (C) 2023 Bishop Fox + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +// OutputFormat represents the supported output formats. +type OutputFormat string + +const ( + FormatText OutputFormat = "text" + FormatJSON OutputFormat = "json" + FormatYAML OutputFormat = "yaml" +) + +// GetOutputFormat resolves the requested output format from the command's +// flags. Shorthand flags (--json, --yaml) take precedence over the +// --output-format flag. Returns FormatText when no format is requested or +// the flag is not bound to the command. +func GetOutputFormat(cmd *cobra.Command) OutputFormat { + if cmd == nil { + return FormatText + } + if f := cmd.Flags().Lookup("json"); f != nil && f.Value.String() == "true" { + return FormatJSON + } + if f := cmd.Flags().Lookup("yaml"); f != nil && f.Value.String() == "true" { + return FormatYAML + } + if f := cmd.Flags().Lookup("output-format"); f != nil { + switch OutputFormat(f.Value.String()) { + case FormatJSON: + return FormatJSON + case FormatYAML: + return FormatYAML + default: + return FormatText + } + } + return FormatText +} + +// PrintStructured renders v as JSON or YAML to stdout. +func PrintStructured(v any, format OutputFormat) error { + switch format { + case FormatJSON: + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + return fmt.Errorf("json marshal: %w", err) + } + fmt.Println(string(data)) + case FormatYAML: + data, err := yaml.Marshal(v) + if err != nil { + return fmt.Errorf("yaml marshal: %w", err) + } + fmt.Print(string(data)) + } + return nil +} + +// BindOutputFlags adds --output-format, --json, and --yaml flags to a command. +func BindOutputFlags(cmd *cobra.Command) { + cmd.Flags().String("output-format", "text", "output format: text, json, or yaml") + cmd.Flags().Bool("json", false, "output as JSON (shorthand for --output-format json)") + cmd.Flags().Bool("yaml", false, "output as YAML (shorthand for --output-format yaml)") +} diff --git a/client/command/sessions/commands.go b/client/command/sessions/commands.go index 2d58b777f4..ee540a2112 100644 --- a/client/command/sessions/commands.go +++ b/client/command/sessions/commands.go @@ -7,6 +7,7 @@ import ( "github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/help" + "github.com/bishopfox/sliver/client/command/output" "github.com/bishopfox/sliver/client/console" consts "github.com/bishopfox/sliver/client/constants" "github.com/bishopfox/sliver/protobuf/commonpb" @@ -39,6 +40,7 @@ func Commands(con *console.SliverClient) []*cobra.Command { f.StringP("filter", "f", "", "filter sessions by substring") f.StringP("filter-re", "e", "", "filter sessions by regular expression") }) + output.BindOutputFlags(sessionsCmd) flags.BindFlagCompletions(sessionsCmd, func(comp *carapace.ActionMap) { (*comp)["interact"] = SessionIDCompleter(con) (*comp)["kill"] = SessionIDCompleter(con) diff --git a/client/command/sessions/sessions.go b/client/command/sessions/sessions.go index fcf6e0702c..756c0a2869 100644 --- a/client/command/sessions/sessions.go +++ b/client/command/sessions/sessions.go @@ -26,6 +26,7 @@ import ( "time" "github.com/bishopfox/sliver/client/command/kill" + "github.com/bishopfox/sliver/client/command/output" "github.com/bishopfox/sliver/client/command/settings" "github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/protobuf/clientpb" @@ -35,6 +36,31 @@ import ( "golang.org/x/term" ) +// SessionResult represents a single session in structured output. +type SessionResult struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Transport string `json:"transport" yaml:"transport"` + RemoteAddress string `json:"remoteAddress" yaml:"remoteAddress"` + Hostname string `json:"hostname" yaml:"hostname"` + Username string `json:"username" yaml:"username"` + Process string `json:"process" yaml:"process"` + PID uint32 `json:"pid" yaml:"pid"` + OS string `json:"os" yaml:"os"` + Arch string `json:"arch" yaml:"arch"` + Locale string `json:"locale" yaml:"locale"` + Integrity string `json:"integrity,omitempty" yaml:"integrity,omitempty"` + LastCheckin int64 `json:"lastCheckin" yaml:"lastCheckin"` + IsDead bool `json:"isDead" yaml:"isDead"` + Burned bool `json:"burned" yaml:"burned"` + Active bool `json:"active" yaml:"active"` +} + +// SessionListResult represents the sessions list in structured output. +type SessionListResult struct { + Sessions []SessionResult `json:"sessions" yaml:"sessions"` +} + // SessionsCmd - Display/interact with sessions. func SessionsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { interact, _ := cmd.Flags().GetString("interact") @@ -118,7 +144,12 @@ func SessionsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { sessionsMap[session.ID] = session } if 0 < len(sessionsMap) { - PrintSessions(sessionsMap, filter, filterRegex, con) + format := output.GetOutputFormat(cmd) + if format != output.FormatText { + PrintSessionsStructured(sessionsMap, filter, filterRegex, con, format) + } else { + PrintSessions(sessionsMap, filter, filterRegex, con) + } } else { con.PrintInfof("No sessions 🙁\n") } @@ -274,3 +305,66 @@ func PrintSessions(sessions map[string]*clientpb.Session, filter string, filterR func ShortSessionID(id string) string { return strings.Split(id, "-")[0] } + +// PrintSessionsStructured prints sessions in JSON or YAML format. +func PrintSessionsStructured(sessions map[string]*clientpb.Session, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, format output.OutputFormat) { + result := SessionListResult{ + Sessions: make([]SessionResult, 0), + } + + for _, session := range sessions { + username := strings.TrimPrefix(session.Username, session.Hostname+"\\") + sr := SessionResult{ + ID: session.ID, + Name: session.Name, + Transport: session.Transport, + RemoteAddress: session.RemoteAddress, + Hostname: session.Hostname, + Username: username, + Process: session.Filename, + PID: session.PID, + OS: session.OS, + Arch: session.Arch, + Locale: session.Locale, + Integrity: session.Integrity, + LastCheckin: session.LastCheckin, + IsDead: session.IsDead, + Burned: session.Burned, + Active: con.ActiveTarget.GetSession() != nil && con.ActiveTarget.GetSession().ID == session.ID, + } + + // Apply filters + if filter != "" || filterRegex != nil { + match := false + fields := []string{ + session.ID, session.Name, session.Transport, session.RemoteAddress, + session.Hostname, username, session.Filename, session.OS, session.Arch, + } + if filter != "" { + for _, field := range fields { + if strings.Contains(field, filter) { + match = true + break + } + } + } + if !match && filterRegex != nil { + for _, field := range fields { + if filterRegex.MatchString(field) { + match = true + break + } + } + } + if !match { + continue + } + } + + result.Sessions = append(result.Sessions, sr) + } + + if err := output.PrintStructured(result, format); err != nil { + con.PrintErrorf("Failed to format output: %s\n", err) + } +} diff --git a/client/mcp/NEW_TOOLS.md b/client/mcp/NEW_TOOLS.md new file mode 100644 index 0000000000..68665c12c0 --- /dev/null +++ b/client/mcp/NEW_TOOLS.md @@ -0,0 +1,175 @@ +# Sliver MCP Server 新增工具 + +本次更新为 Sliver C2 的 MCP Server 添加了 5 个新工具,扩展了远程目标系统的监控和操作能力。 + +## 新增工具列表 + +### 1. list_processes - 列出进程 +**功能**: 列出目标主机上运行的所有进程 + +**参数**: +- `session_id` (可选): Session ID +- `beacon_id` (可选): Beacon ID +- `full_info` (可选): 是否获取完整进程信息 +- `wait` (可选): 是否等待 Beacon 任务完成 +- `timeout_seconds` (可选): 超时时间(秒) + +**返回**: 进程列表,包含 PID、PPID、可执行文件路径、所有者、架构、命令行等信息 + +**RPC 方法**: `Ps` + +--- + +### 2. kill_process - 终止进程 +**功能**: 终止目标主机上的指定进程 + +**参数**: +- `session_id` (可选): Session ID +- `beacon_id` (可选): Beacon ID +- `pid` (必需): 要终止的进程 ID +- `force` (可选): 是否强制终止 +- `wait` (可选): 是否等待 Beacon 任务完成 +- `timeout_seconds` (可选): 超时时间(秒) + +**返回**: 终止结果,包含 PID 和成功状态 + +**RPC 方法**: `Terminate` + +--- + +### 3. list_credentials - 列出凭证 +**功能**: 列出已收集的凭证信息 + +**参数**: 无(全局查询) + +**返回**: 凭证列表,包含: +- ID +- 用户名 +- 明文密码(如果有) +- 哈希值 +- 哈希类型(支持多种格式:MD5, SHA1, SHA256, NTLM, Kerberos 等) +- 是否已破解 +- 来源主机 UUID +- 集合名称 + +**RPC 方法**: `Creds` + +--- + +### 4. network_interfaces - 网络接口信息 +**功能**: 获取目标主机的网络接口配置 + +**参数**: +- `session_id` (可选): Session ID +- `beacon_id` (可选): Beacon ID +- `wait` (可选): 是否等待 Beacon 任务完成 +- `timeout_seconds` (可选): 超时时间(秒) + +**返回**: 网络接口列表,包含: +- 索引 +- 接口名称 +- MAC 地址 +- IP 地址列表 + +**RPC 方法**: `Ifconfig` + +--- + +### 5. netstat - 网络连接状态 +**功能**: 获取目标主机的网络连接状态(类似 netstat/ss 命令) + +**参数**: +- `session_id` (可选): Session ID +- `beacon_id` (可选): Beacon ID +- `tcp` (可选): 是否显示 TCP 连接 +- `udp` (可选): 是否显示 UDP 连接 +- `ip4` (可选): 是否显示 IPv4 连接 +- `ip6` (可选): 是否显示 IPv6 连接 +- `listening` (可选): 是否只显示监听端口 +- `wait` (可选): 是否等待 Beacon 任务完成 +- `timeout_seconds` (可选): 超时时间(秒) + +**返回**: 网络连接列表,包含: +- 协议类型 +- 本地地址(IP 和端口) +- 远程地址(IP 和端口) +- 连接状态 +- UID +- 关联进程信息(PID、可执行文件、所有者) + +**RPC 方法**: `Netstat` + +--- + +## 实现细节 + +### 文件结构 +- `processes.go`: 实现 list_processes 和 kill_process 工具 +- `credentials.go`: 实现 list_credentials 工具 +- `network.go`: 实现 network_interfaces 和 netstat 工具 +- `server.go`: 更新工具注册逻辑 + +### 特性 +1. **完整的错误处理**: 所有工具都包含 RPC 错误、参数验证和超时处理 +2. **Beacon 异步支持**: 支持异步 Beacon 任务,可选择等待或立即返回任务 ID +3. **结构化 JSON 输出**: 使用 `NewToolResultStructuredOnly()` 返回格式化的 JSON 数据 +4. **日志记录**: 所有工具调用都会记录到 MCP 日志系统 +5. **哈希类型映射**: credentials.go 包含完整的 HashType 枚举到字符串的映射 + +### 代码风格 +- 遵循现有 MCP 工具的代码风格 +- 使用相同的参数验证和错误处理模式 +- 保持一致的命名约定和注释风格 +- 支持 Session 和 Beacon 两种模式 + +## 工具总数 +现在 Sliver MCP Server 共有 **20 个工具**: +- 原有 15 个工具 +- 新增 5 个工具 + +## 使用示例 + +```json +// 列出进程 +{ + "tool": "list_processes", + "arguments": { + "session_id": "abc123", + "full_info": true + } +} + +// 终止进程 +{ + "tool": "kill_process", + "arguments": { + "session_id": "abc123", + "pid": 1234, + "force": true + } +} + +// 列出凭证 +{ + "tool": "list_credentials", + "arguments": {} +} + +// 获取网络接口 +{ + "tool": "network_interfaces", + "arguments": { + "session_id": "abc123" + } +} + +// 获取网络连接 +{ + "tool": "netstat", + "arguments": { + "session_id": "abc123", + "tcp": true, + "listening": true + } +} +``` diff --git a/client/mcp/credentials.go b/client/mcp/credentials.go new file mode 100644 index 0000000000..8c5162c532 --- /dev/null +++ b/client/mcp/credentials.go @@ -0,0 +1,151 @@ +package mcp + +import ( + "context" + "fmt" + + "github.com/bishopfox/sliver/protobuf/clientpb" + "github.com/bishopfox/sliver/protobuf/commonpb" + mcpapi "github.com/mark3labs/mcp-go/mcp" +) + +const ( + listCredentialsToolName = "list_credentials" +) + +// list_credentials 结果 +type credentialInfo struct { + ID string `json:"id"` + Username string `json:"username"` + Plaintext string `json:"plaintext,omitempty"` + Hash string `json:"hash,omitempty"` + HashType string `json:"hash_type,omitempty"` + IsCracked bool `json:"is_cracked"` + OriginHostUUID string `json:"origin_host_uuid,omitempty"` + Collection string `json:"collection,omitempty"` +} + +type listCredentialsResult struct { + Credentials []credentialInfo `json:"credentials"` + Count int `json:"count"` +} + +// list_credentials 处理器 +func (s *SliverMCPServer) listCredentialsHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + return s.handleListCredentials(ctx) +} + +func (s *SliverMCPServer) handleListCredentials(ctx context.Context) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + s.logToolCall(listCredentialsToolName, "", "") + + credsResp, err := s.Rpc.Creds(ctx, &commonpb.Empty{}) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to list credentials", err), nil + } + + result := listCredentialsResult{ + Credentials: make([]credentialInfo, 0, len(credsResp.Credentials)), + } + + for _, cred := range credsResp.Credentials { + if cred == nil { + continue + } + result.Credentials = append(result.Credentials, credentialInfo{ + ID: cred.ID, + Username: cred.Username, + Plaintext: cred.Plaintext, + Hash: cred.Hash, + HashType: formatHashType(cred.HashType), + IsCracked: cred.IsCracked, + OriginHostUUID: cred.OriginHostUUID, + Collection: cred.Collection, + }) + } + + result.Count = len(result.Credentials) + return mcpapi.NewToolResultStructuredOnly(result), nil +} + +// formatHashType 将 HashType 枚举转换为字符串 +func formatHashType(hashType clientpb.HashType) string { + // 使用 clientpb 中定义的常量映射 + switch hashType { + case clientpb.HashType_MD5: + return "MD5" + case clientpb.HashType_MD4: + return "MD4" + case clientpb.HashType_SHA1: + return "SHA1" + case clientpb.HashType_SHA2_224: + return "SHA2-224" + case clientpb.HashType_SHA2_256: + return "SHA2-256" + case clientpb.HashType_SHA2_384: + return "SHA2-384" + case clientpb.HashType_SHA2_512: + return "SHA2-512" + case clientpb.HashType_SHA3_224: + return "SHA3-224" + case clientpb.HashType_SHA3_256: + return "SHA3-256" + case clientpb.HashType_SHA3_384: + return "SHA3-384" + case clientpb.HashType_SHA3_512: + return "SHA3-512" + case clientpb.HashType_RIPEMD_160: + return "RIPEMD-160" + case clientpb.HashType_BLAKE2B_256: + return "BLAKE2b-256" + case clientpb.HashType_GOST_R_32_11_2012_256: + return "GOST R 34.11-2012 256-bit" + case clientpb.HashType_GOST_R_32_11_2012_512: + return "GOST R 34.11-2012 512-bit" + case clientpb.HashType_GOST_R_34_11_94: + return "GOST R 34.11-94" + case clientpb.HashType_GPG: + return "GPG" + case clientpb.HashType_HALF_MD5: + return "Half MD5" + case clientpb.HashType_KECCAK_224: + return "Keccak-224" + case clientpb.HashType_KECCAK_256: + return "Keccak-256" + case clientpb.HashType_KECCAK_384: + return "Keccak-384" + case clientpb.HashType_KECCAK_512: + return "Keccak-512" + case clientpb.HashType_WHIRLPOOL: + return "Whirlpool" + case clientpb.HashType_SIPHASH: + return "SipHash" + case clientpb.HashType_MD5_UTF16LE: + return "MD5 (UTF-16LE)" + case clientpb.HashType_SHA1_UTF16LE: + return "SHA1 (UTF-16LE)" + case clientpb.HashType_SHA256_UTF16LE: + return "SHA256 (UTF-16LE)" + case clientpb.HashType_SHA384_UTF16LE: + return "SHA384 (UTF-16LE)" + case clientpb.HashType_SHA512_UTF16LE: + return "SHA512 (UTF-16LE)" + case clientpb.HashType_NTLM: + return "NTLM" + case clientpb.HashType_KERBEROS_5_TGS: + return "Kerberos 5 TGS" + case clientpb.HashType_KERBEROS_5_TGS_3DES: + return "Kerberos 5 TGS (3DES)" + case clientpb.HashType_KERBEROS_5_PA_ETYPE_23: + return "Kerberos 5 PA-ETYPE-23" + case clientpb.HashType_KERBEROS_5_PA_ETYPE_17: + return "Kerberos 5 PA-ETYPE-17" + case clientpb.HashType_KERBEROS_5_PA_ETYPE_18: + return "Kerberos 5 PA-ETYPE-18" + default: + return fmt.Sprintf("Unknown(%d)", hashType) + } +} diff --git a/client/mcp/execute.go b/client/mcp/execute.go new file mode 100644 index 0000000000..99546fc408 --- /dev/null +++ b/client/mcp/execute.go @@ -0,0 +1,133 @@ +package mcp + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + + "github.com/bishopfox/sliver/protobuf/commonpb" + "github.com/bishopfox/sliver/protobuf/sliverpb" + "github.com/bishopfox/sliver/util/encoders" + mcpapi "github.com/mark3labs/mcp-go/mcp" +) + +const ( + executeCommandToolName = "execute_command" +) + +type executeCommandArgs struct { + SessionID string `json:"session_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + Command string `json:"command"` + Args []string `json:"args,omitempty"` + Output bool `json:"output,omitempty"` + Env []string `json:"env,omitempty"` + Background bool `json:"background,omitempty"` + Wait bool `json:"wait,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +type executeCommandResult struct { + StdOut string `json:"stdout,omitempty"` + StdErr string `json:"stderr,omitempty"` + ExitStatus int32 `json:"exit_status"` + Background bool `json:"background"` + Async bool `json:"async"` + Operation string `json:"operation,omitempty"` + TaskID string `json:"task_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + State string `json:"state,omitempty"` +} + +func (s *SliverMCPServer) executeCommandHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + var args executeCommandArgs + if err := request.BindArguments(&args); err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("invalid arguments: %v", err)), nil + } + + if args.Command == "" { + return mcpapi.NewToolResultError("command is required"), nil + } + + return s.handleExecuteCommand(ctx, args) +} + +func (s *SliverMCPServer) handleExecuteCommand(ctx context.Context, args executeCommandArgs) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + extras := []string{fmt.Sprintf("command=%q", args.Command)} + if len(args.Args) > 0 { + extras = append(extras, fmt.Sprintf("args=%v", args.Args)) + } + if args.Background { + extras = append(extras, "background=true") + } + s.logToolCall(executeCommandToolName, args.SessionID, args.BeaconID, extras...) + + args.TimeoutSeconds = applyDefaultTimeout(args.Wait, args.TimeoutSeconds) + ctx, cancel := withTimeout(ctx, args.TimeoutSeconds) + if cancel != nil { + defer cancel() + } + + req, isBeacon, err := buildRequest(args.SessionID, args.BeaconID, args.TimeoutSeconds) + if err != nil { + return mcpapi.NewToolResultError(err.Error()), nil + } + + // Parse environment variables + envMap := make(map[string]string) + for _, env := range args.Env { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + + execReq := &sliverpb.ExecuteReq{ + Request: req, + Path: args.Command, + Args: args.Args, + Output: args.Output || !args.Background, + Env: envMap, + Background: args.Background, + EnvInheritance: len(envMap) == 0, + } + + execResp, err := s.Rpc.Execute(ctx, execReq) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to execute command", err), nil + } + + if execResp.Response != nil && execResp.Response.Err != "" { + return mcpapi.NewToolResultError(execResp.Response.Err), nil + } + + if isBeacon && execResp.Response != nil && execResp.Response.Async { + if !args.Wait { + return newAsyncResult("execute_command", execResp.Response.TaskID, execResp.Response.BeaconID), nil + } + resolved := &sliverpb.Execute{} + if err := s.waitForBeaconTaskResponse(ctx, execResp.Response.TaskID, resolved); err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to await execute task", err), nil + } + execResp = resolved + if execResp.Response != nil && execResp.Response.Err != "" { + return mcpapi.NewToolResultError(execResp.Response.Err), nil + } + } + + result := executeCommandResult{ + ExitStatus: execResp.Status, + Background: execResp.Background, + } + + if execResp.Result != "" { + result.StdOut = execResp.Result + } + + return mcpapi.NewToolResultStructuredOnly(result), nil +} diff --git a/client/mcp/network.go b/client/mcp/network.go new file mode 100644 index 0000000000..b224760669 --- /dev/null +++ b/client/mcp/network.go @@ -0,0 +1,258 @@ +package mcp + +import ( + "context" + "fmt" + + "github.com/bishopfox/sliver/protobuf/sliverpb" + mcpapi "github.com/mark3labs/mcp-go/mcp" +) + +const ( + networkInterfacesToolName = "network_interfaces" + netstatToolName = "netstat" +) + +// network_interfaces 工具参数 +type networkInterfacesArgs struct { + SessionID string `json:"session_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + Wait bool `json:"wait,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +// netstat 工具参数 +type netstatArgs struct { + SessionID string `json:"session_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + TCP bool `json:"tcp,omitempty"` + UDP bool `json:"udp,omitempty"` + IP4 bool `json:"ip4,omitempty"` + IP6 bool `json:"ip6,omitempty"` + Listening bool `json:"listening,omitempty"` + Wait bool `json:"wait,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +// network_interfaces 结果 +type netInterfaceInfo struct { + Index int32 `json:"index"` + Name string `json:"name"` + MAC string `json:"mac"` + IPAddresses []string `json:"ip_addresses"` +} + +type networkInterfacesResult struct { + Interfaces []netInterfaceInfo `json:"interfaces"` + Count int `json:"count"` +} + +// netstat 结果 +type sockAddr struct { + IP string `json:"ip"` + Port uint32 `json:"port"` +} + +type netstatEntry struct { + Protocol string `json:"protocol"` + LocalAddr sockAddr `json:"local_addr"` + RemoteAddr sockAddr `json:"remote_addr"` + State string `json:"state"` + UID uint32 `json:"uid"` + Process *processRef `json:"process,omitempty"` +} + +type processRef struct { + PID int32 `json:"pid"` + Executable string `json:"executable,omitempty"` + Owner string `json:"owner,omitempty"` +} + +type netstatResult struct { + Entries []netstatEntry `json:"entries"` + Count int `json:"count"` +} + +// network_interfaces 处理器 +func (s *SliverMCPServer) networkInterfacesHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + var args networkInterfacesArgs + if err := request.BindArguments(&args); err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("invalid arguments: %v", err)), nil + } + + return s.handleNetworkInterfaces(ctx, args) +} + +func (s *SliverMCPServer) handleNetworkInterfaces(ctx context.Context, args networkInterfacesArgs) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + s.logToolCall(networkInterfacesToolName, args.SessionID, args.BeaconID) + + args.TimeoutSeconds = applyDefaultTimeout(args.Wait, args.TimeoutSeconds) + ctx, cancel := withTimeout(ctx, args.TimeoutSeconds) + if cancel != nil { + defer cancel() + } + + req, isBeacon, err := buildRequest(args.SessionID, args.BeaconID, args.TimeoutSeconds) + if err != nil { + return mcpapi.NewToolResultError(err.Error()), nil + } + + ifconfigResp, err := s.Rpc.Ifconfig(ctx, &sliverpb.IfconfigReq{ + Request: req, + }) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to get network interfaces", err), nil + } + + if ifconfigResp.Response != nil && ifconfigResp.Response.Err != "" { + return mcpapi.NewToolResultError(ifconfigResp.Response.Err), nil + } + + if isBeacon && ifconfigResp.Response != nil && ifconfigResp.Response.Async { + if !args.Wait { + return newAsyncResult("network_interfaces", ifconfigResp.Response.TaskID, ifconfigResp.Response.BeaconID), nil + } + resolved := &sliverpb.Ifconfig{} + if err := s.waitForBeaconTaskResponse(ctx, ifconfigResp.Response.TaskID, resolved); err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to await network interfaces task", err), nil + } + ifconfigResp = resolved + if ifconfigResp.Response != nil && ifconfigResp.Response.Err != "" { + return mcpapi.NewToolResultError(ifconfigResp.Response.Err), nil + } + } + + result := networkInterfacesResult{ + Interfaces: make([]netInterfaceInfo, 0, len(ifconfigResp.NetInterfaces)), + } + + for _, iface := range ifconfigResp.NetInterfaces { + if iface == nil { + continue + } + result.Interfaces = append(result.Interfaces, netInterfaceInfo{ + Index: iface.Index, + Name: iface.Name, + MAC: iface.MAC, + IPAddresses: iface.IPAddresses, + }) + } + + result.Count = len(result.Interfaces) + return mcpapi.NewToolResultStructuredOnly(result), nil +} + +// netstat 处理器 +func (s *SliverMCPServer) netstatHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + var args netstatArgs + if err := request.BindArguments(&args); err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("invalid arguments: %v", err)), nil + } + + return s.handleNetstat(ctx, args) +} + +func (s *SliverMCPServer) handleNetstat(ctx context.Context, args netstatArgs) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + s.logToolCall( + netstatToolName, + args.SessionID, + args.BeaconID, + fmt.Sprintf("tcp=%t", args.TCP), + fmt.Sprintf("udp=%t", args.UDP), + fmt.Sprintf("ip4=%t", args.IP4), + fmt.Sprintf("ip6=%t", args.IP6), + fmt.Sprintf("listening=%t", args.Listening), + ) + + args.TimeoutSeconds = applyDefaultTimeout(args.Wait, args.TimeoutSeconds) + ctx, cancel := withTimeout(ctx, args.TimeoutSeconds) + if cancel != nil { + defer cancel() + } + + req, isBeacon, err := buildRequest(args.SessionID, args.BeaconID, args.TimeoutSeconds) + if err != nil { + return mcpapi.NewToolResultError(err.Error()), nil + } + + netstatResp, err := s.Rpc.Netstat(ctx, &sliverpb.NetstatReq{ + Request: req, + TCP: args.TCP, + UDP: args.UDP, + IP4: args.IP4, + IP6: args.IP6, + Listening: args.Listening, + }) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to get netstat", err), nil + } + + if netstatResp.Response != nil && netstatResp.Response.Err != "" { + return mcpapi.NewToolResultError(netstatResp.Response.Err), nil + } + + if isBeacon && netstatResp.Response != nil && netstatResp.Response.Async { + if !args.Wait { + return newAsyncResult("netstat", netstatResp.Response.TaskID, netstatResp.Response.BeaconID), nil + } + resolved := &sliverpb.Netstat{} + if err := s.waitForBeaconTaskResponse(ctx, netstatResp.Response.TaskID, resolved); err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to await netstat task", err), nil + } + netstatResp = resolved + if netstatResp.Response != nil && netstatResp.Response.Err != "" { + return mcpapi.NewToolResultError(netstatResp.Response.Err), nil + } + } + + result := netstatResult{ + Entries: make([]netstatEntry, 0, len(netstatResp.Entries)), + } + + for _, entry := range netstatResp.Entries { + if entry == nil { + continue + } + + netEntry := netstatEntry{ + Protocol: entry.Protocol, + State: entry.SkState, + UID: entry.UID, + } + + if entry.LocalAddr != nil { + netEntry.LocalAddr = sockAddr{ + IP: entry.LocalAddr.Ip, + Port: entry.LocalAddr.Port, + } + } + + if entry.RemoteAddr != nil { + netEntry.RemoteAddr = sockAddr{ + IP: entry.RemoteAddr.Ip, + Port: entry.RemoteAddr.Port, + } + } + + if entry.Process != nil { + netEntry.Process = &processRef{ + PID: entry.Process.Pid, + Executable: entry.Process.Executable, + Owner: entry.Process.Owner, + } + } + + result.Entries = append(result.Entries, netEntry) + } + + result.Count = len(result.Entries) + return mcpapi.NewToolResultStructuredOnly(result), nil +} diff --git a/client/mcp/pivot.go b/client/mcp/pivot.go new file mode 100644 index 0000000000..3242e34646 --- /dev/null +++ b/client/mcp/pivot.go @@ -0,0 +1,90 @@ +package mcp + +import ( + "context" + "fmt" + + "github.com/bishopfox/sliver/protobuf/clientpb" + "github.com/bishopfox/sliver/protobuf/commonpb" + "github.com/bishopfox/sliver/protobuf/sliverpb" + mcpapi "github.com/mark3labs/mcp-go/mcp" +) + +const ( + listPivotsToolName = "list_pivots" +) + +type listPivotsArgs struct { + SessionID string `json:"session_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + Wait bool `json:"wait,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +type pivotListener struct { + ID string `json:"id"` + Type string `json:"type"` + BindAddress string `json:"bind_address"` + BindPort int32 `json:"bind_port"` + SessionID string `json:"session_id"` +} + +type listPivotsResult struct { + Pivots []pivotListener `json:"pivots"` + PivotsCount int `json:"pivots_count"` +} + +func (s *SliverMCPServer) listPivotsHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + var args listPivotsArgs + if err := request.BindArguments(&args); err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("invalid arguments: %v", err)), nil + } + + return s.handleListPivots(ctx, args) +} + +func (s *SliverMCPServer) handleListPivots(ctx context.Context, args listPivotsArgs) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + s.logToolCall(listPivotsToolName, args.SessionID, args.BeaconID) + + args.TimeoutSeconds = applyDefaultTimeout(args.Wait, args.TimeoutSeconds) + ctx, cancel := withTimeout(ctx, args.TimeoutSeconds) + if cancel != nil { + defer cancel() + } + + // Get all pivot listeners from the graph + pivotGraph, err := s.Rpc.PivotGraph(ctx, &commonpb.Empty{}) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to get pivot graph", err), nil + } + + result := listPivotsResult{ + Pivots: make([]pivotListener, 0), + } + + for _, session := range pivotGraph.GetSessions() { + if session == nil { + continue + } + for _, listener := range session.GetListeners() { + if listener == nil { + continue + } + result.Pivots = append(result.Pivots, pivotListener{ + ID: listener.ID, + Type: listener.Type.String(), + BindAddress: listener.BindAddress, + BindPort: listener.BindPort, + SessionID: session.Session.ID, + }) + } + } + + result.PivotsCount = len(result.Pivots) + + return mcpapi.NewToolResultStructuredOnly(result), nil +} diff --git a/client/mcp/processes.go b/client/mcp/processes.go new file mode 100644 index 0000000000..e6d034427f --- /dev/null +++ b/client/mcp/processes.go @@ -0,0 +1,204 @@ +package mcp + +import ( + "context" + "fmt" + "strings" + + "github.com/bishopfox/sliver/protobuf/sliverpb" + mcpapi "github.com/mark3labs/mcp-go/mcp" +) + +const ( + listProcessesToolName = "list_processes" + killProcessToolName = "kill_process" +) + +// list_processes 工具参数 +type listProcessesArgs struct { + SessionID string `json:"session_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + FullInfo bool `json:"full_info,omitempty"` + Wait bool `json:"wait,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +// kill_process 工具参数 +type killProcessArgs struct { + SessionID string `json:"session_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + PID int32 `json:"pid"` + Force bool `json:"force,omitempty"` + Wait bool `json:"wait,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +// list_processes 结果 +type processInfo struct { + PID int32 `json:"pid"` + PPID int32 `json:"ppid"` + Executable string `json:"executable"` + Owner string `json:"owner"` + Architecture string `json:"architecture,omitempty"` + SessionID int32 `json:"session_id,omitempty"` + CmdLine []string `json:"cmd_line,omitempty"` +} + +type listProcessesResult struct { + Processes []processInfo `json:"processes"` + Count int `json:"count"` +} + +// kill_process 结果 +type killProcessResult struct { + PID int32 `json:"pid"` + Success bool `json:"success"` +} + +// list_processes 处理器 +func (s *SliverMCPServer) listProcessesHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + var args listProcessesArgs + if err := request.BindArguments(&args); err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("invalid arguments: %v", err)), nil + } + + return s.handleListProcesses(ctx, args) +} + +func (s *SliverMCPServer) handleListProcesses(ctx context.Context, args listProcessesArgs) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + s.logToolCall(listProcessesToolName, args.SessionID, args.BeaconID) + + args.TimeoutSeconds = applyDefaultTimeout(args.Wait, args.TimeoutSeconds) + ctx, cancel := withTimeout(ctx, args.TimeoutSeconds) + if cancel != nil { + defer cancel() + } + + req, isBeacon, err := buildRequest(args.SessionID, args.BeaconID, args.TimeoutSeconds) + if err != nil { + return mcpapi.NewToolResultError(err.Error()), nil + } + + psResp, err := s.Rpc.Ps(ctx, &sliverpb.PsReq{ + Request: req, + FullInfo: args.FullInfo, + }) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to list processes", err), nil + } + + if psResp.Response != nil && psResp.Response.Err != "" { + return mcpapi.NewToolResultError(psResp.Response.Err), nil + } + + if isBeacon && psResp.Response != nil && psResp.Response.Async { + if !args.Wait { + return newAsyncResult("list_processes", psResp.Response.TaskID, psResp.Response.BeaconID), nil + } + resolved := &sliverpb.Ps{} + if err := s.waitForBeaconTaskResponse(ctx, psResp.Response.TaskID, resolved); err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to await list processes task", err), nil + } + psResp = resolved + if psResp.Response != nil && psResp.Response.Err != "" { + return mcpapi.NewToolResultError(psResp.Response.Err), nil + } + } + + result := listProcessesResult{ + Processes: make([]processInfo, 0, len(psResp.Processes)), + } + + for _, proc := range psResp.Processes { + if proc == nil { + continue + } + result.Processes = append(result.Processes, processInfo{ + PID: proc.Pid, + PPID: proc.Ppid, + Executable: proc.Executable, + Owner: proc.Owner, + Architecture: proc.Architecture, + SessionID: proc.SessionID, + CmdLine: proc.CmdLine, + }) + } + + result.Count = len(result.Processes) + return mcpapi.NewToolResultStructuredOnly(result), nil +} + +// kill_process 处理器 +func (s *SliverMCPServer) killProcessHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + var args killProcessArgs + if err := request.BindArguments(&args); err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("invalid arguments: %v", err)), nil + } + + if args.PID <= 0 { + return mcpapi.NewToolResultError("pid is required and must be positive"), nil + } + + return s.handleKillProcess(ctx, args) +} + +func (s *SliverMCPServer) handleKillProcess(ctx context.Context, args killProcessArgs) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + s.logToolCall( + killProcessToolName, + args.SessionID, + args.BeaconID, + fmt.Sprintf("pid=%d", args.PID), + fmt.Sprintf("force=%t", args.Force), + ) + + args.TimeoutSeconds = applyDefaultTimeout(args.Wait, args.TimeoutSeconds) + ctx, cancel := withTimeout(ctx, args.TimeoutSeconds) + if cancel != nil { + defer cancel() + } + + req, isBeacon, err := buildRequest(args.SessionID, args.BeaconID, args.TimeoutSeconds) + if err != nil { + return mcpapi.NewToolResultError(err.Error()), nil + } + + terminateResp, err := s.Rpc.Terminate(ctx, &sliverpb.TerminateReq{ + Request: req, + Pid: args.PID, + Force: args.Force, + }) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to terminate process", err), nil + } + + if terminateResp.Response != nil && terminateResp.Response.Err != "" { + return mcpapi.NewToolResultError(terminateResp.Response.Err), nil + } + + if isBeacon && terminateResp.Response != nil && terminateResp.Response.Async { + if !args.Wait { + return newAsyncResult("kill_process", terminateResp.Response.TaskID, terminateResp.Response.BeaconID), nil + } + resolved := &sliverpb.Terminate{} + if err := s.waitForBeaconTaskResponse(ctx, terminateResp.Response.TaskID, resolved); err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to await kill process task", err), nil + } + terminateResp = resolved + if terminateResp.Response != nil && terminateResp.Response.Err != "" { + return mcpapi.NewToolResultError(terminateResp.Response.Err), nil + } + } + + return mcpapi.NewToolResultStructuredOnly(killProcessResult{ + PID: terminateResp.Pid, + Success: true, + }), nil +} diff --git a/client/mcp/server.go b/client/mcp/server.go index 121f285241..c9fd792e07 100644 --- a/client/mcp/server.go +++ b/client/mcp/server.go @@ -152,6 +152,73 @@ func newServer(cfg Config, rpc rpcpb.SliverRPCClient, logger *log.Logger) *Slive mcpapi.WithReadOnlyHintAnnotation(false), mcpapi.WithDestructiveHintAnnotation(true), ) + executeCommandTool := mcpapi.NewTool( + executeCommandToolName, + mcpapi.WithDescription("Execute a command on the remote target system."), + mcpapi.WithInputSchema[executeCommandArgs](), + mcpapi.WithReadOnlyHintAnnotation(false), + mcpapi.WithDestructiveHintAnnotation(true), + ) + uploadFileTool := mcpapi.NewTool( + uploadFileToolName, + mcpapi.WithDescription("Upload a file to the remote target system."), + mcpapi.WithInputSchema[uploadFileArgs](), + mcpapi.WithReadOnlyHintAnnotation(false), + mcpapi.WithDestructiveHintAnnotation(false), + ) + getSystemInfoTool := mcpapi.NewTool( + getSystemInfoToolName, + mcpapi.WithDescription("Get detailed system information from the remote target."), + mcpapi.WithInputSchema[getSystemInfoArgs](), + mcpapi.WithReadOnlyHintAnnotation(true), + mcpapi.WithDestructiveHintAnnotation(false), + mcpapi.WithIdempotentHintAnnotation(true), + ) + listPivotsTool := mcpapi.NewTool( + listPivotsToolName, + mcpapi.WithDescription("List all active pivot listeners and connections."), + mcpapi.WithReadOnlyHintAnnotation(true), + mcpapi.WithDestructiveHintAnnotation(false), + mcpapi.WithIdempotentHintAnnotation(true), + ) + listProcessesTool := mcpapi.NewTool( + listProcessesToolName, + mcpapi.WithDescription("List processes running on the remote target system."), + mcpapi.WithInputSchema[listProcessesArgs](), + mcpapi.WithReadOnlyHintAnnotation(true), + mcpapi.WithDestructiveHintAnnotation(false), + mcpapi.WithIdempotentHintAnnotation(true), + ) + killProcessTool := mcpapi.NewTool( + killProcessToolName, + mcpapi.WithDescription("Terminate a process on the remote target system by PID."), + mcpapi.WithInputSchema[killProcessArgs](), + mcpapi.WithReadOnlyHintAnnotation(false), + mcpapi.WithDestructiveHintAnnotation(true), + ) + listCredentialsTool := mcpapi.NewTool( + listCredentialsToolName, + mcpapi.WithDescription("List credentials collected from target systems."), + mcpapi.WithReadOnlyHintAnnotation(true), + mcpapi.WithDestructiveHintAnnotation(false), + mcpapi.WithIdempotentHintAnnotation(true), + ) + networkInterfacesTool := mcpapi.NewTool( + networkInterfacesToolName, + mcpapi.WithDescription("List network interfaces on the remote target system."), + mcpapi.WithInputSchema[networkInterfacesArgs](), + mcpapi.WithReadOnlyHintAnnotation(true), + mcpapi.WithDestructiveHintAnnotation(false), + mcpapi.WithIdempotentHintAnnotation(true), + ) + netstatTool := mcpapi.NewTool( + netstatToolName, + mcpapi.WithDescription("List network connections on the remote target system (similar to netstat/ss)."), + mcpapi.WithInputSchema[netstatArgs](), + mcpapi.WithReadOnlyHintAnnotation(true), + mcpapi.WithDestructiveHintAnnotation(false), + mcpapi.WithIdempotentHintAnnotation(true), + ) srv := &SliverMCPServer{ Rpc: rpc, server: base, @@ -168,6 +235,15 @@ func newServer(cfg Config, rpc rpcpb.SliverRPCClient, logger *log.Logger) *Slive srv.server.AddTool(mkdirTool, srv.mkdirHandler) srv.server.AddTool(chmodTool, srv.chmodHandler) srv.server.AddTool(chownTool, srv.chownHandler) + srv.server.AddTool(executeCommandTool, srv.executeCommandHandler) + srv.server.AddTool(uploadFileTool, srv.uploadFileHandler) + srv.server.AddTool(getSystemInfoTool, srv.getSystemInfoHandler) + srv.server.AddTool(listPivotsTool, srv.listPivotsHandler) + srv.server.AddTool(listProcessesTool, srv.listProcessesHandler) + srv.server.AddTool(killProcessTool, srv.killProcessHandler) + srv.server.AddTool(listCredentialsTool, srv.listCredentialsHandler) + srv.server.AddTool(networkInterfacesTool, srv.networkInterfacesHandler) + srv.server.AddTool(netstatTool, srv.netstatHandler) return srv } diff --git a/client/mcp/systeminfo.go b/client/mcp/systeminfo.go new file mode 100644 index 0000000000..e21e51fe95 --- /dev/null +++ b/client/mcp/systeminfo.go @@ -0,0 +1,105 @@ +package mcp + +import ( + "context" + "fmt" + + "github.com/bishopfox/sliver/protobuf/sliverpb" + mcpapi "github.com/mark3labs/mcp-go/mcp" +) + +const ( + getSystemInfoToolName = "get_system_info" +) + +type getSystemInfoArgs struct { + SessionID string `json:"session_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + Wait bool `json:"wait,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +type systemInfoResult struct { + Hostname string `json:"hostname"` + OS string `json:"os"` + Version string `json:"version"` + Arch string `json:"arch"` + Username string `json:"username"` + UID string `json:"uid,omitempty"` + GID string `json:"gid,omitempty"` + PID int32 `json:"pid"` + Locale string `json:"locale,omitempty"` + ActiveC2 string `json:"active_c2,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` +} + +func (s *SliverMCPServer) getSystemInfoHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + var args getSystemInfoArgs + if err := request.BindArguments(&args); err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("invalid arguments: %v", err)), nil + } + + return s.handleGetSystemInfo(ctx, args) +} + +func (s *SliverMCPServer) handleGetSystemInfo(ctx context.Context, args getSystemInfoArgs) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + s.logToolCall(getSystemInfoToolName, args.SessionID, args.BeaconID) + + args.TimeoutSeconds = applyDefaultTimeout(args.Wait, args.TimeoutSeconds) + ctx, cancel := withTimeout(ctx, args.TimeoutSeconds) + if cancel != nil { + defer cancel() + } + + req, isBeacon, err := buildRequest(args.SessionID, args.BeaconID, args.TimeoutSeconds) + if err != nil { + return mcpapi.NewToolResultError(err.Error()), nil + } + + sysInfoReq := &sliverpb.SystemInfoReq{ + Request: req, + } + + sysInfoResp, err := s.Rpc.SystemInfo(ctx, sysInfoReq) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to get system info", err), nil + } + + if sysInfoResp.Response != nil && sysInfoResp.Response.Err != "" { + return mcpapi.NewToolResultError(sysInfoResp.Response.Err), nil + } + + if isBeacon && sysInfoResp.Response != nil && sysInfoResp.Response.Async { + if !args.Wait { + return newAsyncResult("get_system_info", sysInfoResp.Response.TaskID, sysInfoResp.Response.BeaconID), nil + } + resolved := &sliverpb.SystemInfo{} + if err := s.waitForBeaconTaskResponse(ctx, sysInfoResp.Response.TaskID, resolved); err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to await system info task", err), nil + } + sysInfoResp = resolved + if sysInfoResp.Response != nil && sysInfoResp.Response.Err != "" { + return mcpapi.NewToolResultError(sysInfoResp.Response.Err), nil + } + } + + result := systemInfoResult{ + Hostname: sysInfoResp.Hostname, + OS: sysInfoResp.OS, + Version: sysInfoResp.Version, + Arch: sysInfoResp.Arch, + Username: sysInfoResp.Username, + UID: sysInfoResp.Uid, + GID: sysInfoResp.Gid, + PID: sysInfoResp.Pid, + Locale: sysInfoResp.Locale, + ActiveC2: sysInfoResp.ActiveC2, + ProxyURL: sysInfoResp.ProxyURL, + } + + return mcpapi.NewToolResultStructuredOnly(result), nil +} diff --git a/client/mcp/upload.go b/client/mcp/upload.go new file mode 100644 index 0000000000..4f98dd2f96 --- /dev/null +++ b/client/mcp/upload.go @@ -0,0 +1,146 @@ +package mcp + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "path/filepath" + + "github.com/bishopfox/sliver/protobuf/sliverpb" + "github.com/bishopfox/sliver/util/encoders" + mcpapi "github.com/mark3labs/mcp-go/mcp" +) + +const ( + uploadFileToolName = "upload_file" +) + +type uploadFileArgs struct { + SessionID string `json:"session_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + LocalPath string `json:"local_path,omitempty"` + RemotePath string `json:"remote_path"` + DataBase64 string `json:"data_base64,omitempty"` + Overwrite bool `json:"overwrite,omitempty"` + Wait bool `json:"wait,omitempty"` + TimeoutSeconds int64 `json:"timeout_seconds,omitempty"` +} + +type uploadFileResult struct { + LocalPath string `json:"local_path,omitempty"` + RemotePath string `json:"remote_path"` + BytesWritten int64 `json:"bytes_written"` + Async bool `json:"async"` + Operation string `json:"operation,omitempty"` + TaskID string `json:"task_id,omitempty"` + BeaconID string `json:"beacon_id,omitempty"` + State string `json:"state,omitempty"` +} + +func (s *SliverMCPServer) uploadFileHandler(ctx context.Context, request mcpapi.CallToolRequest) (*mcpapi.CallToolResult, error) { + var args uploadFileArgs + if err := request.BindArguments(&args); err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("invalid arguments: %v", err)), nil + } + + if args.RemotePath == "" { + return mcpapi.NewToolResultError("remote_path is required"), nil + } + + if args.LocalPath == "" && args.DataBase64 == "" { + return mcpapi.NewToolResultError("either local_path or data_base64 is required"), nil + } + + return s.handleUploadFile(ctx, args) +} + +func (s *SliverMCPServer) handleUploadFile(ctx context.Context, args uploadFileArgs) (*mcpapi.CallToolResult, error) { + if s.Rpc == nil { + return mcpapi.NewToolResultError("rpc client not configured"), nil + } + + extras := []string{fmt.Sprintf("remote_path=%q", args.RemotePath)} + if args.LocalPath != "" { + extras = append(extras, fmt.Sprintf("local_path=%q", args.LocalPath)) + } + if args.Overwrite { + extras = append(extras, "overwrite=true") + } + s.logToolCall(uploadFileToolName, args.SessionID, args.BeaconID, extras...) + + args.TimeoutSeconds = applyDefaultTimeout(args.Wait, args.TimeoutSeconds) + ctx, cancel := withTimeout(ctx, args.TimeoutSeconds) + if cancel != nil { + defer cancel() + } + + req, isBeacon, err := buildRequest(args.SessionID, args.BeaconID, args.TimeoutSeconds) + if err != nil { + return mcpapi.NewToolResultError(err.Error()), nil + } + + var data []byte + if args.LocalPath != "" { + // Read from local file + fileData, err := os.ReadFile(args.LocalPath) + if err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("failed to read local file: %v", err)), nil + } + data = fileData + } else if args.DataBase64 != "" { + // Decode base64 data + decoded, err := base64.StdEncoding.DecodeString(args.DataBase64) + if err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("failed to decode base64 data: %v", err)), nil + } + data = decoded + } + + // Encode data for transmission + encoder := new(encoders.Gzip) + encodedData, err := encoder.Encode(data) + if err != nil { + return mcpapi.NewToolResultError(fmt.Sprintf("failed to encode data: %v", err)), nil + } + + uploadReq := &sliverpb.UploadReq{ + Request: req, + Path: args.RemotePath, + Data: encodedData, + Encoder: "gzip", + FileName: filepath.Base(args.RemotePath), + Overwrite: args.Overwrite, + } + + uploadResp, err := s.Rpc.Upload(ctx, uploadReq) + if err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to upload file", err), nil + } + + if uploadResp.Response != nil && uploadResp.Response.Err != "" { + return mcpapi.NewToolResultError(uploadResp.Response.Err), nil + } + + if isBeacon && uploadResp.Response != nil && uploadResp.Response.Async { + if !args.Wait { + return newAsyncResult("upload_file", uploadResp.Response.TaskID, uploadResp.Response.BeaconID), nil + } + resolved := &sliverpb.Upload{} + if err := s.waitForBeaconTaskResponse(ctx, uploadResp.Response.TaskID, resolved); err != nil { + return mcpapi.NewToolResultErrorFromErr("failed to await upload task", err), nil + } + uploadResp = resolved + if uploadResp.Response != nil && uploadResp.Response.Err != "" { + return mcpapi.NewToolResultError(uploadResp.Response.Err), nil + } + } + + result := uploadFileResult{ + LocalPath: args.LocalPath, + RemotePath: uploadResp.Path, + BytesWritten: int64(len(data)), + } + + return mcpapi.NewToolResultStructuredOnly(result), nil +} diff --git a/docs/ai-agent-integration.md b/docs/ai-agent-integration.md new file mode 100644 index 0000000000..908ca17abb --- /dev/null +++ b/docs/ai-agent-integration.md @@ -0,0 +1,247 @@ +# AI Agent Integration Guide + +This guide explains how to integrate AI Agents with Sliver C2 using the MCP (Model Context Protocol) Server and CLI structured output features. + +## MCP Server + +Sliver provides a built-in MCP Server that allows AI Agents to interact with the C2 framework programmatically. + +### Starting the MCP Server + +```bash +# Start MCP server with HTTP transport +sliver > mcp start --transport http --listen 127.0.0.1:8080 + +# Start MCP server with SSE transport +sliver > mcp start --transport sse --listen 127.0.0.1:8080 + +# Start MCP server via stdio (for direct AI Agent integration) +sliver mcp --stdio +``` + +### Available MCP Tools + +The MCP Server exposes the following tools for AI Agents: + +#### Session Management +- **list_sessions_and_beacons** - List all active sessions and beacons + - Returns: Array of sessions and beacons with their metadata + +#### File System Operations +- **fs_ls** - List directory contents on remote target +- **fs_cd** - Change directory on remote target +- **fs_cat** - Download and read file contents from remote target +- **fs_pwd** - Get current working directory on remote target +- **fs_rm** - Remove files or directories on remote target +- **fs_mv** - Move or rename files on remote target +- **fs_cp** - Copy files on remote target +- **fs_mkdir** - Create directories on remote target +- **fs_chmod** - Change file permissions on remote target +- **fs_chown** - Change file ownership on remote target + +#### Command Execution +- **execute_command** - Execute commands on remote target + - Parameters: `command`, `args[]`, `env[]`, `background`, `output` + - Returns: stdout, stderr, exit status + +#### File Transfer +- **upload_file** - Upload files to remote target + - Parameters: `local_path` or `data_base64`, `remote_path`, `overwrite` + - Returns: bytes written, remote path + +#### System Information +- **get_system_info** - Get detailed system information from remote target + - Returns: hostname, OS, version, arch, username, PID, locale, active C2, proxy URL + +#### Network Operations +- **list_pivots** - List all active pivot listeners and connections + - Returns: Array of pivot listeners with their configuration + +### Authentication + +The MCP Server supports token-based authentication: + +```bash +# Set auth token via environment variable +export SLIVER_MCP_TOKEN=your-secret-token + +# Or configure in mcp.yaml +``` + +### Example: Using with Claude Desktop + +Add to your Claude Desktop configuration: + +```json +{ + "mcpServers": { + "sliver": { + "command": "sliver", + "args": ["mcp", "--stdio"], + "env": { + "SLIVER_MCP_TOKEN": "your-token" + } + } + } +} +``` + +## CLI Structured Output + +Sliver CLI supports structured output formats for automation and AI Agent integration. + +### JSON Output + +```bash +# List sessions in JSON format +sliver sessions --output-format json + +# Example output: +{ + "sessions": [ + { + "id": "abc123", + "name": "target-01", + "remote_address": "192.168.1.100", + "hostname": "WORKSTATION", + "username": "admin", + "os": "Windows 10", + "arch": "amd64", + "pid": 1234, + "last_checkin": 1704123456 + } + ], + "sessions_count": 1 +} +``` + +### YAML Output + +```bash +# List sessions in YAML format +sliver sessions --output-format yaml + +# Example output: +sessions: + - id: abc123 + name: target-01 + remote_address: 192.168.1.100 + hostname: WORKSTATION + username: admin + os: Windows 10 + arch: amd64 + pid: 1234 + last_checkin: 1704123456 +sessions_count: 1 +``` + +### Supported Commands + +The following commands support `--output-format` parameter: +- `sessions` - List active sessions +- `beacons` - List active beacons +- `jobs` - List running jobs +- `operators` - List operators +- `loot` - List loot items +- `creds` - List credentials +- `hosts` - List known hosts + +## AI Agent Best Practices + +### 1. Session Management + +```python +# Example: List sessions and select one +sessions = mcp_client.call_tool("list_sessions_and_beacons", {}) +active_session = sessions["sessions"][0] +session_id = active_session["id"] +``` + +### 2. Command Execution + +```python +# Execute a command and get output +result = mcp_client.call_tool("execute_command", { + "session_id": session_id, + "command": "whoami", + "args": [], + "output": True +}) +print(result["stdout"]) +``` + +### 3. File Operations + +```python +# Upload a file +upload_result = mcp_client.call_tool("upload_file", { + "session_id": session_id, + "local_path": "/path/to/local/file", + "remote_path": "/tmp/uploaded_file" +}) + +# Download a file +download_result = mcp_client.call_tool("fs_cat", { + "session_id": session_id, + "path": "/etc/passwd" +}) +file_content = base64.b64decode(download_result["data_base64"]) +``` + +### 4. System Reconnaissance + +```python +# Get system information +sysinfo = mcp_client.call_tool("get_system_info", { + "session_id": session_id +}) +print(f"OS: {sysinfo['os']}, Arch: {sysinfo['arch']}") +``` + +### 5. Beacon Operations (Async) + +```python +# For beacons, operations are async +result = mcp_client.call_tool("execute_command", { + "beacon_id": beacon_id, + "command": "whoami", + "wait": True, # Wait for result + "timeout_seconds": 60 +}) + +# Or check status later +if result["async"]: + task_id = result["task_id"] + # Poll for completion or use task ID to retrieve results +``` + +## Security Considerations + +1. **Authentication**: Always use authentication tokens for MCP Server +2. **Network Isolation**: Bind MCP Server to localhost unless remote access is required +3. **Token Rotation**: Regularly rotate MCP authentication tokens +4. **Audit Logging**: Monitor MCP Server logs for unauthorized access attempts +5. **Least Privilege**: Grant AI Agents only the permissions they need + +## Troubleshooting + +### MCP Server won't start +- Check if port is already in use +- Verify Sliver server is running and accessible +- Check logs with `sliver mcp` to see current status + +### Authentication failures +- Verify SLIVER_MCP_TOKEN environment variable is set +- Check mcp.yaml configuration file +- Ensure token matches between client and server + +### Command execution timeouts +- Increase `timeout_seconds` parameter for long-running commands +- Use `background: true` for commands that don't need immediate output +- Check network connectivity to target session + +## Additional Resources + +- [MCP Protocol Specification](https://modelcontextprotocol.io/) +- [Sliver Documentation](https://sliver.sh/) +- [AI Agent Integration Examples](https://github.com/BishopFox/sliver/tree/master/examples/ai-agents)