Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensions cmd improvements #1862

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions client/command/extensions/commands.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
package extensions

import (
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/command/use"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// Commands returns the command and its subcommands.
// Commands returns the 'extensions' command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command {
extensionCmd := &cobra.Command{
Use: consts.ExtensionsStr,
Short: "Manage extensions",
Long: help.GetHelpFor([]string{consts.ExtensionsStr}),
GroupID: consts.ExecutionHelpGroup,
GroupID: consts.GenericHelpGroup,
Run: func(cmd *cobra.Command, _ []string) {
ExtensionsCmd(cmd, con)
},
}

extensionCmd.AddCommand(&cobra.Command{
Use: consts.ListStr,
Short: "List extensions loaded in the current session or beacon",
Long: help.GetHelpFor([]string{consts.ExtensionsStr, consts.ListStr}),
Run: func(cmd *cobra.Command, args []string) {
ExtensionsListCmd(cmd, con, args)
},
})

extensionLoadCmd := &cobra.Command{
Use: consts.LoadStr,
Short: "Temporarily load an extension from a local directory",
Expand Down Expand Up @@ -66,3 +60,30 @@ func Commands(con *console.SliverClient) []*cobra.Command {

return []*cobra.Command{extensionCmd}
}

func SliverCommands(con *console.SliverClient) []*cobra.Command {
extensionCmd := &cobra.Command{
Use: consts.ExtensionsStr,
Short: "Manage extensions",
Long: help.GetHelpFor([]string{consts.ExtensionsStr}),
GroupID: consts.InfoHelpGroup,
Annotations: flags.RestrictTargets(consts.SessionCmdsFilter), // restrict to session targets since we cannot `list` "loaded" extensions from beacon mode
}

listCmd := &cobra.Command{
Use: consts.ListStr,
Short: "List extensions loaded in the current session",
Long: help.GetHelpFor([]string{consts.ExtensionsStr, consts.ListStr}),
Run: func(cmd *cobra.Command, args []string) {
ExtensionsListCmd(cmd, con, args)
},
}
flags.Bind("use", false, listCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
extensionCmd.AddCommand(listCmd)

carapace.Gen(listCmd).PositionalCompletion(use.BeaconAndSessionIDCompleter(con))

return []*cobra.Command{extensionCmd}
}
51 changes: 48 additions & 3 deletions client/command/extensions/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/bishopfox/sliver/client/assets"
Expand All @@ -35,7 +36,7 @@ import (

// ExtensionsCmd - List information about installed extensions.
func ExtensionsCmd(cmd *cobra.Command, con *console.SliverClient) {
if 0 < len(getInstalledManifests()) {
if len(GetAllExtensionManifests()) > 0 {
PrintExtensions(con)
} else {
con.PrintInfof("No extensions installed, use the 'armory' command to automatically install some\n")
Expand Down Expand Up @@ -67,8 +68,12 @@ func PrintExtensions(con *console.SliverClient) {
for _, extension := range loadedExtensions {
//for _, extension := range extensionm.ExtCommand {
installed := ""
if _, ok := installedManifests[extension.Manifest.Name]; ok {
installed = "✅"
//if _, ok := installedManifests[extension.Manifest.Name]; ok {
for _, installedManifest := range installedManifests {
if extension.Manifest.RootPath == installedManifest.RootPath {
installed = "✅"
break
}
}
tw.AppendRow(table.Row{
extension.Manifest.Name,
Expand Down Expand Up @@ -97,6 +102,9 @@ func extensionPlatforms(extension *ExtCommand) []string {
return keys
}

// getInstalledManifests - Returns a mapping of extension names to their parsed manifest objects.
// Reads all installed extension manifests from disk, ignoring any that cannot be read or parsed.
// The returned manifests have their RootPath set to the directory containing their manifest file.
func getInstalledManifests() map[string]*ExtensionManifest {
manifestPaths := assets.GetInstalledExtensionManifests()
installedManifests := map[string]*ExtensionManifest{}
Expand All @@ -110,11 +118,48 @@ func getInstalledManifests() map[string]*ExtensionManifest {
if err != nil {
continue
}
manifest.RootPath = filepath.Dir(manifestPath)
installedManifests[manifest.Name] = manifest
}
return installedManifests
}

// getTemporarilyLoadedManifests returns a map of extension manifests that are currently
// loaded into memory but not permanently installed. The map is keyed by the manifest's
// Name field.
func getTemporarilyLoadedManifests() map[string]*ExtensionManifest {
tempManifests := map[string]*ExtensionManifest{}
for name, manifest := range loadedManifests {
tempManifests[name] = manifest
}
return tempManifests
}

// GetAllExtensionManifests returns a combined list of manifest file paths from
// both installed and temporarily loaded extensions
func GetAllExtensionManifests() []string {
manifestPaths := make(map[string]struct{}) // use map for deduplication

// Add installed manifests
for _, manifest := range getInstalledManifests() {
manifestPath := filepath.Join(manifest.RootPath, ManifestFileName)
manifestPaths[manifestPath] = struct{}{}
}

// Add temporarily loaded manifests
for _, manifest := range getTemporarilyLoadedManifests() {
manifestPath := filepath.Join(manifest.RootPath, ManifestFileName)
manifestPaths[manifestPath] = struct{}{}
}

// Convert to slice
paths := make([]string, 0, len(manifestPaths))
for path := range manifestPaths {
paths = append(paths, path)
}
return paths
}

// ExtensionsCommandNameCompleter - Completer for installed extensions command names.
func ExtensionsCommandNameCompleter(con *console.SliverClient) carapace.Action {
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
Expand Down
26 changes: 24 additions & 2 deletions client/command/extensions/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,22 @@ func ExtensionsInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []
InstallFromDir(extLocalPath, true, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
}

// Install an extension from a directory
// InstallFromDir installs a Sliver extension from either a local directory or gzipped archive.
// It reads the extension manifest, validates it, and copies all required files to the extensions
// directory. If an extension with the same name already exists, it can optionally prompt for
// overwrite confirmation.
//
// Parameters:
// - extLocalPath: Path to the source directory or gzipped archive containing the extension
// - promptToOverwrite: If true, prompts for confirmation before overwriting existing extension
// - con: Sliver console client for displaying status and error messages
// - isGz: Whether the source is a gzipped archive (true) or directory (false)
//
// The function will return early with error messages printed to console if:
// - The manifest cannot be read or parsed
// - Required directories cannot be created
// - File copy operations fail
// - User declines overwrite when prompted
func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.SliverClient, isGz bool) {
var manifestData []byte
var err error
Expand All @@ -64,8 +79,15 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
return
}

// Use package name if available, otherwise use extension name
// (Note, for v1 manifests this will actually be command_name)
packageID := manifestF.Name
if manifestF.PackageName != "" {
packageID = manifestF.PackageName
}

//create repo path
minstallPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(manifestF.Name))
minstallPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(packageID))
if _, err := os.Stat(minstallPath); !os.IsNotExist(err) {
if promptToOverwrite {
con.PrintInfof("Extension '%s' already exists", manifestF.Name)
Expand Down
116 changes: 111 additions & 5 deletions client/command/extensions/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,116 @@ package extensions

import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"

"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)

// ExtensionMatch holds the details of a matched extension

type ExtensionMatch struct {
CommandName string
Hash string
BinPath string
}

// FindExtensionMatches searches through loaded extensions for matching hashes
// Returns a map of hash to ExtensionMatch (match will be nil if hash wasn't found)
func FindExtensionMatches(targetHashes []string) map[string]*ExtensionMatch {
results := make(map[string]*ExtensionMatch)
pathCache := make(map[string]*ExtensionMatch)

// Initialize results map with all target hashes
for _, hash := range targetHashes {
results[hash] = nil
}

// Search for matches
for targetHash := range results {
for _, extCmd := range loadedExtensions {
if extCmd == nil || len(extCmd.Files) == 0 {
continue
}

for _, file := range extCmd.Files {
fullPath := filepath.Join(extCmd.Manifest.RootPath, file.Path)

// Check cache first
var match *ExtensionMatch
if cached, exists := pathCache[fullPath]; exists {
match = cached
} else {
// Calculate hash if not cached
fileData, err := os.ReadFile(fullPath)
if err != nil {
continue
}

hashBytes := sha256.Sum256(fileData)
fileHash := hex.EncodeToString(hashBytes[:])

match = &ExtensionMatch{
CommandName: extCmd.CommandName,
Hash: fileHash,
BinPath: fullPath,
}
pathCache[fullPath] = match
}

if match.Hash == targetHash {
results[targetHash] = match
break
}
}

if results[targetHash] != nil {
break
}
}
}

return results
}

// PrintExtensionMatches prints the extension matches in a formatted table
func PrintExtensionMatches(matches map[string]*ExtensionMatch, con *console.SliverClient) {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
"Command Name",
"Sha256 Hash",
"Bin Path",
})
tw.SortBy([]table.SortBy{
{Name: "Command Name", Mode: table.Asc},
})

for hash, match := range matches {
if match != nil {
tw.AppendRow(table.Row{
match.CommandName,
hash,
match.BinPath,
})
} else {
tw.AppendRow(table.Row{
"",
hash,
"",
})
}
}

con.Println(tw.Render())
}

// ExtensionsListCmd - List all extension loaded on the active session/beacon.
func ExtensionsListCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session := con.ActiveTarget.GetSessionInteractive()
Expand All @@ -45,10 +149,12 @@ func ExtensionsListCmd(cmd *cobra.Command, con *console.SliverClient, args []str
con.PrintErrorf("%s\n", extList.Response.Err)
return
}
if len(extList.Names) > 0 {
con.PrintInfof("Loaded extensions:\n")
for _, ext := range extList.Names {
con.Printf("- %s\n", ext)
}

if len(extList.Names) == 0 {
return
}

con.PrintInfof("Loaded extensions:\n\n")
matches := FindExtensionMatches(extList.Names)
PrintExtensionMatches(matches, con)
}
Loading
Loading