mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a05dfad100 | ||
|
|
d9c89c81cf | ||
|
|
061221905b | ||
|
|
d0600e2be7 | ||
|
|
26abbe3860 | ||
|
|
2bfdb218ff | ||
|
|
715b84e799 | ||
|
|
faea2ff639 | ||
|
|
1b253f8dec | ||
|
|
eb38724322 | ||
|
|
05dedf4473 | ||
|
|
28c6b82a79 | ||
|
|
0bf0ac6c7b | ||
|
|
0cf5a47cfd | ||
|
|
8d6759924b | ||
|
|
8d5dbd2050 | ||
|
|
804e149113 | ||
|
|
9ced4cfd50 | ||
|
|
b7abf98c95 | ||
|
|
918ac1bb91 | ||
|
|
1e57818312 | ||
|
|
c44c14dd46 | ||
|
|
35f41cd62d | ||
|
|
4eff1cc053 | ||
|
|
16981da431 | ||
|
|
3830c76292 | ||
|
|
73de93d6fb | ||
|
|
c7770c2f6e | ||
|
|
21db908ce4 | ||
|
|
b7b0247652 | ||
|
|
3dc7dc03d0 | ||
|
|
229452d7ed | ||
|
|
d29ce9a08b | ||
|
|
46f2b2b402 | ||
|
|
6ee2006387 | ||
|
|
2fd13ee2bc | ||
|
|
2697568336 | ||
|
|
5013048b0b | ||
|
|
627b15572c | ||
|
|
42c40c9b8d | ||
|
|
405b282fed | ||
|
|
e32cd36e01 | ||
|
|
492a2aeed6 | ||
|
|
d6dafb0391 | ||
|
|
49a1bcfd7d | ||
|
|
a8d1bf6fb1 | ||
|
|
2f386b7e09 | ||
|
|
d7dce58397 | ||
|
|
15114138a8 | ||
|
|
2716090410 | ||
|
|
a805161245 | ||
|
|
afd57891cd | ||
|
|
bdf4a6ed0d |
No files matched your search
@@ -6,6 +6,8 @@ The server and client support MacOS, Windows, and Linux. Implants are supported
|
||||
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://goreportcard.com/report/github.com/BishopFox/sliver) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
|
||||
|
||||
### Features
|
||||
|
||||
- Dynamic code generation
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -31,6 +32,18 @@ const (
|
||||
settingsLegacyFileName = "tui-settings.json"
|
||||
)
|
||||
|
||||
const (
|
||||
// Accepted values for tui-settings.yaml "prompt:":
|
||||
// - host: show "[host]" prefix on sliver-client, "[server]" on server console
|
||||
// - operator-host: show "[operator@host]" prefix on sliver-client, "[server]" on server console
|
||||
// - basic: show "sliver >" (no prefix)
|
||||
// - custom: render full prompt from prompt_template
|
||||
PromptStyleHost = "host"
|
||||
PromptStyleOperatorHost = "operator-host"
|
||||
PromptStyleBasic = "basic"
|
||||
PromptStyleCustom = "custom"
|
||||
)
|
||||
|
||||
// ClientSettings - Client JSON config
|
||||
type ClientSettings struct {
|
||||
TableStyle string `json:"tables" yaml:"tables"`
|
||||
@@ -41,6 +54,8 @@ type ClientSettings struct {
|
||||
VimMode bool `json:"vim_mode" yaml:"vim_mode"`
|
||||
UserConnect bool `json:"user_connect" yaml:"user_connect"`
|
||||
ConsoleLogs bool `json:"console_logs" yaml:"console_logs"`
|
||||
PromptStyle string `json:"prompt" yaml:"prompt"`
|
||||
PromptTemplate string `json:"prompt_template" yaml:"prompt_template"`
|
||||
}
|
||||
|
||||
// LoadSettings - Load the client settings from disk
|
||||
@@ -67,6 +82,8 @@ func LoadSettings() (*ClientSettings, error) {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
|
||||
// Ensure any missing/unknown values are coerced to a supported prompt style.
|
||||
settings.PromptStyle = NormalizePromptStyle(settings.PromptStyle)
|
||||
if err := SaveSettings(settings); err != nil {
|
||||
return settings, err
|
||||
}
|
||||
@@ -78,6 +95,38 @@ func LoadSettings() (*ClientSettings, error) {
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// NormalizePromptStyle canonicalizes prompt style strings and returns a safe
|
||||
// default if the value is empty/unknown.
|
||||
func NormalizePromptStyle(v string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case PromptStyleOperatorHost:
|
||||
return PromptStyleOperatorHost
|
||||
case PromptStyleHost:
|
||||
return PromptStyleHost
|
||||
case PromptStyleBasic:
|
||||
return PromptStyleBasic
|
||||
case PromptStyleCustom:
|
||||
return PromptStyleCustom
|
||||
|
||||
// Backward compatible aliases.
|
||||
case "show host":
|
||||
return PromptStyleHost
|
||||
case "show user and host":
|
||||
return PromptStyleOperatorHost
|
||||
case "show operator and host":
|
||||
return PromptStyleOperatorHost
|
||||
case "operator@host":
|
||||
return PromptStyleOperatorHost
|
||||
case "user@host":
|
||||
return PromptStyleOperatorHost
|
||||
|
||||
case "":
|
||||
return PromptStyleHost
|
||||
default:
|
||||
return PromptStyleHost
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSettings() *ClientSettings {
|
||||
return &ClientSettings{
|
||||
TableStyle: "SliverDefault",
|
||||
@@ -87,9 +136,13 @@ func defaultSettings() *ClientSettings {
|
||||
AlwaysOverflow: false,
|
||||
VimMode: false,
|
||||
ConsoleLogs: true,
|
||||
PromptStyle: PromptStyleHost,
|
||||
PromptTemplate: DefaultPromptTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultPromptTemplate = `{{- if .IsServer -}}{{ .Styles.Bold.Render "[server]" }} {{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} > {{- else -}}{{- if .Host -}}{{ .Styles.BoldPrimary.Render (printf "[%s]" .Host) }} {{- end -}}{{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} > {{- end -}}`
|
||||
|
||||
// SaveSettings - Save the current settings to disk
|
||||
func SaveSettings(settings *ClientSettings) error {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
|
||||
@@ -21,6 +21,8 @@ always_overflow: true
|
||||
vim_mode: true
|
||||
user_connect: true
|
||||
console_logs: false
|
||||
prompt: "basic"
|
||||
prompt_template: "{{.Host}}"
|
||||
`)
|
||||
if err := os.WriteFile(settingsPath, data, 0o600); err != nil {
|
||||
t.Fatalf("failed to write yaml settings: %v", err)
|
||||
@@ -54,6 +56,12 @@ console_logs: false
|
||||
if settings.ConsoleLogs {
|
||||
t.Fatalf("expected ConsoleLogs false")
|
||||
}
|
||||
if settings.PromptStyle != PromptStyleBasic {
|
||||
t.Fatalf("expected PromptStyle %q, got %q", PromptStyleBasic, settings.PromptStyle)
|
||||
}
|
||||
if settings.PromptTemplate != "{{.Host}}" {
|
||||
t.Fatalf("expected PromptTemplate %q, got %q", "{{.Host}}", settings.PromptTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
||||
@@ -71,6 +79,8 @@ func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
||||
VimMode: true,
|
||||
UserConnect: true,
|
||||
ConsoleLogs: false,
|
||||
PromptStyle: PromptStyleOperatorHost,
|
||||
PromptTemplate: "{{.Operator}}@{{.Host}} sliver > ",
|
||||
}
|
||||
data, err := json.MarshalIndent(legacy, "", " ")
|
||||
if err != nil {
|
||||
@@ -87,6 +97,12 @@ func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
||||
if settings.TableStyle != "Legacy" {
|
||||
t.Fatalf("expected TableStyle %q, got %q", "Legacy", settings.TableStyle)
|
||||
}
|
||||
if settings.PromptStyle != PromptStyleOperatorHost {
|
||||
t.Fatalf("expected PromptStyle %q, got %q", PromptStyleOperatorHost, settings.PromptStyle)
|
||||
}
|
||||
if settings.PromptTemplate != "{{.Operator}}@{{.Host}} sliver > " {
|
||||
t.Fatalf("expected PromptTemplate %q, got %q", "{{.Operator}}@{{.Host}} sliver > ", settings.PromptTemplate)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
||||
t.Fatalf("expected yaml settings to exist: %v", err)
|
||||
}
|
||||
@@ -108,6 +124,12 @@ func TestLoadSettingsWritesDefault(t *testing.T) {
|
||||
if settings.TableStyle != "SliverDefault" {
|
||||
t.Fatalf("expected default TableStyle %q, got %q", "SliverDefault", settings.TableStyle)
|
||||
}
|
||||
if settings.PromptStyle != PromptStyleHost {
|
||||
t.Fatalf("expected default PromptStyle %q, got %q", PromptStyleHost, settings.PromptStyle)
|
||||
}
|
||||
if settings.PromptTemplate != DefaultPromptTemplate {
|
||||
t.Fatalf("expected default PromptTemplate %q, got %q", DefaultPromptTemplate, settings.PromptTemplate)
|
||||
}
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
||||
t.Fatalf("expected default settings to be written: %v", err)
|
||||
|
||||
@@ -26,16 +26,16 @@ import (
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
)
|
||||
|
||||
func selectConfig() *assets.ClientConfig {
|
||||
func selectConfig() (string, *assets.ClientConfig) {
|
||||
configs := assets.GetConfigs()
|
||||
|
||||
if len(configs) == 0 {
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if len(configs) == 1 {
|
||||
for _, config := range configs {
|
||||
return config
|
||||
for key, config := range configs {
|
||||
return key, config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ func selectConfig() *assets.ClientConfig {
|
||||
err := forms.Select("Select a server:", keys, &selection)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return configs[selection]
|
||||
return selection, configs[selection]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
func handleConnectionLost(ln *grpc.ClientConn) {
|
||||
if ln == nil {
|
||||
return
|
||||
}
|
||||
currentState := ln.GetState()
|
||||
// currentState should be "Ready" when the connection is established.
|
||||
if ln.WaitForStateChange(context.Background(), currentState) {
|
||||
newState := ln.GetState()
|
||||
// newState will be "Idle" if the connection is lost.
|
||||
if newState == connectivity.Idle {
|
||||
fmt.Println("\nLost connection to server. Exiting now.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-29
@@ -19,9 +19,7 @@ package cli
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command"
|
||||
@@ -30,7 +28,6 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
// consoleCmd generates the console with required pre/post runners.
|
||||
@@ -46,8 +43,6 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
}
|
||||
|
||||
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
pre = func(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
@@ -55,7 +50,7 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
|
||||
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
|
||||
return nil
|
||||
}
|
||||
config := selectConfig()
|
||||
configKey, config := selectConfig()
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -72,40 +67,20 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
|
||||
}
|
||||
|
||||
var rpc rpcpb.SliverRPCClient
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait for any connection state changes and exit if the connection is lost.
|
||||
go handleConnectionLost(ln)
|
||||
|
||||
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
||||
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
||||
}
|
||||
|
||||
// Close the RPC connection once exiting
|
||||
post = func(_ *cobra.Command, _ []string) error {
|
||||
if ln != nil {
|
||||
return ln.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
return con.CloseConnection()
|
||||
}
|
||||
|
||||
return pre, post
|
||||
}
|
||||
|
||||
func handleConnectionLost(ln *grpc.ClientConn) {
|
||||
currentState := ln.GetState()
|
||||
// currentState should be "Ready" when the connection is established.
|
||||
if ln.WaitForStateChange(context.Background(), currentState) {
|
||||
newState := ln.GetState()
|
||||
// newState will be "Idle" if the connection is lost.
|
||||
if newState == connectivity.Idle {
|
||||
fmt.Println("\nLost connection to server. Exiting now.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,7 +428,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Packages" + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render("Packages"))
|
||||
|
||||
urlMargin := 150 // Extra margin needed to show URL column
|
||||
|
||||
@@ -493,26 +493,26 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
|
||||
rows := []table.Row{}
|
||||
for _, pkg := range entries {
|
||||
color := console.Normal
|
||||
style := console.StyleNormal
|
||||
if extensions.CmdExists(pkg.CommandName, sliverMenu.Command) {
|
||||
color = console.Green
|
||||
style = console.StyleGreen
|
||||
}
|
||||
if con.Settings.SmallTermWidth+urlMargin < width {
|
||||
rows = append(rows, table.Row{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.URL),
|
||||
style.Render(pkg.ArmoryName),
|
||||
style.Render(pkg.CommandName),
|
||||
style.Render(pkg.Version),
|
||||
style.Render(pkg.Type),
|
||||
style.Render(pkg.Help),
|
||||
style.Render(pkg.URL),
|
||||
})
|
||||
} else {
|
||||
rows = append(rows, table.Row{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help),
|
||||
style.Render(pkg.ArmoryName),
|
||||
style.Render(pkg.CommandName),
|
||||
style.Render(pkg.Version),
|
||||
style.Render(pkg.Type),
|
||||
style.Render(pkg.Help),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -524,7 +524,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Bundles" + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render("Bundles"))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Armory Name",
|
||||
"Name",
|
||||
|
||||
@@ -257,7 +257,7 @@ func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdent
|
||||
if len(extensionUpdates) != 1 {
|
||||
extensionSuffix = "s"
|
||||
}
|
||||
tw.SetTitle(console.Bold + fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix) + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render(fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix)))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Package Name",
|
||||
"Package Type",
|
||||
|
||||
@@ -178,10 +178,10 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
}
|
||||
|
||||
for _, beacon := range beacons {
|
||||
color := console.Normal
|
||||
style := console.StyleNormal
|
||||
activeBeacon := con.ActiveTarget.GetBeacon()
|
||||
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
|
||||
color = console.Green
|
||||
style = console.StyleGreen
|
||||
}
|
||||
if beacon.Integrity == "" {
|
||||
beacon.Integrity = "-"
|
||||
@@ -192,34 +192,34 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
|
||||
if wideTermWidth {
|
||||
rowEntries = []string{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name),
|
||||
fmt.Sprintf(color+"%d/%d"+console.Normal, beacon.TasksCountCompleted, beacon.TasksCount),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.RemoteAddress),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
fmt.Sprintf(color+"%s (%d)"+console.Normal, beacon.Filename, beacon.PID),
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
style.Render(fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount)),
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.RemoteAddress),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
style.Render(fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID)),
|
||||
}
|
||||
|
||||
if windowsBeaconInList {
|
||||
rowEntries = append(rowEntries, fmt.Sprintf(color+"%s"+console.Normal, beacon.Integrity))
|
||||
rowEntries = append(rowEntries, style.Render(beacon.Integrity))
|
||||
}
|
||||
|
||||
rowEntries = append(rowEntries, []string{
|
||||
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Locale),
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
style.Render(beacon.Locale),
|
||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||
}...)
|
||||
} else {
|
||||
rowEntries = []string{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
@@ -58,28 +57,28 @@ func printCertificateAuthorityInfo(con *console.SliverClient, authData []*client
|
||||
}
|
||||
|
||||
for _, authority := range authData {
|
||||
rowColor := console.Normal
|
||||
rowStyle := console.StyleNormal
|
||||
|
||||
expiry, err := time.Parse(timeFormat, authority.ValidityExpiry)
|
||||
if err == nil {
|
||||
rowColor = checkCertExpiry(expiry)
|
||||
rowStyle = checkCertExpiry(expiry)
|
||||
}
|
||||
|
||||
if wideTermWidth {
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ID),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CN),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CreationTime),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.Type),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.KeyAlgorithm),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityStart),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityExpiry),
|
||||
rowStyle.Render(authority.ID),
|
||||
rowStyle.Render(authority.CN),
|
||||
rowStyle.Render(authority.CreationTime),
|
||||
rowStyle.Render(authority.Type),
|
||||
rowStyle.Render(authority.KeyAlgorithm),
|
||||
rowStyle.Render(authority.ValidityStart),
|
||||
rowStyle.Render(authority.ValidityExpiry),
|
||||
})
|
||||
} else {
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ID),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CN),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityExpiry),
|
||||
rowStyle.Render(authority.ID),
|
||||
rowStyle.Render(authority.CN),
|
||||
rowStyle.Render(authority.ValidityExpiry),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
@@ -95,22 +94,22 @@ func CertificateInfoCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
printCertificateInfo(con, certificateInfo.Info)
|
||||
}
|
||||
|
||||
func checkCertExpiry(expiryTime time.Time) string {
|
||||
func checkCertExpiry(expiryTime time.Time) console.TextStyle {
|
||||
if expiryTime.Before(time.Now()) || expiryTime.Equal(time.Now()) {
|
||||
return console.Bold + console.Red
|
||||
return console.StyleBoldRed
|
||||
}
|
||||
|
||||
// One week is 168 hours - this is bad
|
||||
if expiryTime.Before(time.Now().Add(168 * time.Hour)) {
|
||||
return console.Bold + console.Red
|
||||
return console.StyleBoldRed
|
||||
}
|
||||
|
||||
// One month is approximately 730 hours - this is a warning
|
||||
if expiryTime.Before(time.Now().Add(730 * time.Hour)) {
|
||||
return console.Bold + console.Orange
|
||||
return console.StyleBoldOrange
|
||||
}
|
||||
|
||||
return console.Normal
|
||||
return console.StyleNormal
|
||||
}
|
||||
|
||||
func printCertificateInfo(con *console.SliverClient, certData []*clientpb.CertificateData) {
|
||||
@@ -148,28 +147,28 @@ func printCertificateInfo(con *console.SliverClient, certData []*clientpb.Certif
|
||||
}
|
||||
|
||||
for _, cert := range certData {
|
||||
rowColor := console.Normal
|
||||
rowStyle := console.StyleNormal
|
||||
|
||||
expiry, err := time.Parse(timeFormat, cert.ValidityExpiry)
|
||||
// This should not error out, but if it does, the row will not be colored
|
||||
if err == nil {
|
||||
rowColor = checkCertExpiry(expiry)
|
||||
rowStyle = checkCertExpiry(expiry)
|
||||
}
|
||||
if wideTermWidth {
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CreationTime),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.Type),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.KeyAlgorithm),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityStart),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry),
|
||||
rowStyle.Render(cert.ID),
|
||||
rowStyle.Render(cert.CN),
|
||||
rowStyle.Render(cert.CreationTime),
|
||||
rowStyle.Render(cert.Type),
|
||||
rowStyle.Render(cert.KeyAlgorithm),
|
||||
rowStyle.Render(cert.ValidityStart),
|
||||
rowStyle.Render(cert.ValidityExpiry),
|
||||
})
|
||||
} else {
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry),
|
||||
rowStyle.Render(cert.ID),
|
||||
rowStyle.Render(cert.CN),
|
||||
rowStyle.Render(cert.ValidityExpiry),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -106,17 +106,17 @@ func PrintCrackFiles(crackFiles *clientpb.CrackFiles, con *console.SliverClient)
|
||||
|
||||
func PrintCrackFilesByType(crackFiles *clientpb.CrackFiles, con *console.SliverClient) {
|
||||
wordlistTable := table.NewWriter()
|
||||
wordlistTable.SetTitle(console.Bold + "Wordlists" + console.Normal)
|
||||
wordlistTable.SetTitle(console.StyleBold.Render("Wordlists"))
|
||||
wordlistTable.SetStyle(settings.GetTableStyle(con))
|
||||
wordlistTable.AppendHeader(table.Row{"Name", "Size"})
|
||||
|
||||
rulesTable := table.NewWriter()
|
||||
rulesTable.SetTitle(console.Bold + "Rules" + console.Normal)
|
||||
rulesTable.SetTitle(console.StyleBold.Render("Rules"))
|
||||
rulesTable.SetStyle(settings.GetTableStyle(con))
|
||||
rulesTable.AppendHeader(table.Row{"Name", "Size"})
|
||||
|
||||
hcTable := table.NewWriter()
|
||||
hcTable.SetTitle(console.Bold + "Markov Hcstat2" + console.Normal)
|
||||
hcTable.SetTitle(console.StyleBold.Render("Markov Hcstat2"))
|
||||
hcTable.SetStyle(settings.GetTableStyle(con))
|
||||
hcTable.AppendHeader(table.Row{"Name", "Size"})
|
||||
|
||||
|
||||
@@ -132,35 +132,35 @@ func PrintCrackers(crackers []*clientpb.Crackstation, con *console.SliverClient,
|
||||
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + console.Orange + fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, cracker.Name, cracker.OperatorName) + console.Normal + "\n")
|
||||
tw.SetTitle(console.StyleBoldOrange.Render(fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, cracker.Name, cracker.OperatorName)) + "\n")
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.Bold + "Operating System" + console.Normal, fmt.Sprintf("%s/%s", cracker.GOOS, cracker.GOARCH)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Hashcat Version" + console.Normal, cracker.HashcatVersion})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Operating System"), fmt.Sprintf("%s/%s", cracker.GOOS, cracker.GOARCH)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Hashcat Version"), cracker.HashcatVersion})
|
||||
if 0 < len(cracker.CUDA) {
|
||||
for _, cuda := range cracker.CUDA {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.Bold + "CUDA Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+"%s (%s)"+console.Normal, cuda.Name, cuda.Version)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", cuda.MemoryFree, cuda.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", cuda.Clock)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", cuda.Processors)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("CUDA Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", cuda.Name, cuda.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", cuda.MemoryFree, cuda.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", cuda.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", cuda.Processors)})
|
||||
}
|
||||
}
|
||||
if 0 < len(cracker.Metal) {
|
||||
for _, metal := range cracker.Metal {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.Bold + "Metal Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+"%s (%s)"+console.Normal, metal.Name, metal.Version)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", metal.MemoryFree, metal.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", metal.Clock)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", metal.Processors)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Metal Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", metal.Name, metal.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", metal.MemoryFree, metal.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", metal.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", metal.Processors)})
|
||||
}
|
||||
}
|
||||
if 0 < len(cracker.OpenCL) {
|
||||
for _, openCL := range cracker.OpenCL {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.Bold + "OpenCL Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+console.Bold+"%s (%s)"+console.Normal, openCL.Name, openCL.Version)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", openCL.MemoryFree, openCL.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", openCL.Clock)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", openCL.Processors)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("OpenCL Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", openCL.Name, openCL.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", openCL.MemoryFree, openCL.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", openCL.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", openCL.Processors)})
|
||||
}
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
@@ -173,7 +173,7 @@ func printCracker(cracker *clientpb.Crackstation, index int, con *console.Sliver
|
||||
func printBenchmarks(cracker *clientpb.Crackstation, con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Benchmarks" + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render("Benchmarks"))
|
||||
tw.SortBy([]table.SortBy{{Name: "Hash Type"}})
|
||||
tw.AppendHeader(table.Row{"Hash Type", "Rate"})
|
||||
if len(cracker.Benchmarks) == 0 {
|
||||
|
||||
@@ -25,7 +25,7 @@ func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
||||
}
|
||||
|
||||
results = append(results, file.Name)
|
||||
desc := fmt.Sprintf("%s (%s chunks, chunk size: %s)", byteCountSI(file.UncompressedSize), len(file.Chunks), byteCountSI(file.ChunkSize))
|
||||
desc := fmt.Sprintf("%s (%d chunks, chunk size: %s)", byteCountSI(file.UncompressedSize), len(file.Chunks), byteCountSI(file.ChunkSize))
|
||||
results = append(results, desc)
|
||||
}
|
||||
|
||||
|
||||
@@ -61,9 +61,9 @@ func printCollection(collection string, creds []*clientpb.Credential, con *conso
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
if collection != "" {
|
||||
tw.SetTitle(console.Bold + collection + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render(collection))
|
||||
} else {
|
||||
tw.SetTitle(console.Bold + "Default Collection" + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render("Default Collection"))
|
||||
}
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
|
||||
@@ -94,7 +94,7 @@ func CursedChromeCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
}
|
||||
if chromeExt != nil {
|
||||
con.Printf("success!\n")
|
||||
con.PrintInfof("Found viable Chrome extension %s%s%s (%s)\n", console.Bold, chromeExt.Title, console.Normal, chromeExt.ID)
|
||||
con.PrintInfof("Found viable Chrome extension %s (%s)\n", console.StyleBold.Render(chromeExt.Title), chromeExt.ID)
|
||||
con.PrintInfof("Injecting payload ... ")
|
||||
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
|
||||
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second)
|
||||
@@ -120,7 +120,7 @@ func avadaKedavraChrome(session *clientpb.Session, cmd *cobra.Command, con *cons
|
||||
if chromeProcess != nil {
|
||||
con.PrintWarnf("Found running Chrome process: %d (ppid: %d)\n", chromeProcess.GetPid(), chromeProcess.GetPpid())
|
||||
con.PrintWarnf("Sliver will need to kill and restart the Chrome process in order to perform code injection.\n")
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = forms.Confirm("Kill and restore existing Chrome process?", &confirm)
|
||||
|
||||
@@ -93,7 +93,7 @@ func startCursedConsole(curse *core.CursedProcess, helpers bool, target *overlor
|
||||
tmpFile, _ := os.CreateTemp("", "cursed")
|
||||
shell := readline.NewShell()
|
||||
shell.History.AddFromFile("cursed history", tmpFile.Name())
|
||||
shell.Prompt.Primary(func() string { return "\033[31mcursed »\033[0m " })
|
||||
shell.Prompt.Primary(func() string { return console.StyleRed.Render("cursed »") + " " })
|
||||
// EOFPrompt: "exit",
|
||||
|
||||
if con.Settings.VimMode {
|
||||
@@ -111,7 +111,7 @@ func startCursedConsole(curse *core.CursedProcess, helpers bool, target *overlor
|
||||
}
|
||||
}
|
||||
|
||||
con.Printf(console.Bold+">>> Cursed Console, use ':help' for options%s\n\n", console.Normal)
|
||||
con.Printf("%s\n\n", console.StyleBold.Render(">>> Cursed Console, use ':help' for options"))
|
||||
|
||||
for {
|
||||
line, err := shell.Readline()
|
||||
|
||||
@@ -77,7 +77,7 @@ func CursedEdgeCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
}
|
||||
if chromeExt != nil {
|
||||
con.Printf("success!\n")
|
||||
con.PrintInfof("Found viable Edge extension %s%s%s (%s)\n", console.Bold, chromeExt.Title, console.Normal, chromeExt.ID)
|
||||
con.PrintInfof("Found viable Edge extension %s (%s)\n", console.StyleBold.Render(chromeExt.Title), chromeExt.ID)
|
||||
con.PrintInfof("Injecting payload ... ")
|
||||
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
|
||||
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second)
|
||||
@@ -103,7 +103,7 @@ func avadaKedavraEdge(session *clientpb.Session, cmd *cobra.Command, con *consol
|
||||
if edgeProcess != nil {
|
||||
con.PrintWarnf("Found running Edge process: %d (ppid: %d)\n", edgeProcess.GetPid(), edgeProcess.GetPpid())
|
||||
con.PrintWarnf("Sliver will need to kill and restart the Edge process in order to perform code injection.\n")
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = forms.Confirm("Kill and restore existing Edge process?", &confirm)
|
||||
|
||||
@@ -85,7 +85,7 @@ func avadaKedavraElectron(electronExe string, session *clientpb.Session, cmd *co
|
||||
if electronProcess != nil {
|
||||
con.PrintWarnf("Found running '%s' process: %d (ppid: %d)\n", path.Base(electronExe), electronProcess.GetPid(), electronProcess.GetPpid())
|
||||
con.PrintWarnf("Sliver will need to kill and restart the process in order to perform code injection.\n")
|
||||
con.PrintWarnf("%sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.PrintWarnf("%s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = forms.Confirm("Kill and restart the process?", &confirm)
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -239,19 +238,19 @@ func printGrep(grep *sliverpb.Grep, searchPattern string, searchPath string, cmd
|
||||
// grepLineResult - Add color or formatting for results for console output
|
||||
func grepLineResult(positions []*sliverpb.GrepLinePosition, line string, colorize bool, allowFormatting bool) string {
|
||||
var result string = ""
|
||||
var matchOutput func(a ...interface{}) string
|
||||
var previousPositionEnd int32 = 0
|
||||
|
||||
matchStyle := console.StyleNormal
|
||||
if colorize {
|
||||
matchOutput = color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
matchStyle = console.StyleBoldRed
|
||||
} else if allowFormatting {
|
||||
matchOutput = color.New(color.Bold).SprintFunc()
|
||||
matchStyle = console.StyleBold
|
||||
}
|
||||
|
||||
for idx, position := range positions {
|
||||
if colorize || allowFormatting {
|
||||
result += line[previousPositionEnd:position.Start]
|
||||
result += matchOutput(line[position.Start:position.End])
|
||||
result += matchStyle.Render(line[position.Start:position.End])
|
||||
if idx == len(positions)-1 {
|
||||
result += line[position.End:]
|
||||
} else {
|
||||
|
||||
@@ -42,20 +42,20 @@ func PrintCanaries(con *console.SliverClient, canaries []*clientpb.DNSCanary, bu
|
||||
if burnedOnly && !canary.Triggered {
|
||||
continue
|
||||
}
|
||||
lineColor := console.Normal
|
||||
lineStyle := console.StyleNormal
|
||||
if canary.Triggered {
|
||||
lineColor = console.Bold + console.Red
|
||||
lineStyle = console.StyleBoldRed
|
||||
}
|
||||
firstTrigger := "Never"
|
||||
latestTrigger := "Never"
|
||||
if canary.Triggered {
|
||||
firstTrigger = fmt.Sprintf(lineColor+"%s"+console.Normal, canary.FirstTriggered)
|
||||
latestTrigger = fmt.Sprintf(lineColor+"%s"+console.Normal, canary.LatestTrigger)
|
||||
firstTrigger = lineStyle.Render(canary.FirstTriggered)
|
||||
latestTrigger = lineStyle.Render(canary.LatestTrigger)
|
||||
}
|
||||
row := table.Row{
|
||||
fmt.Sprintf(lineColor+"%s"+console.Normal, canary.ImplantName),
|
||||
fmt.Sprintf(lineColor+"%s"+console.Normal, canary.Domain),
|
||||
fmt.Sprintf(lineColor+"%v"+console.Normal, canary.Triggered),
|
||||
lineStyle.Render(canary.ImplantName),
|
||||
lineStyle.Render(canary.Domain),
|
||||
lineStyle.Render(fmt.Sprintf("%v", canary.Triggered)),
|
||||
firstTrigger,
|
||||
latestTrigger,
|
||||
}
|
||||
|
||||
@@ -15,19 +15,19 @@ func GenerateInfoCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
con.PrintErrorf("Failed to get compiler information: %s\n", err)
|
||||
return
|
||||
}
|
||||
con.Printf("%sServer:%s %s/%s\n", console.Bold, console.Normal, compiler.GOOS, compiler.GOARCH)
|
||||
con.Printf("%s %s/%s\n", console.StyleBold.Render("Server:"), compiler.GOOS, compiler.GOARCH)
|
||||
con.Println()
|
||||
con.Printf("%sCross Compilers%s\n", console.Bold, console.Normal)
|
||||
con.Printf("%s\n", console.StyleBold.Render("Cross Compilers"))
|
||||
for _, cc := range compiler.CrossCompilers {
|
||||
con.Printf("%s/%s - %s\n", cc.TargetGOOS, cc.TargetGOARCH, cc.GetCCPath())
|
||||
}
|
||||
con.Println()
|
||||
con.Printf("%sSupported Targets%s\n", console.Bold, console.Normal)
|
||||
con.Printf("%s\n", console.StyleBold.Render("Supported Targets"))
|
||||
for _, target := range compiler.Targets {
|
||||
con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format))
|
||||
}
|
||||
con.Println()
|
||||
con.Printf("%sDefault Builds Only%s\n", console.Bold, console.Normal)
|
||||
con.Printf("%s\n", console.StyleBold.Render("Default Builds Only"))
|
||||
for _, target := range compiler.UnsupportedTargets {
|
||||
con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format))
|
||||
}
|
||||
|
||||
@@ -820,9 +820,7 @@ func getTargets(targetOS string, targetArch string, con *console.SliverClient) (
|
||||
|
||||
target := fmt.Sprintf("%s/%s", targetOS, targetArch)
|
||||
if _, ok := SupportedCompilerTargets[target]; !ok {
|
||||
con.Printf("⚠️ Unsupported compiler target %s%s%s, but we can try to compile a generic implant.\n",
|
||||
console.Bold, target, console.Normal,
|
||||
)
|
||||
con.Printf("⚠️ Unsupported compiler target %s, but we can try to compile a generic implant.\n", console.StyleBold.Render(target))
|
||||
con.Printf("⚠️ Generic implants do not support all commands/features.\n")
|
||||
var confirm bool
|
||||
_ = forms.Confirm("Attempt to build generic implant?", &confirm)
|
||||
@@ -1172,9 +1170,9 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
|
||||
con.PrintInfof("Externally generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
|
||||
}
|
||||
if config.ObfuscateSymbols {
|
||||
con.PrintInfof("%sSymbol obfuscation is enabled%s\n", console.Bold, console.Normal)
|
||||
con.PrintInfof("%s\n", console.StyleBold.Render("Symbol obfuscation is enabled"))
|
||||
} else if !config.Debug {
|
||||
con.PrintErrorf("Symbol obfuscation is %sdisabled%s\n", console.Bold, console.Normal)
|
||||
con.PrintErrorf("Symbol obfuscation is %s\n", console.StyleBold.Render("disabled"))
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
@@ -1325,9 +1323,9 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
|
||||
con.PrintInfof("Generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
|
||||
}
|
||||
if config.ObfuscateSymbols {
|
||||
con.PrintInfof("%sSymbol obfuscation is enabled%s\n", console.Bold, console.Normal)
|
||||
con.PrintInfof("%s\n", console.StyleBold.Render("Symbol obfuscation is enabled"))
|
||||
} else if !config.Debug {
|
||||
con.PrintErrorf("Symbol obfuscation is %sdisabled%s\n", console.Bold, console.Normal)
|
||||
con.PrintErrorf("Symbol obfuscation is %s\n", console.StyleBold.Render("disabled"))
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
@@ -71,7 +71,7 @@ func DisplayTrafficEncoders(encoderMap *clientpb.TrafficEncoderMap, con *console
|
||||
allIDs = append(allIDs, encoder.ID)
|
||||
name := encoder.Wasm.Name
|
||||
if isDuplicate {
|
||||
name = fmt.Sprintf(console.Bold+console.Red+"%s (duplicate id)"+console.Normal, name)
|
||||
name = console.StyleBoldRed.Render(fmt.Sprintf("%s (duplicate id)", name))
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
encoder.ID,
|
||||
@@ -212,9 +212,9 @@ func displayTrafficEncoderTests(running bool, tests *clientpb.TrafficEncoderTest
|
||||
for _, test := range tests.Tests {
|
||||
var success string
|
||||
if test.Success {
|
||||
success = console.Bold + console.Green + "Passed" + console.Normal
|
||||
success = console.StyleBoldGreen.Render("Passed")
|
||||
} else {
|
||||
success = console.Bold + console.Red + "Failed!" + console.Normal
|
||||
success = console.StyleBoldRed.Render("Failed!")
|
||||
}
|
||||
errorMsg := "N/A"
|
||||
if test.Err != "" {
|
||||
@@ -234,7 +234,7 @@ func displayTrafficEncoderTests(running bool, tests *clientpb.TrafficEncoderTest
|
||||
if running {
|
||||
con.Println()
|
||||
con.Println()
|
||||
con.Printf(console.Bold+" >>> Running test %d of %d please wait ...\r"+console.Normal, len(tests.Tests), tests.TotalTests)
|
||||
con.Printf("%s\r", console.StyleBold.Render(fmt.Sprintf(" >>> Running test %d of %d please wait ...", len(tests.Tests), tests.TotalTests)))
|
||||
lineCount += 2
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ package help
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1418,24 +1418,6 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
|
||||
To get information about services, you need to be an authenticated user on the system or domain. To control services, you need administrator or higher privileges.`
|
||||
)
|
||||
|
||||
const (
|
||||
// ANSI Colors
|
||||
normal = "\033[0m"
|
||||
black = "\033[30m"
|
||||
red = "\033[31m"
|
||||
green = "\033[32m"
|
||||
orange = "\033[33m"
|
||||
blue = "\033[34m"
|
||||
purple = "\033[35m"
|
||||
cyan = "\033[36m"
|
||||
gray = "\033[37m"
|
||||
bold = "\033[1m"
|
||||
clearln = "\r\x1b[2K"
|
||||
upN = "\033[%dA"
|
||||
downN = "\033[%dB"
|
||||
underline = "\033[4m"
|
||||
)
|
||||
|
||||
// GetHelpFor - Get help string for a command
|
||||
func GetHelpFor(cmdName []string) string {
|
||||
if 0 < len(cmdName) {
|
||||
@@ -1446,34 +1428,100 @@ func GetHelpFor(cmdName []string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// FormatHelpTmpl - Applies format template to help string
|
||||
type helpStyleState struct {
|
||||
bold bool
|
||||
underline bool
|
||||
fgSet bool
|
||||
fg lipgloss.Color
|
||||
}
|
||||
|
||||
func (s *helpStyleState) reset() {
|
||||
s.bold = false
|
||||
s.underline = false
|
||||
s.fgSet = false
|
||||
s.fg = ""
|
||||
}
|
||||
|
||||
func (s *helpStyleState) apply(token string) bool {
|
||||
switch token {
|
||||
case "Normal":
|
||||
s.reset()
|
||||
case "Bold":
|
||||
s.bold = true
|
||||
case "Underline":
|
||||
s.underline = true
|
||||
case "Black":
|
||||
s.fgSet, s.fg = true, theme.DefaultMod(50)
|
||||
case "Red":
|
||||
s.fgSet, s.fg = true, theme.Danger()
|
||||
case "Green":
|
||||
s.fgSet, s.fg = true, theme.Success()
|
||||
case "Orange":
|
||||
s.fgSet, s.fg = true, theme.Warning()
|
||||
case "Blue":
|
||||
s.fgSet, s.fg = true, theme.Primary()
|
||||
case "Purple":
|
||||
s.fgSet, s.fg = true, theme.Secondary()
|
||||
case "Cyan":
|
||||
s.fgSet, s.fg = true, theme.PrimaryMod(500)
|
||||
case "Gray":
|
||||
// ANSI "gray" (37) is typically light; map to the lightest neutral.
|
||||
s.fgSet, s.fg = true, theme.DefaultMod(900)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s helpStyleState) render(text string) string {
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
st := lipgloss.NewStyle()
|
||||
if s.bold {
|
||||
st = st.Bold(true)
|
||||
}
|
||||
if s.underline {
|
||||
st = st.Underline(true)
|
||||
}
|
||||
if s.fgSet {
|
||||
st = st.Foreground(s.fg)
|
||||
}
|
||||
return st.Render(text)
|
||||
}
|
||||
|
||||
// FormatHelpTmpl - Applies the help markup (e.g. [[.Bold]]...[[.Normal]]) using lipgloss.
|
||||
func FormatHelpTmpl(helpStr string) string {
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
tmpl, _ := template.New("help").Delims("[[", "]]").Parse(helpStr)
|
||||
tmpl.Execute(outputBuf, struct {
|
||||
Normal string
|
||||
Bold string
|
||||
Underline string
|
||||
Black string
|
||||
Red string
|
||||
Green string
|
||||
Orange string
|
||||
Blue string
|
||||
Purple string
|
||||
Cyan string
|
||||
Gray string
|
||||
}{
|
||||
Normal: normal,
|
||||
Bold: bold,
|
||||
Underline: underline,
|
||||
Black: black,
|
||||
Red: red,
|
||||
Green: green,
|
||||
Orange: orange,
|
||||
Blue: blue,
|
||||
Purple: purple,
|
||||
Cyan: cyan,
|
||||
Gray: gray,
|
||||
})
|
||||
return outputBuf.String()
|
||||
var (
|
||||
out strings.Builder
|
||||
state helpStyleState
|
||||
)
|
||||
state.reset()
|
||||
|
||||
i := 0
|
||||
for i < len(helpStr) {
|
||||
j := strings.Index(helpStr[i:], "[[.")
|
||||
if j < 0 {
|
||||
out.WriteString(state.render(helpStr[i:]))
|
||||
break
|
||||
}
|
||||
j += i
|
||||
if j > i {
|
||||
out.WriteString(state.render(helpStr[i:j]))
|
||||
}
|
||||
|
||||
k := strings.Index(helpStr[j:], "]]")
|
||||
if k < 0 {
|
||||
out.WriteString(state.render(helpStr[j:]))
|
||||
break
|
||||
}
|
||||
token := helpStr[j+3 : j+k]
|
||||
if !state.apply(token) {
|
||||
// Unknown token; preserve it as plain text.
|
||||
out.WriteString(helpStr[j : j+k+2])
|
||||
}
|
||||
i = j + k + 2
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -61,13 +62,21 @@ type editorModel struct {
|
||||
}
|
||||
|
||||
var (
|
||||
cursorStyle = lipgloss.NewStyle().Reverse(true)
|
||||
modifiedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
|
||||
modifiedCursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215")).Reverse(true)
|
||||
statusStyle = lipgloss.NewStyle().Reverse(true)
|
||||
lineStyle = lipgloss.NewStyle()
|
||||
cursorStyle = lipgloss.NewStyle().Reverse(true)
|
||||
statusStyle = lipgloss.NewStyle().Reverse(true)
|
||||
lineStyle = lipgloss.NewStyle()
|
||||
)
|
||||
|
||||
func modifiedStyle() lipgloss.Style {
|
||||
// Read from the global console theme so theme.yaml changes are reflected.
|
||||
return console.StyleWarning
|
||||
}
|
||||
|
||||
func modifiedCursorStyle() lipgloss.Style {
|
||||
// Read from the global console theme so theme.yaml changes are reflected.
|
||||
return console.StyleWarning.Copy().Reverse(true)
|
||||
}
|
||||
|
||||
func newEditorModel(data []byte, filename string, offset int) *editorModel {
|
||||
copyData := append([]byte(nil), data...)
|
||||
model := &editorModel{
|
||||
@@ -400,8 +409,8 @@ func (m *editorModel) renderLine(lineIndex int) string {
|
||||
isModified := m.isModified(idx)
|
||||
if idx == m.cursor {
|
||||
if isModified {
|
||||
hexCells[i] = modifiedCursorStyle.Render(hexRaw)
|
||||
asciiCells[i] = modifiedCursorStyle.Render(asciiRaw)
|
||||
hexCells[i] = modifiedCursorStyle().Render(hexRaw)
|
||||
asciiCells[i] = modifiedCursorStyle().Render(asciiRaw)
|
||||
} else {
|
||||
hexCells[i] = cursorStyle.Render(hexRaw)
|
||||
asciiCells[i] = cursorStyle.Render(asciiRaw)
|
||||
@@ -409,8 +418,8 @@ func (m *editorModel) renderLine(lineIndex int) string {
|
||||
continue
|
||||
}
|
||||
if isModified {
|
||||
hexCells[i] = modifiedStyle.Render(hexRaw)
|
||||
asciiCells[i] = modifiedStyle.Render(asciiRaw)
|
||||
hexCells[i] = modifiedStyle().Render(hexRaw)
|
||||
asciiCells[i] = modifiedStyle().Render(asciiRaw)
|
||||
continue
|
||||
}
|
||||
hexCells[i] = hexRaw
|
||||
|
||||
+38
-38
@@ -70,47 +70,47 @@ func InfoCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
|
||||
if session != nil {
|
||||
|
||||
con.Printf(console.Bold+" Session ID: %s%s\n", console.Normal, session.ID)
|
||||
con.Printf(console.Bold+" Name: %s%s\n", console.Normal, session.Name)
|
||||
con.Printf(console.Bold+" Hostname: %s%s\n", console.Normal, session.Hostname)
|
||||
con.Printf(console.Bold+" UUID: %s%s\n", console.Normal, session.UUID)
|
||||
con.Printf(console.Bold+" Username: %s%s\n", console.Normal, session.Username)
|
||||
con.Printf(console.Bold+" UID: %s%s\n", console.Normal, session.UID)
|
||||
con.Printf(console.Bold+" GID: %s%s\n", console.Normal, session.GID)
|
||||
con.Printf(console.Bold+" PID: %s%d\n", console.Normal, session.PID)
|
||||
con.Printf(console.Bold+" OS: %s%s\n", console.Normal, session.OS)
|
||||
con.Printf(console.Bold+" Version: %s%s\n", console.Normal, session.Version)
|
||||
con.Printf(console.Bold+" Locale: %s%s\n", console.Normal, session.Locale)
|
||||
con.Printf(console.Bold+" Arch: %s%s\n", console.Normal, session.Arch)
|
||||
con.Printf(console.Bold+" Active C2: %s%s\n", console.Normal, session.ActiveC2)
|
||||
con.Printf(console.Bold+" Remote Address: %s%s\n", console.Normal, session.RemoteAddress)
|
||||
con.Printf(console.Bold+" Proxy URL: %s%s\n", console.Normal, session.ProxyURL)
|
||||
con.Printf(console.Bold+"Reconnect Interval: %s%s\n", console.Normal, time.Duration(session.ReconnectInterval).String())
|
||||
con.Printf(console.Bold+" First Contact: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(session.FirstContact, 0), true, false))
|
||||
con.Printf(console.Bold+" Last Checkin: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(session.LastCheckin, 0), true, false))
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Session ID:"), session.ID)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Name:"), session.Name)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Hostname:"), session.Hostname)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" UUID:"), session.UUID)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Username:"), session.Username)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" UID:"), session.UID)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" GID:"), session.GID)
|
||||
con.Printf("%s %d\n", console.StyleBold.Render(" PID:"), session.PID)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" OS:"), session.OS)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Version:"), session.Version)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Locale:"), session.Locale)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Arch:"), session.Arch)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Active C2:"), session.ActiveC2)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Remote Address:"), session.RemoteAddress)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Proxy URL:"), session.ProxyURL)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render("Reconnect Interval:"), time.Duration(session.ReconnectInterval).String())
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" First Contact:"), con.FormatDateDelta(time.Unix(session.FirstContact, 0), true, false))
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Last Checkin:"), con.FormatDateDelta(time.Unix(session.LastCheckin, 0), true, false))
|
||||
|
||||
} else if beacon != nil {
|
||||
|
||||
con.Printf(console.Bold+" Beacon ID: %s%s\n", console.Normal, beacon.ID)
|
||||
con.Printf(console.Bold+" Name: %s%s\n", console.Normal, beacon.Name)
|
||||
con.Printf(console.Bold+" Hostname: %s%s\n", console.Normal, beacon.Hostname)
|
||||
con.Printf(console.Bold+" UUID: %s%s\n", console.Normal, beacon.UUID)
|
||||
con.Printf(console.Bold+" Username: %s%s\n", console.Normal, beacon.Username)
|
||||
con.Printf(console.Bold+" UID: %s%s\n", console.Normal, beacon.UID)
|
||||
con.Printf(console.Bold+" GID: %s%s\n", console.Normal, beacon.GID)
|
||||
con.Printf(console.Bold+" PID: %s%d\n", console.Normal, beacon.PID)
|
||||
con.Printf(console.Bold+" OS: %s%s\n", console.Normal, beacon.OS)
|
||||
con.Printf(console.Bold+" Version: %s%s\n", console.Normal, beacon.Version)
|
||||
con.Printf(console.Bold+" Locale: %s%s\n", console.Normal, beacon.Locale)
|
||||
con.Printf(console.Bold+" Arch: %s%s\n", console.Normal, beacon.Arch)
|
||||
con.Printf(console.Bold+" Active C2: %s%s\n", console.Normal, beacon.ActiveC2)
|
||||
con.Printf(console.Bold+" Remote Address: %s%s\n", console.Normal, beacon.RemoteAddress)
|
||||
con.Printf(console.Bold+" Proxy URL: %s%s\n", console.Normal, beacon.ProxyURL)
|
||||
con.Printf(console.Bold+" Interval: %s%s\n", console.Normal, time.Duration(beacon.Interval).String())
|
||||
con.Printf(console.Bold+" Jitter: %s%s\n", console.Normal, time.Duration(beacon.Jitter).String())
|
||||
con.Printf(console.Bold+" First Contact: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(beacon.FirstContact, 0), true, false))
|
||||
con.Printf(console.Bold+" Last Checkin: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), true, false))
|
||||
con.Printf(console.Bold+" Next Checkin: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), true, true))
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Beacon ID:"), beacon.ID)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Name:"), beacon.Name)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Hostname:"), beacon.Hostname)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" UUID:"), beacon.UUID)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Username:"), beacon.Username)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" UID:"), beacon.UID)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" GID:"), beacon.GID)
|
||||
con.Printf("%s %d\n", console.StyleBold.Render(" PID:"), beacon.PID)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" OS:"), beacon.OS)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Version:"), beacon.Version)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Locale:"), beacon.Locale)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Arch:"), beacon.Arch)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Active C2:"), beacon.ActiveC2)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Remote Address:"), beacon.RemoteAddress)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Proxy URL:"), beacon.ProxyURL)
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Interval:"), time.Duration(beacon.Interval).String())
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Jitter:"), time.Duration(beacon.Jitter).String())
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" First Contact:"), con.FormatDateDelta(time.Unix(beacon.FirstContact, 0), true, false))
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Last Checkin:"), con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), true, false))
|
||||
con.Printf("%s %s\n", console.StyleBold.Render(" Next Checkin:"), con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), true, true))
|
||||
|
||||
} else {
|
||||
con.PrintErrorf("No target session, see `help %s`\n", consts.InfoStr)
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
package jobs
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/generate"
|
||||
@@ -103,6 +121,10 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("long-poll-timeout", "T", "1s", "server-side long poll timeout")
|
||||
f.StringP("long-poll-jitter", "J", "2s", "server-side long poll jitter")
|
||||
})
|
||||
flags.BindFlagCompletions(httpCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["website"] = WebsiteNameCompleter(con)
|
||||
})
|
||||
registerWebsiteFlagCompletion(httpCmd, "website", con)
|
||||
|
||||
// HTTPS
|
||||
httpsCmd := &cobra.Command{
|
||||
@@ -129,9 +151,11 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.BoolP("disable-randomized-jarm", "E", false, "disable randomized jarm fingerprints")
|
||||
})
|
||||
flags.BindFlagCompletions(httpsCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["website"] = WebsiteNameCompleter(con)
|
||||
(*comp)["cert"] = carapace.ActionFiles().Tag("certificate file")
|
||||
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
|
||||
})
|
||||
registerWebsiteFlagCompletion(httpsCmd, "website", con)
|
||||
|
||||
// Staging listeners
|
||||
stageCmd := &cobra.Command{
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestHTTPAndHTTPSWebsiteFlagCompletionRegistered(t *testing.T) {
|
||||
cmds := Commands(&console.SliverClient{})
|
||||
|
||||
httpCmd := commandByUse(cmds, consts.HttpStr)
|
||||
if httpCmd == nil {
|
||||
t.Fatalf("missing %q command", consts.HttpStr)
|
||||
}
|
||||
if _, ok := httpCmd.GetFlagCompletionFunc("website"); !ok {
|
||||
t.Fatalf("%q missing website flag completion func", consts.HttpStr)
|
||||
}
|
||||
|
||||
httpsCmd := commandByUse(cmds, consts.HttpsStr)
|
||||
if httpsCmd == nil {
|
||||
t.Fatalf("missing %q command", consts.HttpsStr)
|
||||
}
|
||||
if _, ok := httpsCmd.GetFlagCompletionFunc("website"); !ok {
|
||||
t.Fatalf("%q missing website flag completion func", consts.HttpsStr)
|
||||
}
|
||||
}
|
||||
|
||||
func commandByUse(cmds []*cobra.Command, use string) *cobra.Command {
|
||||
for _, cmd := range cmds {
|
||||
if cmd.Use == use {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package jobs
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// WebsiteNameCompleter completes the names of available websites.
|
||||
func WebsiteNameCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
websites, err := websiteNameValues(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("Failed to list websites %s", err)
|
||||
}
|
||||
if len(websites) == 0 {
|
||||
return carapace.ActionMessage("no available websites")
|
||||
}
|
||||
|
||||
return carapace.ActionValues(websites...).Tag("websites").Usage("website name")
|
||||
})
|
||||
}
|
||||
|
||||
func registerWebsiteFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
values, err := websiteNameValues(con)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
|
||||
func websiteNameValues(con *console.SliverClient) ([]string, error) {
|
||||
results := make([]string, 0)
|
||||
if con == nil || con.Rpc == nil {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
websites, err := con.Rpc.Websites(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ws := range websites.Websites {
|
||||
results = append(results, ws.Name)
|
||||
}
|
||||
sort.Strings(results)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func filterCompletionValues(values []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func PrintLootFile(loot *clientpb.Loot, con *console.SliverClient) {
|
||||
return
|
||||
}
|
||||
if loot.File.Name != "" {
|
||||
con.PrintInfof("%sFile Name:%s %s\n\n", console.Bold, console.Normal, loot.File.Name)
|
||||
con.PrintInfof("%s %s\n\n", console.StyleBold.Render("File Name:"), loot.File.Name)
|
||||
}
|
||||
if loot.File.Data != nil && 0 < len(loot.File.Data) {
|
||||
if loot.FileType == clientpb.FileType_TEXT || isText(loot.File.Data) {
|
||||
|
||||
@@ -20,7 +20,6 @@ package monitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -99,14 +98,12 @@ func PrintWTConfig(configs *clientpb.MonitoringProviders, con *console.SliverCli
|
||||
"APIPassword",
|
||||
})
|
||||
|
||||
color := console.Normal
|
||||
|
||||
for _, config := range configs.Providers {
|
||||
row := table.Row{}
|
||||
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.ID))
|
||||
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.Type))
|
||||
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.APIKey))
|
||||
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.APIPassword))
|
||||
row = append(row, config.ID)
|
||||
row = append(row, config.Type)
|
||||
row = append(row, config.APIKey)
|
||||
row = append(row, config.APIPassword)
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func PrintIfconfig(ifconfig *sliverpb.Ifconfig, all bool, con *console.SliverCli
|
||||
for index, iface := range interfaces {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableWithBordersStyle(con))
|
||||
tw.SetTitle(fmt.Sprintf(console.Bold+"%s"+console.Normal, iface.Name))
|
||||
tw.SetTitle(console.StyleBold.Render(iface.Name))
|
||||
tw.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Name: "#", AutoMerge: true},
|
||||
{Name: "IP Address", AutoMerge: true},
|
||||
@@ -100,7 +100,7 @@ func PrintIfconfig(ifconfig *sliverpb.Ifconfig, all bool, con *console.SliverCli
|
||||
}
|
||||
}
|
||||
if 0 < subnet && subnet <= 32 && !isLoopback(ip) {
|
||||
ips = append(ips, fmt.Sprintf(console.Bold+console.Green+"%s"+console.Normal, ip))
|
||||
ips = append(ips, console.StyleBoldGreen.Render(ip))
|
||||
} else if all {
|
||||
ips = append(ips, fmt.Sprintf("%s", ip))
|
||||
}
|
||||
|
||||
@@ -104,11 +104,11 @@ func PrintNetstat(netstat *sliverpb.Netstat, implantPID int32, activeC2 string,
|
||||
}
|
||||
if entry.Process != nil && entry.Process.Pid == implantPID {
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, entry.Protocol),
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, srcAddr),
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, dstAddr),
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, entry.SkState),
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, pid),
|
||||
console.StyleGreen.Render(entry.Protocol),
|
||||
console.StyleGreen.Render(srcAddr),
|
||||
console.StyleGreen.Render(dstAddr),
|
||||
console.StyleGreen.Render(entry.SkState),
|
||||
console.StyleGreen.Render(pid),
|
||||
})
|
||||
} else {
|
||||
tw.AppendRow(table.Row{entry.Protocol, srcAddr, dstAddr, entry.SkState, pid})
|
||||
|
||||
@@ -50,7 +50,7 @@ func displayOperators(operators []*clientpb.Operator, con *console.SliverClient)
|
||||
})
|
||||
for _, operator := range operators {
|
||||
tw.AppendRow(table.Row{
|
||||
console.Bold + operator.Name + console.Normal,
|
||||
console.StyleBold.Render(operator.Name),
|
||||
status(operator.Online),
|
||||
})
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func displayOperators(operators []*clientpb.Operator, con *console.SliverClient)
|
||||
|
||||
func status(isOnline bool) string {
|
||||
if isOnline {
|
||||
return console.Bold + console.Green + "Online" + console.Normal
|
||||
return console.StyleBoldGreen.Render("Online")
|
||||
}
|
||||
return console.Bold + console.Red + "Offline" + console.Normal
|
||||
return console.StyleBoldRed.Render("Offline")
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func PrintPivotListenerDetails(listener *sliverpb.PivotListener, con *console.Sl
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(fmt.Sprintf(console.Bold+"%s Pivots"+console.Normal, PivotTypeToString(listener.Type)))
|
||||
tw.SetTitle(console.StyleBold.Render(fmt.Sprintf("%s Pivots", PivotTypeToString(listener.Type))))
|
||||
tw.AppendSeparator()
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
|
||||
+107
-107
@@ -36,77 +36,77 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
)
|
||||
|
||||
// Stylizes known processes in the `ps` command
|
||||
var knownSecurityTools = map[string][]string{
|
||||
// Process Name -> [Color, Stylized Name]
|
||||
"ccSvcHst.exe": {console.Red, "Symantec Endpoint Protection"}, // Symantec Endpoint Protection (SEP)
|
||||
"cb.exe": {console.Red, "Carbon Black"}, // Carbon Black
|
||||
"RepMgr.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor
|
||||
"RepUtils.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor
|
||||
"RepUx.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor
|
||||
"RepWSC.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor
|
||||
"scanhost.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor
|
||||
"elastic-agent.exe": {console.Red, "Elastic Agent"}, // Elastic Agent
|
||||
"elastic-endpoint.exe": {console.Red, "Elastic Agent"}, // Elastic Agent
|
||||
"filebeat.exe": {console.Red, "Elastic Agent"}, // Elastic Agent - log shipper
|
||||
"metricbeat.exe": {console.Red, "Elastic Agent"}, // Elastic Agent - metric shipper
|
||||
"smartscreen.exe": {console.Red, "Windows Smart Screen"}, // Windows Defender Smart Screen
|
||||
"MpCmdRun.exe": {console.Red, "Windows Defender"}, // Windows Defender Command-line
|
||||
"MonitoringHost.exe": {console.Red, "Windows Defender"}, // Microsoft Monitoring Agent
|
||||
"HealthService.exe": {console.Red, "Windows Defender"}, // Microsoft Monitoring Agent
|
||||
"MsMpEng.exe": {console.Red, "Windows Defender"}, // Windows Defender (Service Executable)
|
||||
"NisSrv.exe": {console.Red, "Windows Defender"}, // Windows Defender (Network Realtime Inspection)
|
||||
"SenseIR.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Live Response Session)
|
||||
"SenseNdr.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Network Detection and Response)
|
||||
"SenseSC.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Screenshot Capture Module)
|
||||
"SenseCE.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Classification Engine Module)
|
||||
"SenseCM.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Configuration Management Module)
|
||||
"SenseSampleUploader.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Sample Uploader Module)
|
||||
"SenseCncProxy.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Communication Module)
|
||||
"MsSense.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Service Executable)
|
||||
"CSFalconService.exe": {console.Red, "CrowdStrike"}, // Crowdstrike Falcon Service
|
||||
"CSFalconContainer.exe": {console.Red, "CrowdStrike"}, // CrowdStrike Falcon Container Security
|
||||
"bdservicehost.exe": {console.Red, "Bitdefender"}, // Bitdefender (Total Security)
|
||||
"bdagent.exe": {console.Red, "Bitdefender"}, // Bitdefender (Total Security)
|
||||
"bdredline.exe": {console.Red, "Bitdefender"}, // Bitdefender Redline Update Service (Source https://community.bitdefender.com/en/discussion/82135/bdredline-exe-bitdefender-total-security-2020)
|
||||
"Deep Security Manager.exe": {console.Red, "Trend Micro"}, // TM Deep Security Manager
|
||||
"coreServiceShell.exe": {console.Red, "Trend Micro"}, // TM Anti-malware scan process
|
||||
"ds_monitor.exe": {console.Red, "Trend Micro"}, // TM Deep Security Monitor
|
||||
"Notifier.exe": {console.Red, "Trend Micro"}, // TM Deep Security Notifier's process
|
||||
"dsa.exe": {console.Red, "Trend Micro"}, // TM Agent's main process
|
||||
"ds_nuagent.exe": {console.Red, "Trend Micro"}, // TM Advanced TLS traffic inspection
|
||||
"coreFrameworkHost.exe": {console.Red, "Trend Micro"}, // TM Anti-malware scan process
|
||||
"SentinelServiceHost.exe": {console.Red, "SentinelOne"}, // Sentinel One
|
||||
"SentinelStaticEngine.exe": {console.Red, "SentinelOne"}, // Sentinel One
|
||||
"SentinelStaticEngineScanner.exe": {console.Red, "SentinelOne"}, // Sentinel One
|
||||
"SentinelAgent.exe": {console.Red, "SentinelOne"}, // Sentinel One
|
||||
"SentinelAgentWorker.exe": {console.Red, "SentinelOne"}, // Sentinel One
|
||||
"SentinelHelperService.exe": {console.Red, "SentinelOne"}, // Sentinel One
|
||||
"SentinelBrowserNativeHost.exe": {console.Red, "SentinelOne"}, // Sentinel One
|
||||
"SentinelUI.exe": {console.Red, "SentinelOne"}, // Sentinel One
|
||||
"Sysmon.exe": {console.Red, "Sysmon"}, // Sysmon
|
||||
"Sysmon64.exe": {console.Red, "Sysmon64"}, // Sysmon64
|
||||
"CylanceSvc.exe": {console.Red, "Cylance"}, // Cylance
|
||||
"CylanceUI.exe": {console.Red, "Cylance"}, // Cylance
|
||||
"TaniumClient.exe": {console.Red, "Tanium"}, // Tanium
|
||||
"TaniumCX.exe": {console.Red, "Tanium"}, // Tanium
|
||||
"TaniumDetectEngine.exe": {console.Red, "Tanium"}, // Tanium
|
||||
"collector.exe": {console.Red, "Rapid 7 Collector"}, // Rapid 7 Insight Platform Collector
|
||||
"ir_agent.exe": {console.Red, "Rapid 7 Insight Agent"}, // Rapid 7 Insight Agent
|
||||
"eguiproxy.exe": {console.Red, "ESET Security"}, // ESET Internet Security
|
||||
"ekrn.exe": {console.Red, "ESET Security"}, // ESET Internet Security
|
||||
"efwd.exe": {console.Red, "ESET Security"}, // ESET Internet Security
|
||||
"AmSvc.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe
|
||||
"CrAmTray.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe
|
||||
"CrsSvc.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe
|
||||
"CybereasonAV.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe
|
||||
"cortex-xdr-payload.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - offline triage
|
||||
"cysandbox.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - sandbox
|
||||
"cyuserservice.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - user service
|
||||
"cywscsvc.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - security center service
|
||||
"tlaworker.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - local analysis worker
|
||||
"AEEngine.exe": {console.Red, "Faronics Anti-Executable"}, // Faronics Anti-Executable - security service
|
||||
"Antiexecutable.exe": {console.Red, "Faronics Anti-Executable"}, // Faronics Anti-Executable - gui and tray icon
|
||||
// Known security tools shown by `ps`/`pstree`.
|
||||
// Process executable -> product name.
|
||||
var knownSecurityTools = map[string]string{
|
||||
"ccSvcHst.exe": "Symantec Endpoint Protection", // Symantec Endpoint Protection (SEP)
|
||||
"cb.exe": "Carbon Black", // Carbon Black
|
||||
"RepMgr.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
|
||||
"RepUtils.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
|
||||
"RepUx.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
|
||||
"RepWSC.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
|
||||
"scanhost.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
|
||||
"elastic-agent.exe": "Elastic Agent", // Elastic Agent
|
||||
"elastic-endpoint.exe": "Elastic Agent", // Elastic Agent
|
||||
"filebeat.exe": "Elastic Agent", // Elastic Agent - log shipper
|
||||
"metricbeat.exe": "Elastic Agent", // Elastic Agent - metric shipper
|
||||
"smartscreen.exe": "Windows Smart Screen", // Windows Defender Smart Screen
|
||||
"MpCmdRun.exe": "Windows Defender", // Windows Defender Command-line
|
||||
"MonitoringHost.exe": "Windows Defender", // Microsoft Monitoring Agent
|
||||
"HealthService.exe": "Windows Defender", // Microsoft Monitoring Agent
|
||||
"MsMpEng.exe": "Windows Defender", // Windows Defender (Service Executable)
|
||||
"NisSrv.exe": "Windows Defender", // Windows Defender (Network Realtime Inspection)
|
||||
"SenseIR.exe": "Windows Defender MDE", // Windows Defender Endpoint (Live Response Session)
|
||||
"SenseNdr.exe": "Windows Defender MDE", // Windows Defender Endpoint (Network Detection and Response)
|
||||
"SenseSC.exe": "Windows Defender MDE", // Windows Defender Endpoint (Screenshot Capture Module)
|
||||
"SenseCE.exe": "Windows Defender MDE", // Windows Defender Endpoint (Classification Engine Module)
|
||||
"SenseCM.exe": "Windows Defender MDE", // Windows Defender Endpoint (Configuration Management Module)
|
||||
"SenseSampleUploader.exe": "Windows Defender MDE", // Windows Defender Endpoint (Sample Uploader Module)
|
||||
"SenseCncProxy.exe": "Windows Defender MDE", // Windows Defender Endpoint (Communication Module)
|
||||
"MsSense.exe": "Windows Defender MDE", // Windows Defender Endpoint (Service Executable)
|
||||
"CSFalconService.exe": "CrowdStrike", // Crowdstrike Falcon Service
|
||||
"CSFalconContainer.exe": "CrowdStrike", // CrowdStrike Falcon Container Security
|
||||
"bdservicehost.exe": "Bitdefender", // Bitdefender (Total Security)
|
||||
"bdagent.exe": "Bitdefender", // Bitdefender (Total Security)
|
||||
"bdredline.exe": "Bitdefender", // Bitdefender Redline Update Service (Source https://community.bitdefender.com/en/discussion/82135/bdredline-exe-bitdefender-total-security-2020)
|
||||
"Deep Security Manager.exe": "Trend Micro", // TM Deep Security Manager
|
||||
"coreServiceShell.exe": "Trend Micro", // TM Anti-malware scan process
|
||||
"ds_monitor.exe": "Trend Micro", // TM Deep Security Monitor
|
||||
"Notifier.exe": "Trend Micro", // TM Deep Security Notifier's process
|
||||
"dsa.exe": "Trend Micro", // TM Agent's main process
|
||||
"ds_nuagent.exe": "Trend Micro", // TM Advanced TLS traffic inspection
|
||||
"coreFrameworkHost.exe": "Trend Micro", // TM Anti-malware scan process
|
||||
"SentinelServiceHost.exe": "SentinelOne", // Sentinel One
|
||||
"SentinelStaticEngine.exe": "SentinelOne", // Sentinel One
|
||||
"SentinelStaticEngineScanner.exe": "SentinelOne", // Sentinel One
|
||||
"SentinelAgent.exe": "SentinelOne", // Sentinel One
|
||||
"SentinelAgentWorker.exe": "SentinelOne", // Sentinel One
|
||||
"SentinelHelperService.exe": "SentinelOne", // Sentinel One
|
||||
"SentinelBrowserNativeHost.exe": "SentinelOne", // Sentinel One
|
||||
"SentinelUI.exe": "SentinelOne", // Sentinel One
|
||||
"Sysmon.exe": "Sysmon", // Sysmon
|
||||
"Sysmon64.exe": "Sysmon64", // Sysmon64
|
||||
"CylanceSvc.exe": "Cylance", // Cylance
|
||||
"CylanceUI.exe": "Cylance", // Cylance
|
||||
"TaniumClient.exe": "Tanium", // Tanium
|
||||
"TaniumCX.exe": "Tanium", // Tanium
|
||||
"TaniumDetectEngine.exe": "Tanium", // Tanium
|
||||
"collector.exe": "Rapid 7 Collector", // Rapid 7 Insight Platform Collector
|
||||
"ir_agent.exe": "Rapid 7 Insight Agent", // Rapid 7 Insight Agent
|
||||
"eguiproxy.exe": "ESET Security", // ESET Internet Security
|
||||
"ekrn.exe": "ESET Security", // ESET Internet Security
|
||||
"efwd.exe": "ESET Security", // ESET Internet Security
|
||||
"AmSvc.exe": "Cybereason ActiveProbe", // Cybereason ActiveProbe
|
||||
"CrAmTray.exe": "Cybereason ActiveProbe", // Cybereason ActiveProbe
|
||||
"CrsSvc.exe": "Cybereason ActiveProbe", // Cybereason ActiveProbe
|
||||
"CybereasonAV.exe": "Cybereason ActiveProbe", // Cybereason ActiveProbe
|
||||
"cortex-xdr-payload.exe": "Palo Alto Cortex", // Cortex XDR - offline triage
|
||||
"cysandbox.exe": "Palo Alto Cortex", // Cortex XDR - sandbox
|
||||
"cyuserservice.exe": "Palo Alto Cortex", // Cortex XDR - user service
|
||||
"cywscsvc.exe": "Palo Alto Cortex", // Cortex XDR - security center service
|
||||
"tlaworker.exe": "Palo Alto Cortex", // Cortex XDR - local analysis worker
|
||||
"AEEngine.exe": "Faronics Anti-Executable", // Faronics Anti-Executable - security service
|
||||
"Antiexecutable.exe": "Faronics Anti-Executable", // Faronics Anti-Executable - gui and tray icon
|
||||
}
|
||||
|
||||
// PsCmd - List processes on the remote system
|
||||
@@ -264,10 +264,10 @@ func PrintPS(os string, ps *sliverpb.Ps, interactive bool, fullInfo bool, flags
|
||||
}
|
||||
|
||||
func findKnownSecurityProducts(ps *sliverpb.Ps) []string {
|
||||
uniqProducts := map[string]string{}
|
||||
uniqProducts := map[string]struct{}{}
|
||||
for _, proc := range ps.Processes {
|
||||
if secTool, ok := knownSecurityTools[proc.Executable]; ok {
|
||||
uniqProducts[secTool[1]] = secTool[0]
|
||||
if product, ok := knownSecurityTools[proc.Executable]; ok {
|
||||
uniqProducts[product] = struct{}{}
|
||||
}
|
||||
}
|
||||
products := make([]string, 0, len(uniqProducts))
|
||||
@@ -281,15 +281,15 @@ func findKnownSecurityProducts(ps *sliverpb.Ps) []string {
|
||||
func procRow(proc *commonpb.Process, cmdLine bool, fullInfo bool, con *console.SliverClient) table.Row {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
|
||||
color := console.Normal
|
||||
if secTool, ok := knownSecurityTools[proc.Executable]; ok {
|
||||
color = secTool[0]
|
||||
style := console.StyleNormal
|
||||
if _, ok := knownSecurityTools[proc.Executable]; ok {
|
||||
style = console.StyleRed
|
||||
}
|
||||
if session != nil && proc.Pid == session.PID {
|
||||
color = console.Green
|
||||
style = console.StyleGreen
|
||||
}
|
||||
if beacon != nil && proc.Pid == beacon.PID {
|
||||
color = console.Green
|
||||
style = console.StyleGreen
|
||||
}
|
||||
|
||||
var row table.Row
|
||||
@@ -303,28 +303,28 @@ func procRow(proc *commonpb.Process, cmdLine bool, fullInfo bool, con *console.S
|
||||
args = proc.Executable
|
||||
}
|
||||
row = table.Row{
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid),
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Owner),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Architecture),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, args),
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.SessionID),
|
||||
style.Render(fmt.Sprintf("%d", proc.Pid)),
|
||||
style.Render(fmt.Sprintf("%d", proc.Ppid)),
|
||||
style.Render(proc.Owner),
|
||||
style.Render(proc.Architecture),
|
||||
style.Render(args),
|
||||
style.Render(fmt.Sprintf("%d", proc.SessionID)),
|
||||
}
|
||||
} else {
|
||||
if fullInfo {
|
||||
row = table.Row{
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid),
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Owner),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Architecture),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Executable),
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.SessionID),
|
||||
style.Render(fmt.Sprintf("%d", proc.Pid)),
|
||||
style.Render(fmt.Sprintf("%d", proc.Ppid)),
|
||||
style.Render(proc.Owner),
|
||||
style.Render(proc.Architecture),
|
||||
style.Render(proc.Executable),
|
||||
style.Render(fmt.Sprintf("%d", proc.SessionID)),
|
||||
}
|
||||
} else {
|
||||
row = table.Row{
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid),
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Executable),
|
||||
style.Render(fmt.Sprintf("%d", proc.Pid)),
|
||||
style.Render(fmt.Sprintf("%d", proc.Ppid)),
|
||||
style.Render(proc.Executable),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,26 +341,26 @@ func procRow(proc *commonpb.Process, cmdLine bool, fullInfo bool, con *console.S
|
||||
args = proc.Executable
|
||||
}
|
||||
row = table.Row{
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid),
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Owner),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Architecture),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, args),
|
||||
style.Render(fmt.Sprintf("%d", proc.Pid)),
|
||||
style.Render(fmt.Sprintf("%d", proc.Ppid)),
|
||||
style.Render(proc.Owner),
|
||||
style.Render(proc.Architecture),
|
||||
style.Render(args),
|
||||
}
|
||||
} else {
|
||||
if fullInfo {
|
||||
row = table.Row{
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid),
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Owner),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Architecture),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Executable),
|
||||
style.Render(fmt.Sprintf("%d", proc.Pid)),
|
||||
style.Render(fmt.Sprintf("%d", proc.Ppid)),
|
||||
style.Render(proc.Owner),
|
||||
style.Render(proc.Architecture),
|
||||
style.Render(proc.Executable),
|
||||
}
|
||||
} else {
|
||||
row = table.Row{
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid),
|
||||
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, proc.Executable),
|
||||
style.Render(fmt.Sprintf("%d", proc.Pid)),
|
||||
style.Render(fmt.Sprintf("%d", proc.Ppid)),
|
||||
style.Render(proc.Executable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package processes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/xlab/treeprint"
|
||||
@@ -108,14 +107,14 @@ func (t *PsTree) AddProcess(proc *commonpb.Process) {
|
||||
}
|
||||
|
||||
func (t *PsTree) filterProc(proc *commonpb.Process) string {
|
||||
color := console.Normal
|
||||
style := console.StyleNormal
|
||||
if proc.Pid == t.implantPID {
|
||||
color = console.Green
|
||||
style = console.StyleGreen
|
||||
}
|
||||
if secTool, ok := knownSecurityTools[proc.Executable]; ok {
|
||||
color = secTool[0]
|
||||
if _, ok := knownSecurityTools[proc.Executable]; ok {
|
||||
style = console.StyleRed
|
||||
}
|
||||
return fmt.Sprintf(color+"%s"+console.Normal, proc.Executable)
|
||||
return style.Render(proc.Executable)
|
||||
}
|
||||
|
||||
func (t *PsTree) String() string {
|
||||
|
||||
@@ -19,7 +19,6 @@ package reaction
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
@@ -51,7 +50,7 @@ func ReactionCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
func displayReactionsTable(eventType string, reactions []core.Reaction, con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(fmt.Sprintf(console.Bold+"%s"+console.Normal, EventTypeToTitle(eventType)))
|
||||
tw.SetTitle(console.StyleBold.Render(EventTypeToTitle(eventType)))
|
||||
tw.AppendSeparator()
|
||||
slackSpace := len(EventTypeToTitle(eventType)) - len("Commands") - len("ID") - 3
|
||||
if slackSpace < 0 {
|
||||
|
||||
@@ -26,6 +26,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("beacon-interval", "i", "", "beacon callback interval")
|
||||
f.StringP("beacon-jitter", "j", "", "beacon callback jitter (random up to)")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.StringP("c2-uri", "c", "", "C2 URI to switch to")
|
||||
})
|
||||
|
||||
renameCmd := &cobra.Command{
|
||||
|
||||
@@ -20,6 +20,7 @@ package reconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -52,6 +53,7 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
var beaconJitter time.Duration
|
||||
binterval, _ := cmd.Flags().GetString("beacon-interval")
|
||||
bjitter, _ := cmd.Flags().GetString("beacon-jitter")
|
||||
C2URI, _ := cmd.Flags().GetString("c2-uri")
|
||||
|
||||
if beacon != nil {
|
||||
if binterval != "" {
|
||||
@@ -73,10 +75,47 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
if C2URI != "" {
|
||||
// validate uri
|
||||
newURI, err := url.Parse(C2URI)
|
||||
if err != nil || newURI.Scheme == "" || newURI.Host == "" {
|
||||
con.PrintErrorf("Invalid C2 URI: %s (expected format: protocol://host:port)\n", C2URI)
|
||||
return
|
||||
}
|
||||
|
||||
// validate the protocol is the same as the beacon (cuz it compiled with only one protocol)
|
||||
var activeC2 string
|
||||
if beacon != nil {
|
||||
activeC2 = beacon.ActiveC2
|
||||
} else if session != nil {
|
||||
activeC2 = session.ActiveC2
|
||||
}
|
||||
|
||||
if activeC2 != "" {
|
||||
currentURI, err := url.Parse(activeC2)
|
||||
if err == nil {
|
||||
// must be the same protocol
|
||||
if newURI.Scheme != currentURI.Scheme {
|
||||
con.PrintErrorf("Cannot switch protocol from %s to %s (protocol must match compiled implant)\n",
|
||||
currentURI.Scheme, newURI.Scheme)
|
||||
return
|
||||
}
|
||||
// for now only support the same Host/C2 Server (becuase the implant crypto keys problem)
|
||||
if newURI.Hostname() != currentURI.Hostname() {
|
||||
con.PrintWarnf("Switching to a different host (%s -> %s). This only works if both servers share the same crypto keys.\n",
|
||||
currentURI.Hostname(), newURI.Hostname())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
con.PrintInfof("Updating C2 endpoint to: %s\n", C2URI)
|
||||
}
|
||||
|
||||
reconfig, err := con.Rpc.Reconfigure(context.Background(), &sliverpb.ReconfigureReq{
|
||||
ReconnectInterval: int64(reconnectInterval),
|
||||
BeaconInterval: int64(beaconInterval),
|
||||
BeaconJitter: int64(beaconJitter),
|
||||
C2URI: C2URI,
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/monitor"
|
||||
"github.com/bishopfox/sliver/client/command/operators"
|
||||
"github.com/bishopfox/sliver/client/command/reaction"
|
||||
"github.com/bishopfox/sliver/client/command/serverctx"
|
||||
"github.com/bishopfox/sliver/client/command/sessions"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
|
||||
@@ -97,6 +98,7 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
|
||||
// Core
|
||||
bind(consts.GenericHelpGroup,
|
||||
exit.Command,
|
||||
serverctx.Commands,
|
||||
licenses.Commands,
|
||||
settings.Commands,
|
||||
alias.Commands,
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
//go:build client
|
||||
|
||||
package serverctx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
clienttheme "github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
// Commands returns client-only server connection context commands.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Show and manage the current server connection",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
serverInfo(cmd, con)
|
||||
},
|
||||
}
|
||||
|
||||
switchCmd := &cobra.Command{
|
||||
Use: "switch",
|
||||
Short: "Switch to another server/operator profile",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
serverSwitch(cmd, con)
|
||||
},
|
||||
}
|
||||
root.AddCommand(switchCmd)
|
||||
|
||||
return []*cobra.Command{root}
|
||||
}
|
||||
|
||||
func serverInfo(_ *cobra.Command, con *console.SliverClient) {
|
||||
details, state, ok := con.CurrentConnection()
|
||||
if !ok || con.Rpc == nil {
|
||||
con.PrintErrorf("Not connected\n")
|
||||
return
|
||||
}
|
||||
|
||||
var hostPort string
|
||||
operator := ""
|
||||
fingerprint := ""
|
||||
if details != nil && details.Config != nil {
|
||||
hostPort = fmt.Sprintf("%s:%d", details.Config.LHost, details.Config.LPort)
|
||||
operator = operatorFromConfig(details.Config)
|
||||
fingerprint = shortCertFingerprint(details.Config)
|
||||
}
|
||||
if operator == "" {
|
||||
operator = "<unknown>"
|
||||
}
|
||||
if hostPort == "" {
|
||||
hostPort = "<unknown>"
|
||||
}
|
||||
|
||||
ver, err := con.Rpc.GetVersion(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("GetVersion failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
const keyWidth = 8 // Align on ": " (e.g., "Operator", "Version")
|
||||
|
||||
label := func(key string) string {
|
||||
// Bold "default/white" label keys, padded so colons align.
|
||||
return console.StyleBoldGray.Render(fmt.Sprintf("%-*s:", keyWidth, key))
|
||||
}
|
||||
valueSecondary := func(s string) string { return console.StylePurple.Render(s) }
|
||||
valueDim := func(s string) string { return console.StyleGray.Render(s) }
|
||||
|
||||
stateStyle := console.StyleGray
|
||||
switch state {
|
||||
case connectivity.Ready:
|
||||
stateStyle = console.StyleGreen
|
||||
case connectivity.Connecting:
|
||||
stateStyle = console.StyleOrange
|
||||
case connectivity.TransientFailure, connectivity.Shutdown:
|
||||
stateStyle = console.StyleRed
|
||||
case connectivity.Idle:
|
||||
stateStyle = console.StyleOrange
|
||||
}
|
||||
|
||||
// Server/operator values should be "default" terminal text (not themed colorized).
|
||||
con.Printf("%s %s\n", label("Server"), hostPort)
|
||||
if details != nil && details.ConfigKey != "" {
|
||||
con.Printf("%s %s\n", label("Profile"), valueSecondary(details.ConfigKey))
|
||||
}
|
||||
con.Printf("%s %s\n", label("Operator"), operator)
|
||||
if fingerprint != "" {
|
||||
con.Printf("%s %s\n", label("Cert"), valueDim(fingerprint))
|
||||
}
|
||||
con.Printf("%s %s\n", label("gRPC"), stateStyle.Render(state.String()))
|
||||
con.Printf("%s %s %s\n",
|
||||
label("Version"),
|
||||
console.StyleBold.Render(fmt.Sprintf("%d.%d.%d", ver.Major, ver.Minor, ver.Patch)),
|
||||
valueDim("("+ver.Commit+")"),
|
||||
)
|
||||
}
|
||||
|
||||
func serverSwitch(_ *cobra.Command, con *console.SliverClient) {
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
con.PrintErrorf("No config files found at %s\n", assets.GetConfigDir())
|
||||
return
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(configs))
|
||||
for k := range configs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
mgr := getConsoleManager(con)
|
||||
mgr.syncActiveFromConsole(con)
|
||||
|
||||
instances, activeID := mgr.snapshot()
|
||||
result, err := runSwitchTUI(instances, activeID, keys, mgr.configInstanceCountsLocked(instances))
|
||||
if err != nil {
|
||||
con.PrintErrorf("TUI error: %s\n", err)
|
||||
return
|
||||
}
|
||||
if result.Action == actionNone {
|
||||
return
|
||||
}
|
||||
switch result.Action {
|
||||
case actionSwitchExisting:
|
||||
inst := mgr.findInstance(result.ExistingID)
|
||||
if inst == nil || inst.Config == nil {
|
||||
con.PrintErrorf("Invalid selection\n")
|
||||
return
|
||||
}
|
||||
if inst.ID == activeID {
|
||||
con.PrintInfof("Already on %s\n", inst.DisplayName())
|
||||
return
|
||||
}
|
||||
|
||||
rpc, conn, err := transport.MTLSConnect(inst.Config)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Connection failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := con.SetConnection(rpc, conn, &console.ConnectionDetails{ConfigKey: inst.ConfigKey, Config: inst.Config}); err != nil {
|
||||
_ = conn.Close()
|
||||
con.PrintErrorf("Switch failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
mgr.setActive(inst.ID)
|
||||
con.PrintSuccessf("Switched to %s:%d (%s)\n", inst.Config.LHost, inst.Config.LPort, inst.DisplayName())
|
||||
|
||||
case actionNewConsole:
|
||||
cfg := configs[result.ConfigKey]
|
||||
if cfg == nil {
|
||||
con.PrintErrorf("Invalid selection\n")
|
||||
return
|
||||
}
|
||||
|
||||
rpc, conn, err := transport.MTLSConnect(cfg)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Connection failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := con.SetConnection(rpc, conn, &console.ConnectionDetails{ConfigKey: result.ConfigKey, Config: cfg}); err != nil {
|
||||
_ = conn.Close()
|
||||
con.PrintErrorf("Switch failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
inst := mgr.addInstance(result.ConfigKey, cfg)
|
||||
con.PrintSuccessf("Connected to %s:%d (%s)\n", cfg.LHost, cfg.LPort, inst.DisplayName())
|
||||
}
|
||||
}
|
||||
|
||||
var consoleManagers sync.Map // *console.SliverClient -> *consoleManager
|
||||
|
||||
type consoleManager struct {
|
||||
mu sync.Mutex
|
||||
nextID int
|
||||
activeID string
|
||||
insts []*consoleInstance
|
||||
}
|
||||
|
||||
type consoleInstance struct {
|
||||
ID string
|
||||
ConfigKey string
|
||||
Config *assets.ClientConfig
|
||||
CreatedAt time.Time
|
||||
LastUsed time.Time
|
||||
}
|
||||
|
||||
func (i *consoleInstance) DisplayName() string {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("console-%s", i.ID)
|
||||
}
|
||||
|
||||
func getConsoleManager(con *console.SliverClient) *consoleManager {
|
||||
if con == nil {
|
||||
return &consoleManager{}
|
||||
}
|
||||
if v, ok := consoleManagers.Load(con); ok {
|
||||
return v.(*consoleManager)
|
||||
}
|
||||
m := &consoleManager{}
|
||||
actual, _ := consoleManagers.LoadOrStore(con, m)
|
||||
return actual.(*consoleManager)
|
||||
}
|
||||
|
||||
func (m *consoleManager) syncActiveFromConsole(con *console.SliverClient) {
|
||||
if m == nil || con == nil {
|
||||
return
|
||||
}
|
||||
details, _, ok := con.CurrentConnection()
|
||||
if !ok || details == nil || details.Config == nil || details.ConfigKey == "" {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// If we already have an active instance, keep it. Otherwise create one from the current connection.
|
||||
if m.activeID != "" {
|
||||
return
|
||||
}
|
||||
|
||||
m.nextID++
|
||||
id := fmt.Sprintf("%d", m.nextID)
|
||||
now := time.Now()
|
||||
m.activeID = id
|
||||
m.insts = append(m.insts, &consoleInstance{
|
||||
ID: id,
|
||||
ConfigKey: details.ConfigKey,
|
||||
Config: details.Config,
|
||||
CreatedAt: now,
|
||||
LastUsed: now,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *consoleManager) snapshot() ([]*consoleInstance, string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
out := make([]*consoleInstance, 0, len(m.insts))
|
||||
out = append(out, m.insts...)
|
||||
return out, m.activeID
|
||||
}
|
||||
|
||||
func (m *consoleManager) findInstance(id string) *consoleInstance {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, inst := range m.insts {
|
||||
if inst != nil && inst.ID == id {
|
||||
return inst
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *consoleManager) setActive(id string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.activeID = id
|
||||
for _, inst := range m.insts {
|
||||
if inst != nil && inst.ID == id {
|
||||
inst.LastUsed = time.Now()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *consoleManager) addInstance(configKey string, cfg *assets.ClientConfig) *consoleInstance {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.nextID++
|
||||
id := fmt.Sprintf("%d", m.nextID)
|
||||
now := time.Now()
|
||||
inst := &consoleInstance{
|
||||
ID: id,
|
||||
ConfigKey: configKey,
|
||||
Config: cfg,
|
||||
CreatedAt: now,
|
||||
LastUsed: now,
|
||||
}
|
||||
m.insts = append(m.insts, inst)
|
||||
m.activeID = id
|
||||
return inst
|
||||
}
|
||||
|
||||
func (m *consoleManager) configInstanceCountsLocked(insts []*consoleInstance) map[string]int {
|
||||
// Caller provides insts snapshot; we just count.
|
||||
counts := map[string]int{}
|
||||
for _, inst := range insts {
|
||||
if inst == nil {
|
||||
continue
|
||||
}
|
||||
counts[inst.ConfigKey]++
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
type switchAction int
|
||||
|
||||
const (
|
||||
actionNone switchAction = iota
|
||||
actionSwitchExisting
|
||||
actionNewConsole
|
||||
)
|
||||
|
||||
type switchResult struct {
|
||||
Action switchAction
|
||||
ExistingID string
|
||||
ConfigKey string
|
||||
}
|
||||
|
||||
type switchTab int
|
||||
|
||||
const (
|
||||
tabExisting switchTab = iota
|
||||
tabNew
|
||||
)
|
||||
|
||||
type switchTUIModel struct {
|
||||
tab switchTab
|
||||
existing *huh.Form
|
||||
newConn *huh.Form
|
||||
result switchResult
|
||||
width int
|
||||
height int
|
||||
theme *huh.Theme
|
||||
styles switchStyles
|
||||
activeID string
|
||||
counts map[string]int
|
||||
initOnce bool
|
||||
keyMap *huh.KeyMap
|
||||
}
|
||||
|
||||
type switchStyles struct {
|
||||
frame lipgloss.Style
|
||||
header lipgloss.Style
|
||||
tabActive lipgloss.Style
|
||||
tabInactive lipgloss.Style
|
||||
footer lipgloss.Style
|
||||
footerDim lipgloss.Style
|
||||
divider lipgloss.Style
|
||||
}
|
||||
|
||||
func runSwitchTUI(instances []*consoleInstance, activeID string, configKeys []string, counts map[string]int) (switchResult, error) {
|
||||
m := newSwitchTUIModel(instances, activeID, configKeys, counts)
|
||||
prog := tea.NewProgram(m, tea.WithAltScreen())
|
||||
finalModel, err := prog.Run()
|
||||
if err != nil {
|
||||
return switchResult{}, err
|
||||
}
|
||||
out, ok := finalModel.(switchTUIModel)
|
||||
if !ok {
|
||||
return switchResult{}, fmt.Errorf("unexpected TUI state")
|
||||
}
|
||||
return out.result, nil
|
||||
}
|
||||
|
||||
func newSwitchTUIModel(instances []*consoleInstance, activeID string, configKeys []string, counts map[string]int) switchTUIModel {
|
||||
keyMap := huh.NewDefaultKeyMap()
|
||||
// Free up tab for switching between views; use enter to select/advance.
|
||||
keyMap.Select.Next = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select"))
|
||||
keyMap.Select.Submit = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit"))
|
||||
|
||||
huhTheme := clienttheme.HuhTheme()
|
||||
|
||||
styles := switchStyles{
|
||||
frame: lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(clienttheme.DefaultMod(300)).
|
||||
Padding(0, 1),
|
||||
header: lipgloss.NewStyle().MarginBottom(1),
|
||||
tabActive: lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(clienttheme.DefaultMod(900)).
|
||||
Background(clienttheme.Primary()).
|
||||
Padding(0, 1),
|
||||
tabInactive: lipgloss.NewStyle().
|
||||
Foreground(clienttheme.DefaultMod(500)).
|
||||
Background(clienttheme.DefaultMod(50)).
|
||||
Padding(0, 1),
|
||||
footer: lipgloss.NewStyle().MarginTop(1),
|
||||
footerDim: lipgloss.NewStyle().Foreground(clienttheme.DefaultMod(400)),
|
||||
divider: lipgloss.NewStyle().Foreground(clienttheme.DefaultMod(300)),
|
||||
}
|
||||
|
||||
existingID := activeID
|
||||
existingOptions := make([]huh.Option[string], 0, len(instances))
|
||||
for _, inst := range instances {
|
||||
if inst == nil {
|
||||
continue
|
||||
}
|
||||
label := fmt.Sprintf("%s %s", inst.DisplayName(), inst.ConfigKey)
|
||||
if inst.ID == activeID && activeID != "" {
|
||||
label = fmt.Sprintf("* %s", label)
|
||||
}
|
||||
existingOptions = append(existingOptions, huh.NewOption(label, inst.ID))
|
||||
}
|
||||
|
||||
if len(existingOptions) == 0 {
|
||||
// Always provide at least one option so Huh doesn't error.
|
||||
existingOptions = append(existingOptions, huh.NewOption("(no existing consoles)", ""))
|
||||
}
|
||||
|
||||
existingSelect := huh.NewSelect[string]().
|
||||
Title("Existing Consoles").
|
||||
Description("Switch to an already-created console context. Use / to filter.").
|
||||
Options(existingOptions...).
|
||||
Key("existing").
|
||||
Value(&existingID)
|
||||
|
||||
existingForm := huh.NewForm(huh.NewGroup(existingSelect)).
|
||||
WithTheme(huhTheme).
|
||||
WithKeyMap(keyMap).
|
||||
WithShowHelp(false)
|
||||
|
||||
newKey := ""
|
||||
configOptions := make([]huh.Option[string], 0, len(configKeys))
|
||||
for _, k := range configKeys {
|
||||
label := k
|
||||
if n := counts[k]; n > 0 {
|
||||
label = fmt.Sprintf("%s (existing: %d)", k, n)
|
||||
}
|
||||
configOptions = append(configOptions, huh.NewOption(label, k))
|
||||
if newKey == "" {
|
||||
newKey = k
|
||||
}
|
||||
}
|
||||
newSelect := huh.NewSelect[string]().
|
||||
Title("New Connection / Console").
|
||||
Description("Create a new console context from a profile in ~/.sliver-client/configs. Use / to filter.").
|
||||
Options(configOptions...).
|
||||
Key("config").
|
||||
Value(&newKey)
|
||||
|
||||
newForm := huh.NewForm(huh.NewGroup(newSelect)).
|
||||
WithTheme(huhTheme).
|
||||
WithKeyMap(keyMap).
|
||||
WithShowHelp(false)
|
||||
|
||||
return switchTUIModel{
|
||||
tab: tabExisting,
|
||||
existing: existingForm,
|
||||
newConn: newForm,
|
||||
theme: huhTheme,
|
||||
styles: styles,
|
||||
activeID: activeID,
|
||||
counts: counts,
|
||||
keyMap: keyMap,
|
||||
}
|
||||
}
|
||||
|
||||
func (m switchTUIModel) Init() tea.Cmd {
|
||||
// Initialize both forms so they're ready when switching tabs.
|
||||
return tea.Batch(m.existing.Init(), m.newConn.Init())
|
||||
}
|
||||
|
||||
func (m switchTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
|
||||
// Forward to both forms so they size correctly.
|
||||
modelA, cmdA := m.existing.Update(msg)
|
||||
modelB, cmdB := m.newConn.Update(msg)
|
||||
m.existing = modelA.(*huh.Form)
|
||||
m.newConn = modelB.(*huh.Form)
|
||||
return m, tea.Batch(cmdA, cmdB)
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "tab":
|
||||
if m.tab == tabExisting {
|
||||
m.tab = tabNew
|
||||
} else {
|
||||
m.tab = tabExisting
|
||||
}
|
||||
return m, nil
|
||||
case "shift+tab":
|
||||
if m.tab == tabExisting {
|
||||
m.tab = tabNew
|
||||
} else {
|
||||
m.tab = tabExisting
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate to active tab's form.
|
||||
switch m.tab {
|
||||
case tabExisting:
|
||||
model, cmd := m.existing.Update(msg)
|
||||
m.existing = model.(*huh.Form)
|
||||
if m.existing.State == huh.StateCompleted {
|
||||
id := m.existing.GetString("existing")
|
||||
if id != "" {
|
||||
m.result = switchResult{Action: actionSwitchExisting, ExistingID: id}
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
if m.existing.State == huh.StateAborted {
|
||||
m.result = switchResult{Action: actionNone}
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, cmd
|
||||
|
||||
case tabNew:
|
||||
model, cmd := m.newConn.Update(msg)
|
||||
m.newConn = model.(*huh.Form)
|
||||
if m.newConn.State == huh.StateCompleted {
|
||||
key := m.newConn.GetString("config")
|
||||
if key != "" {
|
||||
m.result = switchResult{Action: actionNewConsole, ConfigKey: key}
|
||||
}
|
||||
return m, tea.Quit
|
||||
}
|
||||
if m.newConn.State == huh.StateAborted {
|
||||
m.result = switchResult{Action: actionNone}
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m switchTUIModel) View() string {
|
||||
header := m.renderHeader()
|
||||
|
||||
body := ""
|
||||
switch m.tab {
|
||||
case tabExisting:
|
||||
body = m.existing.View()
|
||||
case tabNew:
|
||||
body = m.newConn.View()
|
||||
}
|
||||
|
||||
footer := m.renderFooter()
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left, header, body, footer)
|
||||
return m.styles.frame.Render(content)
|
||||
}
|
||||
|
||||
func (m switchTUIModel) renderHeader() string {
|
||||
left := m.styles.tabInactive.Render("Existing")
|
||||
right := m.styles.tabInactive.Render("New")
|
||||
if m.tab == tabExisting {
|
||||
left = m.styles.tabActive.Render("Existing")
|
||||
} else {
|
||||
right = m.styles.tabActive.Render("New")
|
||||
}
|
||||
line := lipgloss.JoinHorizontal(lipgloss.Left, left, m.styles.divider.Render(" "), right)
|
||||
return m.styles.header.Render(line)
|
||||
}
|
||||
|
||||
func (m switchTUIModel) renderFooter() string {
|
||||
keys := "tab: switch view enter: select /: filter ctrl+c: cancel"
|
||||
return m.styles.footerDim.Render(keys)
|
||||
}
|
||||
|
||||
func operatorFromConfig(cfg *assets.ClientConfig) string {
|
||||
if cfg == nil {
|
||||
return ""
|
||||
}
|
||||
// Prefer the certificate CN, since config.Operator is mostly informational.
|
||||
block, _ := pem.Decode([]byte(cfg.Certificate))
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil && cert.Subject.CommonName != "" {
|
||||
return cert.Subject.CommonName
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(cfg.Operator) != "" {
|
||||
return strings.TrimSpace(cfg.Operator)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func shortCertFingerprint(cfg *assets.ClientConfig) string {
|
||||
if cfg == nil {
|
||||
return ""
|
||||
}
|
||||
digest := sha256.Sum256([]byte(cfg.Certificate))
|
||||
return fmt.Sprintf("%x", digest[:8])
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !client
|
||||
|
||||
package serverctx
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Commands is a no-op when building without the `client` build tag (e.g. sliver-server).
|
||||
func Commands(_ *console.SliverClient) []*cobra.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -192,15 +192,15 @@ func PrintSessions(sessions map[string]*clientpb.Session, filter string, filterR
|
||||
})
|
||||
|
||||
for _, session := range sessions {
|
||||
color := console.Normal
|
||||
style := console.StyleNormal
|
||||
if con.ActiveTarget.GetSession() != nil && con.ActiveTarget.GetSession().ID == session.ID {
|
||||
color = console.Green
|
||||
style = console.StyleGreen
|
||||
}
|
||||
var SessionHealth string
|
||||
if session.IsDead {
|
||||
SessionHealth = console.Bold + console.Red + "[DEAD]" + console.Normal
|
||||
SessionHealth = console.StyleBoldRed.Render("[DEAD]")
|
||||
} else {
|
||||
SessionHealth = console.Bold + console.Green + "[ALIVE]" + console.Normal
|
||||
SessionHealth = console.StyleBoldGreen.Render("[ALIVE]")
|
||||
}
|
||||
burned := ""
|
||||
if session.Burned {
|
||||
@@ -211,33 +211,33 @@ func PrintSessions(sessions map[string]*clientpb.Session, filter string, filterR
|
||||
var rowEntries []string
|
||||
if wideTermWidth {
|
||||
rowEntries = []string{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, ShortSessionID(session.ID)),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, session.Name),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, session.Transport),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, session.RemoteAddress),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, session.Hostname),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, username),
|
||||
fmt.Sprintf(color+"%s (%d)"+console.Normal, session.Filename, session.PID),
|
||||
style.Render(ShortSessionID(session.ID)),
|
||||
style.Render(session.Name),
|
||||
style.Render(session.Transport),
|
||||
style.Render(session.RemoteAddress),
|
||||
style.Render(session.Hostname),
|
||||
style.Render(username),
|
||||
style.Render(fmt.Sprintf("%s (%d)", session.Filename, session.PID)),
|
||||
}
|
||||
|
||||
if windowsSessionInList {
|
||||
rowEntries = append(rowEntries, fmt.Sprintf(color+"%s"+console.Normal, session.Integrity))
|
||||
rowEntries = append(rowEntries, style.Render(session.Integrity))
|
||||
}
|
||||
|
||||
rowEntries = append(rowEntries, []string{
|
||||
fmt.Sprintf(color+"%s/%s"+console.Normal, session.OS, session.Arch),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, session.Locale),
|
||||
style.Render(fmt.Sprintf("%s/%s", session.OS, session.Arch)),
|
||||
style.Render(session.Locale),
|
||||
con.FormatDateDelta(time.Unix(session.LastCheckin, 0), wideTermWidth, false),
|
||||
burned + SessionHealth,
|
||||
}...)
|
||||
} else {
|
||||
rowEntries = []string{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, ShortSessionID(session.ID)),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, session.Transport),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, session.RemoteAddress),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, session.Hostname),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, username),
|
||||
fmt.Sprintf(color+"%s/%s"+console.Normal, session.OS, session.Arch),
|
||||
style.Render(ShortSessionID(session.ID)),
|
||||
style.Render(session.Transport),
|
||||
style.Render(session.RemoteAddress),
|
||||
style.Render(session.Hostname),
|
||||
style.Render(username),
|
||||
style.Render(fmt.Sprintf("%s/%s", session.OS, session.Arch)),
|
||||
burned + SessionHealth,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -58,12 +59,22 @@ func SettingsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.PrintErrorf("Failed to apply settings form values: %s\n", err)
|
||||
return
|
||||
}
|
||||
if err := assets.SaveSettings(con.Settings); err != nil {
|
||||
con.PrintErrorf("Failed to save settings: %s\n", err)
|
||||
return
|
||||
}
|
||||
if assets.NormalizePromptStyle(result.PromptStyle) == assets.PromptStyleCustom {
|
||||
settingsPath := filepath.Join(assets.GetRootAppDir(), "tui-settings.yaml")
|
||||
con.PrintInfof("Prompt style is %q. Edit %s to set %q.\n", assets.PromptStyleCustom, settingsPath, "prompt_template")
|
||||
con.Println("")
|
||||
}
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{"Name", "Value", "Description"})
|
||||
tw.AppendRow(table.Row{"Tables", con.Settings.TableStyle, "Set the stylization of tables"})
|
||||
tw.AppendRow(table.Row{"Prompt", con.Settings.PromptStyle, "Set the console prompt prefix style"})
|
||||
tw.AppendRow(table.Row{"Auto Adult", con.Settings.AutoAdult, "Automatically accept OPSEC warnings"})
|
||||
tw.AppendRow(table.Row{"Auto Beacon Results", con.Settings.BeaconAutoResults, "Automatically display beacon results when tasks complete"})
|
||||
tw.AppendRow(table.Row{"Small Term Width", con.Settings.SmallTermWidth, "Omit some table columns when terminal width is less than this value"})
|
||||
@@ -221,6 +232,7 @@ func applySettingsForm(settings *assets.ClientSettings, result *forms.SettingsFo
|
||||
if width < 1 {
|
||||
return errors.New("small terminal width must be 1 or greater")
|
||||
}
|
||||
promptStyle := assets.NormalizePromptStyle(result.PromptStyle)
|
||||
|
||||
settings.TableStyle = result.TableStyle
|
||||
settings.AutoAdult = result.AutoAdult
|
||||
@@ -230,5 +242,6 @@ func applySettingsForm(settings *assets.ClientSettings, result *forms.SettingsFo
|
||||
settings.VimMode = result.VimMode
|
||||
settings.UserConnect = result.UserConnect
|
||||
settings.ConsoleLogs = result.ConsoleLogs
|
||||
settings.PromptStyle = promptStyle
|
||||
return nil
|
||||
}
|
||||
@@ -218,12 +218,12 @@ func PaginateTable(tw table.Writer, skipPages int, overflow bool, interactive bo
|
||||
}
|
||||
con.Println()
|
||||
} else {
|
||||
con.Printf(console.Bold+"Page [%d/%d]\n", pageNumber+1, len(pages))
|
||||
con.Printf("%s\n", console.StyleBold.Render(fmt.Sprintf("Page [%d/%d]", pageNumber+1, len(pages))))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if 1 < len(pages) {
|
||||
con.Printf(console.Bold+"Page [%d/%d]\n", pageNumber+1, len(pages))
|
||||
con.Printf("%s\n", console.StyleBold.Render(fmt.Sprintf("Page [%d/%d]", pageNumber+1, len(pages))))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ Implements the 'shell' command group for the Sliver client console. Handlers map
|
||||
|
||||
## Go Files
|
||||
|
||||
- `commands.go` – Exposes the interactive shell command and binds its options.
|
||||
- `commands.go` – Exposes the shell command group (`shell`, `shell ls`, `shell attach`, `shell kill`) and binds options.
|
||||
- `completers.go` – Shell-specific command completion helpers.
|
||||
- `filter-reader_generic.go` – Provides output filtering for shell streams on POSIX targets.
|
||||
- `filter-reader_windows.go` – Implements CRLF-aware filtering and decoding for Windows shell sessions.
|
||||
- `shell.go` – Launches interactive command shells over RPC and manages IO loops.
|
||||
- `ioloop.go` – Handles attached shell stdin forwarding, escape handling, and fast local detach/exit behavior.
|
||||
- `manager.go` – Tracks managed shell tunnels so detached shells can be listed and re-attached.
|
||||
- `shell.go` – Launches/attaches shell tunnels and coordinates managed shell lifecycle.
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"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"
|
||||
)
|
||||
@@ -28,5 +29,46 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
shellLsCmd := &cobra.Command{
|
||||
Use: consts.LsStr,
|
||||
Short: "List managed local shell tunnels",
|
||||
Long: help.GetHelpFor([]string{consts.ShellStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ShellLsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
shellCmd.AddCommand(shellLsCmd)
|
||||
|
||||
shellAttachCmd := &cobra.Command{
|
||||
Use: "attach <id>",
|
||||
Short: "Attach to a managed local shell tunnel",
|
||||
Long: help.GetHelpFor([]string{consts.ShellStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ShellAttachCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, shellAttachCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
shellCmd.AddCommand(shellAttachCmd)
|
||||
carapace.Gen(shellAttachCmd).PositionalCompletion(ShellIDCompleter(con).Usage("managed shell ID"))
|
||||
|
||||
shellKillCmd := &cobra.Command{
|
||||
Use: consts.KillStr + " <id>",
|
||||
Short: "Kill a managed shell and clean up remote process state",
|
||||
Long: help.GetHelpFor([]string{consts.ShellStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ShellKillCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, shellKillCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("force", "F", false, "force process cleanup via terminate rpc")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
shellCmd.AddCommand(shellKillCmd)
|
||||
carapace.Gen(shellKillCmd).PositionalCompletion(ShellIDCompleter(con).Usage("managed shell ID"))
|
||||
|
||||
return []*cobra.Command{shellCmd}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/rsteube/carapace"
|
||||
)
|
||||
|
||||
// ShellIDCompleter completes IDs of managed local shells.
|
||||
func ShellIDCompleter(_ *console.SliverClient) carapace.Action {
|
||||
callback := func(_ carapace.Context) carapace.Action {
|
||||
results := make([]string, 0)
|
||||
|
||||
managed := shells.List()
|
||||
if len(managed) == 0 {
|
||||
return carapace.ActionMessage("no managed shells")
|
||||
}
|
||||
|
||||
for _, sh := range managed {
|
||||
results = append(results, strconv.Itoa(sh.ID))
|
||||
results = append(results, fmt.Sprintf("%s, pid=%d, state=%s", sessionLabel(sh.SessionID, sh.SessionName), sh.Pid, sh.State()))
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("managed shells")
|
||||
}
|
||||
|
||||
return carapace.ActionCallback(callback)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
)
|
||||
|
||||
// runAttachedIO forwards local stdin to a shell tunnel while attached to the shell.
|
||||
// It returns:
|
||||
// - detached=true when user hit escape (Ctrl-])
|
||||
// - closeRequested=true when user requested shell close (EOF/exit/logout)
|
||||
func runAttachedIO(tunnel *core.TunnelIO, con *console.SliverClient) (detached bool, closeRequested bool) {
|
||||
// We can't pipe stdin directly into the tunnel: if the gRPC tunnel send path blocks
|
||||
// (network hang, server gone, etc.) tunnel.Write can block forever and "lock" the
|
||||
// client in shell mode. Instead, buffer stdin locally and look for a local escape key.
|
||||
const (
|
||||
shellEscapeByte = byte(0x1d) // Ctrl-]
|
||||
stdinBufSize = 32 * 1024
|
||||
stdinQueueDepth = 128
|
||||
)
|
||||
|
||||
stdinQueue := make(chan []byte, stdinQueueDepth)
|
||||
stopWriter := make(chan struct{})
|
||||
var writerWG sync.WaitGroup
|
||||
writerWG.Add(1)
|
||||
|
||||
go func() {
|
||||
defer writerWG.Done()
|
||||
defer func() {
|
||||
// Tunnel teardown can race with writes (server-side close, disconnect, etc.).
|
||||
// Don't let a send-on-closed-channel panic bring down the whole client.
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Shell tunnel writer stopped: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopWriter:
|
||||
return
|
||||
case data, ok := <-stdinQueue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-stopWriter:
|
||||
return
|
||||
case tunnel.Send <- data:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
close(stopWriter)
|
||||
close(stdinQueue)
|
||||
writerWG.Wait()
|
||||
}()
|
||||
|
||||
stdin := newFilterReader(os.Stdin)
|
||||
buf := make([]byte, stdinBufSize)
|
||||
lineBuf := make([]byte, 0, 128)
|
||||
inEscSeq := false
|
||||
log.Printf("Reading from stdin (escape=Ctrl-]) ...")
|
||||
|
||||
for {
|
||||
n, err := stdin.Read(buf)
|
||||
if n > 0 {
|
||||
data := buf[:n]
|
||||
if i := bytes.IndexByte(data, shellEscapeByte); i >= 0 {
|
||||
if i > 0 {
|
||||
// Best-effort: don't block stdin if the tunnel is wedged.
|
||||
dataCopy := append([]byte(nil), data[:i]...)
|
||||
select {
|
||||
case stdinQueue <- dataCopy:
|
||||
default:
|
||||
}
|
||||
}
|
||||
detached = true
|
||||
break
|
||||
}
|
||||
|
||||
exitRequested := false
|
||||
for _, b := range data {
|
||||
if inEscSeq {
|
||||
if b >= 0x40 && b <= 0x7e {
|
||||
inEscSeq = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch b {
|
||||
case '\r', '\n':
|
||||
line := strings.TrimSpace(string(lineBuf))
|
||||
if line == "exit" || line == "logout" {
|
||||
exitRequested = true
|
||||
}
|
||||
lineBuf = lineBuf[:0]
|
||||
case 0x1b:
|
||||
inEscSeq = true
|
||||
case 0x08, 0x7f:
|
||||
if len(lineBuf) > 0 {
|
||||
lineBuf = lineBuf[:len(lineBuf)-1]
|
||||
}
|
||||
default:
|
||||
// Keep printable bytes only; ignore control sequences (arrows, etc.).
|
||||
if b >= 0x20 && b <= 0x7e {
|
||||
lineBuf = append(lineBuf, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataCopy := append([]byte(nil), data...)
|
||||
select {
|
||||
case stdinQueue <- dataCopy:
|
||||
default:
|
||||
// Drop input if the tunnel send path is blocked; still allow escape.
|
||||
}
|
||||
|
||||
if exitRequested {
|
||||
closeRequested = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
con.PrintErrorf("Error reading from stdin: %s\n", err)
|
||||
}
|
||||
closeRequested = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Exit interactive")
|
||||
bufio.NewWriter(os.Stdout).Flush()
|
||||
return detached, closeRequested
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
)
|
||||
|
||||
type shellManager struct {
|
||||
mu sync.RWMutex
|
||||
nextID int
|
||||
shells map[int]*managedShell
|
||||
}
|
||||
|
||||
func newShellManager() *shellManager {
|
||||
return &shellManager{
|
||||
nextID: 1,
|
||||
shells: map[int]*managedShell{},
|
||||
}
|
||||
}
|
||||
|
||||
var shells = newShellManager()
|
||||
|
||||
type shellState string
|
||||
|
||||
const (
|
||||
shellStateAttached shellState = "attached"
|
||||
shellStateDetached shellState = "detached"
|
||||
shellStateClosing shellState = "closing"
|
||||
)
|
||||
|
||||
type managedShell struct {
|
||||
ID int
|
||||
SessionID string
|
||||
SessionName string
|
||||
Path string
|
||||
Pid uint32
|
||||
TunnelID uint64
|
||||
EnablePTY bool
|
||||
CreatedAt time.Time
|
||||
|
||||
mu sync.RWMutex
|
||||
state shellState
|
||||
tunnel *core.TunnelIO
|
||||
output *swapWriter
|
||||
readerWG sync.WaitGroup
|
||||
}
|
||||
|
||||
func (s *managedShell) State() shellState {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.state
|
||||
}
|
||||
|
||||
func (s *managedShell) setState(state shellState) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.state = state
|
||||
}
|
||||
|
||||
func (s *managedShell) Tunnel() *core.TunnelIO {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.tunnel
|
||||
}
|
||||
|
||||
func (s *managedShell) SetOutput(w io.Writer) {
|
||||
s.output.Set(w)
|
||||
}
|
||||
|
||||
func (s *managedShell) startReader(remove func()) {
|
||||
s.readerWG.Add(1)
|
||||
go func() {
|
||||
defer s.readerWG.Done()
|
||||
_, err := io.Copy(s.output, s.tunnel)
|
||||
log.Printf("Shell tunnel reader (id=%d tunnel=%d) exited: %v", s.ID, s.TunnelID, err)
|
||||
remove()
|
||||
}()
|
||||
}
|
||||
|
||||
// swapWriter is an io.Writer whose underlying destination can be swapped at runtime.
|
||||
// This is used so detached shells can keep draining output without clobbering the REPL.
|
||||
type swapWriter struct {
|
||||
mu sync.RWMutex
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newSwapWriter(w io.Writer) *swapWriter {
|
||||
return &swapWriter{w: w}
|
||||
}
|
||||
|
||||
func (s *swapWriter) Set(w io.Writer) {
|
||||
s.mu.Lock()
|
||||
s.w = w
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *swapWriter) Write(p []byte) (int, error) {
|
||||
s.mu.RLock()
|
||||
w := s.w
|
||||
s.mu.RUnlock()
|
||||
if w == nil {
|
||||
return len(p), nil
|
||||
}
|
||||
return w.Write(p)
|
||||
}
|
||||
|
||||
func (m *shellManager) Add(sh *managedShell) int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
id := m.nextID
|
||||
m.nextID++
|
||||
|
||||
sh.ID = id
|
||||
m.shells[id] = sh
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (m *shellManager) Remove(id int) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.shells, id)
|
||||
}
|
||||
|
||||
func (m *shellManager) Get(id int) (*managedShell, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
sh, ok := m.shells[id]
|
||||
return sh, ok
|
||||
}
|
||||
|
||||
func (m *shellManager) List() []*managedShell {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
results := make([]*managedShell, 0, len(m.shells))
|
||||
for _, sh := range m.shells {
|
||||
results = append(results, sh)
|
||||
}
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].ID < results[j].ID
|
||||
})
|
||||
return results
|
||||
}
|
||||
+351
-90
@@ -19,24 +19,34 @@ package shell
|
||||
*/
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
windows = "windows"
|
||||
darwin = "darwin"
|
||||
linux = "linux"
|
||||
darwin = "darwin"
|
||||
linux = "linux"
|
||||
)
|
||||
|
||||
type shellResult int
|
||||
|
||||
const (
|
||||
shellExited shellResult = iota
|
||||
shellDetached
|
||||
shellAttachFailed
|
||||
)
|
||||
|
||||
// ShellCmd - Start an interactive shell on the remote system.
|
||||
@@ -55,113 +65,364 @@ func ShellCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if con.ActiveTarget.GetSession().OS != linux && con.ActiveTarget.GetSession().OS != darwin {
|
||||
noPty = true // Sliver's PTYs are only supported on linux/darwin
|
||||
}
|
||||
runInteractive(cmd, shellPath, noPty, con)
|
||||
con.Println("Shell exited")
|
||||
|
||||
result := runInteractive(cmd, shellPath, noPty, con, nil)
|
||||
if result == shellDetached {
|
||||
con.Println("Shell detached")
|
||||
} else if result == shellExited {
|
||||
con.Println("Shell exited")
|
||||
}
|
||||
}
|
||||
|
||||
func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *console.SliverClient) {
|
||||
// ShellLsCmd lists local shell tunnels currently managed by the client.
|
||||
func ShellLsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
managed := shells.List()
|
||||
if len(managed) == 0 {
|
||||
con.PrintInfof("No managed shells\n")
|
||||
return
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"State",
|
||||
"Session",
|
||||
"PID",
|
||||
"PTY",
|
||||
"Tunnel ID",
|
||||
"Path",
|
||||
"Age",
|
||||
})
|
||||
|
||||
now := time.Now()
|
||||
for _, sh := range managed {
|
||||
age := now.Sub(sh.CreatedAt).Round(time.Second).String()
|
||||
if sh.CreatedAt.IsZero() {
|
||||
age = "-"
|
||||
}
|
||||
pid := "-"
|
||||
if sh.Pid > 0 {
|
||||
pid = strconv.FormatUint(uint64(sh.Pid), 10)
|
||||
}
|
||||
|
||||
tw.AppendRow(table.Row{
|
||||
sh.ID,
|
||||
sh.State(),
|
||||
sessionLabel(sh.SessionID, sh.SessionName),
|
||||
pid,
|
||||
sh.EnablePTY,
|
||||
sh.TunnelID,
|
||||
sh.Path,
|
||||
age,
|
||||
})
|
||||
}
|
||||
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
// ShellAttachCmd re-attaches local stdin/stdout to a managed shell tunnel.
|
||||
func ShellAttachCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if len(args) < 1 {
|
||||
con.PrintErrorf("Usage: shell attach <id>\n")
|
||||
return
|
||||
}
|
||||
|
||||
shellID, err := strconv.Atoi(args[0])
|
||||
if err != nil || shellID <= 0 {
|
||||
con.PrintErrorf("Invalid shell id: %s\n", args[0])
|
||||
return
|
||||
}
|
||||
|
||||
sh, ok := shells.Get(shellID)
|
||||
if !ok {
|
||||
con.PrintErrorf("Unknown shell id: %d\n", shellID)
|
||||
return
|
||||
}
|
||||
if sh.State() == shellStateAttached {
|
||||
con.PrintErrorf("Shell %d is already attached\n", shellID)
|
||||
return
|
||||
}
|
||||
if sh.State() == shellStateClosing {
|
||||
con.PrintErrorf("Shell %d is closing\n", shellID)
|
||||
return
|
||||
}
|
||||
tunnel := sh.Tunnel()
|
||||
if tunnel == nil || !tunnel.IsOpen() {
|
||||
con.PrintErrorf("Shell %d is no longer active\n", shellID)
|
||||
shells.Remove(shellID)
|
||||
return
|
||||
}
|
||||
|
||||
session := con.ActiveTarget.GetSession()
|
||||
if session == nil || session.ID != sh.SessionID {
|
||||
con.PrintErrorf("Shell %d belongs to %s; switch targets before attaching\n", shellID, sessionLabel(sh.SessionID, sh.SessionName))
|
||||
return
|
||||
}
|
||||
|
||||
result := runInteractive(cmd, sh.Path, !sh.EnablePTY, con, sh)
|
||||
if result == shellDetached {
|
||||
con.Println("Shell detached")
|
||||
} else if result == shellExited {
|
||||
con.Println("Shell exited")
|
||||
}
|
||||
}
|
||||
|
||||
// ShellKillCmd kills a managed shell from the REPL and cleans up remote state.
|
||||
func ShellKillCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if len(args) < 1 {
|
||||
con.PrintErrorf("Usage: shell kill <id>\n")
|
||||
return
|
||||
}
|
||||
|
||||
shellID, err := strconv.Atoi(args[0])
|
||||
if err != nil || shellID <= 0 {
|
||||
con.PrintErrorf("Invalid shell id: %s\n", args[0])
|
||||
return
|
||||
}
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
sh, ok := shells.Get(shellID)
|
||||
if !ok {
|
||||
con.PrintErrorf("Unknown shell id: %d\n", shellID)
|
||||
return
|
||||
}
|
||||
|
||||
session := con.ActiveTarget.GetSession()
|
||||
if session == nil || session.ID != sh.SessionID {
|
||||
con.PrintErrorf("Shell %d belongs to %s; switch targets before killing\n", shellID, sessionLabel(sh.SessionID, sh.SessionName))
|
||||
return
|
||||
}
|
||||
|
||||
sh.setState(shellStateClosing)
|
||||
sh.SetOutput(io.Discard)
|
||||
shells.Remove(sh.ID)
|
||||
|
||||
// Best-effort graceful request before closing the tunnel.
|
||||
requestRemoteShellExit(sh.Tunnel())
|
||||
|
||||
if err := closeShellTunnel(con, sh.TunnelID, sh.SessionID); err != nil {
|
||||
con.PrintWarnf("Error closing shell tunnel %d: %s\n", sh.TunnelID, err)
|
||||
}
|
||||
|
||||
if force && sh.Pid > 1 {
|
||||
terminate, err := con.Rpc.Terminate(context.Background(), &sliverpb.TerminateReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Pid: int32(sh.Pid),
|
||||
Force: force,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintWarnf("Error terminating shell process %d: %s\n", sh.Pid, err)
|
||||
} else if terminate.Response != nil && terminate.Response.GetErr() != "" {
|
||||
errMsg := terminate.Response.GetErr()
|
||||
// If shell already exited, that is an acceptable cleanup outcome.
|
||||
if strings.Contains(strings.ToLower(errMsg), "no such process") ||
|
||||
strings.Contains(strings.ToLower(errMsg), "process not found") {
|
||||
con.PrintInfof("Shell process %d already exited\n", sh.Pid)
|
||||
} else {
|
||||
con.PrintWarnf("Terminate response for shell process %d: %s\n", sh.Pid, errMsg)
|
||||
}
|
||||
} else {
|
||||
con.PrintInfof("Shell process %d terminated\n", sh.Pid)
|
||||
}
|
||||
}
|
||||
|
||||
if force {
|
||||
con.PrintInfof("Shell %d killed (force)\n", sh.ID)
|
||||
} else {
|
||||
con.PrintInfof("Shell %d killed\n", sh.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *console.SliverClient, existing *managedShell) shellResult {
|
||||
con.Println()
|
||||
con.PrintInfof("Wait approximately 10 seconds after exit, and press <enter> to continue\n")
|
||||
con.PrintInfof("Opening shell tunnel (EOF to exit) ...\n\n")
|
||||
con.PrintInfof("Shell management: `shell ls`, `shell attach <id>`\n")
|
||||
con.PrintInfof("Escape: press Ctrl-] to return to the Sliver client\n")
|
||||
|
||||
session := con.ActiveTarget.GetSession()
|
||||
if session == nil {
|
||||
return
|
||||
return shellAttachFailed
|
||||
}
|
||||
|
||||
// Create an RPC tunnel, then start it before binding the shell to the newly created tunnel
|
||||
ctxTunnel, cancelTunnel := context.WithCancel(context.Background())
|
||||
|
||||
rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{
|
||||
SessionID: session.ID,
|
||||
})
|
||||
defer cancelTunnel()
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Created new tunnel with id: %d, binding to shell ...", rpcTunnel.TunnelID)
|
||||
|
||||
// Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object
|
||||
tunnel := core.GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
|
||||
|
||||
var rows uint32
|
||||
var cols uint32
|
||||
if !noPty {
|
||||
colsInt, rowsInt, err := term.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil || rowsInt <= 0 || colsInt <= 0 {
|
||||
colsInt, rowsInt, err = term.GetSize(int(os.Stdin.Fd()))
|
||||
}
|
||||
if err == nil && rowsInt > 0 && colsInt > 0 {
|
||||
rows = uint32(rowsInt)
|
||||
cols = uint32(colsInt)
|
||||
}
|
||||
if existing == nil {
|
||||
con.PrintInfof("Opening shell tunnel ...\n\n")
|
||||
} else {
|
||||
con.PrintInfof("Re-attaching to shell %d ...\n\n", existing.ID)
|
||||
}
|
||||
|
||||
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: shellPath,
|
||||
EnablePTY: !noPty,
|
||||
Rows: rows,
|
||||
Cols: cols,
|
||||
TunnelID: tunnel.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
//
|
||||
if shell.Response != nil && shell.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s\n", shell.Response.Err)
|
||||
_, err = con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{
|
||||
TunnelID: tunnel.ID,
|
||||
var (
|
||||
tunnel *core.TunnelIO
|
||||
managed *managedShell
|
||||
enablePTY bool
|
||||
oldState *term.State
|
||||
stateSaved bool
|
||||
stopPtySize = func() {}
|
||||
err error
|
||||
)
|
||||
|
||||
if existing == nil {
|
||||
// Create an RPC tunnel, then start it before binding the shell to the newly created tunnel.
|
||||
ctxTunnel, cancelTunnel := context.WithCancel(context.Background())
|
||||
defer cancelTunnel()
|
||||
|
||||
rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{
|
||||
SessionID: session.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("RPC Error: %s\n", err)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return shellAttachFailed
|
||||
}
|
||||
return
|
||||
}
|
||||
defer tunnel.Close()
|
||||
log.Printf("Bound remote shell pid %d to tunnel %d", shell.Pid, shell.TunnelID)
|
||||
con.PrintInfof("Started remote shell with pid %d\n\n", shell.Pid)
|
||||
log.Printf("Created new tunnel with id: %d, binding to shell ...", rpcTunnel.TunnelID)
|
||||
|
||||
var oldState *term.State
|
||||
if !noPty {
|
||||
// Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object.
|
||||
tunnel = core.GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
|
||||
|
||||
var rows uint32
|
||||
var cols uint32
|
||||
if !noPty {
|
||||
colsInt, rowsInt, err := term.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil || rowsInt <= 0 || colsInt <= 0 {
|
||||
colsInt, rowsInt, err = term.GetSize(int(os.Stdin.Fd()))
|
||||
}
|
||||
if err == nil && rowsInt > 0 && colsInt > 0 {
|
||||
rows = uint32(rowsInt)
|
||||
cols = uint32(colsInt)
|
||||
}
|
||||
}
|
||||
|
||||
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: shellPath,
|
||||
EnablePTY: !noPty,
|
||||
Rows: rows,
|
||||
Cols: cols,
|
||||
TunnelID: tunnel.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
core.GetTunnels().Close(tunnel.ID)
|
||||
return shellAttachFailed
|
||||
}
|
||||
if shell.Response != nil && shell.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s\n", shell.Response.Err)
|
||||
go backgroundCloseShell(con, tunnel.ID, session.ID)
|
||||
return shellAttachFailed
|
||||
}
|
||||
|
||||
path := shellPath
|
||||
if path == "" {
|
||||
path = shell.Path
|
||||
}
|
||||
enablePTY = !noPty
|
||||
managed = &managedShell{
|
||||
SessionID: session.ID,
|
||||
SessionName: session.Name,
|
||||
Path: path,
|
||||
Pid: shell.Pid,
|
||||
TunnelID: tunnel.ID,
|
||||
EnablePTY: enablePTY,
|
||||
CreatedAt: time.Now(),
|
||||
state: shellStateAttached,
|
||||
tunnel: tunnel,
|
||||
output: newSwapWriter(os.Stdout),
|
||||
}
|
||||
managed.ID = shells.Add(managed)
|
||||
managed.startReader(func() {
|
||||
shells.Remove(managed.ID)
|
||||
})
|
||||
|
||||
log.Printf("Bound remote shell pid %d to tunnel %d", shell.Pid, shell.TunnelID)
|
||||
con.PrintInfof("Started remote shell [%d] with pid %d\n\n", managed.ID, shell.Pid)
|
||||
} else {
|
||||
managed = existing
|
||||
tunnel = managed.Tunnel()
|
||||
enablePTY = managed.EnablePTY
|
||||
managed.setState(shellStateAttached)
|
||||
managed.SetOutput(os.Stdout)
|
||||
con.PrintInfof("Attached shell [%d] (pid %d)\n\n", managed.ID, managed.Pid)
|
||||
}
|
||||
|
||||
if enablePTY {
|
||||
oldState, err = term.MakeRaw(0)
|
||||
log.Printf("Saving terminal state: %v", oldState)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to save terminal state")
|
||||
return
|
||||
con.PrintErrorf("Failed to save terminal state\n")
|
||||
managed.SetOutput(io.Discard)
|
||||
managed.setState(shellStateDetached)
|
||||
return shellAttachFailed
|
||||
}
|
||||
stateSaved = true
|
||||
stopPtySize = startPtyResizeWatcher(con, cmd, tunnel.ID)
|
||||
}
|
||||
defer stopPtySize()
|
||||
|
||||
if stateSaved {
|
||||
defer func() {
|
||||
log.Printf("Restoring terminal state ...")
|
||||
term.Restore(0, oldState)
|
||||
}()
|
||||
}
|
||||
|
||||
stopPtyResize := func() {}
|
||||
if !noPty {
|
||||
stopPtyResize = startPtyResizeWatcher(con, cmd, tunnel.ID)
|
||||
}
|
||||
defer stopPtyResize()
|
||||
|
||||
log.Printf("Starting stdin/stdout shell ...")
|
||||
go func() {
|
||||
n, err := io.Copy(os.Stdout, tunnel)
|
||||
log.Printf("Wrote %d bytes to stdout", n)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error writing to stdout: %s", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
log.Printf("Reading from stdin ...")
|
||||
n, err := io.Copy(tunnel, newFilterReader(os.Stdin))
|
||||
log.Printf("Read %d bytes from stdin", n)
|
||||
if err != nil && err != io.EOF {
|
||||
con.PrintErrorf("Error reading from stdin: %s\n", err)
|
||||
detached, _ := runAttachedIO(tunnel, con)
|
||||
if detached {
|
||||
managed.SetOutput(io.Discard)
|
||||
managed.setState(shellStateDetached)
|
||||
return shellDetached
|
||||
}
|
||||
|
||||
if !noPty {
|
||||
log.Printf("Restoring terminal state ...")
|
||||
term.Restore(0, oldState)
|
||||
}
|
||||
managed.SetOutput(io.Discard)
|
||||
managed.setState(shellStateClosing)
|
||||
shells.Remove(managed.ID)
|
||||
|
||||
log.Printf("Exit interactive")
|
||||
bufio.NewWriter(os.Stdout).Flush()
|
||||
// "exit" should return immediately; tunnel close/cleanup is best-effort in the background.
|
||||
go backgroundCloseShell(con, tunnel.ID, session.ID)
|
||||
|
||||
return shellExited
|
||||
}
|
||||
|
||||
func backgroundCloseShell(con *console.SliverClient, tunnelID uint64, sessionID string) {
|
||||
err := closeShellTunnel(con, tunnelID, sessionID)
|
||||
if err != nil {
|
||||
log.Printf("Background close tunnel %d failed: %v", tunnelID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func closeShellTunnel(con *console.SliverClient, tunnelID uint64, sessionID string) error {
|
||||
core.GetTunnels().Close(tunnelID)
|
||||
if con == nil || con.Rpc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{
|
||||
TunnelID: tunnelID,
|
||||
SessionID: sessionID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func requestRemoteShellExit(tunnel *core.TunnelIO) {
|
||||
if tunnel == nil || !tunnel.IsOpen() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, payload := range [][]byte{[]byte("exit\n"), []byte("logout\n")} {
|
||||
func(data []byte) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Shell exit write skipped: %v", r)
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case tunnel.Send <- append([]byte(nil), data...):
|
||||
default:
|
||||
}
|
||||
}(payload)
|
||||
}
|
||||
}
|
||||
|
||||
func sessionLabel(sessionID, sessionName string) string {
|
||||
if sessionName == "" {
|
||||
return sessionID
|
||||
}
|
||||
return sessionName + " (" + sessionID + ")"
|
||||
}
|
||||
@@ -190,7 +190,7 @@ func SelectMultipleBeaconsAndSessions(con *console.SliverClient) ([]*clientpb.Se
|
||||
session.Username,
|
||||
fmt.Sprintf("%s/%s", session.OS, session.Arch),
|
||||
)
|
||||
fmt.Fprintf(table, option+"\n")
|
||||
fmt.Fprintln(table, option)
|
||||
o := strings.ReplaceAll(option, "\t", "")
|
||||
sessionOptionMap[o] = session
|
||||
}
|
||||
@@ -206,7 +206,7 @@ func SelectMultipleBeaconsAndSessions(con *console.SliverClient) ([]*clientpb.Se
|
||||
beacon.Username,
|
||||
fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch),
|
||||
)
|
||||
fmt.Fprintf(table, option+"\n")
|
||||
fmt.Fprintln(table, option)
|
||||
o := strings.ReplaceAll(option, "\t", "")
|
||||
beaconOptionMap[o] = beacon
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func filterTasksByTaskType(taskType string, tasks []*clientpb.BeaconTask) []*cli
|
||||
func PrintTask(task *clientpb.BeaconTask, con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableWithBordersStyle(con))
|
||||
tw.AppendRow(table.Row{console.Bold + "Beacon Task" + console.Normal, task.ID})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Beacon Task"), task.ID})
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{"State", emojiState(task.State) + " " + prettyState(strings.Title(task.State))})
|
||||
tw.AppendRow(table.Row{"Description", task.Description})
|
||||
|
||||
@@ -93,13 +93,13 @@ func PrintBeaconTasks(tasks []*clientpb.BeaconTask, cmd *cobra.Command, con *con
|
||||
func prettyState(state string) string {
|
||||
switch strings.ToLower(state) {
|
||||
case "pending":
|
||||
return console.Bold + state + console.Normal
|
||||
return console.StyleBold.Render(state)
|
||||
case "sent":
|
||||
return console.Bold + console.Orange + state + console.Normal
|
||||
return console.StyleBoldOrange.Render(state)
|
||||
case "completed":
|
||||
return console.Bold + console.Green + state + console.Normal
|
||||
return console.StyleBoldGreen.Render(state)
|
||||
case "canceled":
|
||||
return console.Bold + console.Gray + state + console.Normal
|
||||
return console.StyleBoldGray.Render(state)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ func WasmLsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
cacheLine = " (cached)"
|
||||
} else {
|
||||
wasmRegistrationCache[idOf(con)] = append(wasmRegistrationCache[idOf(con)], extName)
|
||||
cacheLine = console.Bold + console.Green + " +++" + console.Normal
|
||||
cacheLine = console.StyleBoldGreen.Render(" +++")
|
||||
}
|
||||
con.PrintInfof(" %s%s\n", extName, cacheLine)
|
||||
}
|
||||
|
||||
@@ -66,14 +66,28 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
websitesCmd.AddCommand(websitesShowCmd)
|
||||
|
||||
websitesRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr,
|
||||
Use: consts.RmStr + " [name]",
|
||||
Short: "Remove an entire website and all of its contents",
|
||||
Long: help.GetHelpFor([]string{consts.WebsitesStr, consts.RmStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
WebsiteRmCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(websitesRmCmd).PositionalCompletion(WebsiteNameCompleter(con))
|
||||
websitesRmCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
ws, err := con.Rpc.Websites(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
out := []string{}
|
||||
for _, w := range ws.Websites {
|
||||
if strings.HasPrefix(w.Name, toComplete) {
|
||||
out = append(out, w.Name)
|
||||
}
|
||||
}
|
||||
return out, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
websitesCmd.AddCommand(websitesRmCmd)
|
||||
|
||||
websitesRmWebContentCmd := &cobra.Command{
|
||||
|
||||
@@ -20,8 +20,10 @@ package websites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -32,12 +34,43 @@ func WebsiteRmCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
if len(args) > 0 {
|
||||
name = args[0]
|
||||
}
|
||||
if name == "" {
|
||||
con.PrintErrorf("No website name specified\n")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := con.Rpc.WebsiteRemove(context.Background(), &clientpb.Website{
|
||||
site, err := con.Rpc.Website(context.Background(), &clientpb.Website{
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to fetch website %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
confirm := false
|
||||
_ = forms.Confirm(websiteRemoveConfirmationPrompt(name, len(site.Contents)), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = con.Rpc.WebsiteRemove(context.Background(), &clientpb.Website{
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to remove website %s", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("%s\n", websiteRemoveSuccessMessage(name, len(site.Contents)))
|
||||
}
|
||||
|
||||
func websiteRemoveConfirmationPrompt(name string, contentCount int) string {
|
||||
contentLabel := "content item"
|
||||
if contentCount != 1 {
|
||||
contentLabel = "content items"
|
||||
}
|
||||
return fmt.Sprintf("Delete website '%s' and %d %s?", name, contentCount, contentLabel)
|
||||
}
|
||||
|
||||
func websiteRemoveSuccessMessage(name string, contentCount int) string {
|
||||
return fmt.Sprintf("Removed %s and %d content items", name, contentCount)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package websites
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWebsiteRemoveConfirmationPrompt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
websiteName string
|
||||
contentCount int
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "zero content",
|
||||
websiteName: "test",
|
||||
contentCount: 0,
|
||||
want: "Delete website 'test' and 0 content items?",
|
||||
},
|
||||
{
|
||||
name: "single content",
|
||||
websiteName: "test",
|
||||
contentCount: 1,
|
||||
want: "Delete website 'test' and 1 content item?",
|
||||
},
|
||||
{
|
||||
name: "multiple content",
|
||||
websiteName: "test",
|
||||
contentCount: 2,
|
||||
want: "Delete website 'test' and 2 content items?",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := websiteRemoveConfirmationPrompt(tt.websiteName, tt.contentCount)
|
||||
if got != tt.want {
|
||||
t.Fatalf("unexpected prompt: got %q want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsiteRemoveSuccessMessage(t *testing.T) {
|
||||
got := websiteRemoveSuccessMessage("test", 2)
|
||||
want := "removed test and 2 content items"
|
||||
if got != want {
|
||||
t.Fatalf("unexpected message: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
// ConnectionDetails provides (optional) metadata about the active connection.
|
||||
// In the sliver-client binary this is typically sourced from ~/.sliver-client/configs/*.
|
||||
type ConnectionDetails struct {
|
||||
ConfigKey string
|
||||
Config *assets.ClientConfig
|
||||
}
|
||||
|
||||
// CurrentConnection returns the current connection metadata, if any.
|
||||
func (con *SliverClient) CurrentConnection() (*ConnectionDetails, connectivity.State, bool) {
|
||||
con.connMu.Lock()
|
||||
defer con.connMu.Unlock()
|
||||
|
||||
if con.grpcConn == nil {
|
||||
return con.connDetails, connectivity.Idle, false
|
||||
}
|
||||
return con.connDetails, con.grpcConn.GetState(), true
|
||||
}
|
||||
|
||||
// SetConnection swaps the active RPC connection, restarting streams (events/tunnels/logs).
|
||||
// It is safe to call multiple times (e.g., on server switch).
|
||||
func (con *SliverClient) SetConnection(rpc rpcpb.SliverRPCClient, grpcConn *grpc.ClientConn, details *ConnectionDetails) error {
|
||||
con.connMu.Lock()
|
||||
defer con.connMu.Unlock()
|
||||
|
||||
con.detachConnectionLocked()
|
||||
|
||||
con.Rpc = rpc
|
||||
con.grpcConn = grpcConn
|
||||
con.connDetails = details
|
||||
|
||||
if con.Rpc == nil || con.grpcConn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear per-server state that should not be carried across connections.
|
||||
con.EventListeners = &sync.Map{}
|
||||
con.BeaconTaskCallbacksMutex.Lock()
|
||||
con.BeaconTaskCallbacks = map[string]BeaconTaskCallback{}
|
||||
con.BeaconTaskCallbacksMutex.Unlock()
|
||||
con.ActiveTarget.Set(nil, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
con.connCancel = cancel
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
con.connWg = wg
|
||||
|
||||
wg.Add(1)
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
con.startEventLoop(ctx)
|
||||
}(wg)
|
||||
|
||||
wg.Add(1)
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
if err := core.TunnelLoop(ctx, con.Rpc); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Printf("TunnelLoop error: %v", err)
|
||||
}
|
||||
}(wg)
|
||||
|
||||
wg.Add(1)
|
||||
go func(wg *sync.WaitGroup, conn *grpc.ClientConn) {
|
||||
defer wg.Done()
|
||||
con.monitorConnectionLost(ctx, conn)
|
||||
}(wg, con.grpcConn)
|
||||
|
||||
con.refreshRemoteLogStreamsLocked()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseConnection stops background loops and closes the current gRPC connection.
|
||||
func (con *SliverClient) CloseConnection() error {
|
||||
con.connMu.Lock()
|
||||
defer con.connMu.Unlock()
|
||||
|
||||
con.detachConnectionLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (con *SliverClient) detachConnectionLocked() {
|
||||
// Stop sending remote logs first so background io.Copy loops can't break.
|
||||
con.setRemoteLogStreamsLocked(nil, nil)
|
||||
|
||||
cancel := con.connCancel
|
||||
con.connCancel = nil
|
||||
wg := con.connWg
|
||||
con.connWg = &sync.WaitGroup{}
|
||||
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
if wg != nil {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
// Best-effort; we don't want to deadlock a switch/exit.
|
||||
}
|
||||
}
|
||||
|
||||
if con.grpcConn != nil {
|
||||
_ = con.grpcConn.Close()
|
||||
con.grpcConn = nil
|
||||
}
|
||||
con.Rpc = nil
|
||||
con.connDetails = nil
|
||||
|
||||
// Tear down any singleton network tooling that was tied to the previous server.
|
||||
core.ResetClientState()
|
||||
}
|
||||
|
||||
func (con *SliverClient) monitorConnectionLost(ctx context.Context, conn *grpc.ClientConn) {
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
state := conn.GetState()
|
||||
if state == connectivity.Idle {
|
||||
// Mirror the previous behavior from cli/console.go, but only when not canceled.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
fmt.Println("\nLost connection to server. Exiting now.")
|
||||
con.FlushOutput()
|
||||
//nolint:forbidigo // Explicitly exits to match legacy behavior.
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !conn.WaitForStateChange(ctx, state) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+204
-152
@@ -35,6 +35,7 @@ import (
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/spin"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
@@ -46,6 +47,7 @@ import (
|
||||
"github.com/reeflective/readline"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slog"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -54,32 +56,10 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
// ANSI Colors.
|
||||
Normal = "\033[0m"
|
||||
Black = "\033[30m"
|
||||
Red = "\033[31m"
|
||||
Green = "\033[32m"
|
||||
Orange = "\033[33m"
|
||||
Blue = "\033[34m"
|
||||
Purple = "\033[35m"
|
||||
Cyan = "\033[36m"
|
||||
Gray = "\033[37m"
|
||||
Bold = "\033[1m"
|
||||
Clearln = "\r\x1b[2K"
|
||||
UpN = "\033[%dA"
|
||||
DownN = "\033[%dB"
|
||||
Underline = "\033[4m"
|
||||
|
||||
// Info - Display colorful information.
|
||||
Info = Bold + Cyan + "[*] " + Normal
|
||||
// Warn - Warn a user.
|
||||
Warn = Bold + Red + "[!] " + Normal
|
||||
// Debug - Display debug information.
|
||||
Debug = Bold + Purple + "[-] " + Normal
|
||||
// Woot - Display success.
|
||||
Woot = Bold + Green + "[$] " + Normal
|
||||
// Success - Diplay success.
|
||||
Success = Bold + Green + "[+] " + Normal
|
||||
// Terminal control sequences (not "styling").
|
||||
Clearln = "\r\x1b[2K"
|
||||
UpN = "\033[%dA"
|
||||
DownN = "\033[%dB"
|
||||
)
|
||||
|
||||
// Observer - A function to call when the sessions changes.
|
||||
@@ -106,6 +86,21 @@ type SliverClient struct {
|
||||
stdoutPipeWriter *os.File
|
||||
stdoutPipeDone chan struct{}
|
||||
stdoutPipeOnce sync.Once
|
||||
|
||||
connMu sync.Mutex
|
||||
grpcConn *grpc.ClientConn
|
||||
connDetails *ConnectionDetails
|
||||
connCancel context.CancelFunc
|
||||
connWg *sync.WaitGroup
|
||||
connectionHooksApplied bool
|
||||
logCommandHookApplied bool
|
||||
|
||||
// These writers are always safe to use (never error); the underlying stream
|
||||
// can be swapped on server switch to avoid breaking io.MultiWriter/io.Copy.
|
||||
jsonRemoteWriter *optionalRemoteWriter
|
||||
asciicastRemoteWriter *optionalRemoteWriter
|
||||
jsonRemoteStream rpcpb.SliverRPC_ClientLogClient
|
||||
asciicastRemoteStream rpcpb.SliverRPC_ClientLogClient
|
||||
}
|
||||
|
||||
// NewConsole creates the sliver client (and console), creating menus and prompts.
|
||||
@@ -113,6 +108,8 @@ type SliverClient struct {
|
||||
// thus has not started monitoring any server events, or started the application.
|
||||
func NewConsole(isServer bool) *SliverClient {
|
||||
assets.Setup(false, false)
|
||||
_ = theme.LoadAndSetCurrentTheme()
|
||||
ApplyTheme(theme.Current())
|
||||
settings, _ := assets.LoadSettings()
|
||||
|
||||
con := &SliverClient{
|
||||
@@ -126,7 +123,10 @@ func NewConsole(isServer bool) *SliverClient {
|
||||
BeaconTaskCallbacksMutex: &sync.Mutex{},
|
||||
IsServer: isServer,
|
||||
Settings: settings,
|
||||
connWg: &sync.WaitGroup{},
|
||||
}
|
||||
// Ensure logging never panics even if console logs are disabled.
|
||||
con.jsonHandler = slog.NewJSONHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug})
|
||||
|
||||
// The active target needs access to the console
|
||||
// to automatically switch between command menus.
|
||||
@@ -165,8 +165,7 @@ func NewConsole(isServer bool) *SliverClient {
|
||||
// Init requires a working RPC connection to the sliver server, and 2 different sets of commands.
|
||||
// If run is true, the console application is started, making this call blocking. Otherwise, commands and
|
||||
// RPC connection are bound to the console (making the console ready to run), but the console does not start.
|
||||
func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, sliverCmds console.Commands, run bool, rcScript string) error {
|
||||
con.Rpc = rpc
|
||||
func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, grpcConn *grpc.ClientConn, details *ConnectionDetails, serverCmds, sliverCmds console.Commands, run bool, rcScript string) error {
|
||||
con.IsCLI = !run
|
||||
con.serverCmds = serverCmds
|
||||
con.sliverCmds = sliverCmds
|
||||
@@ -188,39 +187,29 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, slive
|
||||
sliver := con.App.Menu(consts.ImplantMenu)
|
||||
sliver.SetCommands(sliverCmds)
|
||||
|
||||
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.allowServerRootCommands)
|
||||
if shell := con.App.Shell(); shell != nil && shell.Completer != nil {
|
||||
baseCompleter := shell.Completer
|
||||
shell.Completer = func(line []rune, cursor int) readline.Completions {
|
||||
con.prepareCompletion(line, cursor)
|
||||
return baseCompleter(line, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
// Events
|
||||
go con.startEventLoop()
|
||||
go core.TunnelLoop(rpc)
|
||||
con.applyConnectionHooksOnce()
|
||||
|
||||
// console logger
|
||||
if con.Settings.ConsoleLogs {
|
||||
// Classic logs
|
||||
consoleLog := getConsoleLogFile()
|
||||
consoleLogStream, err := con.ClientLogStream("json")
|
||||
if err != nil {
|
||||
log.Printf("Could not get client json log stream: %s", err)
|
||||
}
|
||||
con.setupLogger(consoleLog, consoleLogStream)
|
||||
con.ensureJSONRemoteWriter()
|
||||
con.setupLogger(consoleLog, con.jsonRemoteWriter)
|
||||
defer consoleLog.Close()
|
||||
|
||||
// Ascii cast sessions (complete terminal interface).
|
||||
asciicastLog := getConsoleAsciicastFile()
|
||||
defer asciicastLog.Close()
|
||||
// Ascii cast sessions (complete terminal interface) are only useful
|
||||
// for the interactive console. In CLI mode they would clobber stdout.
|
||||
if !con.IsCLI {
|
||||
asciicastLog := getConsoleAsciicastFile()
|
||||
defer asciicastLog.Close()
|
||||
|
||||
asciicastStream, err := con.ClientLogStream("asciicast")
|
||||
if err != nil {
|
||||
log.Printf("Could not get client asciicast log stream: %s", err)
|
||||
con.ensureAsciicastRemoteWriter()
|
||||
con.setupAsciicastRecord(asciicastLog, con.asciicastRemoteWriter)
|
||||
}
|
||||
con.setupAsciicastRecord(asciicastLog, asciicastStream)
|
||||
}
|
||||
|
||||
if err := con.SetConnection(rpc, grpcConn, details); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rcScript != "" {
|
||||
@@ -319,10 +308,29 @@ func (con *SliverClient) runRCLine(serverCmds, sliverCmds console.Commands, line
|
||||
return nil
|
||||
}
|
||||
|
||||
func (con *SliverClient) startEventLoop() {
|
||||
eventStream, err := con.Rpc.Events(context.Background(), &commonpb.Empty{})
|
||||
func (con *SliverClient) applyConnectionHooksOnce() {
|
||||
con.connMu.Lock()
|
||||
defer con.connMu.Unlock()
|
||||
|
||||
if con.connectionHooksApplied {
|
||||
return
|
||||
}
|
||||
con.connectionHooksApplied = true
|
||||
|
||||
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.allowServerRootCommands)
|
||||
if shell := con.App.Shell(); shell != nil && shell.Completer != nil {
|
||||
baseCompleter := shell.Completer
|
||||
shell.Completer = func(line []rune, cursor int) readline.Completions {
|
||||
con.prepareCompletion(line, cursor)
|
||||
return baseCompleter(line, cursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (con *SliverClient) startEventLoop(ctx context.Context) {
|
||||
eventStream, err := con.Rpc.Events(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
fmt.Printf(Warn+"%s\n", err)
|
||||
fmt.Printf("%s%s\n", Warn, err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
@@ -337,7 +345,7 @@ func (con *SliverClient) startEventLoop() {
|
||||
switch event.EventType {
|
||||
|
||||
case consts.CanaryEvent:
|
||||
con.PrintEventErrorf(Bold+"WARNING: %s%s has been burned (DNS Canary)", Normal, event.Session.Name)
|
||||
con.PrintEventErrorf("%s %s has been burned (DNS Canary)", StyleBold.Render("WARNING:"), event.Session.Name)
|
||||
sessions := con.GetSessionsByName(event.Session.Name)
|
||||
for _, session := range sessions {
|
||||
shortID := strings.Split(session.ID, "-")[0]
|
||||
@@ -346,7 +354,7 @@ func (con *SliverClient) startEventLoop() {
|
||||
|
||||
case consts.WatchtowerEvent:
|
||||
msg := string(event.Data)
|
||||
con.PrintEventErrorf(Bold+"WARNING: %s%s has been burned (seen on %s)", Normal, event.Session.Name, msg)
|
||||
con.PrintEventErrorf("%s %s has been burned (seen on %s)", StyleBold.Render("WARNING:"), event.Session.Name, msg)
|
||||
sessions := con.GetSessionsByName(event.Session.Name)
|
||||
for _, session := range sessions {
|
||||
shortID := strings.Split(session.ID, "-")[0]
|
||||
@@ -462,7 +470,7 @@ func (con *SliverClient) triggerReactions(event *clientpb.Event) {
|
||||
|
||||
for _, reaction := range reactions {
|
||||
for _, line := range reaction.Commands {
|
||||
con.PrintInfof(Bold+"Execute reaction: '%s'"+Normal, line)
|
||||
con.PrintInfof("%s '%s'", StyleBold.Render("Execute reaction:"), line)
|
||||
err := con.App.ActiveMenu().RunCommandLine(context.Background(), line)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Reaction command error: %s\n", err)
|
||||
@@ -514,41 +522,107 @@ func (con *SliverClient) AddBeaconCallback(taskID string, callback BeaconTaskCal
|
||||
}
|
||||
|
||||
func (con *SliverClient) GetPrompt() string {
|
||||
prompt := Underline + "sliver" + Normal
|
||||
if con.IsServer {
|
||||
prompt = Bold + "[server] " + Normal + Underline + "sliver" + Normal
|
||||
promptStyle := assets.PromptStyleHost
|
||||
if con.Settings != nil {
|
||||
promptStyle = assets.NormalizePromptStyle(con.Settings.PromptStyle)
|
||||
}
|
||||
if con.ActiveTarget.GetSession() != nil {
|
||||
prompt += fmt.Sprintf(Bold+Red+" (%s)%s", con.ActiveTarget.GetSession().Name, Normal)
|
||||
} else if con.ActiveTarget.GetBeacon() != nil {
|
||||
prompt += fmt.Sprintf(Bold+Blue+" (%s)%s", con.ActiveTarget.GetBeacon().Name, Normal)
|
||||
|
||||
if promptStyle == assets.PromptStyleCustom {
|
||||
src := assets.DefaultPromptTemplate
|
||||
if con.Settings != nil && strings.TrimSpace(con.Settings.PromptTemplate) != "" {
|
||||
src = con.Settings.PromptTemplate
|
||||
}
|
||||
out, err := renderPromptTemplate(con, src)
|
||||
if err != nil {
|
||||
return Clearln + " > "
|
||||
}
|
||||
if strings.TrimSpace(out) == "" {
|
||||
return Clearln + " > "
|
||||
}
|
||||
return Clearln + out
|
||||
}
|
||||
|
||||
prompt := StyleUnderline.Render("sliver")
|
||||
if con.IsServer {
|
||||
if promptStyle != assets.PromptStyleBasic {
|
||||
prompt = StyleBold.Render("[server]") + " " + prompt
|
||||
}
|
||||
} else if promptStyle != assets.PromptStyleBasic {
|
||||
// sliver-client only: optionally include operator/host context.
|
||||
if details, _, ok := con.CurrentConnection(); ok && details != nil && details.Config != nil {
|
||||
operator := strings.TrimSpace(details.Config.Operator)
|
||||
host := strings.TrimSpace(details.Config.LHost)
|
||||
|
||||
prefix := ""
|
||||
switch promptStyle {
|
||||
case assets.PromptStyleOperatorHost:
|
||||
if operator != "" && host != "" {
|
||||
prefix = operator + "@" + host
|
||||
} else if operator != "" {
|
||||
prefix = operator
|
||||
} else if host != "" {
|
||||
prefix = host
|
||||
}
|
||||
case assets.PromptStyleHost:
|
||||
if host != "" {
|
||||
prefix = host
|
||||
}
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
prompt = StyleBoldPrimary.Render("["+prefix+"]") + " " + prompt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if session := con.ActiveTarget.GetSession(); session != nil {
|
||||
prompt += StyleBoldRed.Render(" (" + session.Name + ")")
|
||||
} else if beacon := con.ActiveTarget.GetBeacon(); beacon != nil {
|
||||
prompt += StyleBoldBlue.Render(" (" + beacon.Name + ")")
|
||||
}
|
||||
prompt += " > "
|
||||
return Clearln + prompt
|
||||
}
|
||||
|
||||
func (con *SliverClient) PrintLogo() {
|
||||
serverVer, err := con.Rpc.GetVersion(context.Background(), &commonpb.Empty{})
|
||||
// Always show a logo even if the server connection is transiently unavailable.
|
||||
logo := asciiLogos[util.Intn(len(asciiLogos))]
|
||||
fmt.Println(strings.ReplaceAll(logo.Render(), "\n", "\r\n"))
|
||||
fmt.Println("All hackers gain " + abilities[util.Intn(len(abilities))] + "\r")
|
||||
|
||||
if con.Rpc == nil {
|
||||
fmt.Println(Warn + "Not connected to a server\r")
|
||||
fmt.Printf("%sClient %s\r\n", Info, version.FullVersion())
|
||||
fmt.Println(Info + "Welcome to the sliver shell, please type 'help' for options\r")
|
||||
fmt.Println("")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
serverVer, err := con.Rpc.GetVersion(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
fmt.Printf("%sCould not query server version: %s\r\n", Warn, err)
|
||||
fmt.Printf("%sClient %s\r\n", Info, version.FullVersion())
|
||||
fmt.Println(Info + "Welcome to the sliver shell, please type 'help' for options\r")
|
||||
fmt.Println("")
|
||||
con.CheckLastUpdate()
|
||||
return
|
||||
}
|
||||
dirty := ""
|
||||
if serverVer.Dirty {
|
||||
dirty = fmt.Sprintf(" - %sDirty%s", Bold, Normal)
|
||||
dirty = " - " + StyleBold.Render("Dirty")
|
||||
}
|
||||
serverSemVer := fmt.Sprintf("%d.%d.%d", serverVer.Major, serverVer.Minor, serverVer.Patch)
|
||||
|
||||
logo := asciiLogos[util.Intn(len(asciiLogos))]
|
||||
fmt.Println(strings.ReplaceAll(logo, "\n", "\r\n"))
|
||||
fmt.Println("All hackers gain " + abilities[util.Intn(len(abilities))] + "\r")
|
||||
fmt.Printf(Info+"Server v%s - %s%s\r\n", serverSemVer, serverVer.Commit, dirty)
|
||||
fmt.Printf("%sServer v%s - %s%s\r\n", Info, serverSemVer, serverVer.Commit, dirty)
|
||||
if version.GitCommit != serverVer.Commit {
|
||||
fmt.Printf(Info+"Client %s\r\n", version.FullVersion())
|
||||
fmt.Printf("%sClient %s\r\n", Info, version.FullVersion())
|
||||
}
|
||||
fmt.Println(Info + "Welcome to the sliver shell, please type 'help' for options\r")
|
||||
if serverVer.Major != int32(version.SemanticVersion()[0]) {
|
||||
fmt.Printf(Warn + "Warning: Client and server may be running incompatible versions.\r\n")
|
||||
fmt.Print(Warn + "Warning: Client and server may be running incompatible versions.\r\n")
|
||||
}
|
||||
fmt.Println("")
|
||||
con.CheckLastUpdate()
|
||||
}
|
||||
|
||||
@@ -564,7 +638,7 @@ func (con *SliverClient) CheckLastUpdate() {
|
||||
day := 24 * time.Hour
|
||||
if compiledAt.Add(30 * day).Before(now) {
|
||||
if lastUpdate == nil || lastUpdate.Add(30*day).Before(now) {
|
||||
con.Printf(Info + "Check for updates with the 'update' command\n\n")
|
||||
con.Printf("%sCheck for updates with the 'update' command\n\n", Info)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,7 +678,7 @@ func (con *SliverClient) GetSession(arg string) *clientpb.Session {
|
||||
func (con *SliverClient) GetSessionsByName(name string) []*clientpb.Session {
|
||||
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
fmt.Printf(Warn+"%s\n", err)
|
||||
fmt.Printf("%s%s\n", Warn, err)
|
||||
return nil
|
||||
}
|
||||
matched := []*clientpb.Session{}
|
||||
@@ -745,7 +819,7 @@ func (con *SliverClient) FormatDateDelta(t time.Time, includeDate bool, color bo
|
||||
interval = time.Since(t).Round(time.Second).String()
|
||||
}
|
||||
if color {
|
||||
interval = fmt.Sprintf("%s%s%s", Bold+Red, interval, Normal)
|
||||
interval = StyleBoldRed.Render(interval)
|
||||
}
|
||||
} else {
|
||||
if includeDate {
|
||||
@@ -754,7 +828,7 @@ func (con *SliverClient) FormatDateDelta(t time.Time, includeDate bool, color bo
|
||||
interval = time.Until(t).Round(time.Second).String()
|
||||
}
|
||||
if color {
|
||||
interval = fmt.Sprintf("%s%s%s", Bold+Green, interval, Normal)
|
||||
interval = StyleBoldGreen.Render(interval)
|
||||
}
|
||||
}
|
||||
return interval
|
||||
@@ -789,7 +863,7 @@ type ActiveTarget struct {
|
||||
// GetSessionInteractive - Get the active target(s).
|
||||
func (s *ActiveTarget) GetInteractive() (*clientpb.Session, *clientpb.Beacon) {
|
||||
if s.session == nil && s.beacon == nil {
|
||||
fmt.Printf(Warn + "Please select a session or beacon via `use`\n")
|
||||
fmt.Print(Warn + "Please select a session or beacon via `use`\n")
|
||||
return nil, nil
|
||||
}
|
||||
return s.session, s.beacon
|
||||
@@ -803,7 +877,7 @@ func (s *ActiveTarget) Get() (*clientpb.Session, *clientpb.Beacon) {
|
||||
// GetSessionInteractive - GetSessionInteractive the active session.
|
||||
func (s *ActiveTarget) GetSessionInteractive() *clientpb.Session {
|
||||
if s.session == nil {
|
||||
fmt.Printf(Warn + "Please select a session via `use`\n")
|
||||
fmt.Print(Warn + "Please select a session via `use`\n")
|
||||
return nil
|
||||
}
|
||||
return s.session
|
||||
@@ -817,7 +891,7 @@ func (s *ActiveTarget) GetSession() *clientpb.Session {
|
||||
// GetBeaconInteractive - Get beacon interactive the active session.
|
||||
func (s *ActiveTarget) GetBeaconInteractive() *clientpb.Beacon {
|
||||
if s.beacon == nil {
|
||||
fmt.Printf(Warn + "Please select a beacon via `use`\n")
|
||||
fmt.Print(Warn + "Please select a beacon via `use`\n")
|
||||
return nil
|
||||
}
|
||||
return s.beacon
|
||||
@@ -1037,82 +1111,60 @@ var abilities = []string{
|
||||
"jump-start",
|
||||
}
|
||||
|
||||
var asciiLogos = []string{
|
||||
Red + `
|
||||
██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
|
||||
▒██ ▒ ▓██▒ ▓██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒
|
||||
░ ▓██▄ ▒██░ ▒██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒
|
||||
▒ ██▒▒██░ ░██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄
|
||||
▒██████▒▒░██████▒░██░ ▒▀█░ ░▒████▒░██▓ ▒██▒
|
||||
▒ ▒▓▒ ▒ ░░ ▒░▓ ░░▓ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░
|
||||
░ ░▒ ░ ░░ ░ ▒ ░ ▒ ░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░
|
||||
░ ░ ░ ░ ░ ▒ ░ ░░ ░ ░░ ░
|
||||
░ ░ ░ ░ ░ ░ ░ ░
|
||||
` + Normal,
|
||||
type asciiLogoStyle int
|
||||
|
||||
Green + `
|
||||
███████╗██╗ ██╗██╗ ██╗███████╗██████╗
|
||||
██╔════╝██║ ██║██║ ██║██╔════╝██╔══██╗
|
||||
███████╗██║ ██║██║ ██║█████╗ ██████╔╝
|
||||
╚════██║██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗
|
||||
███████║███████╗██║ ╚████╔╝ ███████╗██║ ██║
|
||||
╚══════╝╚══════╝╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
|
||||
` + Normal,
|
||||
const (
|
||||
logoStyleRed asciiLogoStyle = iota
|
||||
logoStyleGreen
|
||||
logoStyleBoldGray
|
||||
)
|
||||
|
||||
Bold + Gray + `
|
||||
type asciiLogo struct {
|
||||
style asciiLogoStyle
|
||||
art string
|
||||
}
|
||||
|
||||
func (l asciiLogo) Render() string {
|
||||
switch l.style {
|
||||
case logoStyleRed:
|
||||
return StyleRed.Render(l.art)
|
||||
case logoStyleGreen:
|
||||
return StyleGreen.Render(l.art)
|
||||
case logoStyleBoldGray:
|
||||
return StyleBoldGray.Render(l.art)
|
||||
default:
|
||||
return l.art
|
||||
}
|
||||
}
|
||||
|
||||
var asciiLogos = []asciiLogo{
|
||||
{style: logoStyleRed, art: `
|
||||
██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
|
||||
▒██ ▒ ▓██▒ ▓██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒
|
||||
░ ▓██▄ ▒██░ ▒██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒
|
||||
▒ ██▒▒██░ ░██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄
|
||||
▒██████▒▒░██████▒░██░ ▒▀█░ ░▒████▒░██▓ ▒██▒
|
||||
▒ ▒▓▒ ▒ ░░ ▒░▓ ░░▓ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░
|
||||
░ ░▒ ░ ░░ ░ ▒ ░ ▒ ░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░
|
||||
░ ░ ░ ░ ░ ▒ ░ ░░ ░ ░░ ░
|
||||
░ ░ ░ ░ ░ ░ ░ ░
|
||||
`},
|
||||
|
||||
{style: logoStyleGreen, art: `
|
||||
███████╗██╗ ██╗██╗ ██╗███████╗██████╗
|
||||
██╔════╝██║ ██║██║ ██║██╔════╝██╔══██╗
|
||||
███████╗██║ ██║██║ ██║█████╗ ██████╔╝
|
||||
╚════██║██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗
|
||||
███████║███████╗██║ ╚████╔╝ ███████╗██║ ██║
|
||||
╚══════╝╚══════╝╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
|
||||
`},
|
||||
|
||||
{style: logoStyleBoldGray, art: `
|
||||
.------..------..------..------..------..------.
|
||||
|S.--. ||L.--. ||I.--. ||V.--. ||E.--. ||R.--. |
|
||||
| :/\: || :/\: || (\/) || :(): || (\/) || :(): |
|
||||
| :\/: || (__) || :\/: || ()() || :\/: || ()() |
|
||||
| '--'S|| '--'L|| '--'I|| '--'V|| '--'E|| '--'R|
|
||||
` + "`------'`------'`------'`------'`------'`------'" + `
|
||||
` + Normal,
|
||||
|
||||
Purple + `
|
||||
****@@ @@****
|
||||
@@@@@@***@ @***@@@@@@
|
||||
@%%@@@%%#***@ @***#%%@@@%%@
|
||||
%%%%%##%%%%****@ @****%%%%##%%%%%
|
||||
%%%%#####%%%%#*###@ @#####%%%%#####%%%%
|
||||
@%%%*@#####%%%%@####@ @####@%%%%#####@#%%%@
|
||||
%%%+@**#####%%%%#####@ @#####%%%%#####**@+%%%@
|
||||
#%%=+*+**###%#%%%###### ######%%%%%###**+*+=%%%
|
||||
%#%===++@*####%@@@@###### ######@@@@%####*@++===%%%
|
||||
@#@--===+**%%%@@@@@@#####%% %%#####@@@@@@%%#**+===--@#@
|
||||
#@----%=+*%%%%@@@%%%####%%%@ @ @ @%%%####%%%@@@%%%%*+=%----@#
|
||||
#%----=#++*%%%%@%%%%%@%%%%%%%%%@ @%%%%%%%%%@%%%%%@%%%%*++#=----%#
|
||||
%+----===+**@%%@#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%@**+===----+%
|
||||
@=----==++***@%@#%%%%%%%@%%%%@%%%%%%%%%%%%%%%%%%%%@%%%%@%%%%%%%#@%@***++==-----@
|
||||
-----==+#%**##@@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%@@##**%#+==-----
|
||||
%----==+**%%%%@@@#@%%%%%%%%%%@%%%%%%%%%%%%%%%%%%@%%%%%%%%%%@#@@@%%%%*++==----%
|
||||
=--#+=+**#@%%####%####%%%%@@@@@%%%%%%##%%%%%%@@@@@@%%%####%####%%@#**+=+#--=
|
||||
+-==+*%%%%########%#######%@@@@@@%%%%%#%%@@@@@@%#######%########%%%%*+==-=
|
||||
-----==++**#%######################%@@@%%%%@@@%######################%#**++==-----
|
||||
=--===+**######@##############%%%%%%%%%%%%%%%%%%%%##############@######**+===--=
|
||||
%-==+%@@@#######%%%%%%%%%@@%%%%%%%%%%%%%%%%%%%%%%%%@@%%%%%%%%%#######@@@%+==-%
|
||||
@=@@@@@@@@#%%%%%%%%%%%%%%@@%%%%%%%%%%%%%%%%%%%%%%@@@%%%%%%%%%%%%%#@@@@@@@@=@
|
||||
@@@@@@@@@@@@@%%%%%%%%%%%%@@@@%%%%%%%%**%%%%%%%%@@@@%%%%%%%%%%%%@@@@@@@@@@@@@
|
||||
@@@@@@@@@@@@@@@@@%%#%@@%%%%@@@@@%%%%%@#**#@%%%%%@@@@@%%%%@@%#%%@@@@@@@@@@@@@@@@@
|
||||
@@@@@@@@@@@@@@@@@@@@%#####%#%%%%%%%@%###**###%@%%%%%%%#######%@@@@@@@@@@@@@@@@@@@@
|
||||
@@@@@@@@@@@@@@@@@@@@@@@*##%%%%%%%%%%%%%##**##%%%%%%%%%%%%###*@@@@@@@@@@@@@@@@@@@@@@@
|
||||
@%%%%%@@@@@@@@@@@@@@@@@@@ %%%%%%%%%%%%%##**##%%%%%%%%%%%%% @@@@@@@@@@@@@@@@@@@%%%%%@
|
||||
@%%%%%%%%%%%%%%@@@@@@@@@@@@ . %%%%%@%%%%%##**##%%%%%@%%%%% @@@@@@@@@@@@%%%%%%%%%%%%%%@
|
||||
%%%%%%%@+. % :@%%%%%%%%@@@@@@@@@%@%%@@@@##**##@@@@%%@%@@@@@@@@@%%%%%%%%@: % ..+@%%%%%%%
|
||||
%#. . : .-@%%%%%@@@@@@@@@@@@@@##**##@@@@@@@@@@@@@@%%%%%@-. : .*%
|
||||
@%%@* #..:--=++*@%@@@@@@@@@@@@##**##@@@@@@@@@@@@@@*++==-:. # *@%%@
|
||||
%%%%%%@..*.:+% ..::+-==++++@-=@@@@@@@@@@@%####%@@@@@@@@@@@=-@++++==-+::.. #+:.*..@%%%%%%
|
||||
@###%.......::%-==++**#@++#*: .:-+@@@@@@@@@@%####%@@@@@@@@@@+-:. .*#++@#**++==-%::.......%###@
|
||||
#%..........:::-==%+*******@* . .:-+@@@@@@@@@@####@@@@@@@@@@+-: #@*******+%=--:::..........%#
|
||||
@@@+:...::::--=++**@***##*==+ :-+@@@.@@@@%%##%%@@@@.@@@+-: ++=*##***@**++=--::::...:+@@@
|
||||
@%%%***###%@#****##@##++++++. .:=*@+ @@@@%%##%%@@@@ =@*=:. .+++++=##@##****#@%###***%%%@
|
||||
@@=====+++++***#####%%%%@++++**#.:=*@ .@@%%####%%@@. @*=:.#***+++@%%%%#####***+++++=====@@
|
||||
@@@#+++++***##%%%%%***##%%*# @@*------*@@ #*%%##***%%%%%##***+++++#@@@
|
||||
@@@@%%%@%%%%%. **--------** .%%%%%%%%%@@@@
|
||||
@%%%% %@@@@@@@@@@@@@ %%%%@
|
||||
@@ @::@@@@@@@@@@::@ @@
|
||||
-::::::@@@@:::::::
|
||||
*-----========-----+
|
||||
@###%%%@@@@%%%###@
|
||||
@%%%%%%@
|
||||
` + Normal,
|
||||
`},
|
||||
}
|
||||
+111
-2
@@ -26,6 +26,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
@@ -51,6 +52,109 @@ func (l *ConsoleClientLogger) Write(buf []byte) (int, error) {
|
||||
return len(buf), err
|
||||
}
|
||||
|
||||
// optionalRemoteWriter is an io.Writer that forwards to an optional remote writer.
|
||||
// It always returns len(buf), nil to avoid breaking io.MultiWriter/io.Copy when
|
||||
// a remote stream disconnects during a server switch.
|
||||
type optionalRemoteWriter struct {
|
||||
mu sync.RWMutex
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (o *optionalRemoteWriter) Set(w io.Writer) {
|
||||
o.mu.Lock()
|
||||
o.w = w
|
||||
o.mu.Unlock()
|
||||
}
|
||||
|
||||
func (o *optionalRemoteWriter) Write(buf []byte) (int, error) {
|
||||
o.mu.RLock()
|
||||
w := o.w
|
||||
o.mu.RUnlock()
|
||||
if w != nil {
|
||||
_, _ = w.Write(buf)
|
||||
}
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (con *SliverClient) ensureJSONRemoteWriter() {
|
||||
con.connMu.Lock()
|
||||
defer con.connMu.Unlock()
|
||||
|
||||
if con.jsonRemoteWriter == nil {
|
||||
con.jsonRemoteWriter = &optionalRemoteWriter{}
|
||||
}
|
||||
}
|
||||
|
||||
func (con *SliverClient) ensureAsciicastRemoteWriter() {
|
||||
con.connMu.Lock()
|
||||
defer con.connMu.Unlock()
|
||||
|
||||
if con.asciicastRemoteWriter == nil {
|
||||
con.asciicastRemoteWriter = &optionalRemoteWriter{}
|
||||
}
|
||||
}
|
||||
|
||||
func (con *SliverClient) refreshRemoteLogStreamsLocked() {
|
||||
// Only refresh if the console logging stack has been initialized.
|
||||
if con.jsonRemoteWriter == nil && con.asciicastRemoteWriter == nil {
|
||||
return
|
||||
}
|
||||
if con.Rpc == nil {
|
||||
con.setRemoteLogStreamsLocked(nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var jsonStream *ConsoleClientLogger
|
||||
var asciicastStream *ConsoleClientLogger
|
||||
|
||||
if con.jsonRemoteWriter != nil {
|
||||
s, err := con.ClientLogStream("json")
|
||||
if err != nil {
|
||||
log.Printf("Could not get client json log stream: %s", err)
|
||||
} else {
|
||||
jsonStream = s
|
||||
}
|
||||
}
|
||||
if con.asciicastRemoteWriter != nil {
|
||||
s, err := con.ClientLogStream("asciicast")
|
||||
if err != nil {
|
||||
log.Printf("Could not get client asciicast log stream: %s", err)
|
||||
} else {
|
||||
asciicastStream = s
|
||||
}
|
||||
}
|
||||
|
||||
con.setRemoteLogStreamsLocked(jsonStream, asciicastStream)
|
||||
}
|
||||
|
||||
func (con *SliverClient) setRemoteLogStreamsLocked(jsonStream, asciicastStream *ConsoleClientLogger) {
|
||||
// Detach writers first so background pipes can't hit a closing stream.
|
||||
if con.jsonRemoteWriter != nil {
|
||||
con.jsonRemoteWriter.Set(nil)
|
||||
}
|
||||
if con.asciicastRemoteWriter != nil {
|
||||
con.asciicastRemoteWriter.Set(nil)
|
||||
}
|
||||
|
||||
if con.jsonRemoteStream != nil {
|
||||
_ = con.jsonRemoteStream.CloseSend()
|
||||
con.jsonRemoteStream = nil
|
||||
}
|
||||
if con.asciicastRemoteStream != nil {
|
||||
_ = con.asciicastRemoteStream.CloseSend()
|
||||
con.asciicastRemoteStream = nil
|
||||
}
|
||||
|
||||
if jsonStream != nil && con.jsonRemoteWriter != nil {
|
||||
con.jsonRemoteWriter.Set(jsonStream)
|
||||
con.jsonRemoteStream = jsonStream.Stream
|
||||
}
|
||||
if asciicastStream != nil && con.asciicastRemoteWriter != nil {
|
||||
con.asciicastRemoteWriter.Set(asciicastStream)
|
||||
con.asciicastRemoteStream = asciicastStream.Stream
|
||||
}
|
||||
}
|
||||
|
||||
// ClientLogStream requires a log stream name, used to save the logs
|
||||
// going through this stream in a specific log subdirectory/file.
|
||||
func (con *SliverClient) ClientLogStream(name string) (*ConsoleClientLogger, error) {
|
||||
@@ -69,7 +173,12 @@ func (con *SliverClient) setupLogger(writers ...io.Writer) {
|
||||
con.jsonHandler = slog.NewJSONHandler(logWriter, jsonOptions)
|
||||
|
||||
// Log all commands before running them.
|
||||
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.logCommand)
|
||||
con.connMu.Lock()
|
||||
defer con.connMu.Unlock()
|
||||
if !con.logCommandHookApplied {
|
||||
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.logCommand)
|
||||
con.logCommandHookApplied = true
|
||||
}
|
||||
}
|
||||
|
||||
// logCommand logs non empty commands to the client log file.
|
||||
@@ -242,7 +351,7 @@ func (con *SliverClient) PrintWarnf(format string, args ...any) {
|
||||
|
||||
logger.Warn(fmt.Sprintf(format, args...))
|
||||
|
||||
con.printf(Clearln+"⚠️ "+Normal+format, args...)
|
||||
con.printf(Clearln+"⚠️ "+format, args...)
|
||||
}
|
||||
|
||||
// PrintErrorf prints an error message immediately below the last line of output.
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package console
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type promptPalette struct {
|
||||
Default lipgloss.Color
|
||||
Mods map[int]lipgloss.Color
|
||||
}
|
||||
|
||||
type promptColors struct {
|
||||
Primary promptPalette
|
||||
Secondary promptPalette
|
||||
Default promptPalette
|
||||
Success promptPalette
|
||||
Warning promptPalette
|
||||
Danger promptPalette
|
||||
}
|
||||
|
||||
type promptStyles struct {
|
||||
Normal TextStyle
|
||||
Bold TextStyle
|
||||
Underline TextStyle
|
||||
|
||||
Primary TextStyle
|
||||
Secondary TextStyle
|
||||
Default TextStyle
|
||||
Success TextStyle
|
||||
Warning TextStyle
|
||||
Danger TextStyle
|
||||
|
||||
BoldPrimary TextStyle
|
||||
BoldSecondary TextStyle
|
||||
BoldDefault TextStyle
|
||||
BoldSuccess TextStyle
|
||||
BoldWarning TextStyle
|
||||
BoldDanger TextStyle
|
||||
|
||||
// Legacy names used across the codebase.
|
||||
Black TextStyle
|
||||
Red TextStyle
|
||||
Green TextStyle
|
||||
Orange TextStyle
|
||||
Blue TextStyle
|
||||
Purple TextStyle
|
||||
Cyan TextStyle
|
||||
Gray TextStyle
|
||||
}
|
||||
|
||||
type promptTarget struct {
|
||||
SessionName string
|
||||
BeaconName string
|
||||
|
||||
// Suffix is a pre-rendered, themed " (name)" segment matching the default prompt.
|
||||
Suffix string
|
||||
}
|
||||
|
||||
type promptTemplateContext struct {
|
||||
Now time.Time
|
||||
|
||||
IsServer bool
|
||||
Connected bool
|
||||
|
||||
Operator string
|
||||
Host string
|
||||
Port int
|
||||
HostPort string
|
||||
|
||||
Target promptTarget
|
||||
|
||||
Colors promptColors
|
||||
Styles promptStyles
|
||||
}
|
||||
|
||||
var promptTemplateCache struct {
|
||||
mu sync.RWMutex
|
||||
src string
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func toPromptPalette(p theme.Palette) promptPalette {
|
||||
out := promptPalette{
|
||||
Default: lipgloss.Color(p.Default),
|
||||
Mods: map[int]lipgloss.Color{},
|
||||
}
|
||||
for k, v := range p.Mods {
|
||||
out.Mods[k] = lipgloss.Color(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func promptTemplateForSource(src string) (*template.Template, error) {
|
||||
promptTemplateCache.mu.RLock()
|
||||
if promptTemplateCache.tpl != nil && promptTemplateCache.src == src {
|
||||
t := promptTemplateCache.tpl
|
||||
promptTemplateCache.mu.RUnlock()
|
||||
return t, nil
|
||||
}
|
||||
promptTemplateCache.mu.RUnlock()
|
||||
|
||||
t, err := template.New("sliver-prompt").Option("missingkey=zero").Parse(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
promptTemplateCache.mu.Lock()
|
||||
promptTemplateCache.src = src
|
||||
promptTemplateCache.tpl = t
|
||||
promptTemplateCache.mu.Unlock()
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func renderPromptTemplate(con *SliverClient, src string) (string, error) {
|
||||
if con == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
details, _, ok := con.CurrentConnection()
|
||||
operator := ""
|
||||
host := ""
|
||||
port := 0
|
||||
hostPort := ""
|
||||
if ok && details != nil && details.Config != nil {
|
||||
operator = strings.TrimSpace(details.Config.Operator)
|
||||
host = strings.TrimSpace(details.Config.LHost)
|
||||
port = int(details.Config.LPort)
|
||||
}
|
||||
if host != "" && port != 0 {
|
||||
hostPort = fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
|
||||
// Active target context.
|
||||
var suffix string
|
||||
sessionName := ""
|
||||
beaconName := ""
|
||||
if session := con.ActiveTarget.GetSession(); session != nil {
|
||||
sessionName = session.Name
|
||||
suffix = StyleBoldRed.Render(" (" + sessionName + ")")
|
||||
} else if beacon := con.ActiveTarget.GetBeacon(); beacon != nil {
|
||||
beaconName = beacon.Name
|
||||
suffix = StyleBoldBlue.Render(" (" + beaconName + ")")
|
||||
}
|
||||
|
||||
th := theme.Current()
|
||||
ctx := promptTemplateContext{
|
||||
Now: time.Now(),
|
||||
IsServer: con.IsServer,
|
||||
Connected: ok && details != nil && details.Config != nil,
|
||||
Operator: operator,
|
||||
Host: host,
|
||||
Port: port,
|
||||
HostPort: hostPort,
|
||||
Target: promptTarget{
|
||||
SessionName: sessionName,
|
||||
BeaconName: beaconName,
|
||||
Suffix: suffix,
|
||||
},
|
||||
Colors: promptColors{
|
||||
Primary: toPromptPalette(th.Primary),
|
||||
Secondary: toPromptPalette(th.Secondary),
|
||||
Default: toPromptPalette(th.Default),
|
||||
Success: toPromptPalette(th.Success),
|
||||
Warning: toPromptPalette(th.Warning),
|
||||
Danger: toPromptPalette(th.Danger),
|
||||
},
|
||||
Styles: promptStyles{
|
||||
Normal: StyleNormal,
|
||||
Bold: StyleBold,
|
||||
Underline: StyleUnderline,
|
||||
|
||||
Primary: StylePrimary,
|
||||
Secondary: StyleSecondary,
|
||||
Default: StyleDefault,
|
||||
Success: StyleSuccess,
|
||||
Warning: StyleWarning,
|
||||
Danger: StyleDanger,
|
||||
|
||||
BoldPrimary: StyleBoldPrimary,
|
||||
BoldSecondary: StyleBoldSecondary,
|
||||
BoldDefault: StyleBoldDefault,
|
||||
BoldSuccess: StyleBoldSuccess,
|
||||
BoldWarning: StyleBoldWarning,
|
||||
BoldDanger: StyleBoldDanger,
|
||||
|
||||
Black: StyleBlack,
|
||||
Red: StyleRed,
|
||||
Green: StyleGreen,
|
||||
Orange: StyleOrange,
|
||||
Blue: StyleBlue,
|
||||
Purple: StylePurple,
|
||||
Cyan: StyleCyan,
|
||||
Gray: StyleGray,
|
||||
},
|
||||
}
|
||||
|
||||
tpl, err := promptTemplateForSource(src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package console
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// TextStyle is the shared styling type for the client/server consoles.
|
||||
// Keep this type in the console package so most callsites don't need to import
|
||||
// lipgloss directly.
|
||||
type TextStyle = lipgloss.Style
|
||||
|
||||
var (
|
||||
StyleNormal = lipgloss.NewStyle()
|
||||
StyleBold = lipgloss.NewStyle().Bold(true)
|
||||
StyleUnderline = lipgloss.NewStyle().Underline(true)
|
||||
|
||||
// Theme palette styles.
|
||||
StylePrimary = lipgloss.NewStyle()
|
||||
StyleSecondary = lipgloss.NewStyle()
|
||||
StyleDefault = lipgloss.NewStyle()
|
||||
StyleSuccess = lipgloss.NewStyle()
|
||||
StyleWarning = lipgloss.NewStyle()
|
||||
StyleDanger = lipgloss.NewStyle()
|
||||
|
||||
StyleBoldPrimary = lipgloss.NewStyle()
|
||||
StyleBoldSecondary = lipgloss.NewStyle()
|
||||
StyleBoldDefault = lipgloss.NewStyle()
|
||||
StyleBoldSuccess = lipgloss.NewStyle()
|
||||
StyleBoldWarning = lipgloss.NewStyle()
|
||||
StyleBoldDanger = lipgloss.NewStyle()
|
||||
|
||||
// Legacy color names used across the codebase.
|
||||
StyleBlack = lipgloss.NewStyle()
|
||||
StyleRed = lipgloss.NewStyle()
|
||||
StyleGreen = lipgloss.NewStyle()
|
||||
StyleOrange = lipgloss.NewStyle()
|
||||
StyleBlue = lipgloss.NewStyle()
|
||||
StylePurple = lipgloss.NewStyle()
|
||||
StyleCyan = lipgloss.NewStyle()
|
||||
StyleGray = lipgloss.NewStyle()
|
||||
|
||||
StyleBoldBlack = lipgloss.NewStyle()
|
||||
StyleBoldRed = lipgloss.NewStyle()
|
||||
StyleBoldGreen = lipgloss.NewStyle()
|
||||
StyleBoldOrange = lipgloss.NewStyle()
|
||||
StyleBoldBlue = lipgloss.NewStyle()
|
||||
StyleBoldPurple = lipgloss.NewStyle()
|
||||
StyleBoldCyan = lipgloss.NewStyle()
|
||||
StyleBoldGray = lipgloss.NewStyle()
|
||||
)
|
||||
|
||||
// Prefix tags used across the client/server consoles.
|
||||
var (
|
||||
Info string
|
||||
Warn string
|
||||
Debug string
|
||||
Woot string
|
||||
Success string
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Apply a usable theme without doing any disk I/O.
|
||||
ApplyTheme(theme.Current())
|
||||
}
|
||||
|
||||
// ApplyTheme updates all exported styles/prefixes to match the provided theme.
|
||||
//
|
||||
// Note: this is intentionally a "global theme" for the console, since most of
|
||||
// the existing code expects these styles to be package-level values.
|
||||
func ApplyTheme(t theme.Theme) {
|
||||
palette := func(p theme.Palette) lipgloss.Color {
|
||||
if p.Default != "" {
|
||||
return lipgloss.Color(p.Default)
|
||||
}
|
||||
return lipgloss.Color("#ffffff")
|
||||
}
|
||||
paletteMod := func(p theme.Palette, mod int) lipgloss.Color {
|
||||
if mod != 0 {
|
||||
if v, ok := p.Mods[mod]; ok && v != "" {
|
||||
return lipgloss.Color(v)
|
||||
}
|
||||
}
|
||||
return palette(p)
|
||||
}
|
||||
|
||||
// Palette styles.
|
||||
StylePrimary = lipgloss.NewStyle().Foreground(palette(t.Primary))
|
||||
StyleSecondary = lipgloss.NewStyle().Foreground(palette(t.Secondary))
|
||||
StyleDefault = lipgloss.NewStyle().Foreground(palette(t.Default))
|
||||
StyleSuccess = lipgloss.NewStyle().Foreground(palette(t.Success))
|
||||
StyleWarning = lipgloss.NewStyle().Foreground(palette(t.Warning))
|
||||
StyleDanger = lipgloss.NewStyle().Foreground(palette(t.Danger))
|
||||
|
||||
StyleBoldPrimary = lipgloss.NewStyle().Bold(true).Foreground(palette(t.Primary))
|
||||
StyleBoldSecondary = lipgloss.NewStyle().Bold(true).Foreground(palette(t.Secondary))
|
||||
StyleBoldDefault = lipgloss.NewStyle().Bold(true).Foreground(palette(t.Default))
|
||||
StyleBoldSuccess = lipgloss.NewStyle().Bold(true).Foreground(palette(t.Success))
|
||||
StyleBoldWarning = lipgloss.NewStyle().Bold(true).Foreground(palette(t.Warning))
|
||||
StyleBoldDanger = lipgloss.NewStyle().Bold(true).Foreground(palette(t.Danger))
|
||||
|
||||
// Legacy mapping.
|
||||
StyleBlack = lipgloss.NewStyle().Foreground(paletteMod(t.Default, 50))
|
||||
// ANSI "gray" (37) is typically light; map to the lightest neutral.
|
||||
StyleGray = lipgloss.NewStyle().Foreground(paletteMod(t.Default, 900))
|
||||
|
||||
StyleBlue = StylePrimary
|
||||
// Keep Cyan distinct from Blue using a lighter primary tint.
|
||||
StyleCyan = lipgloss.NewStyle().Foreground(paletteMod(t.Primary, 500))
|
||||
StylePurple = StyleSecondary
|
||||
|
||||
StyleGreen = StyleSuccess
|
||||
StyleOrange = StyleWarning
|
||||
StyleRed = StyleDanger
|
||||
|
||||
StyleBoldBlack = lipgloss.NewStyle().Bold(true).Foreground(paletteMod(t.Default, 50))
|
||||
StyleBoldGray = lipgloss.NewStyle().Bold(true).Foreground(paletteMod(t.Default, 900))
|
||||
|
||||
StyleBoldBlue = StyleBoldPrimary
|
||||
StyleBoldCyan = lipgloss.NewStyle().Bold(true).Foreground(paletteMod(t.Primary, 500))
|
||||
StyleBoldPurple = StyleBoldSecondary
|
||||
|
||||
StyleBoldGreen = StyleBoldSuccess
|
||||
StyleBoldOrange = StyleBoldWarning
|
||||
StyleBoldRed = StyleBoldDanger
|
||||
|
||||
// Prefix tags.
|
||||
Info = StyleBoldCyan.Render("[*] ")
|
||||
Warn = StyleBoldDanger.Render("[!] ")
|
||||
Debug = StyleBoldSecondary.Render("[-] ")
|
||||
Woot = StyleBoldSuccess.Render("[$] ")
|
||||
Success = StyleBoldSuccess.Render("[+] ")
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package core
|
||||
|
||||
import "net"
|
||||
|
||||
// ResetClientState tears down singleton client-side state that is tied to a
|
||||
// specific server connection (tunnels, portfwds, SOCKS, cursed procs, etc).
|
||||
//
|
||||
// This is primarily used to support switching servers inside a single
|
||||
// sliver-client process.
|
||||
func ResetClientState() {
|
||||
// Port forwards
|
||||
for _, meta := range Portfwds.List() {
|
||||
Portfwds.Remove(meta.ID)
|
||||
}
|
||||
|
||||
// SOCKS
|
||||
for _, meta := range SocksProxies.List() {
|
||||
SocksProxies.Remove(meta.ID)
|
||||
}
|
||||
SocksConnPool.Range(func(key, value any) bool {
|
||||
if conn, ok := value.(net.Conn); ok {
|
||||
_ = conn.Close()
|
||||
}
|
||||
SocksConnPool.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
// Tunnels
|
||||
GetTunnels().Reset()
|
||||
|
||||
// Cursed processes
|
||||
CursedProcesses.Range(func(key, value any) bool {
|
||||
if cp, ok := value.(*CursedProcess); ok && cp != nil && cp.PortFwd != nil {
|
||||
Portfwds.Remove(cp.PortFwd.ID)
|
||||
}
|
||||
CursedProcesses.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
@@ -27,21 +28,21 @@ import (
|
||||
)
|
||||
|
||||
// TunnelLoop - Parses incoming tunnel messages and distributes them
|
||||
// to session/tunnel objects
|
||||
// Expected to be called only once during initialization
|
||||
func TunnelLoop(rpc rpcpb.SliverRPCClient) error {
|
||||
//
|
||||
// to session/tunnel objects
|
||||
// Expected to be called only once during initialization
|
||||
func TunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) error {
|
||||
log.Println("Starting tunnel data loop ...")
|
||||
defer log.Printf("Warning: TunnelLoop exited")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
stream, err := rpc.TunnelData(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
GetTunnels().SetStream(stream)
|
||||
defer GetTunnels().SetStream(nil)
|
||||
|
||||
for {
|
||||
log.Printf("Waiting for TunnelData ...")
|
||||
@@ -54,6 +55,9 @@ func TunnelLoop(rpc rpcpb.SliverRPCClient) error {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
log.Printf("Tunnel data read error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -160,3 +160,19 @@ func (t *tunnels) CloseForSession(sessionID string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset closes all open tunnels and clears the underlying storage/stream.
|
||||
// This is used when switching servers inside a single client process.
|
||||
func (t *tunnels) Reset() {
|
||||
t.SetStream(nil)
|
||||
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
for tunnelID, tunnel := range *t.tunnels {
|
||||
if tunnel != nil {
|
||||
_ = tunnel.Close()
|
||||
}
|
||||
delete(*t.tunnels, tunnelID)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package forms
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
@@ -44,7 +45,7 @@ func ArmoryInstallForm(options []ArmoryInstallOption) (*ArmoryInstallFormResult,
|
||||
Height(listHeight(len(selectOptions)) + 2).
|
||||
Value(&result.SelectedNames)
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field))
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package forms
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
@@ -43,7 +44,7 @@ func ArmoryUpdateForm(options []ArmoryUpdateOption) (*ArmoryUpdateFormResult, er
|
||||
Height(listHeight(len(selectOptions)) + 2).
|
||||
Value(&result.SelectedIDs)
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field))
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
@@ -124,6 +125,7 @@ func GenerateForm(compiler *clientpb.Compiler) (*GenerateFormResult, error) {
|
||||
),
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
@@ -154,6 +155,7 @@ func GenerateBeaconForm(compiler *clientpb.Compiler) (*GenerateBeaconFormResult,
|
||||
),
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
@@ -134,6 +135,7 @@ func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
|
||||
),
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
@@ -166,6 +167,7 @@ func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, erro
|
||||
),
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+10
-5
@@ -5,6 +5,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/huh"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
@@ -32,7 +33,7 @@ func Confirm(title string, value *bool) error {
|
||||
Title(title).
|
||||
Value(value),
|
||||
),
|
||||
)
|
||||
).WithTheme(theme.HuhTheme())
|
||||
|
||||
return form.Run()
|
||||
}
|
||||
@@ -49,7 +50,7 @@ func Input(title string, value *string) error {
|
||||
Title(title).
|
||||
Value(value),
|
||||
),
|
||||
)
|
||||
).WithTheme(theme.HuhTheme())
|
||||
|
||||
return form.Run()
|
||||
}
|
||||
@@ -67,7 +68,7 @@ func Text(title string, value *string) error {
|
||||
ExternalEditor(false).
|
||||
Value(value),
|
||||
),
|
||||
)
|
||||
).WithTheme(theme.HuhTheme())
|
||||
|
||||
return form.Run()
|
||||
}
|
||||
@@ -98,7 +99,9 @@ func MultiSelect(title string, options []string, value *[]string) error {
|
||||
Height(listHeight(len(options)) + 1).
|
||||
Value(value)
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithWidth(getTerminalWidth())
|
||||
form := huh.NewForm(huh.NewGroup(field)).
|
||||
WithTheme(theme.HuhTheme()).
|
||||
WithWidth(getTerminalWidth())
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
@@ -130,7 +133,9 @@ func selectPrompt(title string, options []string, value *string, required bool)
|
||||
})
|
||||
}
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field)).WithWidth(getTerminalWidth())
|
||||
form := huh.NewForm(huh.NewGroup(field)).
|
||||
WithTheme(theme.HuhTheme()).
|
||||
WithWidth(getTerminalWidth())
|
||||
err := form.Run()
|
||||
|
||||
// On error restore the originalValue and return err
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/theme"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,7 @@ type SettingsFormResult struct {
|
||||
VimMode bool
|
||||
UserConnect bool
|
||||
ConsoleLogs bool
|
||||
PromptStyle string
|
||||
}
|
||||
|
||||
// SettingsForm prompts for client settings and returns the collected values.
|
||||
@@ -39,10 +41,14 @@ func SettingsForm(settings *assets.ClientSettings, tableStyleOptions []string) (
|
||||
VimMode: settings.VimMode,
|
||||
UserConnect: settings.UserConnect,
|
||||
ConsoleLogs: settings.ConsoleLogs,
|
||||
PromptStyle: assets.NormalizePromptStyle(settings.PromptStyle),
|
||||
}
|
||||
if result.TableStyle == "" {
|
||||
result.TableStyle = tableStyleOptions[0]
|
||||
}
|
||||
if result.PromptStyle == "" {
|
||||
result.PromptStyle = assets.PromptStyleHost
|
||||
}
|
||||
|
||||
styleOptions := make([]huh.Option[string], 0, len(tableStyleOptions))
|
||||
for _, option := range tableStyleOptions {
|
||||
@@ -51,6 +57,17 @@ func SettingsForm(settings *assets.ClientSettings, tableStyleOptions []string) (
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("Prompt style").
|
||||
Description("Customize the sliver prompt prefix").
|
||||
Options(
|
||||
huh.NewOption(assets.PromptStyleHost, assets.PromptStyleHost),
|
||||
huh.NewOption(assets.PromptStyleOperatorHost, assets.PromptStyleOperatorHost),
|
||||
huh.NewOption(assets.PromptStyleBasic, assets.PromptStyleBasic),
|
||||
huh.NewOption(assets.PromptStyleCustom, assets.PromptStyleCustom),
|
||||
).
|
||||
Height(4).
|
||||
Value(&result.PromptStyle),
|
||||
huh.NewSelect[string]().
|
||||
Title("Table style").
|
||||
Options(styleOptions...).
|
||||
@@ -90,6 +107,7 @@ func SettingsForm(settings *assets.ClientSettings, tableStyleOptions []string) (
|
||||
).Title("Behavior"),
|
||||
)
|
||||
|
||||
form = form.WithTheme(theme.HuhTheme())
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
-15
@@ -10,21 +10,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// ANSI Colors
|
||||
normal = "\033[0m"
|
||||
black = "\033[30m"
|
||||
red = "\033[31m"
|
||||
green = "\033[32m"
|
||||
orange = "\033[33m"
|
||||
blue = "\033[34m"
|
||||
purple = "\033[35m"
|
||||
cyan = "\033[36m"
|
||||
gray = "\033[37m"
|
||||
bold = "\033[1m"
|
||||
clearln = "\r\x1b[2K"
|
||||
upN = "\033[%dA"
|
||||
downN = "\033[%dB"
|
||||
underline = "\033[4m"
|
||||
// Terminal control sequence for clearing the current line.
|
||||
clearln = "\r\x1b[2K"
|
||||
)
|
||||
|
||||
// Spinner types.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package theme
|
||||
|
||||
// DefaultThemeYAML is the initial theme created at <client app dir>/theme.yaml.
|
||||
//
|
||||
// Palettes:
|
||||
// - default: neutral base
|
||||
// - primary/secondary: accent colors
|
||||
// - success/warning/danger: semantic colors
|
||||
//
|
||||
// Each palette has a "default" value and a set of numeric modifiers:
|
||||
// 50, 100, 200, 300, 400, 500, 600, 700, 800, 900.
|
||||
const DefaultThemeYAML = `# Sliver client console theme.
|
||||
# Colors are hex strings (e.g. "#006FEE").
|
||||
|
||||
primary:
|
||||
default: "#006FEE"
|
||||
mods:
|
||||
50: "#001731"
|
||||
100: "#002e62"
|
||||
200: "#004493"
|
||||
300: "#005bc4"
|
||||
400: "#006FEE"
|
||||
500: "#338ef7"
|
||||
600: "#66aaf9"
|
||||
700: "#99c7fb"
|
||||
800: "#cce3fd"
|
||||
900: "#e6f1fe"
|
||||
|
||||
secondary:
|
||||
default: "#9353d3"
|
||||
mods:
|
||||
50: "#180828"
|
||||
100: "#301050"
|
||||
200: "#481878"
|
||||
300: "#6020a0"
|
||||
400: "#7828c8"
|
||||
500: "#9353d3"
|
||||
600: "#ae7ede"
|
||||
700: "#c9a9e9"
|
||||
800: "#e4d4f4"
|
||||
900: "#f2eafa"
|
||||
|
||||
default:
|
||||
default: "#3f3f46"
|
||||
mods:
|
||||
50: "#18181b"
|
||||
100: "#27272a"
|
||||
200: "#3f3f46"
|
||||
300: "#52525b"
|
||||
400: "#71717a"
|
||||
500: "#a1a1aa"
|
||||
600: "#d4d4d8"
|
||||
700: "#e4e4e7"
|
||||
800: "#f4f4f5"
|
||||
900: "#fafafa"
|
||||
|
||||
success:
|
||||
default: "#17c964"
|
||||
mods:
|
||||
50: "#052814"
|
||||
100: "#095028"
|
||||
200: "#0e793c"
|
||||
300: "#12a150"
|
||||
400: "#17c964"
|
||||
500: "#45d483"
|
||||
600: "#74dfa2"
|
||||
700: "#a2e9c1"
|
||||
800: "#d1f4e0"
|
||||
900: "#e8faf0"
|
||||
|
||||
warning:
|
||||
default: "#f5a524"
|
||||
mods:
|
||||
50: "#312107"
|
||||
100: "#62420e"
|
||||
200: "#936316"
|
||||
300: "#c4841d"
|
||||
400: "#f5a524"
|
||||
500: "#f7b750"
|
||||
600: "#f9c97c"
|
||||
700: "#fbdba7"
|
||||
800: "#fdedd3"
|
||||
900: "#fefce8"
|
||||
|
||||
danger:
|
||||
default: "#f31260"
|
||||
mods:
|
||||
50: "#310413"
|
||||
100: "#610726"
|
||||
200: "#920b3a"
|
||||
300: "#c20e4d"
|
||||
400: "#f31260"
|
||||
500: "#f54180"
|
||||
600: "#f871a0"
|
||||
700: "#faa0bf"
|
||||
800: "#fdd0df"
|
||||
900: "#fee7ef"
|
||||
`
|
||||
@@ -0,0 +1,67 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// HuhTheme returns a huh Theme derived from the current Sliver client theme.
|
||||
//
|
||||
// This is used for all interactive TUI forms in the client so that colors are
|
||||
// consistent with the rest of the console styling (theme.yaml).
|
||||
func HuhTheme() *huh.Theme {
|
||||
t := huh.ThemeBase()
|
||||
|
||||
border := DefaultMod(300)
|
||||
text := DefaultMod(900)
|
||||
dim := DefaultMod(500)
|
||||
faint := DefaultMod(400)
|
||||
|
||||
primary := Primary()
|
||||
accent := Secondary()
|
||||
|
||||
success := Success()
|
||||
danger := Danger()
|
||||
|
||||
// Focused styles.
|
||||
t.Focused.Base = t.Focused.Base.BorderForeground(border)
|
||||
t.Focused.Card = t.Focused.Base
|
||||
t.Focused.Title = t.Focused.Title.Foreground(primary).Bold(true)
|
||||
t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(primary).Bold(true).MarginBottom(1)
|
||||
t.Focused.Description = t.Focused.Description.Foreground(dim)
|
||||
t.Focused.Directory = t.Focused.Directory.Foreground(primary)
|
||||
t.Focused.File = t.Focused.File.Foreground(text)
|
||||
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(danger)
|
||||
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(danger)
|
||||
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(accent)
|
||||
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(accent)
|
||||
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(accent)
|
||||
t.Focused.Option = t.Focused.Option.Foreground(text)
|
||||
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(accent)
|
||||
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(success)
|
||||
t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(success)
|
||||
t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(text)
|
||||
t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(faint)
|
||||
|
||||
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(text).Background(primary)
|
||||
t.Focused.Next = t.Focused.FocusedButton
|
||||
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(lipgloss.Color(DefaultMod(100)))
|
||||
|
||||
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primary)
|
||||
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(faint)
|
||||
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(accent)
|
||||
|
||||
// Blurred styles (same palette, but without the focused border/indicators).
|
||||
t.Blurred = t.Focused
|
||||
t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||
t.Blurred.Card = t.Blurred.Base
|
||||
t.Blurred.MultiSelectSelector = lipgloss.NewStyle().SetString(" ")
|
||||
t.Blurred.NextIndicator = lipgloss.NewStyle()
|
||||
t.Blurred.PrevIndicator = lipgloss.NewStyle()
|
||||
|
||||
// Group styles.
|
||||
t.Group.Title = t.Focused.Title
|
||||
t.Group.Description = t.Focused.Description
|
||||
|
||||
return t
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
ThemeFileName = "theme.yaml"
|
||||
)
|
||||
|
||||
type Palette struct {
|
||||
Default string `yaml:"default"`
|
||||
Mods map[int]string `yaml:"mods"`
|
||||
}
|
||||
|
||||
type Theme struct {
|
||||
Primary Palette `yaml:"primary"`
|
||||
Secondary Palette `yaml:"secondary"`
|
||||
Default Palette `yaml:"default"`
|
||||
Success Palette `yaml:"success"`
|
||||
Warning Palette `yaml:"warning"`
|
||||
Danger Palette `yaml:"danger"`
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
current Theme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Ensure there is always a usable theme without I/O.
|
||||
t, err := parseThemeYAML([]byte(DefaultThemeYAML))
|
||||
if err == nil {
|
||||
current = t
|
||||
} else {
|
||||
// Minimal fallback; should never happen unless DefaultThemeYAML is corrupted.
|
||||
current = Theme{
|
||||
Primary: Palette{Default: "#006FEE", Mods: map[int]string{}},
|
||||
Default: Palette{Default: "#ffffff", Mods: map[int]string{}},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ThemePath() string {
|
||||
return filepath.Join(assets.GetRootAppDir(), ThemeFileName)
|
||||
}
|
||||
|
||||
func SetCurrentTheme(t Theme) {
|
||||
mu.Lock()
|
||||
current = t
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
func EnsureThemeFile() error {
|
||||
p := ThemePath()
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, []byte(DefaultThemeYAML), 0o600)
|
||||
}
|
||||
|
||||
func LoadThemeFromDisk() (Theme, error) {
|
||||
data, err := os.ReadFile(ThemePath())
|
||||
if err != nil {
|
||||
return Theme{}, err
|
||||
}
|
||||
return parseThemeYAML(data)
|
||||
}
|
||||
|
||||
func LoadAndSetCurrentTheme() error {
|
||||
_ = EnsureThemeFile()
|
||||
t, err := LoadThemeFromDisk()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SetCurrentTheme(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseThemeYAML(data []byte) (Theme, error) {
|
||||
var t Theme
|
||||
if err := yaml.Unmarshal(data, &t); err != nil {
|
||||
return Theme{}, err
|
||||
}
|
||||
normalizePalette(&t.Primary)
|
||||
normalizePalette(&t.Secondary)
|
||||
normalizePalette(&t.Default)
|
||||
normalizePalette(&t.Success)
|
||||
normalizePalette(&t.Warning)
|
||||
normalizePalette(&t.Danger)
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func normalizePalette(p *Palette) {
|
||||
if p.Mods == nil {
|
||||
p.Mods = map[int]string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (t Theme) paletteColor(p Palette) lipgloss.Color {
|
||||
if p.Default != "" {
|
||||
return lipgloss.Color(p.Default)
|
||||
}
|
||||
// Defensive fallback.
|
||||
return lipgloss.Color("#ffffff")
|
||||
}
|
||||
|
||||
func (t Theme) paletteMod(p Palette, mod int) lipgloss.Color {
|
||||
if mod != 0 {
|
||||
if v, ok := p.Mods[mod]; ok && v != "" {
|
||||
return lipgloss.Color(v)
|
||||
}
|
||||
}
|
||||
return t.paletteColor(p)
|
||||
}
|
||||
|
||||
// Primary returns the theme primary color.
|
||||
func Primary() lipgloss.Color { return Current().paletteColor(Current().Primary) }
|
||||
func PrimaryMod(mod int) lipgloss.Color {
|
||||
t := Current()
|
||||
return t.paletteMod(t.Primary, mod)
|
||||
}
|
||||
|
||||
// Secondary returns the theme secondary color.
|
||||
func Secondary() lipgloss.Color { return Current().paletteColor(Current().Secondary) }
|
||||
func SecondaryMod(mod int) lipgloss.Color {
|
||||
t := Current()
|
||||
return t.paletteMod(t.Secondary, mod)
|
||||
}
|
||||
|
||||
// Default returns the theme default/neutral color.
|
||||
func Default() lipgloss.Color { return Current().paletteColor(Current().Default) }
|
||||
func DefaultMod(mod int) lipgloss.Color {
|
||||
t := Current()
|
||||
return t.paletteMod(t.Default, mod)
|
||||
}
|
||||
|
||||
// Success returns the theme success color.
|
||||
func Success() lipgloss.Color { return Current().paletteColor(Current().Success) }
|
||||
func SuccessMod(mod int) lipgloss.Color {
|
||||
t := Current()
|
||||
return t.paletteMod(t.Success, mod)
|
||||
}
|
||||
|
||||
// Warning returns the theme warning color.
|
||||
func Warning() lipgloss.Color { return Current().paletteColor(Current().Warning) }
|
||||
func WarningMod(mod int) lipgloss.Color {
|
||||
t := Current()
|
||||
return t.paletteMod(t.Warning, mod)
|
||||
}
|
||||
|
||||
// Danger returns the theme danger color.
|
||||
func Danger() lipgloss.Color { return Current().paletteColor(Current().Danger) }
|
||||
func DangerMod(mod int) lipgloss.Color {
|
||||
t := Current()
|
||||
return t.paletteMod(t.Danger, mod)
|
||||
}
|
||||
|
||||
// Current returns the in-memory current theme (defaults to DefaultThemeYAML).
|
||||
func Current() Theme {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return current
|
||||
}
|
||||
@@ -31,9 +31,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
normal = "\033[0m"
|
||||
bold = "\033[1m"
|
||||
|
||||
defaultVersion = "devel"
|
||||
defaultReleasesURL = "https://api.github.com/repos/BishopFox/sliver/releases"
|
||||
)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Spinner } from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
const LoadingState: React.FC = () => {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center">
|
||||
<Spinner label="Loading ..." labelColor="foreground" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingState;
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
oneLight,
|
||||
} from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import AsciinemaPlayer from "./asciinema";
|
||||
import Mermaid from "./mermaid";
|
||||
import Youtube from "./youtube";
|
||||
|
||||
export type MarkdownProps = {
|
||||
key?: string;
|
||||
markdown: string;
|
||||
};
|
||||
|
||||
@@ -42,6 +42,49 @@ type HeadingProps = ComponentPropsWithoutRef<"h1"> & {
|
||||
node?: unknown;
|
||||
};
|
||||
|
||||
const parseMermaidMinHeight = (lang: string, node: any): number | undefined => {
|
||||
const [, ...langOptionParts] = lang.split(":");
|
||||
const langOptions = langOptionParts.join(":").trim();
|
||||
|
||||
const nodeMeta =
|
||||
typeof node?.data?.meta === "string"
|
||||
? node.data.meta
|
||||
: typeof node?.meta === "string"
|
||||
? node.meta
|
||||
: typeof node?.properties?.metastring === "string"
|
||||
? node.properties.metastring
|
||||
: "";
|
||||
|
||||
const optionSources = [langOptions, nodeMeta]
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const parseValue = (raw: string): number | undefined => {
|
||||
const directNumber = raw.match(/^(\d{2,4})$/);
|
||||
if (directNumber) {
|
||||
return Number.parseInt(directNumber[1], 10);
|
||||
}
|
||||
|
||||
const keyedNumber = raw.match(
|
||||
/(?:^|[,\s;])(?:min[-_]?h(?:eight)?|h(?:eight)?)\s*[:=]\s*(\d{2,4})\b/i
|
||||
);
|
||||
if (keyedNumber) {
|
||||
return Number.parseInt(keyedNumber[1], 10);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
for (const source of optionSources) {
|
||||
const parsed = parseValue(source);
|
||||
if (parsed !== undefined && Number.isFinite(parsed)) {
|
||||
return Math.min(1200, Math.max(120, parsed));
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const mergeClassNames = (
|
||||
...classes: Array<string | false | null | undefined>
|
||||
) => {
|
||||
@@ -252,7 +295,6 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<div className="relative">
|
||||
<div className={proseClassName}>
|
||||
<Markdown
|
||||
key={props.key || `${Math.random()}`}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
...headingComponents,
|
||||
@@ -543,6 +585,12 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
// a monaco code block in a <pre> tag
|
||||
const { children, className, node, ...rest } = preProps as any;
|
||||
const childClass = (children as any)?.props?.className;
|
||||
if (
|
||||
typeof childClass === "string" &&
|
||||
childClass.startsWith("language-mermaid")
|
||||
) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
if (
|
||||
childClass &&
|
||||
childClass.startsWith("language-") &&
|
||||
@@ -598,7 +646,8 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
const lang = languageClass
|
||||
? languageClass.replace("language-", "")
|
||||
: "plaintext";
|
||||
const normalizedLang = lang.toLowerCase();
|
||||
const [baseLang] = lang.split(":");
|
||||
const normalizedLang = baseLang.toLowerCase();
|
||||
const childValue = Array.isArray(children)
|
||||
? children.join("")
|
||||
: children;
|
||||
@@ -631,6 +680,15 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (normalizedLang === "mermaid") {
|
||||
return (
|
||||
<Mermaid
|
||||
diagram={sourceCode.replace(/\n$/, "")}
|
||||
minHeight={parseMermaidMinHeight(lang, node)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (inline || normalizedLang === "plaintext") {
|
||||
return (
|
||||
<code
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Themes } from "@/util/themes";
|
||||
import { useTheme } from "next-themes";
|
||||
import React from "react";
|
||||
|
||||
type MermaidProps = {
|
||||
diagram: string;
|
||||
minHeight?: number;
|
||||
};
|
||||
|
||||
const Mermaid = ({ diagram, minHeight }: MermaidProps) => {
|
||||
const { theme } = useTheme();
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const normalizedMinHeight =
|
||||
Number.isFinite(minHeight) && minHeight
|
||||
? Math.min(1200, Math.max(120, Math.floor(minHeight)))
|
||||
: undefined;
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const target = containerRef.current;
|
||||
const source = diagram.trim();
|
||||
|
||||
if (!target || !source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderDiagram = async () => {
|
||||
try {
|
||||
const mermaidModule = await import("mermaid");
|
||||
const mermaid = mermaidModule.default;
|
||||
const mermaidTheme = theme === Themes.LIGHT ? "default" : "dark";
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "strict",
|
||||
theme: mermaidTheme,
|
||||
});
|
||||
|
||||
const graphId = `sliver-mermaid-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 10)}`;
|
||||
const { svg, bindFunctions } = await mermaid.render(graphId, source);
|
||||
|
||||
if (cancelled || !containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
containerRef.current.innerHTML = svg;
|
||||
bindFunctions?.(containerRef.current);
|
||||
|
||||
if (normalizedMinHeight) {
|
||||
const svgEl = containerRef.current.querySelector("svg");
|
||||
if (svgEl) {
|
||||
const viewBox = svgEl.getAttribute("viewBox") || "";
|
||||
const parts = viewBox
|
||||
.split(/\s+/)
|
||||
.map((part) => Number.parseFloat(part))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
|
||||
let naturalWidth = 0;
|
||||
let naturalHeight = 0;
|
||||
if (parts.length === 4) {
|
||||
naturalWidth = parts[2] || 0;
|
||||
naturalHeight = parts[3] || 0;
|
||||
}
|
||||
|
||||
if ((naturalWidth <= 0 || naturalHeight <= 0) && svgEl.getBBox) {
|
||||
try {
|
||||
const bounds = svgEl.getBBox();
|
||||
naturalWidth = bounds.width;
|
||||
naturalHeight = bounds.height;
|
||||
} catch {
|
||||
// No-op: leave natural dimensions unset if bbox is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
if (naturalWidth > 0 && naturalHeight > 0 && naturalHeight < normalizedMinHeight) {
|
||||
const scale = normalizedMinHeight / naturalHeight;
|
||||
const targetWidth = Math.round(naturalWidth * scale);
|
||||
const targetHeight = Math.round(naturalHeight * scale);
|
||||
|
||||
svgEl.setAttribute("width", `${targetWidth}`);
|
||||
svgEl.setAttribute("height", `${targetHeight}`);
|
||||
svgEl.style.width = `${targetWidth}px`;
|
||||
svgEl.style.height = `${targetHeight}px`;
|
||||
svgEl.style.maxWidth = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setError(null);
|
||||
} catch (renderError) {
|
||||
if (cancelled || !containerRef.current) {
|
||||
return;
|
||||
}
|
||||
containerRef.current.innerHTML = "";
|
||||
const message =
|
||||
renderError instanceof Error
|
||||
? renderError.message
|
||||
: "Unknown Mermaid rendering error";
|
||||
setError(message);
|
||||
}
|
||||
};
|
||||
|
||||
void renderDiagram();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (target) {
|
||||
target.innerHTML = "";
|
||||
}
|
||||
};
|
||||
}, [diagram, normalizedMinHeight, theme]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="not-prose my-6 text-sm text-rose-700 dark:text-rose-300">
|
||||
Failed to render Mermaid diagram: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="not-prose my-6 overflow-x-auto">
|
||||
<div
|
||||
className="mx-auto flex w-fit min-w-max items-center justify-center"
|
||||
style={normalizedMinHeight ? { minHeight: normalizedMinHeight } : undefined}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="min-w-max [&_svg]:h-auto [&_svg]:max-w-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mermaid;
|
||||
@@ -3,7 +3,7 @@
|
||||
import { SliversIcon } from "@/components/icons/slivers";
|
||||
import { useSearchContext } from "@/util/search-context";
|
||||
import { Themes } from "@/util/themes";
|
||||
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faBars,
|
||||
faHome,
|
||||
@@ -133,6 +133,17 @@ export default function TopNavbar(props: TopNavbarProps) {
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
|
||||
<NavbarItem isActive={router.pathname.startsWith("/talks")}>
|
||||
<Button
|
||||
variant="light"
|
||||
color={router.pathname === "/talks" ? "primary" : "default"}
|
||||
as={Link}
|
||||
onPress={() => router.push("/talks")}
|
||||
>
|
||||
Talks
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
|
||||
<NavbarItem isActive={router.pathname.startsWith("/docs")}>
|
||||
<Button
|
||||
variant="light"
|
||||
@@ -147,43 +158,69 @@ export default function TopNavbar(props: TopNavbarProps) {
|
||||
|
||||
<NavbarContent as="div" justify="end" className="hidden md:flex">
|
||||
{renderSearchInput("w-64")}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
isDisabled={!mounted}
|
||||
<Tooltip
|
||||
content={theme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
isDisabled={!mounted}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open("https://github.com/BishopFox/sliver", "_blank");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
</Button>
|
||||
<Tooltip content="Join Discord">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="View on GitHub">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open("https://github.com/BishopFox/sliver", "_blank");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</NavbarContent>
|
||||
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
isDisabled={!mounted}
|
||||
<Tooltip
|
||||
content={theme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
onPress={() => setIsMobileMenuOpen((current) => !current)}
|
||||
>
|
||||
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
isDisabled={!mounted}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content={isMobileMenuOpen ? "Close menu" : "Open menu"}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
onPress={() => setIsMobileMenuOpen((current) => !current)}
|
||||
>
|
||||
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Navbar>
|
||||
|
||||
@@ -212,6 +249,16 @@ export default function TopNavbar(props: TopNavbarProps) {
|
||||
>
|
||||
Tutorials
|
||||
</Button>
|
||||
<Button
|
||||
variant={router.pathname.startsWith("/talks") ? "solid" : "light"}
|
||||
color={router.pathname.startsWith("/talks") ? "primary" : "default"}
|
||||
onPress={() => {
|
||||
router.push("/talks");
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
Talks
|
||||
</Button>
|
||||
<Button
|
||||
variant={router.pathname.startsWith("/docs") ? "solid" : "light"}
|
||||
color={router.pathname.startsWith("/docs") ? "primary" : "default"}
|
||||
@@ -222,6 +269,18 @@ export default function TopNavbar(props: TopNavbarProps) {
|
||||
>
|
||||
Docs
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
startContent={<FontAwesomeIcon icon={faDiscord} />}
|
||||
>
|
||||
Discord
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
|
||||
@@ -1,19 +1,240 @@
|
||||
import React from "react";
|
||||
|
||||
type YoutubePlayerState = {
|
||||
PLAYING: number;
|
||||
};
|
||||
|
||||
type YoutubePlayerStateChangeEvent = {
|
||||
data: number;
|
||||
};
|
||||
|
||||
type YoutubePlayer = {
|
||||
destroy?: () => void;
|
||||
};
|
||||
|
||||
type YoutubePlayerCtor = new (
|
||||
element: HTMLIFrameElement | string,
|
||||
options?: {
|
||||
events?: {
|
||||
onStateChange?: (event: YoutubePlayerStateChangeEvent) => void;
|
||||
};
|
||||
}
|
||||
) => YoutubePlayer;
|
||||
|
||||
type YoutubeNamespace = {
|
||||
Player: YoutubePlayerCtor;
|
||||
PlayerState: YoutubePlayerState;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
YT?: YoutubeNamespace;
|
||||
onYouTubeIframeAPIReady?: () => void;
|
||||
__sliverYoutubeApiPromise?: Promise<YoutubeNamespace>;
|
||||
}
|
||||
}
|
||||
|
||||
export type YoutubeProps = {
|
||||
className?: string;
|
||||
iframeClassName?: string;
|
||||
|
||||
width?: number;
|
||||
height?: number;
|
||||
responsive?: boolean;
|
||||
title?: string;
|
||||
startAt?: number;
|
||||
|
||||
embedId: string;
|
||||
embedId?: string;
|
||||
url?: string;
|
||||
|
||||
onInteract?: () => void;
|
||||
onPlay?: () => void;
|
||||
};
|
||||
|
||||
const YOUTUBE_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
|
||||
|
||||
function parseYoutubeEmbedIdFromUrl(input: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(input);
|
||||
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
||||
const parts = parsed.pathname.split("/").filter(Boolean);
|
||||
|
||||
let candidate = "";
|
||||
if (host === "youtu.be" && parts.length > 0) {
|
||||
candidate = parts[0] || "";
|
||||
} else if (host === "youtube.com" || host === "m.youtube.com") {
|
||||
if (parts[0] === "watch") {
|
||||
candidate = parsed.searchParams.get("v") || "";
|
||||
} else if ((parts[0] === "embed" || parts[0] === "shorts") && parts.length > 1) {
|
||||
candidate = parts[1] || "";
|
||||
}
|
||||
}
|
||||
|
||||
return YOUTUBE_ID_PATTERN.test(candidate) ? candidate : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveYoutubeEmbedId(embedId?: string, url?: string): string | null {
|
||||
const idCandidate = (embedId || "").trim();
|
||||
if (YOUTUBE_ID_PATTERN.test(idCandidate)) {
|
||||
return idCandidate;
|
||||
}
|
||||
if (idCandidate) {
|
||||
const parsedFromEmbedId = parseYoutubeEmbedIdFromUrl(idCandidate);
|
||||
if (parsedFromEmbedId) {
|
||||
return parsedFromEmbedId;
|
||||
}
|
||||
}
|
||||
|
||||
const urlCandidate = (url || "").trim();
|
||||
if (urlCandidate) {
|
||||
return parseYoutubeEmbedIdFromUrl(urlCandidate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadYoutubeIframeApi(): Promise<YoutubeNamespace> {
|
||||
if (typeof window === "undefined") {
|
||||
return Promise.reject(new Error("YouTube API cannot load during SSR"));
|
||||
}
|
||||
if (window.YT?.Player) {
|
||||
return Promise.resolve(window.YT);
|
||||
}
|
||||
if (window.__sliverYoutubeApiPromise) {
|
||||
return window.__sliverYoutubeApiPromise;
|
||||
}
|
||||
|
||||
window.__sliverYoutubeApiPromise = new Promise<YoutubeNamespace>((resolve, reject) => {
|
||||
const previousReadyHandler = window.onYouTubeIframeAPIReady;
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
previousReadyHandler?.();
|
||||
if (window.YT?.Player) {
|
||||
resolve(window.YT);
|
||||
} else {
|
||||
reject(new Error("YouTube API loaded without Player support"));
|
||||
}
|
||||
};
|
||||
|
||||
const existingScript = document.querySelector<HTMLScriptElement>(
|
||||
"script[data-youtube-iframe-api='true']"
|
||||
);
|
||||
if (existingScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://www.youtube.com/iframe_api";
|
||||
script.async = true;
|
||||
script.dataset.youtubeIframeApi = "true";
|
||||
script.onerror = () => reject(new Error("Failed to load YouTube iframe API"));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return window.__sliverYoutubeApiPromise;
|
||||
}
|
||||
|
||||
export default function Youtube(props: YoutubeProps) {
|
||||
const {
|
||||
className,
|
||||
iframeClassName,
|
||||
width,
|
||||
height,
|
||||
responsive,
|
||||
title,
|
||||
startAt,
|
||||
embedId: embedIdProp,
|
||||
url,
|
||||
onInteract,
|
||||
onPlay,
|
||||
} = props;
|
||||
const embedId = resolveYoutubeEmbedId(embedIdProp, url);
|
||||
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
|
||||
const onPlayRef = React.useRef(onPlay);
|
||||
|
||||
React.useEffect(() => {
|
||||
onPlayRef.current = onPlay;
|
||||
}, [onPlay]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!embedId || !onPlayRef.current || !iframeRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let player: YoutubePlayer | null = null;
|
||||
let disposed = false;
|
||||
|
||||
loadYoutubeIframeApi()
|
||||
.then((YT) => {
|
||||
if (disposed || !iframeRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
player = new YT.Player(iframeRef.current, {
|
||||
events: {
|
||||
onStateChange: (event) => {
|
||||
if (event.data === YT.PlayerState.PLAYING) {
|
||||
onPlayRef.current?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// No-op: if API fails, videos still load via standard iframe behavior.
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
player?.destroy?.();
|
||||
};
|
||||
}, [embedId]);
|
||||
|
||||
if (!embedId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startAtParam =
|
||||
Number.isFinite(startAt) && startAt && startAt > 0
|
||||
? `&start=${Math.floor(startAt)}`
|
||||
: "";
|
||||
const enableJsApi = onPlay ? "&enablejsapi=1" : "";
|
||||
const src = `https://www.youtube-nocookie.com/embed/${embedId}?rel=0${startAtParam}${enableJsApi}`;
|
||||
const playerTitle = title || "YouTube video player";
|
||||
const isResponsive = responsive ?? true;
|
||||
const handleInteract = () => onInteract?.();
|
||||
|
||||
if (isResponsive) {
|
||||
return (
|
||||
<div className={className} onPointerDown={handleInteract}>
|
||||
<div className="relative w-full overflow-hidden rounded-xl bg-black pb-[56.25%]">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
className={`absolute left-0 top-0 h-full w-full ${iframeClassName || ""}`.trim()}
|
||||
src={src}
|
||||
title={playerTitle}
|
||||
loading="lazy"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div className={className} onPointerDown={handleInteract}>
|
||||
<iframe
|
||||
width={`${props.width ? props.width : 640}`}
|
||||
height={`${props.height ? props.height : 360}`}
|
||||
src={`https://youtu.be/embed/${props.embedId}`}
|
||||
ref={iframeRef}
|
||||
width={`${width ? width : 640}`}
|
||||
height={`${height ? height : 360}`}
|
||||
className={iframeClassName}
|
||||
src={src}
|
||||
title={playerTitle}
|
||||
loading="lazy"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||
|
||||
const config = [
|
||||
...nextCoreWebVitals,
|
||||
{
|
||||
// Monaco bundles are vendor artifacts, not authored source files.
|
||||
ignores: ["public/js/**"],
|
||||
},
|
||||
];
|
||||
|
||||
export default config;
|
||||
@@ -1,6 +1,3 @@
|
||||
const { spawnSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
@@ -11,18 +8,6 @@ const nextConfig = {
|
||||
experimental: {
|
||||
scrollRestoration: true,
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
if (isServer) {
|
||||
const result = spawnSync(process.execPath, [path.join(__dirname, 'prebuild/run-prebuild.js')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error('[prebuild] webpack hook failed');
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
Generated
+3716
-1029
File diff suppressed because it is too large.
Load diff
@@ -3,10 +3,10 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run prebuild && concurrently -k -n prebuild,dev \"npm run prebuild:watch -- --skip-initial\" \"next dev\"",
|
||||
"build": "npm run prebuild && next build",
|
||||
"dev": "npm run prebuild && concurrently -k -n prebuild,dev \"npm run prebuild:watch -- --skip-initial\" \"next dev --webpack\"",
|
||||
"build": "npm run prebuild && next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint .",
|
||||
"prebuild": "node ./prebuild/run-prebuild.js",
|
||||
"prebuild:watch": "node ./prebuild/watch.js",
|
||||
"offline": "npm run build && zip -r www.zip ./out",
|
||||
@@ -26,7 +26,8 @@
|
||||
"framer-motion": "^12.23.22",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lunr": "^2.3.9",
|
||||
"next": "^15.5.9",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "^16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
@@ -44,7 +45,7 @@
|
||||
"autoprefixer": "^10.4.21",
|
||||
"concurrently": "^9.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-next": "^15.5.4",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -2,7 +2,7 @@ import Document, { Head, Html, Main, NextScript } from "next/document";
|
||||
import React from "react";
|
||||
|
||||
// This generates the https: and wss: "connect-src" directives based on the above backends list so its a little easier to edit.
|
||||
const CSP = `default-src 'none'; script-src 'self' 'unsafe-eval'; img-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-src https://youtu.be; connect-src 'self'`;
|
||||
const CSP = `default-src 'none'; script-src 'self' 'unsafe-eval' 'sha256-HSN8D632WcMJaQDjNrIwVtSeQLAfKAVZRSyJiv6bjQw=' https://www.youtube.com https://s.ytimg.com; img-src 'self' data: https://i.ytimg.com https://*.ytimg.com; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-src https://youtu.be https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com; connect-src 'self'`;
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: any) {
|
||||
@@ -15,7 +15,7 @@ class MyDocument extends Document {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en" suppressHydrationWarning>
|
||||
<Html lang="en" className="dark" suppressHydrationWarning>
|
||||
<Head>
|
||||
<meta httpEquiv="Content-Security-Policy" content={CSP} />
|
||||
</Head>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import MarkdownViewer from "@/components/markdown";
|
||||
import LoadingState from "@/components/loading-state";
|
||||
import { Docs } from "@/util/docs";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
|
||||
@@ -34,14 +35,17 @@ const DocsIndexPage: NextPage = () => {
|
||||
});
|
||||
|
||||
const params = useSearchParams();
|
||||
const [name, setName] = React.useState("");
|
||||
const [markdown, setMarkdown] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
const _name = params.get("name");
|
||||
setName(_name || "");
|
||||
setMarkdown(docs?.docs.find((doc) => doc.name === _name)?.content || "");
|
||||
}, [params, docs]);
|
||||
const name = params.get("name") || "";
|
||||
const markdown = React.useMemo(() => {
|
||||
return docs?.docs.find((doc) => doc.name === name)?.content || "";
|
||||
}, [docs, name]);
|
||||
const hasNameQueryInPath = React.useMemo(() => {
|
||||
const query = router.asPath.split("?")[1];
|
||||
if (!query) {
|
||||
return false;
|
||||
}
|
||||
return new URLSearchParams(query).has("name");
|
||||
}, [router.asPath]);
|
||||
|
||||
const [filterValue, setFilterValue] = React.useState("");
|
||||
const fuse = React.useMemo(() => {
|
||||
@@ -79,8 +83,8 @@ const DocsIndexPage: NextPage = () => {
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
if (isLoading || !docs) {
|
||||
return <div>Loading...</div>;
|
||||
if (isLoading || !docs || (hasNameQueryInPath && !name)) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -127,7 +131,7 @@ const DocsIndexPage: NextPage = () => {
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
|
||||
<aside className="hidden lg:block lg:col-span-3">
|
||||
<div className="sticky top-24 ml-4 flex flex-col gap-3">
|
||||
<div className="sticky top-16 ml-4 flex flex-col gap-3">
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="Filter..."
|
||||
@@ -171,7 +175,7 @@ const DocsIndexPage: NextPage = () => {
|
||||
<Divider />
|
||||
<CardBody>
|
||||
<MarkdownViewer
|
||||
key={name || `${Math.random()}`}
|
||||
key={name}
|
||||
markdown={markdown || ""}
|
||||
/>
|
||||
</CardBody>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
You'll want to compile from a MacOS or Linux machine. Compiling from native Windows is possible in theory, but none of the asset scripts are designed to run on Windows. You can cross-compile the Windows server/client binaries from a better operating system like Linux or MacOS. If you only have a Windows machine, see "Windows Builds" below (TL;DR: use WSL).
|
||||
|
||||
# Sliver v1.6.x
|
||||
You'll want to compile Sliver from a MacOS or Linux machine. Compiling from native Windows is possible in theory, but none of the scripts are designed to run on Windows. You can cross-compile the Windows server/client binaries from a better operating system like Linux or MacOS. If you only have a Windows machine, you should be able to use the Windows Subsystem for Linux (WSL).
|
||||
|
||||
To compile from source you'll need:
|
||||
|
||||
@@ -62,7 +60,7 @@ The Docker build includes mingw and Metasploit, so it can take a while to build
|
||||
|
||||
### Windows Builds
|
||||
|
||||
If all you have is a Windows machine, the easiest way to build Sliver is using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) and following the Linux/cross-compile instructions above. To cross-compile a native Windows binary use `make windows-amd64` and copy it to your Windows file system (i.e. `/mnt/c/Users/foo/Desktop`) and run it using a terminal that supports ANSI sequences such as the [Windows Terminal](https://github.com/microsoft/terminal).
|
||||
If all you have is a Windows machine, the easiest way to build Sliver is using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) and following the Linux/cross-compile instructions above. To cross-compile a Windows binary use `make windows-amd64` and copy it to your Windows file system (i.e. `/mnt/c/Users/foo/Desktop`) and, you will need to run it using a terminal that supports ANSI sequences such as the [Windows Terminal](https://github.com/microsoft/terminal).
|
||||
|
||||
# Developers
|
||||
|
||||
|
||||
@@ -1,94 +1,131 @@
|
||||
### General Server Configuration
|
||||
|
||||
The Sliver server has a configuration file located in the `configs` sub-directory of the [`SLIVER_ROOT_DIR`](/docs?name=Environment+Variables); by default this is `~/.sliver/configs/server.json`. If no configuration file exists, a default configuration will be generated and written to disk on startup. The default configuration is shown below:
|
||||
The Sliver server configuration file is located in the `configs` sub-directory of [`SLIVER_ROOT_DIR`](/docs?name=Environment+Variables); by default this is `~/.sliver/configs/server.yaml`.
|
||||
|
||||
If no configuration file exists, a default YAML configuration is generated and written to disk on startup. If a legacy `server.json` file exists, Sliver reads it, writes `server.yaml`, and renames the old file to `.server.json-old`.
|
||||
|
||||
#### Default Server Config
|
||||
|
||||
```json
|
||||
{
|
||||
"daemon_mode": false,
|
||||
"daemon": {
|
||||
"host": "",
|
||||
"port": 31337
|
||||
},
|
||||
"logs": {
|
||||
"level": 4,
|
||||
"grpc_unary_payloads": false,
|
||||
"grpc_stream_payloads": false
|
||||
},
|
||||
"grpc": {
|
||||
"keepalive": {
|
||||
"min_time_seconds": 30,
|
||||
"permit_without_stream": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```yaml
|
||||
daemon_mode: false
|
||||
daemon:
|
||||
host: ""
|
||||
port: 31337
|
||||
tailscale: false
|
||||
logs:
|
||||
level: 4
|
||||
grpc_unary_payloads: false
|
||||
grpc_stream_payloads: false
|
||||
tls_key_logger: false
|
||||
grpc:
|
||||
keepalive:
|
||||
min_time_seconds: 30
|
||||
permit_without_stream: true
|
||||
watch_tower: null
|
||||
go_proxy: ""
|
||||
http_default:
|
||||
headers:
|
||||
- method: GET
|
||||
name: Cache-Control
|
||||
value: no-store, no-cache, must-revalidate
|
||||
probability: 100
|
||||
notifications:
|
||||
enabled: false
|
||||
services: {}
|
||||
cc: {}
|
||||
cxx: {}
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
- `daemon_mode` - Enable [daemon mode](/docs?name=Daemon+Mode)
|
||||
- `daemon` - An object containing options related to `daemon_mode`, these values are only used when `daemon_mode` is set to `true`.
|
||||
- `host` - What network interface to bind the `daemon_mode` client listener to. By default this is an empty string, which indicates binding to all interfaces.
|
||||
- `port` - TCP port to bind the `daemon_mode` client listener to.
|
||||
- `logs` - An object containing configuration options for server logs
|
||||
- `level` - The `logrus` logging level (1 - 5), by default this is set to `4`, which is the `INFO` level; increasing this value increases the verbosity of the logs e.g. `DEBUG` is `5` and `SILENT` is `1`
|
||||
- `grpc_unary_payloads` - Log gRPC unary payloads
|
||||
- `grpc_stream_payloads` - Log gRPC streaming payloads
|
||||
- `grpc` - An object containing options for the server gRPC stack
|
||||
- `keepalive` - gRPC keepalive enforcement configuration
|
||||
- `min_time_seconds` - Minimum amount of time (in seconds) between client pings before the server sends a GOAWAY with `too_many_pings`
|
||||
- `permit_without_stream` - Allow client pings when there are no active streams
|
||||
- `daemon_mode` - Enable [daemon mode](/docs?name=Daemon+Mode).
|
||||
- `daemon` - Options for daemon mode.
|
||||
- `host` - Interface to bind the daemon listener to. Empty string means all interfaces.
|
||||
- `port` - TCP port for the daemon listener.
|
||||
- `tailscale` - Enable Tailscale for daemon listener setup.
|
||||
- `logs` - Server logging options.
|
||||
- `level` - `logrus` level (`0`-`6`, clamped by Sliver). `4` is `INFO`, `5` is `DEBUG`, `6` is `TRACE`.
|
||||
- `grpc_unary_payloads` - Log gRPC unary payloads.
|
||||
- `grpc_stream_payloads` - Log gRPC streaming payloads.
|
||||
- `tls_key_logger` - Enable TLS key logging (for debugging/traffic analysis only; sensitive).
|
||||
- `grpc.keepalive` - gRPC keepalive enforcement configuration.
|
||||
- `min_time_seconds` - Minimum time (seconds) between client pings before sending GOAWAY (`too_many_pings`).
|
||||
- `permit_without_stream` - Allow client pings when there are no active streams.
|
||||
- `watch_tower` - Optional API keys for Watchtower integrations.
|
||||
- `vt_api_key` - VirusTotal API key.
|
||||
- `xforce_api_key` - IBM X-Force API key.
|
||||
- `xforce_api_password` - IBM X-Force API password.
|
||||
- `go_proxy` - Optional `GOPROXY` override used by server-side builds.
|
||||
- `http_default.headers` - Default HTTP headers for anonymous server responses.
|
||||
- `method` - HTTP method to apply to (for example `GET`).
|
||||
- `name` - Header name.
|
||||
- `value` - Header value.
|
||||
- `probability` - Percent chance (0-100) that the header is added.
|
||||
- `notifications` - Server notification config. See [Notifications](/docs?name=Notifications) for the full schema.
|
||||
- `cc` - Optional compiler overrides map (`GOOS/GOARCH` -> C compiler path).
|
||||
- `cxx` - Optional compiler overrides map (`GOOS/GOARCH` -> C++ compiler path).
|
||||
|
||||
## Database Configuration
|
||||
|
||||
Sliver supports SQL database configurations. By default this configuration file is located at `~/.sliver/configs/database.json`:
|
||||
Sliver supports SQL database configurations in `~/.sliver/configs/database.yaml`.
|
||||
|
||||
If no configuration file exists, a default YAML configuration is generated and written to disk on startup. If a legacy `database.json` exists, Sliver reads it, writes `database.yaml`, and renames the old file to `.database.json-old`.
|
||||
|
||||
#### Default Database Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"dialect": "sqlite3",
|
||||
"database": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"host": "",
|
||||
"port": 0,
|
||||
"params": null,
|
||||
"max_idle_conns": 10,
|
||||
"max_open_conns": 100,
|
||||
"log_level": "warn"
|
||||
}
|
||||
```yaml
|
||||
dialect: sqlite3
|
||||
database: ""
|
||||
username: ""
|
||||
password: ""
|
||||
host: ""
|
||||
port: 0
|
||||
params:
|
||||
cache: shared
|
||||
pragmas:
|
||||
busy_timeout: "5000"
|
||||
journal_mode: WAL
|
||||
synchronous: NORMAL
|
||||
temp_store: MEMORY
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
log_level: warn
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
- `dialect` - Defines the type of database to connect to, Sliver supports `mysql`, `postgresql`, and `sqlite3`. By default an internal version of `sqlite3` is used, which is saved to `~/.sliver/sliver.db` by default.
|
||||
- `database` - The name of the database to connect to, note that this option is ignored when using `sqlite3`.
|
||||
- `dialect` - Database backend (`mysql`, `postgresql`, or `sqlite3`).
|
||||
- `database` - Database name (ignored for `sqlite3`).
|
||||
- `username` - Database username.
|
||||
- `password` - Database password.
|
||||
- `host` - Database host.
|
||||
- `port` - Database TCP port.
|
||||
- `params` - Is a key/value string map that can be used to pass dialect-specific options to the DSN/connection string. This can be used, for example, to enable SSL/TLS on certain database connections.
|
||||
- `max_idle_conns` - Max idle connections in the database connection pool.
|
||||
- `max_open_conns` - Max open connections in the database connection pool.
|
||||
- `log_level` - Configures the log level for database logging, can be set to: `silent`, `error`, `warn`, or `info`. Default level is `warn`, note that setting this option to `info` may result in sensitive information being logged to disk!
|
||||
- `params` - Key/value map of dialect-specific DSN parameters.
|
||||
- `pragmas` - SQLite pragma map. Used when `dialect` is `sqlite3` and `params` does not already set `_pragma`.
|
||||
- `max_idle_conns` - Max idle DB connections (minimum enforced value is `1`).
|
||||
- `max_open_conns` - Max open DB connections (minimum enforced value is `1`).
|
||||
- `log_level` - GORM DB logging level (`silent`, `error`, `warn`, `info`).
|
||||
|
||||
With the default SQLite configuration, Sliver stores the DB at `~/.sliver/sliver.db`.
|
||||
|
||||
# Client Configuration
|
||||
|
||||
### Operator
|
||||
|
||||
Client configurations must be generated by the server (they contain per-user key pairs). For more details see [multiplayer mode](/docs?name=Multi-player+Mode), and example configuration is shown below. Client configuration files can be copied into `~/.sliver-client/configs/`:
|
||||
Operator client configs are generated by the server and should be copied into `~/.sliver-client/configs/`. These files are currently JSON (`*.cfg`), not YAML.
|
||||
|
||||
For details on generating them, see [multiplayer mode](/docs?name=Multi-player+Mode).
|
||||
|
||||
```json
|
||||
{
|
||||
"operator": "moloch",
|
||||
"operator": "alice",
|
||||
"token": "<operator-auth-token>",
|
||||
"lhost": "localhost",
|
||||
"lport": 31337,
|
||||
"ca_certificate": "-----BEGIN CERTIFICATE-----\nMIIBoTCCASegAwIBAgIRAPhwFTptJKJM4Bef71nA29YwCgYIKoZIzj0EAwMwCzEJ\nMAcGA1UEChMAMB4XDTIwMDExNDIyNTY1NloXDTIzMDExMzIyNTY1NlowCzEJMAcG\nA1UEChMAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmvGJZ8zDqLxge2PfhK1QcdOi\n6kEV8vgz/S1fSiU9h21JDVaX+FFhG2cYIzr1Q3zE2Ml+pUdnwUSk24pFFTUhjlsO\n4H2YQC/W46DXSa8VQdvpfDaTPYLzXuBhsdVcn2rdo08wTTAOBgNVHQ8BAf8EBAMC\nAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMB\nAf8wCwYDVR0RBAQwAoIAMAoGCCqGSM49BAMDA2gAMGUCMQDOj0sVrVoJSkN4qiqn\nS9wMFNeVOZ+5TVZOOpTn19nc4C/wq9jwdYRHW1dlmvWu2LMCMCm2y1TkaWLa/i4t\nArTCIiCmNVRxAR1xFsWama9yv7wFY0+5xKS7W944418v0jYJKA==\n-----END CERTIFICATE-----\n",
|
||||
"private_key": "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDBido1KtKSwQah/WIoGkDZDX2WPXdexUVAmi0tf6Pd9vK5pfpt2II+n\n8BqkV0ifyjugBwYFK4EEACKhZANiAATHATh7e8uX/MN5mokguQf4ywXgTaihYD//\nyraEUcZAYsrCtDHmdAH356GPrxlPSpwyWhFV3fIQJKI2Hf/8/mAd3wqCGQQzq5Mt\ncM0B36+vP9wOGsTI/tc32+0glRZoxo8=\n-----END EC PRIVATE KEY-----\n",
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\nMIIBiTCCAQ+gAwIBAgIQZLa8QUXWDnuht3G9+g9yKTAKBggqhkjOPQQDAzALMQkw\nBwYDVQQKEwAwHhcNMTkwNzIyMjE1MzAzWhcNMjIwNzIxMjE1MzAzWjAcMQkwBwYD\nVQQKEwAxDzANBgNVBAMTBm1vbG9jaDB2MBAGByqGSM49AgEGBSuBBAAiA2IABMcB\nOHt7y5f8w3maiSC5B/jLBeBNqKFgP//KtoRRxkBiysK0MeZ0AffnoY+vGU9KnDJa\nEVXd8hAkojYd//z+YB3fCoIZBDOrky1wzQHfr68/3A4axMj+1zfb7SCVFmjGj6Mn\nMCUwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAoGCCqGSM49\nBAMDA2gAMGUCMH0hbeghZfMkhp9Wf2a7eqj8IruwKH6vQql/6nxQ8aunfCUUxWo8\nOrxxaqp+bGCCCQIxAK5Icqww8m9llhprfzENaFZUCkpyvBMm9EB8B69SBFBHcVFm\n7bqHjFFMH39g2JCy9Q==\n-----END CERTIFICATE-----\n"
|
||||
"ca_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
|
||||
"private_key": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n",
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `operator` field in this configuration file only modifies the value displayed by the client when selecting configurations, modifying it will not change what name the server uses to identify the user. The server determines the user based on the `CommonName` in the certificate. If you want to change the user name you need to regenerate the client configuration with the new name.
|
||||
**Note:** The `operator` field is mostly a local label in the client. Server-side identity is derived from the client certificate (`CommonName`) and token material generated by the server. If you need to rename an operator, generate a new client configuration.
|
||||
@@ -1,14 +1,14 @@
|
||||
**NOTE:** Any platform can cross-compile a standalone executable to any other platform out of the box; you primarily need cross-compilers when using CGO-based formats like `--format shared`, and for `--format shellcode` on macOS/Linux.
|
||||
Any platform can cross-compile a standalone executable to any other platform out of the box; you primarily need cross-compilers when using CGO-based formats like `--format shared`, and for `--format shellcode` on macOS/Linux.
|
||||
|
||||
Sliver can tell you which platforms it can likely target based on the server's platform and available cross-compilers by running the `generate info` command in the console.
|
||||
|
||||
Sliver supports [External Builders](/docs?name=External+Builders), which can be used to easily cross-compile implants.
|
||||
|
||||
## From Linux to MacOS/Windows
|
||||
## From Linux to MacOS
|
||||
|
||||
Sliver embeds a Zig cross-compiler out of the box, so Windows shared library and shellcode implants from Linux do not require mingw-w64.
|
||||
|
||||
To compile MacOS shared library implants from Linux, we recommend using https://github.com/tpoechtrager/osxcross by default Sliver will look in `/opt/osxcross` but you can override this via [environment variables](/docs?name=Environment+Variables). If you do not have a MacOS-based machine you can use GitHub Actions' MacOS instances to build OSXCross.
|
||||
To compile MacOS shared library or shellcode payloads from Linux, we recommend using [OSX Cross](https://github.com/tpoechtrager/osxcross) by default Sliver will look in `/opt/osxcross` but you can override this via [environment variables](/docs?name=Environment+Variables). If you do not have a MacOS-based machine you can use GitHub Actions' MacOS instances to build OSXCross.
|
||||
|
||||
**NOTE:** Sliver expects the root of the osxcross git repo to be located at `/opt/osxcross` and the actual binaries in `/opt/osxcross/target/bin`.
|
||||
|
||||
|
||||
@@ -1,34 +1,53 @@
|
||||
Sliver supports running in "daemon mode," which automatically starts a client listener (but not an interactive console). In order to connect to a server running in daemon mode you'll need to use [multiplayer mode](/docs?name=Multi-player+Mode).
|
||||
Sliver supports running in daemon mode, which automatically starts the multiplayer client listener (but not an interactive console). To connect to a daemonized server, use [multiplayer mode](/docs?name=Multi-player+Mode) operator configs.
|
||||
|
||||
There are two ways to start the server in daemon mode:
|
||||
There are two ways to run in daemon mode:
|
||||
|
||||
1. Using the command line interface: `sliver-server daemon`
|
||||
2. Set `daemon_mode` to `true` in the `configs/server.json` located in `SLIVER_ROOT_DIR` (by default: `~/.sliver/configs/server.json`). The listener can be configured via the `daemon` object. The process will respond gracefully to `SIGTERM` on Unix-like operating systems.
|
||||
1. Start daemon directly from CLI: `sliver-server daemon`
|
||||
2. Set `daemon_mode: true` in `~/.sliver/configs/server.yaml`, then start normally with `sliver-server`
|
||||
|
||||
#### Example Config
|
||||
If `server.yaml` does not exist, Sliver generates it. If a legacy `server.json` exists, Sliver migrates it to YAML.
|
||||
|
||||
```
|
||||
$ cat ~/.sliver/configs/server.json
|
||||
{
|
||||
"daemon_mode": true,
|
||||
"daemon": {
|
||||
"host": "",
|
||||
"port": 31337
|
||||
},
|
||||
"logs": {
|
||||
"level": 5,
|
||||
"grpc_unary_payloads": false,
|
||||
"grpc_stream_payloads": false
|
||||
},
|
||||
"grpc": {
|
||||
"keepalive": {
|
||||
"min_time_seconds": 30,
|
||||
"permit_without_stream": true
|
||||
}
|
||||
}
|
||||
}
|
||||
### CLI Behavior
|
||||
|
||||
- `sliver-server daemon` supports:
|
||||
- `-l, --lhost` multiplayer listener host
|
||||
- `-p, --lport` multiplayer listener port
|
||||
- `-t, --tailscale` enable Tailscale listener
|
||||
- `-f, --force` force unpack static assets
|
||||
- For `sliver-server daemon`, `--lhost` and `--lport` override config values. If omitted, Sliver uses `daemon.host` and `daemon.port` from `server.yaml`.
|
||||
- For normal startup (`sliver-server`) with `daemon_mode: true`, Sliver uses `daemon.tailscale` from `server.yaml`.
|
||||
|
||||
### Example Config
|
||||
|
||||
```yaml
|
||||
daemon_mode: true
|
||||
daemon:
|
||||
host: ""
|
||||
port: 31337
|
||||
tailscale: false
|
||||
logs:
|
||||
level: 4
|
||||
grpc_unary_payloads: false
|
||||
grpc_stream_payloads: false
|
||||
tls_key_logger: false
|
||||
grpc:
|
||||
keepalive:
|
||||
min_time_seconds: 30
|
||||
permit_without_stream: true
|
||||
```
|
||||
|
||||
#### systemd
|
||||
### Generating Operator Configs Without Console
|
||||
|
||||
With this config you can easily set up a [systemd service](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) or init script. See the [Linux install script](/docs?name=Linux+Install+Script) for an example.
|
||||
Since daemon mode does not provide an interactive server console, generate operator configs using the server CLI:
|
||||
|
||||
```bash
|
||||
./sliver-server operator --name zer0cool --lhost 1.2.3.4 --permissions all --save zer0cool.cfg
|
||||
```
|
||||
|
||||
### Shutdown Behavior
|
||||
|
||||
On Unix-like systems, daemon mode handles `SIGTERM` and exits cleanly.
|
||||
|
||||
### systemd
|
||||
|
||||
With daemon mode enabled in `server.yaml`, you can run Sliver under [systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) or another init system. See the [Linux install script](/docs?name=Linux+Install+Script) for an example.
|
||||
@@ -2,22 +2,13 @@ Sliver supports "external builders," which allow a Sliver server to offload impl
|
||||
|
||||
External builders can also be used to create custom modifications to the implant source code, or potentially replace the default Sliver implant entirely.
|
||||
|
||||
```
|
||||
MacOS .dylib Implant Builds
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────────┐ ┌─────┴─────┐
|
||||
│ MacOS │ Multiplayer │ Linux │
|
||||
│ Builder ├────────────────────────►│ Server │
|
||||
│ │ │ │
|
||||
└───────────┘ └───────────┘
|
||||
▲
|
||||
┌───────────┐ │
|
||||
│Windows │ Multiplayer │
|
||||
│ Operator ├─────────────────────────────┘
|
||||
│ │
|
||||
└───────────┘
|
||||
```mermaid:minh=340
|
||||
flowchart LR
|
||||
subgraph "External MacOS Builder"
|
||||
builder["MacOS Builder"] -- "Multiplayer" --> server["Linux Server"]
|
||||
operator["Windows Operator"] -- "Multiplayer" --> server
|
||||
server -- ".dylib build task" --> builder
|
||||
end
|
||||
```
|
||||
|
||||
## Builders
|
||||
@@ -32,8 +23,6 @@ Any `sliver-server` binary can be started as a builder process using [operator c
|
||||
|
||||
When started as a builder, the Sliver process will mirror log output to stdout by default, however this can be disabled (see `sliver-server builder --help`).
|
||||
|
||||
**⚠️ IMPORTANT:** Make sure the builder and server have identical `http-c2.json` configuration files to avoid incompatibility problems.
|
||||
|
||||
**⚠️ IMPORTANT:** Builders must have unique names, by default the builder's hostname will be used, but this can be changed using the `--name` cli flag.
|
||||
|
||||
#### External Builds
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
This page summarizes implant payload format compatibility by target OS/architecture (`GOOS/GOARCH`).
|
||||
|
||||
## Common Platforms
|
||||
|
||||
| Target OS/Arch | Executable | Shared Library | Shellcode | Service |
|
||||
| --------------- | ---------- | -------------- | --------- | ------- |
|
||||
| `windows/386` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `windows/amd64` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `windows/arm64` | ⚠️ | ⚠️ | ❌ | ⚠️ |
|
||||
| `linux/386` | ✅ | ✅ | ❌ | N/A |
|
||||
| `linux/amd64` | ✅ | ✅ | ✅ | N/A |
|
||||
| `linux/arm64` | ✅ | ✅ | ✅ | N/A |
|
||||
| `darwin/amd64` | ✅ | ✅ | ❌ | N/A |
|
||||
| `darwin/arm64` | ✅ | ✅ | ✅ | N/A |
|
||||
|
||||
### Important Notes
|
||||
|
||||
- `✅` = first-class support in Sliver's built-in target matrix.
|
||||
- `⚠️` = generic/experimental target (not first-class; may fail depending on toolchain/target).
|
||||
- `❌` = not currently supported for that payload format.
|
||||
- `Service` is a Windows-only format.
|
||||
- `Shellcode` is currently supported for `windows/{386,amd64}`, `linux/{amd64,arm64}`, and `darwin/arm64`.
|
||||
- `Shellcode` and `Shared Library` for MacOS may require a [cross-compiler](/docs?name=Cross-compiling+Implants) or an [external builder](/docs?name=External+Builders) depending on your platform.
|
||||
- On macOS hosts, targeting `linux/386` for shared library/shellcode builds is currently not reliable.
|
||||
|
||||
Use `generate info` in the Sliver console to see what your current server can build with its configured toolchains.
|
||||
|
||||
## All GOOS/GOARCH Targets
|
||||
|
||||
The table below covers all `GOOS/GOARCH` entries from `go tool dist list` (Go 1.24+), including common platforms:
|
||||
|
||||
| Target GOOS/GOARCH | Common Platform | Executable | Shared Library | Shellcode | Service |
|
||||
| ------------------ | --------------- | ---------- | -------------- | --------- | ------- |
|
||||
| `aix/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `android/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `android/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `android/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `android/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `darwin/amd64` | Yes | ✅ | ✅ | ❌ | N/A |
|
||||
| `darwin/arm64` | Yes | ✅ | ✅ | ✅ | N/A |
|
||||
| `dragonfly/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `illumos/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `ios/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `ios/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `js/wasm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/386` | Yes | ✅ | ✅ | ❌ | N/A |
|
||||
| `linux/amd64` | Yes | ✅ | ✅ | ✅ | N/A |
|
||||
| `linux/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/arm64` | Yes | ✅ | ✅ | ✅ | N/A |
|
||||
| `linux/loong64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/mips` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/mips64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/mips64le` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/mipsle` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/ppc64le` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/s390x` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `netbsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `netbsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `netbsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `netbsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `plan9/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `plan9/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `plan9/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `solaris/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `wasip1/wasm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `windows/386` | Yes | ✅ | ✅ | ✅ | ✅ |
|
||||
| `windows/amd64` | Yes | ✅ | ✅ | ✅ | ✅ |
|
||||
| `windows/arm64` | Yes | ⚠️ | ⚠️ | ❌ | ⚠️ |
|
||||
@@ -0,0 +1,133 @@
|
||||
### Console Themes
|
||||
|
||||
Sliver’s interactive consoles (both `sliver-client` and the server-only console) use a theme file to drive all lipgloss styling.
|
||||
|
||||
- Theme path: `~/.sliver-client/theme.yaml`
|
||||
- Settings path: `~/.sliver-client/tui-settings.yaml`
|
||||
|
||||
If `theme.yaml` is missing, Sliver will create it with the default palette.
|
||||
|
||||
#### Palette Structure
|
||||
|
||||
`theme.yaml` defines six palettes:
|
||||
|
||||
- `primary`
|
||||
- `secondary`
|
||||
- `default`
|
||||
- `success`
|
||||
- `warning`
|
||||
- `danger`
|
||||
|
||||
Each palette has a base `default` color and a set of modifier colors (`50..900`):
|
||||
|
||||
```yaml
|
||||
primary:
|
||||
default: "#006FEE"
|
||||
mods:
|
||||
50: "#001731"
|
||||
100: "#002e62"
|
||||
200: "#004493"
|
||||
300: "#005bc4"
|
||||
400: "#006FEE"
|
||||
500: "#338ef7"
|
||||
600: "#66aaf9"
|
||||
700: "#99c7fb"
|
||||
800: "#cce3fd"
|
||||
900: "#e6f1fe"
|
||||
```
|
||||
|
||||
### Prompt Settings
|
||||
|
||||
The console prompt is configurable via `tui-settings.yaml`:
|
||||
|
||||
```yaml
|
||||
prompt: "host"
|
||||
prompt_template: |
|
||||
{{- if .IsServer -}}
|
||||
{{ .Styles.Bold.Render "[server]" }} {{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} >
|
||||
{{- else -}}
|
||||
{{- if .Host -}}{{ .Styles.BoldPrimary.Render (printf "[%s]" .Host) }} {{- end -}}
|
||||
{{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} >
|
||||
{{- end -}}
|
||||
```
|
||||
|
||||
#### `prompt` Modes
|
||||
|
||||
- `operator-host`
|
||||
- `sliver-client`: `[operator@host] sliver >`
|
||||
- server console: `[server] sliver >`
|
||||
- `host`
|
||||
- `sliver-client`: `[host] sliver >`
|
||||
- server console: `[server] sliver >`
|
||||
- `basic`
|
||||
- both: `sliver >`
|
||||
- `custom`
|
||||
- the entire prompt text is rendered from `prompt_template` (Go `text/template`)
|
||||
- if rendering fails, Sliver falls back to a minimal prompt: ` > `
|
||||
|
||||
### Custom Prompt Templates
|
||||
|
||||
When `prompt: custom`, Sliver evaluates `prompt_template` as a Go `text/template` every time the prompt is displayed.
|
||||
|
||||
#### Template Variables
|
||||
|
||||
Top-level values:
|
||||
|
||||
- `.IsServer` (bool)
|
||||
- `.Connected` (bool)
|
||||
- `.Operator` (string)
|
||||
- `.Host` (string)
|
||||
- `.Port` (int)
|
||||
- `.HostPort` (string, `host:port` when available)
|
||||
- `.Now` (time)
|
||||
|
||||
Active target:
|
||||
|
||||
- `.Target.SessionName` (string)
|
||||
- `.Target.BeaconName` (string)
|
||||
- `.Target.Suffix` (string)
|
||||
- pre-rendered themed suffix matching the default prompt, like ` (my-session)` when a session or beacon is selected
|
||||
|
||||
Theme colors:
|
||||
|
||||
- `.Colors.Primary.Default`
|
||||
- `.Colors.Primary.Mods` (map; use `index`, e.g. `index .Colors.Primary.Mods 500`)
|
||||
- same structure for `.Colors.Secondary`, `.Colors.Default`, `.Colors.Success`, `.Colors.Warning`, `.Colors.Danger`
|
||||
|
||||
Theme styles (lipgloss styles already wired to `theme.yaml`):
|
||||
|
||||
- `.Styles.Bold`, `.Styles.Underline`
|
||||
- `.Styles.Primary`, `.Styles.Secondary`, `.Styles.Default`, `.Styles.Success`, `.Styles.Warning`, `.Styles.Danger`
|
||||
- `.Styles.BoldPrimary`, `.Styles.BoldSecondary`, `.Styles.BoldDefault`, `.Styles.BoldSuccess`, `.Styles.BoldWarning`, `.Styles.BoldDanger`
|
||||
- legacy names: `.Styles.Red`, `.Styles.Green`, `.Styles.Orange`, `.Styles.Blue`, `.Styles.Purple`, `.Styles.Cyan`, `.Styles.Gray`, `.Styles.Black`
|
||||
|
||||
#### Examples
|
||||
|
||||
Show user and host (client), show server tag (server console):
|
||||
|
||||
```gotemplate
|
||||
{{- if .IsServer -}}
|
||||
{{ .Styles.Bold.Render "[server]" }} {{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} >
|
||||
{{- else -}}
|
||||
{{- if and .Operator .Host -}}
|
||||
{{ .Styles.BoldPrimary.Render (printf "[%s@%s]" .Operator .Host) }}
|
||||
{{- else if .Host -}}
|
||||
{{ .Styles.BoldPrimary.Render (printf "[%s]" .Host) }}
|
||||
{{- end -}}
|
||||
{{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} >
|
||||
{{- end -}}
|
||||
```
|
||||
|
||||
Basic prompt everywhere:
|
||||
|
||||
```gotemplate
|
||||
{{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} >
|
||||
```
|
||||
|
||||
Use a lighter primary modifier for the host tag:
|
||||
|
||||
```gotemplate
|
||||
{{- $c := index .Colors.Primary.Mods 500 -}}
|
||||
{{- if .Host -}}{{ (.Styles.Bold.Foreground $c).Render (printf "[%s]" .Host) }} {{- end -}}
|
||||
{{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} >
|
||||
```
|
||||
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user