Compare commits

..
Author SHA1 Message Date
moloch-- 82d979436b Add support for additional wasm-donute options in windows execute-shellcode 2026-03-01 22:35:37 -08:00
Joe d2632e75bd Merge pull request #2199 from BishopFox/feature/go-tests2
Refactor unit tests to not write to main db
2026-03-01 09:47:23 -07:00
moloch-- a55831fb44 Go v1.25.7 2026-02-28 22:39:46 -08:00
moloch-- 39d4edec43 Tweak go-tests.sh 2026-02-28 22:08:59 -08:00
moloch-- bbdb08e7a4 Refactor go-tests.sh 2026-02-28 14:12:37 -08:00
moloch-- 39b488bd25 Refactor unit tests to not write to main db 2026-02-28 13:11:14 -08:00
Joe e7b8a24534 Merge pull request #2198 from BishopFox/feature/handler-recover
Feature/handler recover
2026-02-28 11:39:00 -07:00
moloch-- 17b9d611f8 Add panic/recover to mtls, dns, and wg handlers 2026-02-28 09:56:48 -08:00
moloch-- ed9075d065 Add better nil checks to various pb handlers 2026-02-28 09:52:40 -08:00
Joe 1f7221933d Merge pull request #2194 from BishopFox/crush/v2
Upgrade crush libs to v2
2026-02-25 09:22:24 -07:00
moloch-- 6333e0691a Upgrade crush libs to v2 2026-02-24 17:40:13 -08:00
moloch-- 3bbaf80510 v1.7.3 2026-02-23 20:49:28 -08:00
moloch-- 25726d8105 Revert wasm-donut to v0.0.3 2026-02-23 20:48:53 -08:00
moloch-- 67e9a7f37f Update wasm-donut to v0.0.6 2026-02-23 19:33:26 -08:00
moloch-- a5b6935da6 Tweak AGENTS.md 2026-02-23 07:10:28 -08:00
moloch-- 2e36f45bdf v1.7.2 2026-02-22 17:33:08 -08:00
Joe 3e06a9d5ee Merge pull request #2191 from BishopFox/feature/file-tab-completer
Feature/file tab completer
2026-02-22 18:32:27 -07:00
moloch-- 952b2df49d Update beignet 2026-02-22 16:42:32 -08:00
moloch-- 9769f1bb57 Fix mod vendor 2026-02-22 16:41:16 -08:00
moloch-- f5f2c80ad3 Update malasada to v0.0.3 2026-02-22 16:38:41 -08:00
moloch-- bb2edf63cf Add file tab completer to more commands 2026-02-22 16:37:30 -08:00
moloch-- cd711afe8a Add tab completer to generate --save 2026-02-22 16:26:32 -08:00
Joe f9bbbb34bb Merge pull request #2190 from BishopFox/skoveit-feature/PE-Metadata-Spoofing
Skoveit feature/pe metadata spoofing
2026-02-21 23:29:33 -07:00
moloch-- 64b91c56c0 Fix unit tests 2026-02-21 15:14:26 -08:00
moloch-- a6c7867a26 Fix unit tests 2026-02-21 14:22:18 -08:00
moloch-- 7f25c3bb06 Expand pe metadata spoofing feature 2026-02-21 12:36:47 -08:00
moloch-- 153cf29551 Fix unit tests 2026-02-21 06:30:01 -08:00
jjensn b85161e797 Declare vars before use 2026-02-21 06:29:59 -08:00
jjensn ed906fcffe typo: Add yamux client constructor 2026-02-21 06:29:58 -08:00
jjensn 3bbcf5697a Yamux logging for debug implants 2026-02-21 06:29:56 -08:00
moloch-- 4a425d2fd6 Fix 'websites rm' add tab completer issue #2185 2026-02-21 06:29:55 -08:00
Adam d0f752e118 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-21 06:29:54 -08:00
Adam 7167621577 Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-21 06:29:47 -08:00
Joe c3582fc203 Merge pull request #2189 from BishopFox/jjensn-fix/yamux-disable-logging
Jjensn fix/yamux disable logging
2026-02-19 22:29:46 -07:00
moloch-- 07c95b203f Fix unit tests 2026-02-19 18:05:57 -08:00
moloch-- 958e44deef Fix 'websites rm' add tab completer issue #2185 2026-02-19 17:54:55 -08:00
Adam 836395a2d6 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-19 17:54:53 -08:00
Adam 1a7a1c9b2d Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-19 17:54:27 -08:00
Joe 4ba285796f Merge pull request #2188 from BishopFox/fix/websites-rm
Fix 'websites rm' add tab completer issue #2185
2026-02-19 18:51:41 -07:00
moloch-- a05dfad100 Fix 'websites rm' add tab completer issue #2185 2026-02-19 17:30:33 -08:00
Joe 0eb29ad9b8 Merge pull request #2187 from Aptimex/execute-fix
Fix: Execute default env inheritance issues
2026-02-19 18:06:43 -07:00
Adam ad8b329f85 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-19 16:45:39 -07:00
Adam c6206257b6 Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-19 16:20:59 -07:00
Ronan Kervella 8bbf06c156 Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-19 12:42:31 -08:00
jjensn 4d4eab998f Declare vars before use 2026-02-18 13:34:24 -05:00
jjensn c081f17380 typo: Add yamux client constructor 2026-02-17 13:17:44 -05:00
jjensn fafbac3fa6 Yamux logging for debug implants 2026-02-17 12:59:34 -05:00
moloch-- d9c89c81cf Fix typo in docs 2026-02-16 13:14:40 -08:00
moloch-- 061221905b Add discord button to upper right 2026-02-16 13:09:03 -08:00
moloch-- d0600e2be7 Tweak docs site loading 2026-02-16 10:44:51 -08:00
moloch-- 26abbe3860 Update wasm-donut to v0.0.5 2026-02-16 09:58:57 -08:00
Anas 55a386ae1c Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-16 19:54:15 +02:00
moloch-- 2bfdb218ff Update wasm-donut to v0.0.5 2026-02-16 09:44:22 -08:00
moloch-- 715b84e799 Update readme 2026-02-16 09:26:49 -08:00
moloch-- faea2ff639 Add link buttons to home page 2026-02-16 08:58:39 -08:00
moloch-- 1b253f8dec Add community talks section 2026-02-16 08:46:12 -08:00
moloch-- eb38724322 Add mtls and wg c2 docs 2026-02-16 08:35:01 -08:00
moloch-- 05dedf4473 Add mermaid chart component to md renderer 2026-02-16 08:06:36 -08:00
moloch-- 28c6b82a79 Update config file docs 2026-02-16 07:46:18 -08:00
Anas 1f553496e3 Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-16 15:50:07 +02:00
0xOral cd5db464e0 fix protoc version 2026-02-16 15:25:21 +02:00
moloch-- 0bf0ac6c7b Update config file md 2026-02-15 21:52:57 -08:00
Joe 0cf5a47cfd Merge pull request #2181 from BishopFox/fix/gzip-bomb
Add limit to gzip size
2026-02-15 22:27:56 -07:00
moloch-- 8d6759924b Add limit to gzip size 2026-02-15 21:13:54 -08:00
Joe 8d5dbd2050 Merge pull request #2180 from BishopFox/feature/shell-mgmt
Feature/Shell Management
2026-02-15 22:12:08 -07:00
moloch-- 804e149113 Add shell kill option 2026-02-15 20:58:07 -08:00
moloch-- 9ced4cfd50 Add shell mgmt features 2026-02-15 20:51:18 -08:00
0xOral c8be5a5b1b add --spoof feature 2026-02-16 02:27:59 +02:00
moloch-- b7abf98c95 Tweak yt header 2026-02-15 07:30:51 -08:00
moloch-- 918ac1bb91 Tweak payload compatibility tables 2026-02-14 16:24:44 -08:00
moloch-- 1e57818312 Tweak cross-compile docs 2026-02-14 11:34:11 -08:00
moloch-- c44c14dd46 Tweak cross-compile docs 2026-02-14 11:33:48 -08:00
moloch-- 35f41cd62d Tweak compile from source instructions 2026-02-14 11:29:35 -08:00
moloch-- 4eff1cc053 Add getting started workshop 2026-02-14 11:22:12 -08:00
moloch-- 16981da431 upgrade docs to nextjs v16 2026-02-14 11:16:38 -08:00
moloch-- 3830c76292 npm update 2026-02-14 09:43:13 -08:00
moloch-- 73de93d6fb Fix content scrolling 2026-02-14 09:42:04 -08:00
moloch-- c7770c2f6e Add more yt links 2026-02-14 09:31:02 -08:00
moloch-- 21db908ce4 Hide footer on yt play 2026-02-14 09:23:11 -08:00
moloch-- b7b0247652 Add basic yt embeds 2026-02-14 08:52:15 -08:00
moloch-- 3dc7dc03d0 Fix moving sidebar 2026-02-14 08:40:26 -08:00
moloch-- 229452d7ed Fix moving sidebar 2026-02-14 08:39:23 -08:00
moloch-- d29ce9a08b Add payload compatibility table to docs 2026-02-14 08:19:18 -08:00
Joe 46f2b2b402 Merge pull request #2177 from BishopFox/skoveit-feature/dynamic-c2-endpoint
Skoveit feature/dynamic c2 endpoint
2026-02-13 11:50:36 -07:00
Joe 6ee2006387 Merge pull request #2170 from xtle0o0/fix/external-builder-authz-scope
Restrict external builder RPCs to assigned build jobs
2026-02-13 11:50:22 -07:00
moloch-- 2fd13ee2bc Regenerate pb 2026-02-13 07:22:06 -08:00
0xOral 2697568336 fix-export-ca-error-message 2026-02-13 07:21:54 -08:00
Joe 5013048b0b Merge pull request #2176 from skoveit/fix-export-ca-error-message
fix-export-ca-error-message
2026-02-13 08:13:46 -07:00
Joe 627b15572c Merge branch 'master' into feature/dynamic-c2-endpoint 2026-02-13 08:11:56 -07:00
0xOral 42c40c9b8d fix-export-ca-error-message 2026-02-12 19:45:52 +02:00
0xOral 405b282fed Dynamic C2 Endpoint Switching 2026-02-12 18:47:26 +02:00
Joe e32cd36e01 Merge pull request #2172 from skoveit/fix-off-by-one-naming
Fix: server panic on single-word lists and off-by-one in codename generation
2026-02-10 22:30:32 -07:00
Joe 492a2aeed6 Merge pull request #2171 from loresuso/loresuso/fix-zombie-shell
fix: wait system shell to cleanup zombie processes
2026-02-10 22:29:53 -07:00
Lorenzo Susini d6dafb0391 fix: wait system shell to cleanup zombie processes
Signed-off-by: Lorenzo Susini <[email protected]>
2026-02-10 18:01:39 +01:00
0xOral 49a1bcfd7d fix(codenames): fix off-by-one error and prevent panic on single-word lists 2026-02-10 17:05:13 +02:00
xtle0o0 a8d1bf6fb1 Restrict external builder RPCs to assigned builds 2026-02-10 12:00:24 +01:00
moloch-- 2f386b7e09 v1.7.1 2026-02-08 14:19:47 -08:00
Joe d7dce58397 Merge pull request #2168 from BishopFox/feature/mutli-console
Feature/mutli console
2026-02-08 07:47:05 -07:00
moloch-- 15114138a8 Save interactive settings to disk 2026-02-08 06:32:17 -08:00
moloch-- 2716090410 Add custom prompt configurations 2026-02-08 06:28:45 -08:00
moloch-- a805161245 Add themes and console switcher 2026-02-07 15:46:17 -08:00
Joe afd57891cd Merge pull request #2166 from BishopFox/linux/shellcode
Linux/shellcode
2026-02-07 14:32:19 -07:00
moloch--andCodex bdf4a6ed0d Codex changes
Co-authored-by: Codex
2026-02-07 12:54:03 -08:00
467 changed files with 53831 additions and 9217 deletions

No files matched your search

-23
View File
@@ -1,41 +1,18 @@
# Repository Guidelines # Repository Guidelines
## Project Structure & Module Organization
- `client/` holds the CLI client and interactive commands.
- `server/` contains the server runtime, C2 logic, configs, and web assets.
- `implant/` is the implant runtime and transport implementations.
- `protobuf/` stores protobuf definitions and generated Go types.
- `util/` is shared helpers (crypto, encoders, minisign, etc.).
- `docs/sliver-docs/` is the Next.js documentation site and prebuild scripts.
- `vendor/` is the vendored Go dependency tree used by builds.
- Build outputs land in the repo root as `sliver-client*` and `sliver-server*`.
## Build, Test, and Development Commands ## Build, Test, and Development Commands
- `make` builds server + client using vendored modules and runs `go-assets.sh`. - `make` builds server + client using vendored modules and runs `go-assets.sh`.
- `make client` builds a CGO-free client; `make debug` keeps symbols. - `make client` builds a CGO-free client; `make debug` keeps symbols.
- `make pb` regenerates protobuf stubs from `protobuf/`. - `make pb` regenerates protobuf stubs from `protobuf/`.
- `./go-tests.sh` runs the curated Go test suite with build tags. - `./go-tests.sh` runs the curated Go test suite with build tags.
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build. - Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
- Go toolchain: the Makefile enforces Go >= 1.24.
## Coding Style & Naming Conventions ## Coding Style & Naming Conventions
- Run `gofmt` on all Go code; keep package names lowercase and idiomatic. - Run `gofmt` on all Go code; keep package names lowercase and idiomatic.
- Avoid `CGO` and empty interfaces; prefer explicit types and interfaces. - Avoid `CGO` and empty interfaces; prefer explicit types and interfaces.
- Do not import `server` packages from `client`. - Do not import `server` packages from `client`.
- If `math/rand` is required, import it as `insecureRand` and avoid security use.
## Testing Guidelines ## Testing Guidelines
- Go tests follow standard `_test.go` naming within each package.
- Prefer `docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .` - Prefer `docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .`
- Add or update tests when behavior changes; some server packages run longer tests. - Add or update tests when behavior changes; some server packages run longer tests.
## Commit & Pull Request Guidelines
- Commits must be signed for PRs to `master`; work in a feature branch.
- Commit subjects are short and imperative (e.g., "Fix ...", "Add ...", "Update ...", "Bump ...").
- Changes to `vendor/` should be in a distinct commit.
- PRs should link the tracking issue, note tests run, and confirm `gofmt` plus unit tests as applicable.
## Security Notes
- Default to secure configuration; fail closed rather than open.
- Avoid insecure algorithms (MD5/SHA1/AES-ECB); prefer AES-GCM and modern curves.
- Treat user input as untrusted: canonicalize paths and use restrictive file permissions.
+1 -1
View File
@@ -26,7 +26,7 @@ K := $(foreach exec,$(EXECUTABLES),\
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1) GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2) GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1 MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 24 MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION) GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
+2
View File
@@ -6,6 +6,8 @@ The server and client support MacOS, Windows, and Linux. Implants are supported
[![Release](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/BishopFox/sliver)](https://goreportcard.com/report/github.com/BishopFox/sliver) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Release](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/BishopFox/sliver)](https://goreportcard.com/report/github.com/BishopFox/sliver) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
### Features ### Features
- Dynamic code generation - Dynamic code generation
+1
View File
@@ -124,4 +124,5 @@ under certain conditions; type 'licenses' for details.`)
saveAssetVersion(appDir) saveAssetVersion(appDir)
} }
_, _ = LoadSettings() _, _ = LoadSettings()
_, _ = LoadSpoofMetadataConfig()
} }
+53
View File
@@ -22,6 +22,7 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -31,6 +32,18 @@ const (
settingsLegacyFileName = "tui-settings.json" 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 // ClientSettings - Client JSON config
type ClientSettings struct { type ClientSettings struct {
TableStyle string `json:"tables" yaml:"tables"` TableStyle string `json:"tables" yaml:"tables"`
@@ -41,6 +54,8 @@ type ClientSettings struct {
VimMode bool `json:"vim_mode" yaml:"vim_mode"` VimMode bool `json:"vim_mode" yaml:"vim_mode"`
UserConnect bool `json:"user_connect" yaml:"user_connect"` UserConnect bool `json:"user_connect" yaml:"user_connect"`
ConsoleLogs bool `json:"console_logs" yaml:"console_logs"` 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 // LoadSettings - Load the client settings from disk
@@ -67,6 +82,8 @@ func LoadSettings() (*ClientSettings, error) {
return defaultSettings(), err 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 { if err := SaveSettings(settings); err != nil {
return settings, err return settings, err
} }
@@ -78,6 +95,38 @@ func LoadSettings() (*ClientSettings, error) {
return settings, nil 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 { func defaultSettings() *ClientSettings {
return &ClientSettings{ return &ClientSettings{
TableStyle: "SliverDefault", TableStyle: "SliverDefault",
@@ -87,9 +136,13 @@ func defaultSettings() *ClientSettings {
AlwaysOverflow: false, AlwaysOverflow: false,
VimMode: false, VimMode: false,
ConsoleLogs: true, 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 // SaveSettings - Save the current settings to disk
func SaveSettings(settings *ClientSettings) error { func SaveSettings(settings *ClientSettings) error {
rootDir, _ := filepath.Abs(GetRootAppDir()) rootDir, _ := filepath.Abs(GetRootAppDir())
+22
View File
@@ -21,6 +21,8 @@ always_overflow: true
vim_mode: true vim_mode: true
user_connect: true user_connect: true
console_logs: false console_logs: false
prompt: "basic"
prompt_template: "{{.Host}}"
`) `)
if err := os.WriteFile(settingsPath, data, 0o600); err != nil { if err := os.WriteFile(settingsPath, data, 0o600); err != nil {
t.Fatalf("failed to write yaml settings: %v", err) t.Fatalf("failed to write yaml settings: %v", err)
@@ -54,6 +56,12 @@ console_logs: false
if settings.ConsoleLogs { if settings.ConsoleLogs {
t.Fatalf("expected ConsoleLogs false") 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) { func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
@@ -71,6 +79,8 @@ func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
VimMode: true, VimMode: true,
UserConnect: true, UserConnect: true,
ConsoleLogs: false, ConsoleLogs: false,
PromptStyle: PromptStyleOperatorHost,
PromptTemplate: "{{.Operator}}@{{.Host}} sliver > ",
} }
data, err := json.MarshalIndent(legacy, "", " ") data, err := json.MarshalIndent(legacy, "", " ")
if err != nil { if err != nil {
@@ -87,6 +97,12 @@ func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
if settings.TableStyle != "Legacy" { if settings.TableStyle != "Legacy" {
t.Fatalf("expected TableStyle %q, got %q", "Legacy", settings.TableStyle) 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 { if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
t.Fatalf("expected yaml settings to exist: %v", err) t.Fatalf("expected yaml settings to exist: %v", err)
} }
@@ -108,6 +124,12 @@ func TestLoadSettingsWritesDefault(t *testing.T) {
if settings.TableStyle != "SliverDefault" { if settings.TableStyle != "SliverDefault" {
t.Fatalf("expected default TableStyle %q, got %q", "SliverDefault", settings.TableStyle) 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()) rootDir, _ := filepath.Abs(GetRootAppDir())
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil { if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
t.Fatalf("expected default settings to be written: %v", err) t.Fatalf("expected default settings to be written: %v", err)
+216
View File
@@ -0,0 +1,216 @@
package assets
/*
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 (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
const spoofMetadataFileName = "spoof-metadata.yaml"
const defaultSpoofMetadataConfigTemplate = `# Sliver executable metadata spoofing configuration.
# Used by generate commands when --spoof-metadata is set.
#
# Fields that reference file content support either:
# - path: read bytes from a local file path
# - base64: inline base64 file content
# Do not set both path and base64 for the same object.
#
# Example 1: Minimal path-based metadata cloning (most common)
pe:
source:
name: metadata-source.exe
path: ""
# Optional icon donor. Reserved for future standalone icon mutation support.
icon:
name: icon.ico
path: ""
# Example 2: Inline/base64 source (uncomment and remove "path" above)
# source:
# name: metadata-source.exe
# base64: "TVqQAAMAAAAEAAAA..."
# Example 3: Optional PE structure overrides (advanced)
# Applied after metadata/resources are cloned from source.
# Omitted fields keep cloned values.
# resource_directory:
# characteristics: 0
# time_date_stamp: 0
# major_version: 1
# minor_version: 0
# number_of_named_entries: 0
# number_of_id_entries: 0
#
# resource_directory_entries:
# - name: 1
# offset_to_data: 0
#
# resource_data_entries:
# - offset_to_data: 0
# size: 0
# code_page: 0
# reserved: 0
#
# export_directory:
# characteristics: 0
# time_date_stamp: 0
# major_version: 1
# minor_version: 0
# name: 0
# base: 0
# number_of_functions: 0
# number_of_names: 0
# address_of_functions: 0
# address_of_names: 0
# address_of_name_ordinals: 0
`
// SpoofMetadataConfig stores default metadata spoofing inputs for generate commands.
type SpoofMetadataConfig struct {
PE *PESpoofMetadataConfig `json:"pe,omitempty" yaml:"pe,omitempty"`
}
// PESpoofMetadataConfig stores PE-specific spoofing inputs.
type PESpoofMetadataConfig struct {
Source *SpoofMetadataDataSource `json:"source,omitempty" yaml:"source,omitempty"`
Icon *SpoofMetadataDataSource `json:"icon,omitempty" yaml:"icon,omitempty"`
ResourceDirectory *ImageResourceDirectory `json:"resource_directory,omitempty" yaml:"resource_directory,omitempty"`
ResourceDirectoryEntries []*ImageResourceDirectoryEntry `json:"resource_directory_entries,omitempty" yaml:"resource_directory_entries,omitempty"`
ResourceDataEntries []*ImageResourceDataEntry `json:"resource_data_entries,omitempty" yaml:"resource_data_entries,omitempty"`
ExportDirectory *ImageExportDirectory `json:"export_directory,omitempty" yaml:"export_directory,omitempty"`
}
// SpoofMetadataDataSource points to bytes that should be sent to the server.
// Either Path or Base64 may be set.
type SpoofMetadataDataSource struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Base64 string `json:"base64,omitempty" yaml:"base64,omitempty"`
}
type ImageResourceDirectory struct {
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
NumberOfNamedEntries uint32 `json:"number_of_named_entries,omitempty" yaml:"number_of_named_entries,omitempty"`
NumberOfIDEntries uint32 `json:"number_of_id_entries,omitempty" yaml:"number_of_id_entries,omitempty"`
}
type ImageResourceDirectoryEntry struct {
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
}
type ImageResourceDataEntry struct {
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
Size uint32 `json:"size,omitempty" yaml:"size,omitempty"`
CodePage uint32 `json:"code_page,omitempty" yaml:"code_page,omitempty"`
Reserved uint32 `json:"reserved,omitempty" yaml:"reserved,omitempty"`
}
type ImageExportDirectory struct {
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
Base uint32 `json:"base,omitempty" yaml:"base,omitempty"`
NumberOfFunctions uint32 `json:"number_of_functions,omitempty" yaml:"number_of_functions,omitempty"`
NumberOfNames uint32 `json:"number_of_names,omitempty" yaml:"number_of_names,omitempty"`
AddressOfFunctions uint32 `json:"address_of_functions,omitempty" yaml:"address_of_functions,omitempty"`
AddressOfNames uint32 `json:"address_of_names,omitempty" yaml:"address_of_names,omitempty"`
AddressOfNameOrdinals uint32 `json:"address_of_name_ordinals,omitempty" yaml:"address_of_name_ordinals,omitempty"`
}
// SpoofMetadataConfigPath returns the spoof metadata config file path.
func SpoofMetadataConfigPath() string {
rootDir, _ := filepath.Abs(GetRootAppDir())
return filepath.Join(rootDir, spoofMetadataFileName)
}
// LoadSpoofMetadataConfig loads spoof metadata config from disk, writing defaults
// if the file is missing.
func LoadSpoofMetadataConfig() (*SpoofMetadataConfig, error) {
config := defaultSpoofMetadataConfig()
configPath := SpoofMetadataConfigPath()
data, err := os.ReadFile(configPath)
if err != nil {
if !os.IsNotExist(err) {
return defaultSpoofMetadataConfig(), err
}
if err := os.WriteFile(configPath, []byte(defaultSpoofMetadataConfigTemplate), 0o600); err != nil {
return defaultSpoofMetadataConfig(), err
}
data = []byte(defaultSpoofMetadataConfigTemplate)
}
if err := yaml.Unmarshal(data, config); err != nil {
return defaultSpoofMetadataConfig(), err
}
normalizeSpoofMetadataConfig(config)
return config, nil
}
// SaveSpoofMetadataConfig writes spoof metadata config to disk.
func SaveSpoofMetadataConfig(config *SpoofMetadataConfig) error {
if config == nil {
config = defaultSpoofMetadataConfig()
}
normalizeSpoofMetadataConfig(config)
data, err := yaml.Marshal(config)
if err != nil {
return err
}
return os.WriteFile(SpoofMetadataConfigPath(), data, 0o600)
}
func normalizeSpoofMetadataConfig(config *SpoofMetadataConfig) {
if config.PE == nil {
config.PE = &PESpoofMetadataConfig{}
}
if config.PE.Source == nil {
config.PE.Source = &SpoofMetadataDataSource{Name: "metadata-source.exe"}
}
if config.PE.Icon == nil {
config.PE.Icon = &SpoofMetadataDataSource{Name: "icon.ico"}
}
}
func defaultSpoofMetadataConfig() *SpoofMetadataConfig {
return &SpoofMetadataConfig{
PE: &PESpoofMetadataConfig{
Source: &SpoofMetadataDataSource{
Name: "metadata-source.exe",
Path: "",
},
Icon: &SpoofMetadataDataSource{
Name: "icon.ico",
Path: "",
},
},
}
}
+6 -6
View File
@@ -26,16 +26,16 @@ import (
"github.com/bishopfox/sliver/client/forms" "github.com/bishopfox/sliver/client/forms"
) )
func selectConfig() *assets.ClientConfig { func selectConfig() (string, *assets.ClientConfig) {
configs := assets.GetConfigs() configs := assets.GetConfigs()
if len(configs) == 0 { if len(configs) == 0 {
return nil return "", nil
} }
if len(configs) == 1 { if len(configs) == 1 {
for _, config := range configs { for key, config := range configs {
return config return key, config
} }
} }
@@ -49,8 +49,8 @@ func selectConfig() *assets.ClientConfig {
err := forms.Select("Select a server:", keys, &selection) err := forms.Select("Select a server:", keys, &selection)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
return nil return "", nil
} }
return configs[selection] return selection, configs[selection]
} }
+26
View File
@@ -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
View File
@@ -19,9 +19,7 @@ package cli
*/ */
import ( import (
"context"
"fmt" "fmt"
"os"
"github.com/bishopfox/sliver/client/assets" "github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command" "github.com/bishopfox/sliver/client/command"
@@ -30,7 +28,6 @@ import (
"github.com/bishopfox/sliver/protobuf/rpcpb" "github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
) )
// consoleCmd generates the console with required pre/post runners. // 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) { 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 { pre = func(cmd *cobra.Command, _ []string) error {
configs := assets.GetConfigs() 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()) fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
return nil return nil
} }
config := selectConfig() configKey, config := selectConfig()
if config == nil { if config == nil {
return nil return nil
} }
@@ -72,40 +67,20 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
} }
var rpc rpcpb.SliverRPCClient var rpc rpcpb.SliverRPCClient
var ln *grpc.ClientConn
rpc, ln, err = transport.MTLSConnect(config) rpc, ln, err = transport.MTLSConnect(config)
if err != nil { if err != nil {
fmt.Printf("Connection to server failed %s", err) fmt.Printf("Connection to server failed %s", err)
return nil return nil
} }
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
// 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)
} }
// Close the RPC connection once exiting // Close the RPC connection once exiting
post = func(_ *cobra.Command, _ []string) error { post = func(_ *cobra.Command, _ []string) error {
if ln != nil { return con.CloseConnection()
return ln.Close()
}
return nil
} }
return pre, post 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)
}
}
}
+3
View File
@@ -1,6 +1,7 @@
package alias package alias
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -32,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
} }
aliasCmd.AddCommand(aliasLoadCmd) aliasCmd.AddCommand(aliasLoadCmd)
flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory")) flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory"))
completers.RegisterLocalFilePathPositionalCompletion(aliasLoadCmd, 0)
aliasInstallCmd := &cobra.Command{ aliasInstallCmd := &cobra.Command{
Use: consts.InstallStr + " [ALIAS]", Use: consts.InstallStr + " [ALIAS]",
@@ -44,6 +46,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
} }
aliasCmd.AddCommand(aliasInstallCmd) aliasCmd.AddCommand(aliasInstallCmd)
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file")) flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
completers.RegisterLocalFilePathPositionalCompletion(aliasInstallCmd, 0)
aliasRemove := &cobra.Command{ aliasRemove := &cobra.Command{
Use: consts.RmStr + " [ALIAS]", Use: consts.RmStr + " [ALIAS]",
+15 -15
View File
@@ -428,7 +428,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con)) 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 urlMargin := 150 // Extra margin needed to show URL column
@@ -493,26 +493,26 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
rows := []table.Row{} rows := []table.Row{}
for _, pkg := range entries { for _, pkg := range entries {
color := console.Normal style := console.StyleNormal
if extensions.CmdExists(pkg.CommandName, sliverMenu.Command) { if extensions.CmdExists(pkg.CommandName, sliverMenu.Command) {
color = console.Green style = console.StyleGreen
} }
if con.Settings.SmallTermWidth+urlMargin < width { if con.Settings.SmallTermWidth+urlMargin < width {
rows = append(rows, table.Row{ rows = append(rows, table.Row{
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName), style.Render(pkg.ArmoryName),
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName), style.Render(pkg.CommandName),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version), style.Render(pkg.Version),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type), style.Render(pkg.Type),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help), style.Render(pkg.Help),
fmt.Sprintf(color+"%s"+console.Normal, pkg.URL), style.Render(pkg.URL),
}) })
} else { } else {
rows = append(rows, table.Row{ rows = append(rows, table.Row{
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName), style.Render(pkg.ArmoryName),
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName), style.Render(pkg.CommandName),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version), style.Render(pkg.Version),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type), style.Render(pkg.Type),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help), style.Render(pkg.Help),
}) })
} }
} }
@@ -524,7 +524,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) { func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con)) tw.SetStyle(settings.GetTableStyle(con))
tw.SetTitle(console.Bold + "Bundles" + console.Normal) tw.SetTitle(console.StyleBold.Render("Bundles"))
tw.AppendHeader(table.Row{ tw.AppendHeader(table.Row{
"Armory Name", "Armory Name",
"Name", "Name",
+1 -1
View File
@@ -257,7 +257,7 @@ func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdent
if len(extensionUpdates) != 1 { if len(extensionUpdates) != 1 {
extensionSuffix = "s" 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{ tw.AppendHeader(table.Row{
"Package Name", "Package Name",
"Package Type", "Package Type",
+19 -19
View File
@@ -178,10 +178,10 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
} }
for _, beacon := range beacons { for _, beacon := range beacons {
color := console.Normal style := console.StyleNormal
activeBeacon := con.ActiveTarget.GetBeacon() activeBeacon := con.ActiveTarget.GetBeacon()
if activeBeacon != nil && activeBeacon.ID == beacon.ID { if activeBeacon != nil && activeBeacon.ID == beacon.ID {
color = console.Green style = console.StyleGreen
} }
if beacon.Integrity == "" { if beacon.Integrity == "" {
beacon.Integrity = "-" beacon.Integrity = "-"
@@ -192,34 +192,34 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
if wideTermWidth { if wideTermWidth {
rowEntries = []string{ rowEntries = []string{
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]), style.Render(strings.Split(beacon.ID, "-")[0]),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name), style.Render(beacon.Name),
fmt.Sprintf(color+"%d/%d"+console.Normal, beacon.TasksCountCompleted, beacon.TasksCount), style.Render(fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount)),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport), style.Render(beacon.Transport),
fmt.Sprintf(color+"%s"+console.Normal, beacon.RemoteAddress), style.Render(beacon.RemoteAddress),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname), style.Render(beacon.Hostname),
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")), style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
fmt.Sprintf(color+"%s (%d)"+console.Normal, beacon.Filename, beacon.PID), style.Render(fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID)),
} }
if windowsBeaconInList { if windowsBeaconInList {
rowEntries = append(rowEntries, fmt.Sprintf(color+"%s"+console.Normal, beacon.Integrity)) rowEntries = append(rowEntries, style.Render(beacon.Integrity))
} }
rowEntries = append(rowEntries, []string{ rowEntries = append(rowEntries, []string{
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch), style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Locale), style.Render(beacon.Locale),
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false), con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true), con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
}...) }...)
} else { } else {
rowEntries = []string{ rowEntries = []string{
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]), style.Render(strings.Split(beacon.ID, "-")[0]),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name), style.Render(beacon.Name),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport), style.Render(beacon.Transport),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname), style.Render(beacon.Hostname),
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")), style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch), style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false), con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true), con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
} }
+4
View File
@@ -1,6 +1,7 @@
package c2profiles package c2profiles
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate" "github.com/bishopfox/sliver/client/command/generate"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
@@ -27,6 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("file", "f", "", "Path to C2 configuration file to import") f.StringP("file", "f", "", "Path to C2 configuration file to import")
f.BoolP("overwrite", "o", false, "Overwrite profile if it exists") f.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
}) })
completers.RegisterLocalFilePathFlagCompletion(importC2ProfileCmd, "file")
exportC2ProfileCmd := &cobra.Command{ exportC2ProfileCmd := &cobra.Command{
Use: consts.ExportC2ProfileStr, Use: consts.ExportC2ProfileStr,
@@ -40,6 +42,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("file", "f", "", "Path to file to export C2 configuration to") f.StringP("file", "f", "", "Path to file to export C2 configuration to")
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name") f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
}) })
completers.RegisterLocalFilePathFlagCompletion(exportC2ProfileCmd, "file")
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["name"] = generate.HTTPC2Completer(con) (*comp)["name"] = generate.HTTPC2Completer(con)
}) })
@@ -76,6 +79,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as") f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as")
f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile") f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile")
}) })
completers.RegisterLocalFilePathFlagCompletion(generateC2ProfileCmd, "file")
flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["template"] = generate.HTTPC2Completer(con) (*comp)["template"] = generate.HTTPC2Completer(con)
+12 -13
View File
@@ -2,7 +2,6 @@ package certificates
import ( import (
"context" "context"
"fmt"
"time" "time"
"github.com/bishopfox/sliver/client/command/settings" "github.com/bishopfox/sliver/client/command/settings"
@@ -58,28 +57,28 @@ func printCertificateAuthorityInfo(con *console.SliverClient, authData []*client
} }
for _, authority := range authData { for _, authority := range authData {
rowColor := console.Normal rowStyle := console.StyleNormal
expiry, err := time.Parse(timeFormat, authority.ValidityExpiry) expiry, err := time.Parse(timeFormat, authority.ValidityExpiry)
if err == nil { if err == nil {
rowColor = checkCertExpiry(expiry) rowStyle = checkCertExpiry(expiry)
} }
if wideTermWidth { if wideTermWidth {
tw.AppendRow(table.Row{ tw.AppendRow(table.Row{
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ID), rowStyle.Render(authority.ID),
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CN), rowStyle.Render(authority.CN),
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CreationTime), rowStyle.Render(authority.CreationTime),
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.Type), rowStyle.Render(authority.Type),
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.KeyAlgorithm), rowStyle.Render(authority.KeyAlgorithm),
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityStart), rowStyle.Render(authority.ValidityStart),
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityExpiry), rowStyle.Render(authority.ValidityExpiry),
}) })
} else { } else {
tw.AppendRow(table.Row{ tw.AppendRow(table.Row{
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ID), rowStyle.Render(authority.ID),
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CN), rowStyle.Render(authority.CN),
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityExpiry), rowStyle.Render(authority.ValidityExpiry),
}) })
} }
} }
+17 -18
View File
@@ -2,7 +2,6 @@ package certificates
import ( import (
"context" "context"
"fmt"
"time" "time"
"github.com/bishopfox/sliver/client/command/settings" "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) 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()) { 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 // One week is 168 hours - this is bad
if expiryTime.Before(time.Now().Add(168 * time.Hour)) { 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 // One month is approximately 730 hours - this is a warning
if expiryTime.Before(time.Now().Add(730 * time.Hour)) { 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) { func printCertificateInfo(con *console.SliverClient, certData []*clientpb.CertificateData) {
@@ -148,28 +147,28 @@ func printCertificateInfo(con *console.SliverClient, certData []*clientpb.Certif
} }
for _, cert := range certData { for _, cert := range certData {
rowColor := console.Normal rowStyle := console.StyleNormal
expiry, err := time.Parse(timeFormat, cert.ValidityExpiry) expiry, err := time.Parse(timeFormat, cert.ValidityExpiry)
// This should not error out, but if it does, the row will not be colored // This should not error out, but if it does, the row will not be colored
if err == nil { if err == nil {
rowColor = checkCertExpiry(expiry) rowStyle = checkCertExpiry(expiry)
} }
if wideTermWidth { if wideTermWidth {
tw.AppendRow(table.Row{ tw.AppendRow(table.Row{
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID), rowStyle.Render(cert.ID),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN), rowStyle.Render(cert.CN),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CreationTime), rowStyle.Render(cert.CreationTime),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.Type), rowStyle.Render(cert.Type),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.KeyAlgorithm), rowStyle.Render(cert.KeyAlgorithm),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityStart), rowStyle.Render(cert.ValidityStart),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry), rowStyle.Render(cert.ValidityExpiry),
}) })
} else { } else {
tw.AppendRow(table.Row{ tw.AppendRow(table.Row{
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID), rowStyle.Render(cert.ID),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN), rowStyle.Render(cert.CN),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry), rowStyle.Render(cert.ValidityExpiry),
}) })
} }
+58
View File
@@ -22,6 +22,7 @@ import (
"net" "net"
"github.com/rsteube/carapace" "github.com/rsteube/carapace"
"github.com/spf13/cobra"
) )
// ClientInterfacesCompleter completes interface addresses on the client host. // ClientInterfacesCompleter completes interface addresses on the client host.
@@ -84,3 +85,60 @@ func LocalProxyCompleter() carapace.Action {
}).Invoke(c).Prefix(prefix).ToA() }).Invoke(c).Prefix(prefix).ToA()
}) })
} }
// LocalFilePathCompleter completes local file and directory paths.
func LocalFilePathCompleter() carapace.Action {
return carapace.ActionFiles().Tag("local file path")
}
// RegisterLocalFilePathFlagCompletion registers local file-path completion for a flag.
func RegisterLocalFilePathFlagCompletion(cmd *cobra.Command, name string) {
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, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
})
}
// RegisterLocalFilePathFlagCompletions registers local file-path completion for a list of flags.
func RegisterLocalFilePathFlagCompletions(cmd *cobra.Command, names ...string) {
for _, name := range names {
RegisterLocalFilePathFlagCompletion(cmd, name)
}
}
// RegisterLocalFilePathPositionalCompletion registers local file-path completion for a positional argument index.
func RegisterLocalFilePathPositionalCompletion(cmd *cobra.Command, index int) {
if cmd == nil || index < 0 {
return
}
if cmd.ValidArgsFunction != nil {
return
}
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == index {
return nil, cobra.ShellCompDirectiveDefault
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// RegisterLocalFilePathPositionalAnyCompletion registers local file-path completion for all positional arguments.
func RegisterLocalFilePathPositionalAnyCompletion(cmd *cobra.Command) {
if cmd == nil {
return
}
if cmd.ValidArgsFunction != nil {
return
}
cmd.ValidArgsFunction = func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
}
}
+4
View File
@@ -1,6 +1,7 @@
package crack package crack
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace" "github.com/rsteube/carapace"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -61,6 +62,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "wordlist name (blank = filename)") f.StringP("name", "n", "", "wordlist name (blank = filename)")
}) })
carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file")) carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file"))
completers.RegisterLocalFilePathPositionalCompletion(wordlistsAddCmd, 0)
wordlistsCmd.AddCommand(wordlistsAddCmd) wordlistsCmd.AddCommand(wordlistsAddCmd)
wordlistsRmCmd := &cobra.Command{ wordlistsRmCmd := &cobra.Command{
@@ -95,6 +97,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "rules name (blank = filename)") f.StringP("name", "n", "", "rules name (blank = filename)")
}) })
carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file")) carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file"))
completers.RegisterLocalFilePathPositionalCompletion(rulesAddCmd, 0)
rulesCmd.AddCommand(rulesAddCmd) rulesCmd.AddCommand(rulesAddCmd)
rulesRmCmd := &cobra.Command{ rulesRmCmd := &cobra.Command{
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "hcstat2 name (blank = filename)") f.StringP("name", "n", "", "hcstat2 name (blank = filename)")
}) })
carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file")) carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file"))
completers.RegisterLocalFilePathPositionalCompletion(hcstat2AddCmd, 0)
hcstat2Cmd.AddCommand(hcstat2AddCmd) hcstat2Cmd.AddCommand(hcstat2AddCmd)
hcstat2RmCmd := &cobra.Command{ hcstat2RmCmd := &cobra.Command{
+3 -3
View File
@@ -106,17 +106,17 @@ func PrintCrackFiles(crackFiles *clientpb.CrackFiles, con *console.SliverClient)
func PrintCrackFilesByType(crackFiles *clientpb.CrackFiles, con *console.SliverClient) { func PrintCrackFilesByType(crackFiles *clientpb.CrackFiles, con *console.SliverClient) {
wordlistTable := table.NewWriter() wordlistTable := table.NewWriter()
wordlistTable.SetTitle(console.Bold + "Wordlists" + console.Normal) wordlistTable.SetTitle(console.StyleBold.Render("Wordlists"))
wordlistTable.SetStyle(settings.GetTableStyle(con)) wordlistTable.SetStyle(settings.GetTableStyle(con))
wordlistTable.AppendHeader(table.Row{"Name", "Size"}) wordlistTable.AppendHeader(table.Row{"Name", "Size"})
rulesTable := table.NewWriter() rulesTable := table.NewWriter()
rulesTable.SetTitle(console.Bold + "Rules" + console.Normal) rulesTable.SetTitle(console.StyleBold.Render("Rules"))
rulesTable.SetStyle(settings.GetTableStyle(con)) rulesTable.SetStyle(settings.GetTableStyle(con))
rulesTable.AppendHeader(table.Row{"Name", "Size"}) rulesTable.AppendHeader(table.Row{"Name", "Size"})
hcTable := table.NewWriter() hcTable := table.NewWriter()
hcTable.SetTitle(console.Bold + "Markov Hcstat2" + console.Normal) hcTable.SetTitle(console.StyleBold.Render("Markov Hcstat2"))
hcTable.SetStyle(settings.GetTableStyle(con)) hcTable.SetStyle(settings.GetTableStyle(con))
hcTable.AppendHeader(table.Row{"Name", "Size"}) hcTable.AppendHeader(table.Row{"Name", "Size"})
+16 -16
View File
@@ -132,35 +132,35 @@ func PrintCrackers(crackers []*clientpb.Crackstation, con *console.SliverClient,
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool) { func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool) {
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con)) 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.AppendSeparator()
tw.AppendRow(table.Row{console.Bold + "Operating System" + console.Normal, fmt.Sprintf("%s/%s", cracker.GOOS, cracker.GOARCH)}) tw.AppendRow(table.Row{console.StyleBold.Render("Operating System"), 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("Hashcat Version"), cracker.HashcatVersion})
if 0 < len(cracker.CUDA) { if 0 < len(cracker.CUDA) {
for _, cuda := range cracker.CUDA { for _, cuda := range cracker.CUDA {
tw.AppendSeparator() 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.StyleBold.Render("CUDA Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", 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.StyleBold.Render("Memory"), 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.StyleBold.Render("Clock"), 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("Processors"), fmt.Sprintf("%d", cuda.Processors)})
} }
} }
if 0 < len(cracker.Metal) { if 0 < len(cracker.Metal) {
for _, metal := range cracker.Metal { for _, metal := range cracker.Metal {
tw.AppendSeparator() 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.StyleBold.Render("Metal Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", 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.StyleBold.Render("Memory"), 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.StyleBold.Render("Clock"), 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("Processors"), fmt.Sprintf("%d", metal.Processors)})
} }
} }
if 0 < len(cracker.OpenCL) { if 0 < len(cracker.OpenCL) {
for _, openCL := range cracker.OpenCL { for _, openCL := range cracker.OpenCL {
tw.AppendSeparator() 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.StyleBold.Render("OpenCL Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", 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.StyleBold.Render("Memory"), 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.StyleBold.Render("Clock"), 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("Processors"), fmt.Sprintf("%d", openCL.Processors)})
} }
} }
con.Printf("%s\n", tw.Render()) 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) { func printBenchmarks(cracker *clientpb.Crackstation, con *console.SliverClient) {
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con)) 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.SortBy([]table.SortBy{{Name: "Hash Type"}})
tw.AppendHeader(table.Row{"Hash Type", "Rate"}) tw.AppendHeader(table.Row{"Hash Type", "Rate"})
if len(cracker.Benchmarks) == 0 { if len(cracker.Benchmarks) == 0 {
+1 -1
View File
@@ -25,7 +25,7 @@ func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
} }
results = append(results, file.Name) 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) results = append(results, desc)
} }
+2
View File
@@ -1,6 +1,7 @@
package creds package creds
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -65,6 +66,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["hash-type"] = CredsHashTypeCompleter(con) (*comp)["hash-type"] = CredsHashTypeCompleter(con)
}) })
carapace.Gen(credsAddFileCmd).PositionalCompletion(carapace.ActionFiles().Tag("credential file")) carapace.Gen(credsAddFileCmd).PositionalCompletion(carapace.ActionFiles().Tag("credential file"))
completers.RegisterLocalFilePathPositionalCompletion(credsAddFileCmd, 0)
credsAddCmd.AddCommand(credsAddFileCmd) credsAddCmd.AddCommand(credsAddFileCmd)
credsRmCmd := &cobra.Command{ credsRmCmd := &cobra.Command{
+2 -2
View File
@@ -61,9 +61,9 @@ func printCollection(collection string, creds []*clientpb.Credential, con *conso
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con)) tw.SetStyle(settings.GetTableStyle(con))
if collection != "" { if collection != "" {
tw.SetTitle(console.Bold + collection + console.Normal) tw.SetTitle(console.StyleBold.Render(collection))
} else { } else {
tw.SetTitle(console.Bold + "Default Collection" + console.Normal) tw.SetTitle(console.StyleBold.Render("Default Collection"))
} }
tw.AppendHeader(table.Row{ tw.AppendHeader(table.Row{
"ID", "ID",
+5
View File
@@ -1,6 +1,7 @@
package cursed package cursed
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -75,6 +76,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(cursedChromeCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(cursedChromeCmd, func(comp *carapace.ActionMap) {
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files") (*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
}) })
completers.RegisterLocalFilePathFlagCompletion(cursedChromeCmd, "payload")
cursedChromeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true cursedChromeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
carapace.Gen(cursedChromeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Chrome CLI arguments")) carapace.Gen(cursedChromeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Chrome CLI arguments"))
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(cursedEdgeCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(cursedEdgeCmd, func(comp *carapace.ActionMap) {
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files") (*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
}) })
completers.RegisterLocalFilePathFlagCompletion(cursedEdgeCmd, "payload")
cursedEdgeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true cursedEdgeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
carapace.Gen(cursedEdgeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Edge CLI arguments")) carapace.Gen(cursedEdgeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Edge CLI arguments"))
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, CursedCookiesCmd, func(f *pflag.FlagSet) { flags.Bind("", false, CursedCookiesCmd, func(f *pflag.FlagSet) {
f.StringP("save", "s", "", "save to file") f.StringP("save", "s", "", "save to file")
}) })
completers.RegisterLocalFilePathFlagCompletion(CursedCookiesCmd, "save")
cursedScreenshotCmd := &cobra.Command{ cursedScreenshotCmd := &cobra.Command{
Use: consts.ScreenshotStr, Use: consts.ScreenshotStr,
@@ -144,6 +148,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.Int64P("quality", "q", 100, "screenshot quality (1 - 100)") f.Int64P("quality", "q", 100, "screenshot quality (1 - 100)")
f.StringP("save", "s", "", "save to file") f.StringP("save", "s", "", "save to file")
}) })
completers.RegisterLocalFilePathFlagCompletion(cursedScreenshotCmd, "save")
return []*cobra.Command{cursedCmd} return []*cobra.Command{cursedCmd}
} }
+2 -2
View File
@@ -94,7 +94,7 @@ func CursedChromeCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
} }
if chromeExt != nil { if chromeExt != nil {
con.Printf("success!\n") 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 ... ") con.PrintInfof("Injecting payload ... ")
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse) cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second) // 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 { if chromeProcess != nil {
con.PrintWarnf("Found running Chrome process: %d (ppid: %d)\n", chromeProcess.GetPid(), chromeProcess.GetPpid()) 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 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") con.Printf("\n")
confirm := false confirm := false
err = forms.Confirm("Kill and restore existing Chrome process?", &confirm) err = forms.Confirm("Kill and restore existing Chrome process?", &confirm)
+2 -2
View File
@@ -93,7 +93,7 @@ func startCursedConsole(curse *core.CursedProcess, helpers bool, target *overlor
tmpFile, _ := os.CreateTemp("", "cursed") tmpFile, _ := os.CreateTemp("", "cursed")
shell := readline.NewShell() shell := readline.NewShell()
shell.History.AddFromFile("cursed history", tmpFile.Name()) 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", // EOFPrompt: "exit",
if con.Settings.VimMode { 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 { for {
line, err := shell.Readline() line, err := shell.Readline()
+2 -2
View File
@@ -77,7 +77,7 @@ func CursedEdgeCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
} }
if chromeExt != nil { if chromeExt != nil {
con.Printf("success!\n") 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 ... ") con.PrintInfof("Injecting payload ... ")
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse) cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second) // 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 { if edgeProcess != nil {
con.PrintWarnf("Found running Edge process: %d (ppid: %d)\n", edgeProcess.GetPid(), edgeProcess.GetPpid()) 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 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") con.Printf("\n")
confirm := false confirm := false
err = forms.Confirm("Kill and restore existing Edge process?", &confirm) err = forms.Confirm("Kill and restore existing Edge process?", &confirm)
+1 -1
View File
@@ -85,7 +85,7 @@ func avadaKedavraElectron(electronExe string, session *clientpb.Session, cmd *co
if electronProcess != nil { if electronProcess != nil {
con.PrintWarnf("Found running '%s' process: %d (ppid: %d)\n", path.Base(electronExe), electronProcess.GetPid(), electronProcess.GetPpid()) 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("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") con.Printf("\n")
confirm := false confirm := false
err = forms.Confirm("Kill and restart the process?", &confirm) err = forms.Confirm("Kill and restart the process?", &confirm)
+2
View File
@@ -1,6 +1,7 @@
package dllhijack package dllhijack
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace" "github.com/rsteube/carapace"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -37,6 +38,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["file"] = carapace.ActionFiles() (*comp)["file"] = carapace.ActionFiles()
(*comp)["profile"] = generate.ProfileNameCompleter(con) (*comp)["profile"] = generate.ProfileNameCompleter(con)
}) })
completers.RegisterLocalFilePathFlagCompletions(dllhijackCmd, "reference-file", "file")
carapace.Gen(dllhijackCmd).PositionalCompletion(carapace.ActionValues().Usage("Path to upload the DLL to on the remote system")) carapace.Gen(dllhijackCmd).PositionalCompletion(carapace.ActionValues().Usage("Path to upload the DLL to on the remote system"))
return []*cobra.Command{dllhijackCmd} return []*cobra.Command{dllhijackCmd}
+12 -2
View File
@@ -9,6 +9,7 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
tea "charm.land/bubbletea/v2"
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/lexers"
"github.com/bishopfox/sliver/client/command/filesystem" "github.com/bishopfox/sliver/client/command/filesystem"
@@ -17,8 +18,9 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb" "github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb" "github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util/encoders" "github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -151,7 +153,15 @@ func EditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func runEditor(content, filename string, lexer chroma.Lexer, lexerName string, showLineNumbers bool) (editorResult, error) { func runEditor(content, filename string, lexer chroma.Lexer, lexerName string, showLineNumbers bool) (editorResult, error) {
model := newEditorModel(content, filename, lexer, lexerName, showLineNumbers) model := newEditorModel(content, filename, lexer, lexerName, showLineNumbers)
program := tea.NewProgram(model, tea.WithAltScreen()) width, height := 80, 24
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
finalModel, err := program.Run() finalModel, err := program.Run()
if err != nil { if err != nil {
return editorResult{}, err return editorResult{}, err
+51 -31
View File
@@ -6,11 +6,11 @@ import (
"strings" "strings"
"unicode" "unicode"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/styles" "github.com/alecthomas/chroma/v2/styles"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi"
"golang.org/x/term" "golang.org/x/term"
) )
@@ -69,9 +69,12 @@ type editorModel struct {
} }
var ( var (
cursorStyle = lipgloss.NewStyle().Reverse(true) lineStyle = lipgloss.NewStyle()
statusStyle = lipgloss.NewStyle().Reverse(true) )
lineStyle = lipgloss.NewStyle()
const (
ansiReverseOn = "\x1b[7m"
ansiReverseOff = "\x1b[0m"
) )
func newEditorModel(content, filename string, lexer chroma.Lexer, syntaxName string, showLineNumbers bool) *editorModel { func newEditorModel(content, filename string, lexer chroma.Lexer, syntaxName string, showLineNumbers bool) *editorModel {
@@ -108,28 +111,29 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height m.height = msg.Height
m.ensureCursorVisible() m.ensureCursorVisible()
return m, nil return m, nil
case tea.KeyMsg: case tea.KeyPressMsg:
if m.clearMessage { if m.clearMessage {
m.message = "" m.message = ""
m.clearMessage = false m.clearMessage = false
} }
if msg.Type == tea.KeyCtrlC { key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
m.action = actionQuit m.action = actionQuit
return m, tea.Quit return m, tea.Quit
} }
switch m.mode { switch m.mode {
case modeCommand: case modeCommand:
return m.handleCommand(msg) return m.handleCommand(key)
case modeInsert: case modeInsert:
return m.handleInsert(msg) return m.handleInsert(key)
default: default:
return m.handleNormal(msg) return m.handleNormal(key)
} }
} }
return m, nil return m, nil
} }
func (m *editorModel) View() string { func (m *editorModel) View() tea.View {
if m.width == 0 || m.height == 0 { if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 { if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w m.width = w
@@ -152,19 +156,33 @@ func (m *editorModel) View() string {
} }
if m.height < 2 { if m.height < 2 {
return b.String() return editorView(b.String())
} }
b.WriteByte('\n') b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine())) b.WriteString(renderStatusBar(m.width, m.statusLine()))
if m.height < 3 { if m.height < 3 {
return b.String() return editorView(b.String())
} }
b.WriteByte('\n') b.WriteByte('\n')
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine())) b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
return b.String() return editorView(b.String())
}
func renderStatusBar(width int, text string) string {
return ansiReverseOn + lineStyle.Width(width).Render(text) + ansiReverseOff
}
func renderCursorCell(cell string) string {
return ansiReverseOn + cell + ansiReverseOff
}
func editorView(content string) tea.View {
v := tea.NewView(content)
v.AltScreen = true
return v
} }
func (m *editorModel) result() editorResult { func (m *editorModel) result() editorResult {
@@ -176,8 +194,8 @@ func (m *editorModel) result() editorResult {
} }
} }
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
switch msg.Type { switch key.Code {
case tea.KeyEsc: case tea.KeyEsc:
m.mode = modeNormal m.mode = modeNormal
m.command = "" m.command = ""
@@ -196,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
case tea.KeyBackspace: case tea.KeyBackspace:
m.command = popRune(m.command) m.command = popRune(m.command)
case tea.KeyRunes: default:
m.command += string(msg.Runes) if key.Text != "" {
m.command += key.Text
}
} }
return m, nil return m, nil
} }
func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *editorModel) handleInsert(key tea.Key) (tea.Model, tea.Cmd) {
switch msg.Type { switch key.Code {
case tea.KeyEsc: case tea.KeyEsc:
m.mode = modeNormal m.mode = modeNormal
case tea.KeyEnter: case tea.KeyEnter:
@@ -222,8 +242,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveDown() m.moveDown()
case tea.KeyTab: case tea.KeyTab:
m.insertRune('\t') m.insertRune('\t')
case tea.KeyRunes: default:
for _, r := range msg.Runes { for _, r := range key.Text {
m.insertRune(r) m.insertRune(r)
} }
} }
@@ -231,8 +251,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
switch msg.Type { switch key.Code {
case tea.KeyEsc: case tea.KeyEsc:
m.pending = 0 m.pending = 0
case tea.KeyLeft: case tea.KeyLeft:
@@ -243,8 +263,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp() m.moveUp()
case tea.KeyDown: case tea.KeyDown:
m.moveDown() m.moveDown()
case tea.KeyRunes: default:
for _, r := range msg.Runes { for _, r := range key.Text {
if m.handleNormalRune(r) { if m.handleNormalRune(r) {
break break
} }
@@ -409,9 +429,9 @@ func (m *editorModel) renderLine(index int) string {
if cursor >= start && cursor <= start+contentWidth { if cursor >= start && cursor <= start+contentWidth {
rel := cursor - start rel := cursor - start
if rel < len(visible) { if rel < len(visible) {
lineText = string(visible[:rel]) + cursorStyle.Render(string(visible[rel])) + string(visible[rel+1:]) lineText = string(visible[:rel]) + renderCursorCell(string(visible[rel])) + string(visible[rel+1:])
} else { } else {
lineText = string(visible) + cursorStyle.Render(" ") lineText = string(visible) + renderCursorCell(" ")
} }
} }
} }
@@ -917,11 +937,11 @@ func (m *editorModel) applyCursor(rendered, highlighted string, line []rune, sta
} }
if cursor == len(line) { if cursor == len(line) {
left := ansi.Cut(highlighted, start, cursor) left := ansi.Cut(highlighted, start, cursor)
return left + cursorStyle.Render(" ") return left + renderCursorCell(" ")
} }
left := ansi.Cut(highlighted, start, cursor) left := ansi.Cut(highlighted, start, cursor)
right := ansi.Cut(highlighted, cursor+1, end) right := ansi.Cut(highlighted, cursor+1, end)
return left + cursorStyle.Render(string(line[cursor])) + right return left + renderCursorCell(string(line[cursor])) + right
} }
func (m *editorModel) executeSearch(pattern string) { func (m *editorModel) executeSearch(pattern string) {
+16 -1
View File
@@ -1,6 +1,7 @@
package exec package exec
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate" "github.com/bishopfox/sliver/client/command/generate"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
@@ -33,7 +34,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("ignore-stderr", "S", false, "don't print STDERR output") f.BoolP("ignore-stderr", "S", false, "don't print STDERR output")
f.StringP("stdout", "O", "", "remote path to redirect STDOUT to") f.StringP("stdout", "O", "", "remote path to redirect STDOUT to")
f.StringP("stderr", "E", "", "remote path to redirect STDERR to") f.StringP("stderr", "E", "", "remote path to redirect STDERR to")
f.Bool("env-inheritance", true, "inherit environment variables from the current process") f.Bool("env-inheritance", false, "inherit environment variables from the current process")
f.StringArray("env", nil, "set an environment variable for the child process (key=value)") f.StringArray("env", nil, "set an environment variable for the child process (key=value)")
f.StringP("name", "n", "", "name to assign loot (optional)") f.StringP("name", "n", "", "name to assign loot (optional)")
f.Uint32P("ppid", "P", 0, "parent process id (optional, Windows only)") f.Uint32P("ppid", "P", 0, "parent process id (optional, Windows only)")
@@ -93,6 +94,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.Gen(executeAssemblyCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to assembly file (required)")) carapace.Gen(executeAssemblyCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to assembly file (required)"))
carapace.Gen(executeAssemblyCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint (optional)")) carapace.Gen(executeAssemblyCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(executeAssemblyCmd, 0)
executeShellcodeCmd := &cobra.Command{ executeShellcodeCmd := &cobra.Command{
Use: consts.ExecuteShellcodeStr, Use: consts.ExecuteShellcodeStr,
@@ -112,6 +114,14 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution") f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution")
f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)") f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)")
f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)") f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)")
f.Uint32("shellcode-entropy", 1, "Shellcode entropy (Donut: 1=none, 2=random names, 3=random+encrypt) (windows PE input only)")
f.Bool("shellcode-compress", false, "Enable shellcode compression (aPLib) (windows PE input only)")
f.Uint32("shellcode-exitopt", 1, "Shellcode exit option (Donut: 1=exit thread, 2=exit process, 3=block) (windows PE input only)")
f.Uint32("shellcode-bypass", 3, "Shellcode bypass mode (Donut: 1=none, 2=abort, 3=continue) (windows PE input only)")
f.Uint32("shellcode-headers", 1, "Shellcode headers handling (Donut: 1=overwrite, 2=keep) (windows PE input only)")
f.Bool("shellcode-thread", false, "Run unmanaged EXE entrypoint as a new thread (Donut) (windows PE input only)")
f.Bool("shellcode-unicode", false, "Use Unicode command line for unmanaged DLL entrypoints (Donut) (windows PE input only)")
f.Uint32("shellcode-oep", 0, "Override original entry point (OEP) (Donut) (windows PE input only)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds") f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
}) })
@@ -119,6 +129,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["shikata-ga-nai"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures") (*comp)["shikata-ga-nai"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures")
}) })
carapace.Gen(executeShellcodeCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shellcode file (required)")) carapace.Gen(executeShellcodeCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shellcode file (required)"))
completers.RegisterLocalFilePathPositionalCompletion(executeShellcodeCmd, 0)
sideloadCmd := &cobra.Command{ sideloadCmd := &cobra.Command{
Use: consts.SideloadStr, Use: consts.SideloadStr,
@@ -147,6 +158,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.Gen(sideloadCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shared library file (required)")) carapace.Gen(sideloadCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shared library file (required)"))
carapace.Gen(sideloadCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the binary (optional)")) carapace.Gen(sideloadCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the binary (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(sideloadCmd, 0)
spawnDllCmd := &cobra.Command{ spawnDllCmd := &cobra.Command{
Use: consts.SpawnDllStr, Use: consts.SpawnDllStr,
@@ -175,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.Gen(spawnDllCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to DLL file (required)")) carapace.Gen(spawnDllCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to DLL file (required)"))
carapace.Gen(spawnDllCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the DLL entrypoint (optional)")) carapace.Gen(spawnDllCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the DLL entrypoint (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(spawnDllCmd, 0)
migrateCmd := &cobra.Command{ migrateCmd := &cobra.Command{
Use: consts.MigrateStr, Use: consts.MigrateStr,
@@ -260,6 +273,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["custom-exe"] = carapace.ActionFiles() (*comp)["custom-exe"] = carapace.ActionFiles()
(*comp)["profile"] = generate.ProfileNameCompleter(con) (*comp)["profile"] = generate.ProfileNameCompleter(con)
}) })
completers.RegisterLocalFilePathFlagCompletion(psExecCmd, "custom-exe")
carapace.Gen(psExecCmd).PositionalCompletion(carapace.ActionValues().Usage("hostname (required)")) carapace.Gen(psExecCmd).PositionalCompletion(carapace.ActionValues().Usage("hostname (required)"))
sshCmd := &cobra.Command{ sshCmd := &cobra.Command{
@@ -290,6 +304,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["private-key"] = carapace.ActionFiles() (*comp)["private-key"] = carapace.ActionFiles()
(*comp)["kerberos-keytab"] = carapace.ActionFiles() (*comp)["kerberos-keytab"] = carapace.ActionFiles()
}) })
completers.RegisterLocalFilePathFlagCompletions(sshCmd, "private-key", "kerberos-keytab")
carapace.Gen(sshCmd).PositionalCompletion(carapace.ActionValues().Usage("remote host to SSH to (required)")) carapace.Gen(sshCmd).PositionalCompletion(carapace.ActionValues().Usage("remote host to SSH to (required)"))
carapace.Gen(sshCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("command line with arguments")) carapace.Gen(sshCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("command line with arguments"))
+219 -1
View File
@@ -20,27 +20,44 @@ package exec
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"log" "log"
"os" "os"
"path/filepath"
"strings"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core" "github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/clientpb" "github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb" "github.com/bishopfox/sliver/protobuf/sliverpb"
wasmdonut "github.com/sliverarmory/wasm-donut"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/term" "golang.org/x/term"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
const (
defaultExecuteShellcodeDonutEntropy = wasmdonut.DonutEntropyNone
defaultExecuteShellcodeDonutCompress = wasmdonut.DonutCompressNone
defaultExecuteShellcodeDonutExitOpt = wasmdonut.DonutExitThread
defaultExecuteShellcodeDonutBypass = wasmdonut.DonutBypassContinue
defaultExecuteShellcodeDonutHeaders = wasmdonut.DonutHeadersOverwrite
imageFileDLLMask = 0x2000
)
// ExecuteShellcodeCmd - Execute shellcode in-memory. // ExecuteShellcodeCmd - Execute shellcode in-memory.
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive() session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil { if session == nil && beacon == nil {
return return
} }
targetName := activeTargetName(session, beacon)
targetOS := activeTargetOS(session, beacon)
targetArch := activeTargetArch(session, beacon)
rwxPages, _ := cmd.Flags().GetBool("rwx-pages") rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
interactive, _ := cmd.Flags().GetBool("interactive") interactive, _ := cmd.Flags().GetBool("interactive")
@@ -60,6 +77,27 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n") con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n")
return return
} }
shellcodeConfig, shellcodeFlagsChanged, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
shouldConvertPE, isDLLHint := shouldConvertExecuteShellcodePE(shellcodePath, shellcodeFlagsChanged)
if shouldConvertPE {
if targetOS != "windows" {
con.PrintErrorf("PE input and --shellcode-* options are only supported for Windows targets in execute-shellcode\n")
return
}
con.PrintInfof("Converting PE input to shellcode ...\n")
shellcodeBin, err = donutShellcodeFromPE(shellcodeBin, targetArch, isDLLHint, shellcodeConfig)
if err != nil {
con.PrintErrorf("Failed to convert PE input to shellcode: %s\n", err)
return
}
}
shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai") shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai")
if shikataGaNai { if shikataGaNai {
if !rwxPages { if !rwxPages {
@@ -96,7 +134,7 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
return return
} }
ctrl := make(chan bool) ctrl := make(chan bool)
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName()) msg := fmt.Sprintf("Sending shellcode to %s ...", targetName)
con.SpinUntil(msg, ctrl) con.SpinUntil(msg, ctrl)
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{ shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
Data: shellcodeBin, Data: shellcodeBin,
@@ -135,6 +173,186 @@ func PrintExecuteShellcode(task *sliverpb.Task, con *console.SliverClient) {
} }
} }
func parseExecuteShellcodeFlags(cmd *cobra.Command) (*clientpb.ShellcodeConfig, bool, error) {
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
shellcodeCompressEnabled, _ := cmd.Flags().GetBool("shellcode-compress")
shellcodeExitOpt, _ := cmd.Flags().GetUint32("shellcode-exitopt")
shellcodeBypass, _ := cmd.Flags().GetUint32("shellcode-bypass")
shellcodeHeaders, _ := cmd.Flags().GetUint32("shellcode-headers")
shellcodeThread, _ := cmd.Flags().GetBool("shellcode-thread")
shellcodeUnicode, _ := cmd.Flags().GetBool("shellcode-unicode")
shellcodeOEP, _ := cmd.Flags().GetUint32("shellcode-oep")
anyChanged := cmd.Flags().Changed("shellcode-entropy") ||
cmd.Flags().Changed("shellcode-compress") ||
cmd.Flags().Changed("shellcode-exitopt") ||
cmd.Flags().Changed("shellcode-bypass") ||
cmd.Flags().Changed("shellcode-headers") ||
cmd.Flags().Changed("shellcode-thread") ||
cmd.Flags().Changed("shellcode-unicode") ||
cmd.Flags().Changed("shellcode-oep")
if shellcodeEntropy < 1 || shellcodeEntropy > 3 {
return nil, false, fmt.Errorf("shellcode-entropy must be between 1 and 3")
}
if shellcodeExitOpt < 1 || shellcodeExitOpt > 3 {
return nil, false, fmt.Errorf("shellcode-exitopt must be between 1 and 3")
}
if shellcodeBypass < 1 || shellcodeBypass > 3 {
return nil, false, fmt.Errorf("shellcode-bypass must be between 1 and 3")
}
if shellcodeHeaders < 1 || shellcodeHeaders > 2 {
return nil, false, fmt.Errorf("shellcode-headers must be 1 or 2")
}
shellcodeCompress := uint32(1)
if shellcodeCompressEnabled {
shellcodeCompress = 2
}
return &clientpb.ShellcodeConfig{
Entropy: shellcodeEntropy,
Compress: shellcodeCompress,
ExitOpt: shellcodeExitOpt,
Bypass: shellcodeBypass,
Headers: shellcodeHeaders,
Thread: shellcodeThread,
Unicode: shellcodeUnicode,
OEP: shellcodeOEP,
}, anyChanged, nil
}
func shouldConvertExecuteShellcodePE(shellcodePath string, shellcodeFlagsChanged bool) (bool, bool) {
ext := strings.ToLower(filepath.Ext(shellcodePath))
isPEByExt := ext == ".exe" || ext == ".dll"
return isPEByExt || shellcodeFlagsChanged, ext == ".dll"
}
func activeTargetName(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetName()
}
if beacon != nil {
return beacon.GetName()
}
return "target"
}
func activeTargetOS(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return strings.ToLower(session.GetOS())
}
if beacon != nil {
return strings.ToLower(beacon.GetOS())
}
return ""
}
func activeTargetArch(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetArch()
}
if beacon != nil {
return beacon.GetArch()
}
return ""
}
type executeShellcodeDonutOptions struct {
entropy int
compress int
exitOpt int
bypass int
headers int
thread bool
unicode bool
oep uint32
}
func normalizeExecuteShellcodeDonutConfig(config *clientpb.ShellcodeConfig) executeShellcodeDonutOptions {
opts := executeShellcodeDonutOptions{
entropy: defaultExecuteShellcodeDonutEntropy,
compress: defaultExecuteShellcodeDonutCompress,
exitOpt: defaultExecuteShellcodeDonutExitOpt,
bypass: defaultExecuteShellcodeDonutBypass,
headers: defaultExecuteShellcodeDonutHeaders,
}
if config == nil {
return opts
}
if config.Entropy >= 1 && config.Entropy <= 3 {
opts.entropy = int(config.Entropy)
}
if config.Compress >= 1 && config.Compress <= 2 {
opts.compress = int(config.Compress)
}
if config.ExitOpt >= 1 && config.ExitOpt <= 3 {
opts.exitOpt = int(config.ExitOpt)
}
if config.Bypass >= 1 && config.Bypass <= 3 {
opts.bypass = int(config.Bypass)
}
if config.Headers >= 1 && config.Headers <= 2 {
opts.headers = int(config.Headers)
}
opts.thread = config.Thread
opts.unicode = config.Unicode
if config.OEP > 0 {
opts.oep = config.OEP
}
return opts
}
func donutShellcodeFromPE(data []byte, arch string, isDLLHint bool, config *clientpb.ShellcodeConfig) ([]byte, error) {
peFile, err := pe.NewFile(bytes.NewReader(data))
if err != nil {
return nil, err
}
isDLL := isDLLHint || ((peFile.FileHeader.Characteristics & imageFileDLLMask) != 0)
ext := ".exe"
if isDLL {
ext = ".dll"
}
opts := normalizeExecuteShellcodeDonutConfig(config)
result, err := wasmdonut.Generate(context.Background(), data, ext, wasmdonut.GenerateOptions{
Ext: ext,
Arch: getExecuteShellcodeDonutArch(arch),
Bypass: opts.bypass,
Headers: opts.headers,
Entropy: opts.entropy,
Compress: opts.compress,
ExitOpt: opts.exitOpt,
Thread: opts.thread,
Unicode: opts.unicode,
OEP: opts.oep,
})
if err != nil {
return nil, err
}
return addExecuteShellcodeStackCheck(result.Loader), nil
}
func getExecuteShellcodeDonutArch(arch string) int {
donutArch := wasmdonut.DonutArchX84
switch strings.ToLower(arch) {
case "x32", "x86", "386":
donutArch = wasmdonut.DonutArchX86
case "x64", "amd64":
donutArch = wasmdonut.DonutArchX64
case "x84":
donutArch = wasmdonut.DonutArchX84
}
return donutArch
}
func addExecuteShellcodeStackCheck(shellcode []byte) []byte {
stackCheckPrologue := []byte{
0x48, 0x83, 0xE4, 0xF0, // and rsp,0xfffffffffffffff0
0x48, 0x83, 0xC4, 0x08, // add rsp,0x8
}
return append(stackCheckPrologue, shellcode...)
}
func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) { func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) {
// Check active session // Check active session
session := con.ActiveTarget.GetSessionInteractive() session := con.ActiveTarget.GetSessionInteractive()
@@ -0,0 +1,100 @@
package exec
import (
"testing"
"github.com/spf13/cobra"
)
func TestParseExecuteShellcodeFlagsDefaults(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if changed {
t.Fatalf("expected changed=false with defaults")
}
if cfg == nil {
t.Fatalf("expected non-nil config")
}
if cfg.Entropy != 1 || cfg.Compress != 1 || cfg.ExitOpt != 1 || cfg.Bypass != 3 || cfg.Headers != 1 {
t.Fatalf("unexpected defaults: %+v", cfg)
}
if cfg.Thread || cfg.Unicode || cfg.OEP != 0 {
t.Fatalf("unexpected default bool/OEP values: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsChanged(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-entropy", "2"); err != nil {
t.Fatalf("set shellcode-entropy: %v", err)
}
if err := cmd.Flags().Set("shellcode-compress", "true"); err != nil {
t.Fatalf("set shellcode-compress: %v", err)
}
if err := cmd.Flags().Set("shellcode-thread", "true"); err != nil {
t.Fatalf("set shellcode-thread: %v", err)
}
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if !changed {
t.Fatalf("expected changed=true")
}
if cfg.Entropy != 2 || cfg.Compress != 2 || !cfg.Thread {
t.Fatalf("unexpected changed config: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsValidation(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-bypass", "9"); err != nil {
t.Fatalf("set shellcode-bypass: %v", err)
}
if _, _, err := parseExecuteShellcodeFlags(cmd); err == nil {
t.Fatalf("expected validation error for shellcode-bypass")
}
}
func TestShouldConvertExecuteShellcodePE(t *testing.T) {
testCases := []struct {
name string
path string
flagsChanged bool
wantConvert bool
wantDLL bool
}{
{name: "exe by extension", path: "/tmp/payload.exe", flagsChanged: false, wantConvert: true, wantDLL: false},
{name: "dll by extension", path: "/tmp/payload.dll", flagsChanged: false, wantConvert: true, wantDLL: true},
{name: "raw shellcode", path: "/tmp/payload.bin", flagsChanged: false, wantConvert: false, wantDLL: false},
{name: "flags force conversion", path: "/tmp/payload.bin", flagsChanged: true, wantConvert: true, wantDLL: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotConvert, gotDLL := shouldConvertExecuteShellcodePE(tc.path, tc.flagsChanged)
if gotConvert != tc.wantConvert || gotDLL != tc.wantDLL {
t.Fatalf("unexpected result convert=%v dll=%v, expected convert=%v dll=%v", gotConvert, gotDLL, tc.wantConvert, tc.wantDLL)
}
})
}
}
func newExecuteShellcodeFlagsTestCommand(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "execute-shellcode"}
cmd.Flags().Uint32("shellcode-entropy", 1, "")
cmd.Flags().Bool("shellcode-compress", false, "")
cmd.Flags().Uint32("shellcode-exitopt", 1, "")
cmd.Flags().Uint32("shellcode-bypass", 3, "")
cmd.Flags().Uint32("shellcode-headers", 1, "")
cmd.Flags().Bool("shellcode-thread", false, "")
cmd.Flags().Bool("shellcode-unicode", false, "")
cmd.Flags().Uint32("shellcode-oep", 0, "")
return cmd
}
+4
View File
@@ -79,10 +79,14 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if token || hidden || ppid != 0 { if token || hidden || ppid != 0 {
if (session != nil && session.OS != "windows") || (beacon != nil && beacon.OS != "windows") { if (session != nil && session.OS != "windows") || (beacon != nil && beacon.OS != "windows") {
con.PrintErrorf("The token, hide window, and ppid options are not valid on %s\n", session.OS) con.PrintErrorf("The token, hide window, and ppid options are not valid on %s\n", session.OS)
ctrl <- true
<-ctrl
return return
} }
if envInheritance || len(envVars) > 0 { if envInheritance || len(envVars) > 0 {
con.PrintErrorf("The env and env-inheritance options are not supported with token, hidden, or ppid\n") con.PrintErrorf("The env and env-inheritance options are not supported with token, hidden, or ppid\n")
ctrl <- true
<-ctrl
return return
} }
exec, err = con.Rpc.ExecuteWindows(context.Background(), &sliverpb.ExecuteWindowsReq{ exec, err = con.Rpc.ExecuteWindows(context.Background(), &sliverpb.ExecuteWindowsReq{
+3
View File
@@ -1,6 +1,7 @@
package extensions package extensions
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/command/use" "github.com/bishopfox/sliver/client/command/use"
@@ -33,6 +34,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
} }
extensionCmd.AddCommand(extensionLoadCmd) extensionCmd.AddCommand(extensionLoadCmd)
carapace.Gen(extensionLoadCmd).PositionalCompletion(carapace.ActionDirectories().Usage("path to the extension directory")) carapace.Gen(extensionLoadCmd).PositionalCompletion(carapace.ActionDirectories().Usage("path to the extension directory"))
completers.RegisterLocalFilePathPositionalCompletion(extensionLoadCmd, 0)
extensionInstallCmd := &cobra.Command{ extensionInstallCmd := &cobra.Command{
Use: consts.InstallStr, Use: consts.InstallStr,
@@ -45,6 +47,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
} }
extensionCmd.AddCommand(extensionInstallCmd) extensionCmd.AddCommand(extensionInstallCmd)
carapace.Gen(extensionInstallCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to the extension .tar.gz or directory")) carapace.Gen(extensionInstallCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to the extension .tar.gz or directory"))
completers.RegisterLocalFilePathPositionalCompletion(extensionInstallCmd, 0)
extensionRmCmd := &cobra.Command{ extensionRmCmd := &cobra.Command{
Use: consts.RmStr, Use: consts.RmStr,
+3
View File
@@ -1,6 +1,7 @@
package filesystem package filesystem
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/command/loot" "github.com/bishopfox/sliver/client/command/loot"
@@ -174,6 +175,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.ActionValues().Usage("path to the file or directory to download"), carapace.ActionValues().Usage("path to the file or directory to download"),
carapace.ActionFiles().Usage("local path where the downloaded file will be saved (optional)"), carapace.ActionFiles().Usage("local path where the downloaded file will be saved (optional)"),
) )
completers.RegisterLocalFilePathPositionalCompletion(downloadCmd, 1)
uploadCmd := &cobra.Command{ uploadCmd := &cobra.Command{
Use: consts.UploadStr, Use: consts.UploadStr,
@@ -194,6 +196,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.ActionFiles().Usage("local path to the file to upload"), carapace.ActionFiles().Usage("local path to the file to upload"),
carapace.ActionValues().Usage("path to the file or directory to upload to (optional)"), carapace.ActionValues().Usage("path to the file or directory to upload to (optional)"),
) )
completers.RegisterLocalFilePathPositionalCompletion(uploadCmd, 0)
memfilesCmd := &cobra.Command{ memfilesCmd := &cobra.Command{
Use: consts.MemfilesStr, Use: consts.MemfilesStr,
+4 -5
View File
@@ -29,7 +29,6 @@ import (
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb" "github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb" "github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/protobuf/proto" "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 // grepLineResult - Add color or formatting for results for console output
func grepLineResult(positions []*sliverpb.GrepLinePosition, line string, colorize bool, allowFormatting bool) string { func grepLineResult(positions []*sliverpb.GrepLinePosition, line string, colorize bool, allowFormatting bool) string {
var result string = "" var result string = ""
var matchOutput func(a ...interface{}) string
var previousPositionEnd int32 = 0 var previousPositionEnd int32 = 0
matchStyle := console.StyleNormal
if colorize { if colorize {
matchOutput = color.New(color.FgRed, color.Bold).SprintFunc() matchStyle = console.StyleBoldRed
} else if allowFormatting { } else if allowFormatting {
matchOutput = color.New(color.Bold).SprintFunc() matchStyle = console.StyleBold
} }
for idx, position := range positions { for idx, position := range positions {
if colorize || allowFormatting { if colorize || allowFormatting {
result += line[previousPositionEnd:position.Start] 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 { if idx == len(positions)-1 {
result += line[position.End:] result += line[position.End:]
} else { } else {
+7 -7
View File
@@ -42,20 +42,20 @@ func PrintCanaries(con *console.SliverClient, canaries []*clientpb.DNSCanary, bu
if burnedOnly && !canary.Triggered { if burnedOnly && !canary.Triggered {
continue continue
} }
lineColor := console.Normal lineStyle := console.StyleNormal
if canary.Triggered { if canary.Triggered {
lineColor = console.Bold + console.Red lineStyle = console.StyleBoldRed
} }
firstTrigger := "Never" firstTrigger := "Never"
latestTrigger := "Never" latestTrigger := "Never"
if canary.Triggered { if canary.Triggered {
firstTrigger = fmt.Sprintf(lineColor+"%s"+console.Normal, canary.FirstTriggered) firstTrigger = lineStyle.Render(canary.FirstTriggered)
latestTrigger = fmt.Sprintf(lineColor+"%s"+console.Normal, canary.LatestTrigger) latestTrigger = lineStyle.Render(canary.LatestTrigger)
} }
row := table.Row{ row := table.Row{
fmt.Sprintf(lineColor+"%s"+console.Normal, canary.ImplantName), lineStyle.Render(canary.ImplantName),
fmt.Sprintf(lineColor+"%s"+console.Normal, canary.Domain), lineStyle.Render(canary.Domain),
fmt.Sprintf(lineColor+"%v"+console.Normal, canary.Triggered), lineStyle.Render(fmt.Sprintf("%v", canary.Triggered)),
firstTrigger, firstTrigger,
latestTrigger, latestTrigger,
} }
+46 -1
View File
@@ -1,6 +1,9 @@
package generate package generate
import ( import (
"strings"
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders" shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
@@ -11,6 +14,15 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
const spoofMetadataLongHelp = `
Spoof Metadata:
--spoof-metadata
Use metadata settings from <client app dir>/spoof-metadata.yaml.
--spoof-metadata /path/to/donor.exe
Use metadata from the specified donor file. The donor must match the target format/goos/goarch.
`
// Commands returns the “ command and its subcommands. // Commands returns the “ command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command { func Commands(con *console.SliverClient) []*cobra.Command {
// [ Generate ] -------------------------------------------------------------- // [ Generate ] --------------------------------------------------------------
@@ -83,6 +95,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("skip-tests", "s", false, "skip testing the traffic encoder (not recommended)") f.BoolP("skip-tests", "s", false, "skip testing the traffic encoder (not recommended)")
}) })
carapace.Gen(trafficEncodersAddCmd).PositionalCompletion(carapace.ActionFiles("wasm").Tag("wasm files").Usage("local file path (expects .wasm)")) carapace.Gen(trafficEncodersAddCmd).PositionalCompletion(carapace.ActionFiles("wasm").Tag("wasm files").Usage("local file path (expects .wasm)"))
completers.RegisterLocalFilePathPositionalCompletion(trafficEncodersAddCmd, 0)
trafficEncodersCmd.AddCommand(trafficEncodersAddCmd) trafficEncodersCmd.AddCommand(trafficEncodersAddCmd)
trafficEncodersRmCmd := &cobra.Command{ trafficEncodersRmCmd := &cobra.Command{
@@ -115,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(regenerateCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(regenerateCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant") (*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
}) })
completers.RegisterLocalFilePathFlagCompletion(regenerateCmd, "save")
carapace.Gen(regenerateCmd).PositionalCompletion(ImplantBuildNameCompleter(con)) carapace.Gen(regenerateCmd).PositionalCompletion(ImplantBuildNameCompleter(con))
// [ Profiles ] -------------------------------------------------------------- // [ Profiles ] --------------------------------------------------------------
@@ -151,6 +165,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant") (*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con) (*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
}) })
completers.RegisterLocalFilePathFlagCompletion(profilesGenerateCmd, "save")
carapace.Gen(profilesGenerateCmd).PositionalCompletion(ProfileNameCompleter(con)) carapace.Gen(profilesGenerateCmd).PositionalCompletion(ProfileNameCompleter(con))
profilesCmd.AddCommand(profilesGenerateCmd) profilesCmd.AddCommand(profilesGenerateCmd)
@@ -182,6 +197,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("prepend-size", "p", false, "Prepend stage size") f.BoolP("prepend-size", "p", false, "Prepend stage size")
f.StringP("compress", "c", "", "Compress stage (zlib, gzip, deflate9 or deflate)") f.StringP("compress", "c", "", "Compress stage (zlib, gzip, deflate9 or deflate)")
}) })
completers.RegisterLocalFilePathFlagCompletion(profilesStageCmd, "save")
carapace.Gen(profilesStageCmd).PositionalCompletion(ProfileNameCompleter(con)) carapace.Gen(profilesStageCmd).PositionalCompletion(ProfileNameCompleter(con))
profilesCmd.AddCommand(profilesStageCmd) profilesCmd.AddCommand(profilesStageCmd)
@@ -342,6 +358,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
f.StringP("limit-hostname", "z", "", "limit execution to specified hostname") f.StringP("limit-hostname", "z", "", "limit execution to specified hostname")
f.StringP("limit-fileexists", "F", "", "limit execution to hosts with this file in the filesystem") f.StringP("limit-fileexists", "F", "", "limit execution to hosts with this file in the filesystem")
f.StringP("limit-locale", "L", "", "limit execution to hosts that match this locale") f.StringP("limit-locale", "L", "", "limit execution to hosts that match this locale")
bindSpoofMetadataFlag(f)
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)") f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
@@ -384,6 +401,32 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
_ = f.MarkDeprecated("donut-oep", "use --shellcode-oep") _ = f.MarkDeprecated("donut-oep", "use --shellcode-oep")
_ = f.MarkHidden("donut-oep") _ = f.MarkHidden("donut-oep")
}) })
appendSpoofMetadataLongHelp(cmd)
}
func bindSpoofMetadataFlag(f *pflag.FlagSet) {
if f == nil {
return
}
spoofMetadataFlag := f.VarPF(newSpoofMetadataFlagValue(), spoofMetadataFlagName, "", "spoof executable metadata (optional donor path)")
if spoofMetadataFlag != nil {
spoofMetadataFlag.NoOptDefVal = "true"
}
}
func appendSpoofMetadataLongHelp(cmd *cobra.Command) {
if cmd == nil {
return
}
if strings.Contains(cmd.Long, "--spoof-metadata") {
return
}
trimmed := strings.TrimRight(cmd.Long, "\n")
if trimmed == "" {
cmd.Long = strings.TrimLeft(spoofMetadataLongHelp, "\n")
return
}
cmd.Long = trimmed + spoofMetadataLongHelp
} }
// coreImplantFlagCompletions binds completions to flags registered in coreImplantFlags. // coreImplantFlagCompletions binds completions to flags registered in coreImplantFlags.
@@ -394,11 +437,13 @@ func coreImplantFlagCompletions(cmd *cobra.Command, con *console.SliverClient) {
(*comp)["arch"] = ArchCompleter(con) (*comp)["arch"] = ArchCompleter(con)
(*comp)["strategy"] = carapace.ActionValuesDescribed([]string{"r", "random", "rd", "random domain", "s", "sequential"}...).Tag("C2 strategy") (*comp)["strategy"] = carapace.ActionValuesDescribed([]string{"r", "random", "rd", "random domain", "s", "sequential"}...).Tag("C2 strategy")
(*comp)["format"] = FormatCompleter() (*comp)["format"] = FormatCompleter()
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant") (*comp)["save"] = completers.LocalFilePathCompleter().Tag("directory/file to save implant")
(*comp)[spoofMetadataFlagName] = carapace.ActionFiles().Tag("optional donor metadata file")
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con) (*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
(*comp)["traffic-encoders"] = TrafficEncodersCompleter(con).UniqueList(",") (*comp)["traffic-encoders"] = TrafficEncodersCompleter(con).UniqueList(",")
(*comp)["c2profile"] = HTTPC2Completer(con) (*comp)["c2profile"] = HTTPC2Completer(con)
}) })
completers.RegisterLocalFilePathFlagCompletions(cmd, "save", "debug-file", spoofMetadataFlagName)
registerImplantTargetFlagCompletions(cmd, con) registerImplantTargetFlagCompletions(cmd, con)
} }
+51
View File
@@ -0,0 +1,51 @@
package generate
import (
"testing"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
func TestGenerateSaveFlagCompletionRegistered(t *testing.T) {
cmds := Commands(&console.SliverClient{})
generateCmd := commandByUse(cmds, consts.GenerateStr)
if generateCmd == nil {
t.Fatalf("missing %q command", consts.GenerateStr)
}
assertFlagHasDefaultFileCompletion(t, generateCmd, "save")
beaconCmd := commandByUse(generateCmd.Commands(), consts.BeaconStr)
if beaconCmd == nil {
t.Fatalf("missing %q subcommand", consts.BeaconStr)
}
assertFlagHasDefaultFileCompletion(t, beaconCmd, "save")
}
func assertFlagHasDefaultFileCompletion(t *testing.T, cmd *cobra.Command, flag string) {
t.Helper()
completionFn, ok := cmd.GetFlagCompletionFunc(flag)
if !ok {
t.Fatalf("%q missing %q flag completion func", cmd.Use, flag)
}
values, directive := completionFn(cmd, nil, "")
if len(values) != 0 {
t.Fatalf("%q %q completion should return no explicit values, got %d", cmd.Use, flag, len(values))
}
if directive != cobra.ShellCompDirectiveDefault {
t.Fatalf("%q %q completion directive mismatch: got=%v want=%v", cmd.Use, flag, directive, cobra.ShellCompDirectiveDefault)
}
}
func commandByUse(cmds []*cobra.Command, use string) *cobra.Command {
for _, cmd := range cmds {
if cmd.Use == use {
return cmd
}
}
return nil
}
+8 -3
View File
@@ -41,8 +41,13 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
if config == nil { if config == nil {
return return
} }
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
config.IsBeacon = true config.IsBeacon = true
err := parseBeaconFlags(cmd, config) err = parseBeaconFlags(cmd, config)
if err != nil { if err != nil {
con.PrintErrorf("%s\n", err) con.PrintErrorf("%s\n", err)
return return
@@ -52,9 +57,9 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
save, _ = os.Getwd() save, _ = os.Getwd()
} }
if external, _ := cmd.Flags().GetBool("external-builder"); !external { if external, _ := cmd.Flags().GetBool("external-builder"); !external {
compile(name, config, save, con) compile(name, config, spoofMetadata, save, con)
} else { } else {
externalBuild(name, config, save, con) externalBuild(name, config, spoofMetadata, save, con)
} }
} }
+4 -4
View File
@@ -15,19 +15,19 @@ func GenerateInfoCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
con.PrintErrorf("Failed to get compiler information: %s\n", err) con.PrintErrorf("Failed to get compiler information: %s\n", err)
return 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.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 { for _, cc := range compiler.CrossCompilers {
con.Printf("%s/%s - %s\n", cc.TargetGOOS, cc.TargetGOARCH, cc.GetCCPath()) con.Printf("%s/%s - %s\n", cc.TargetGOOS, cc.TargetGOARCH, cc.GetCCPath())
} }
con.Println() 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 { for _, target := range compiler.Targets {
con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format)) con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format))
} }
con.Println() 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 { for _, target := range compiler.UnsupportedTargets {
con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format)) con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format))
} }
+502 -13
View File
@@ -19,7 +19,9 @@ package generate
*/ */
import ( import (
"bytes"
"context" "context"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@@ -33,6 +35,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants" consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms" "github.com/bishopfox/sliver/client/forms"
@@ -90,6 +94,11 @@ var (
ErrNoSelection = errors.New("no selection") ErrNoSelection = errors.New("no selection")
) )
const (
spoofMetadataFlagName = "spoof-metadata"
imageFileDLLCharacteristic = 0x2000
)
// GenerateCmd - The main command used to generate implant binaries // GenerateCmd - The main command used to generate implant binaries
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) { func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if shouldRunGenerateForm(cmd, con, args) { if shouldRunGenerateForm(cmd, con, args) {
@@ -112,14 +121,19 @@ func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if config == nil { if config == nil {
return return
} }
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
save, _ := cmd.Flags().GetString("save") save, _ := cmd.Flags().GetString("save")
if save == "" { if save == "" {
save, _ = os.Getwd() save, _ = os.Getwd()
} }
if external, _ := cmd.Flags().GetBool("external-builder"); !external { if external, _ := cmd.Flags().GetBool("external-builder"); !external {
compile(name, config, save, con) compile(name, config, spoofMetadata, save, con)
} else { } else {
_, err := externalBuild(name, config, save, con) _, err := externalBuild(name, config, spoofMetadata, save, con)
if err != nil { if err != nil {
switch err { switch err {
case ErrNoExternalBuilder: case ErrNoExternalBuilder:
@@ -518,6 +532,315 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
return name, config return name, config
} }
func parseSpoofMetadataFlag(cmd *cobra.Command, config *clientpb.ImplantConfig) (*clientpb.SpoofMetadataConfig, error) {
if cmd == nil {
return nil, nil
}
spoofMetadataFlag := cmd.Flags().Lookup(spoofMetadataFlagName)
if spoofMetadataFlag == nil || !spoofMetadataFlag.Changed {
return nil, nil
}
spoofMetadataValue := ""
if spoofMetadataFlag.Value != nil {
spoofMetadataValue = strings.TrimSpace(spoofMetadataFlag.Value.String())
}
switch strings.ToLower(spoofMetadataValue) {
case "false":
if len(cmd.Flags().Args()) > 0 {
return nil, fmt.Errorf("--%s=false does not accept a donor path argument", spoofMetadataFlagName)
}
return nil, nil
case "", "true":
pathArg, err := spoofMetadataPathArg(cmd)
if err != nil {
return nil, err
}
if pathArg == "" {
return parseSpoofMetadataFromConfigFile()
}
return parseSpoofMetadataFromPath(pathArg, config)
default:
if len(cmd.Flags().Args()) > 0 {
return nil, fmt.Errorf("--%s accepts a donor path from either the flag value or positional argument, not both", spoofMetadataFlagName)
}
return parseSpoofMetadataFromPath(spoofMetadataValue, config)
}
}
func spoofMetadataPathArg(cmd *cobra.Command) (string, error) {
if cmd == nil {
return "", nil
}
switch args := cmd.Flags().Args(); len(args) {
case 0:
return "", nil
case 1:
return strings.TrimSpace(args[0]), nil
default:
return "", fmt.Errorf("--%s accepts at most one donor path argument", spoofMetadataFlagName)
}
}
func parseSpoofMetadataFromConfigFile() (*clientpb.SpoofMetadataConfig, error) {
configPath := assets.SpoofMetadataConfigPath()
config, err := assets.LoadSpoofMetadataConfig()
if err != nil {
return nil, fmt.Errorf("failed to load spoof metadata config %s: %w", configPath, err)
}
spoofMetadata, err := spoofMetadataConfigToProtobuf(config)
if err != nil {
return nil, fmt.Errorf("invalid spoof metadata config %s: %w", configPath, err)
}
return spoofMetadata, nil
}
func parseSpoofMetadataFromPath(pathValue string, config *clientpb.ImplantConfig) (*clientpb.SpoofMetadataConfig, error) {
if config == nil {
return nil, errors.New("missing implant configuration for spoof metadata validation")
}
pathValue = expandPath(pathValue)
sourceData, err := os.ReadFile(pathValue)
if err != nil {
return nil, fmt.Errorf("failed to read spoof metadata source path %s: %w", pathValue, err)
}
if err := validateSpoofMetadataSourceForTarget(config, pathValue, sourceData); err != nil {
return nil, err
}
return &clientpb.SpoofMetadataConfig{
PE: &clientpb.PESpoofMetadataConfig{
Source: &clientpb.SpoofMetadataFile{
Name: filepath.Base(pathValue),
Data: sourceData,
},
},
}, nil
}
func validateSpoofMetadataSourceForTarget(config *clientpb.ImplantConfig, sourcePath string, sourceData []byte) error {
if config == nil {
return errors.New("missing implant configuration for spoof metadata validation")
}
if strings.ToLower(config.GetGOOS()) != "windows" {
return fmt.Errorf("--%s path source currently supports only windows PE targets (got %s/%s)", spoofMetadataFlagName, config.GetGOOS(), config.GetGOARCH())
}
switch config.GetFormat() {
case clientpb.OutputFormat_EXECUTABLE, clientpb.OutputFormat_SERVICE, clientpb.OutputFormat_SHARED_LIB:
default:
return fmt.Errorf("--%s path source requires an executable/service/shared target format (got %s)", spoofMetadataFlagName, nameOfOutputFormat(config.GetFormat()))
}
peFile, err := pe.NewFile(bytes.NewReader(sourceData))
if err != nil {
return fmt.Errorf("spoof metadata source %s is not a valid PE: %w", sourcePath, err)
}
defer peFile.Close()
expectedMachine, err := expectedPEMachineForGoArch(config.GetGOARCH())
if err != nil {
return err
}
if peFile.FileHeader.Machine != expectedMachine {
return fmt.Errorf(
"spoof metadata source %s has machine %s, expected %s for target arch %s",
sourcePath,
peMachineName(peFile.FileHeader.Machine),
peMachineName(expectedMachine),
config.GetGOARCH(),
)
}
isDLL := peFile.FileHeader.Characteristics&imageFileDLLCharacteristic != 0
if config.GetFormat() == clientpb.OutputFormat_SHARED_LIB && !isDLL {
return fmt.Errorf("spoof metadata source %s must be a DLL for shared target format", sourcePath)
}
if (config.GetFormat() == clientpb.OutputFormat_EXECUTABLE || config.GetFormat() == clientpb.OutputFormat_SERVICE) && isDLL {
return fmt.Errorf("spoof metadata source %s appears to be a DLL but target format is %s", sourcePath, nameOfOutputFormat(config.GetFormat()))
}
return nil
}
func expectedPEMachineForGoArch(goarch string) (uint16, error) {
switch strings.ToLower(strings.TrimSpace(goarch)) {
case "amd64", "x64":
return pe.IMAGE_FILE_MACHINE_AMD64, nil
case "386", "x86", "i386":
return pe.IMAGE_FILE_MACHINE_I386, nil
case "arm64", "aarch64":
return pe.IMAGE_FILE_MACHINE_ARM64, nil
default:
return 0, fmt.Errorf("unsupported windows architecture for spoof metadata source validation: %s", goarch)
}
}
func peMachineName(machine uint16) string {
switch machine {
case pe.IMAGE_FILE_MACHINE_AMD64:
return "amd64"
case pe.IMAGE_FILE_MACHINE_I386:
return "386"
case pe.IMAGE_FILE_MACHINE_ARM64:
return "arm64"
default:
return fmt.Sprintf("0x%x", machine)
}
}
func spoofMetadataConfigToProtobuf(config *assets.SpoofMetadataConfig) (*clientpb.SpoofMetadataConfig, error) {
if config == nil {
return nil, errors.New("missing spoof metadata configuration")
}
spoofMetadata := &clientpb.SpoofMetadataConfig{}
if config.PE != nil {
source, err := resolveSpoofMetadataDataSource("pe.source", config.PE.Source)
if err != nil {
return nil, err
}
icon, err := resolveSpoofMetadataDataSource("pe.icon", config.PE.Icon)
if err != nil {
return nil, err
}
spoofMetadata.PE = &clientpb.PESpoofMetadataConfig{
Source: source,
Icon: icon,
ResourceDirectory: imageResourceDirectoryToProtobuf(config.PE.ResourceDirectory),
ResourceDirectoryEntries: imageResourceDirectoryEntriesToProtobuf(config.PE.ResourceDirectoryEntries),
ResourceDataEntries: imageResourceDataEntriesToProtobuf(config.PE.ResourceDataEntries),
ExportDirectory: imageExportDirectoryToProtobuf(config.PE.ExportDirectory),
}
}
if spoofMetadata.GetPE() == nil {
return nil, errors.New("no supported executable metadata configuration found")
}
if spoofMetadata.GetPE().GetSource() == nil || len(spoofMetadata.GetPE().GetSource().GetData()) == 0 {
return nil, errors.New("pe.source requires either path or base64 content")
}
return spoofMetadata, nil
}
func resolveSpoofMetadataDataSource(name string, source *assets.SpoofMetadataDataSource) (*clientpb.SpoofMetadataFile, error) {
if source == nil {
return nil, nil
}
pathValue := strings.TrimSpace(source.Path)
base64Value := strings.TrimSpace(source.Base64)
if pathValue != "" && base64Value != "" {
return nil, fmt.Errorf("%s cannot set both path and base64", name)
}
if pathValue == "" && base64Value == "" {
return nil, nil
}
var (
data []byte
err error
)
fileName := strings.TrimSpace(source.Name)
if pathValue != "" {
pathValue = expandPath(pathValue)
data, err = os.ReadFile(pathValue)
if err != nil {
return nil, fmt.Errorf("%s failed to read path %s: %w", name, pathValue, err)
}
if fileName == "" {
fileName = filepath.Base(pathValue)
}
} else {
data, err = base64.StdEncoding.DecodeString(base64Value)
if err != nil {
data, err = base64.RawStdEncoding.DecodeString(base64Value)
if err != nil {
return nil, fmt.Errorf("%s has invalid base64 data: %w", name, err)
}
}
if fileName == "" {
fileName = "inline-data"
}
}
return &clientpb.SpoofMetadataFile{
Name: fileName,
Data: data,
}, nil
}
func imageResourceDirectoryToProtobuf(src *assets.ImageResourceDirectory) *clientpb.IMAGE_RESOURCE_DIRECTORY {
if src == nil {
return nil
}
return &clientpb.IMAGE_RESOURCE_DIRECTORY{
Characteristics: src.Characteristics,
TimeDateStamp: src.TimeDateStamp,
MajorVersion: src.MajorVersion,
MinorVersion: src.MinorVersion,
NumberOfNamedEntries: src.NumberOfNamedEntries,
NumberOfIdEntries: src.NumberOfIDEntries,
}
}
func imageResourceDirectoryEntriesToProtobuf(src []*assets.ImageResourceDirectoryEntry) []*clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY {
if len(src) == 0 {
return nil
}
out := make([]*clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY, 0, len(src))
for _, entry := range src {
if entry == nil {
continue
}
out = append(out, &clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY{
Name: entry.Name,
OffsetToData: entry.OffsetToData,
})
}
return out
}
func imageResourceDataEntriesToProtobuf(src []*assets.ImageResourceDataEntry) []*clientpb.IMAGE_RESOURCE_DATA_ENTRY {
if len(src) == 0 {
return nil
}
out := make([]*clientpb.IMAGE_RESOURCE_DATA_ENTRY, 0, len(src))
for _, entry := range src {
if entry == nil {
continue
}
out = append(out, &clientpb.IMAGE_RESOURCE_DATA_ENTRY{
OffsetToData: entry.OffsetToData,
Size: entry.Size,
CodePage: entry.CodePage,
Reserved: entry.Reserved,
})
}
return out
}
func imageExportDirectoryToProtobuf(src *assets.ImageExportDirectory) *clientpb.IMAGE_EXPORT_DIRECTORY {
if src == nil {
return nil
}
return &clientpb.IMAGE_EXPORT_DIRECTORY{
Characteristics: src.Characteristics,
TimeDateStamp: src.TimeDateStamp,
MajorVersion: src.MajorVersion,
MinorVersion: src.MinorVersion,
Name: src.Name,
Base: src.Base,
NumberOfFunctions: src.NumberOfFunctions,
NumberOfNames: src.NumberOfNames,
AddressOfFunctions: src.AddressOfFunctions,
AddressOfNames: src.AddressOfNames,
AddressOfNameOrdinals: src.AddressOfNameOrdinals,
}
}
func parseShellcodeFlags(cmd *cobra.Command, targetOS string, configFormat clientpb.OutputFormat, con *console.SliverClient) (*clientpb.ShellcodeConfig, error) { func parseShellcodeFlags(cmd *cobra.Command, targetOS string, configFormat clientpb.OutputFormat, con *console.SliverClient) (*clientpb.ShellcodeConfig, error) {
// Prefer the new `--shellcode-*` flags. Fall back to hidden deprecated `--donut-*` flags for compatibility. // Prefer the new `--shellcode-*` flags. Fall back to hidden deprecated `--donut-*` flags for compatibility.
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy") shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
@@ -820,9 +1143,7 @@ func getTargets(targetOS string, targetArch string, con *console.SliverClient) (
target := fmt.Sprintf("%s/%s", targetOS, targetArch) target := fmt.Sprintf("%s/%s", targetOS, targetArch)
if _, ok := SupportedCompilerTargets[target]; !ok { if _, ok := SupportedCompilerTargets[target]; !ok {
con.Printf("⚠️ Unsupported compiler target %s%s%s, but we can try to compile a generic implant.\n", con.Printf("⚠️ Unsupported compiler target %s, but we can try to compile a generic implant.\n", console.StyleBold.Render(target))
console.Bold, target, console.Normal,
)
con.Printf("⚠️ Generic implants do not support all commands/features.\n") con.Printf("⚠️ Generic implants do not support all commands/features.\n")
var confirm bool var confirm bool
_ = forms.Confirm("Attempt to build generic implant?", &confirm) _ = forms.Confirm("Attempt to build generic implant?", &confirm)
@@ -1148,7 +1469,150 @@ func ParseTCPPivotc2(args string) ([]*clientpb.ImplantC2, error) {
return c2s, nil return c2s, nil
} }
func externalBuild(name string, config *clientpb.ImplantConfig, save string, con *console.SliverClient) (*commonpb.File, error) { func applySpoofMetadataToBuild(implantBuildID string, implantName string, spoofMetadata *clientpb.SpoofMetadataConfig, con *console.SliverClient) error {
if spoofMetadata == nil {
return nil
}
req := &clientpb.GenerateSpoofMetadataReq{
SpoofMetadata: spoofMetadata,
}
if buildID := strings.TrimSpace(implantBuildID); buildID != "" {
req.ImplantBuildID = buildID
} else if buildName := strings.TrimSpace(implantName); buildName != "" {
req.ImplantName = buildName
}
if req.GetImplantBuildID() == "" && req.GetImplantName() == "" {
return errors.New("unable to determine build selector for spoof metadata")
}
printSpoofMetadataSummary(spoofMetadata, con)
con.PrintInfof("Applying spoof metadata to %s ... ", spoofMetadataBuildSelectorDescription(req))
_, err := con.Rpc.GenerateSpoofMetadata(context.Background(), req)
if err != nil {
con.Printf("failed\n")
return err
}
con.Printf("done\n")
return nil
}
func printSpoofMetadataSummary(spoofMetadata *clientpb.SpoofMetadataConfig, con *console.SliverClient) {
if spoofMetadata == nil || con == nil {
return
}
peMetadata := spoofMetadata.GetPE()
if peMetadata == nil {
return
}
sourceName := "<unset>"
sourceSize := 0
if source := peMetadata.GetSource(); source != nil {
sourceName = strings.TrimSpace(source.GetName())
if sourceName == "" {
sourceName = "inline-data"
}
sourceSize = len(source.GetData())
}
iconSummary := "none"
if icon := peMetadata.GetIcon(); icon != nil && len(icon.GetData()) > 0 {
iconName := strings.TrimSpace(icon.GetName())
if iconName == "" {
iconName = "inline-data"
}
iconSummary = fmt.Sprintf("%s (%d bytes)", iconName, len(icon.GetData()))
}
overrideLabels := []string{}
if peMetadata.GetResourceDirectory() != nil {
overrideLabels = append(overrideLabels, "resource-directory")
}
if len(peMetadata.GetResourceDirectoryEntries()) > 0 {
overrideLabels = append(overrideLabels, fmt.Sprintf("resource-entries(%d)", len(peMetadata.GetResourceDirectoryEntries())))
}
if len(peMetadata.GetResourceDataEntries()) > 0 {
overrideLabels = append(overrideLabels, fmt.Sprintf("resource-data-entries(%d)", len(peMetadata.GetResourceDataEntries())))
}
if peMetadata.GetExportDirectory() != nil {
overrideLabels = append(overrideLabels, "export-directory")
}
overridesSummary := "none"
if len(overrideLabels) > 0 {
overridesSummary = strings.Join(overrideLabels, ", ")
}
con.PrintInfof("%s\n", console.StyleBold.Render("Spoof Metadata Details:"))
con.PrintInfof(" Source: %s (%d bytes)\n", sourceName, sourceSize)
con.PrintInfof(" Icon: %s\n", iconSummary)
con.PrintInfof(" Overrides: %s\n", overridesSummary)
}
func spoofMetadataBuildSelectorDescription(req *clientpb.GenerateSpoofMetadataReq) string {
if req == nil {
return "build"
}
if buildID := strings.TrimSpace(req.GetImplantBuildID()); buildID != "" {
return fmt.Sprintf("build id %s", buildID)
}
if buildName := strings.TrimSpace(req.GetImplantName()); buildName != "" {
return fmt.Sprintf("build name %s", buildName)
}
return "build"
}
func requiresWindowsExecutableSuffix(config *clientpb.ImplantConfig) bool {
if config == nil || !strings.EqualFold(config.GetGOOS(), "windows") {
return false
}
switch config.GetFormat() {
case clientpb.OutputFormat_EXECUTABLE, clientpb.OutputFormat_SERVICE:
return true
default:
return false
}
}
func ensureOutputPathSuffix(pathValue string, config *clientpb.ImplantConfig) string {
if pathValue == "" || !requiresWindowsExecutableSuffix(config) {
return pathValue
}
if strings.HasSuffix(strings.ToLower(pathValue), ".exe") {
return pathValue
}
return pathValue + ".exe"
}
func outputSavePath(save string, defaultName string, config *clientpb.ImplantConfig, con *console.SliverClient) (string, error) {
defaultName = ensureOutputPathSuffix(defaultName, config)
saveTo, err := saveLocation(save, defaultName, con)
if err != nil {
return "", err
}
adjustedPath := ensureOutputPathSuffix(saveTo, config)
if adjustedPath == saveTo {
return saveTo, nil
}
if info, err := os.Stat(adjustedPath); err == nil && !info.IsDir() {
var confirm bool
_ = forms.Confirm("Overwrite existing file?", &confirm)
if !confirm {
return "", errors.New("file already exists")
}
} else if err != nil && !os.IsNotExist(err) {
return "", err
}
if con != nil {
con.PrintInfof("Adjusted output filename to %s for Windows executable format\n", adjustedPath)
}
return adjustedPath, nil
}
func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
potentialBuilders, err := findExternalBuilders(config, con) potentialBuilders, err := findExternalBuilders(config, con)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1172,9 +1636,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) con.PrintInfof("Externally generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
} }
if config.ObfuscateSymbols { 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 { } 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() start := time.Now()
@@ -1248,6 +1712,12 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
elapsed := time.Since(start) elapsed := time.Since(start)
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second)) con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
if spoofMetadata != nil {
if err := applySpoofMetadataToBuild("", name, spoofMetadata, con); err != nil {
return nil, err
}
}
generated, err := con.Rpc.Regenerate(context.Background(), &clientpb.RegenerateReq{ generated, err := con.Rpc.Regenerate(context.Background(), &clientpb.RegenerateReq{
ImplantName: name, ImplantName: name,
}) })
@@ -1302,7 +1772,7 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
} }
} }
saveTo, err := saveLocation(save, filepath.Base(generated.File.Name), con) saveTo, err := outputSavePath(save, filepath.Base(generated.File.Name), config, con)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1317,7 +1787,7 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
return nil, nil return nil, nil
} }
func compile(name string, config *clientpb.ImplantConfig, save string, con *console.SliverClient) (*commonpb.File, error) { func compile(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
if config.IsBeacon { if config.IsBeacon {
interval := time.Duration(config.BeaconInterval) interval := time.Duration(config.BeaconInterval)
con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval) con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval)
@@ -1325,9 +1795,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) con.PrintInfof("Generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
} }
if config.ObfuscateSymbols { 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 { } 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() start := time.Now()
@@ -1345,6 +1815,25 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
return nil, err return nil, err
} }
if spoofMetadata != nil {
if err := applySpoofMetadataToBuild(generated.GetImplantBuildID(), generated.GetImplantName(), spoofMetadata, con); err != nil {
con.PrintErrorf("%s\n", err)
return nil, err
}
implantName := generated.GetImplantName()
if implantName == "" {
implantName = name
}
generated, err = con.Rpc.Regenerate(context.Background(), &clientpb.RegenerateReq{
ImplantName: implantName,
})
if err != nil {
con.PrintErrorf("%s\n", err)
return nil, err
}
}
elapsed := time.Since(start) elapsed := time.Since(start)
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second)) con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
if len(generated.File.Data) == 0 { if len(generated.File.Data) == 0 {
@@ -1401,7 +1890,7 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
} }
} }
saveTo, err := saveLocation(save, generated.File.Name, con) saveTo, err := outputSavePath(save, generated.File.Name, config, con)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+1 -1
View File
@@ -81,7 +81,7 @@ func ProfilesGenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []s
profile.Config.SGNEnabled = encoder == clientpb.ShellcodeEncoder_SHIKATA_GA_NAI profile.Config.SGNEnabled = encoder == clientpb.ShellcodeEncoder_SHIKATA_GA_NAI
} }
} }
_, err := compile(implantName, profile.Config, save, con) _, err := compile(implantName, profile.Config, nil, save, con)
if err != nil { if err != nil {
return return
} }
@@ -0,0 +1,193 @@
package generate
import (
"encoding/base64"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/spf13/cobra"
)
func TestParseSpoofMetadataFlagDefaultOff(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
}
if spoofMetadata != nil {
t.Fatal("expected spoof metadata to be nil when flag is not set")
}
}
func TestParseSpoofMetadataFlagNoArgUsesConfig(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
cmd := newSpoofMetadataTestCommand(t)
if err := cmd.ParseFlags([]string{"--" + spoofMetadataFlagName}); err != nil {
t.Fatalf("ParseFlags() error: %v", err)
}
rootDir := os.Getenv("SLIVER_CLIENT_ROOT_DIR")
configPath := filepath.Join(rootDir, "spoof-metadata.yaml")
sourceData := []byte{0x01, 0x02, 0x03}
configData := "pe:\n source:\n name: donor.exe\n base64: " + base64.StdEncoding.EncodeToString(sourceData) + "\n"
if err := os.WriteFile(configPath, []byte(configData), 0o600); err != nil {
t.Fatalf("write spoof metadata config: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
}
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
t.Fatal("expected spoof metadata source from config file")
}
if got := spoofMetadata.GetPE().GetSource().GetData(); string(got) != string(sourceData) {
t.Fatalf("source data mismatch: got=%x want=%x", got, sourceData)
}
}
func TestParseSpoofMetadataFlagPathFromPositionalArg(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
sourcePath := goPEFixturePath(t, "gcc-amd64-mingw-exec")
if err := cmd.ParseFlags([]string{"--" + spoofMetadataFlagName, sourcePath}); err != nil {
t.Fatalf("ParseFlags() error: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
}
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
t.Fatal("expected spoof metadata source from positional path")
}
if spoofMetadata.GetPE().GetSource().GetName() != filepath.Base(sourcePath) {
t.Fatalf("source name mismatch: got=%q want=%q", spoofMetadata.GetPE().GetSource().GetName(), filepath.Base(sourcePath))
}
}
func TestParseSpoofMetadataFlagPathRejectsNonWindowsTarget(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
sourcePath := writeTempFile(t, "not-pe.bin", []byte("abc"))
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
t.Fatalf("set flag: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "linux",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
_, err := parseSpoofMetadataFlag(cmd, config)
if err == nil {
t.Fatal("expected error for non-windows target")
}
if !strings.Contains(err.Error(), "supports only windows PE targets") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestParseSpoofMetadataFlagPathChecksMachine(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
sourcePath := goPEFixturePath(t, "gcc-386-mingw-exec")
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
t.Fatalf("set flag: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
_, err := parseSpoofMetadataFlag(cmd, config)
if err == nil {
t.Fatal("expected machine mismatch error")
}
if !strings.Contains(err.Error(), "expected amd64") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestParseSpoofMetadataFlagPathSuccess(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
sourcePath := goPEFixturePath(t, "gcc-amd64-mingw-exec")
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
t.Fatalf("set flag: %v", err)
}
config := &clientpb.ImplantConfig{
GOOS: "windows",
GOARCH: "amd64",
Format: clientpb.OutputFormat_EXECUTABLE,
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
}
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
t.Fatal("expected spoof metadata source from path")
}
if spoofMetadata.GetPE().GetSource().GetName() != filepath.Base(sourcePath) {
t.Fatalf("source name mismatch: got=%q want=%q", spoofMetadata.GetPE().GetSource().GetName(), filepath.Base(sourcePath))
}
if len(spoofMetadata.GetPE().GetSource().GetData()) == 0 {
t.Fatal("expected non-empty source data")
}
}
func TestSpoofMetadataFlagUsageIsClean(t *testing.T) {
cmd := newSpoofMetadataTestCommand(t)
usage := cmd.Flags().FlagUsages()
if !strings.Contains(usage, "--"+spoofMetadataFlagName) {
t.Fatalf("expected usage to include spoof metadata flag, got: %s", usage)
}
if strings.Contains(usage, "string[=") {
t.Fatalf("unexpected optional-string marker in usage: %s", usage)
}
if strings.Contains(usage, `[="`) {
t.Fatalf("unexpected no-opt default rendering in usage: %s", usage)
}
}
func newSpoofMetadataTestCommand(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "test"}
bindSpoofMetadataFlag(cmd.Flags())
return cmd
}
func writeTempFile(t *testing.T, name string, data []byte) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatalf("write temp file: %v", err)
}
return path
}
func goPEFixturePath(t *testing.T, fixtureName string) string {
t.Helper()
fixturePath := filepath.Join(runtime.GOROOT(), "src", "debug", "pe", "testdata", fixtureName)
if _, err := os.Stat(fixturePath); err != nil {
t.Skipf("missing PE fixture %s: %v", fixturePath, err)
}
return fixturePath
}
@@ -0,0 +1,36 @@
package generate
import "strings"
type spoofMetadataFlagValue struct {
value string
}
func newSpoofMetadataFlagValue() *spoofMetadataFlagValue {
return &spoofMetadataFlagValue{}
}
func (s *spoofMetadataFlagValue) Set(value string) error {
value = strings.TrimSpace(value)
switch strings.ToLower(value) {
case "", "true":
s.value = "true"
case "false":
s.value = "false"
default:
s.value = value
}
return nil
}
func (s *spoofMetadataFlagValue) String() string {
return s.value
}
func (s *spoofMetadataFlagValue) Type() string {
return "bool"
}
func (s *spoofMetadataFlagValue) IsBoolFlag() bool {
return true
}
+4 -4
View File
@@ -71,7 +71,7 @@ func DisplayTrafficEncoders(encoderMap *clientpb.TrafficEncoderMap, con *console
allIDs = append(allIDs, encoder.ID) allIDs = append(allIDs, encoder.ID)
name := encoder.Wasm.Name name := encoder.Wasm.Name
if isDuplicate { 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{ tw.AppendRow(table.Row{
encoder.ID, encoder.ID,
@@ -212,9 +212,9 @@ func displayTrafficEncoderTests(running bool, tests *clientpb.TrafficEncoderTest
for _, test := range tests.Tests { for _, test := range tests.Tests {
var success string var success string
if test.Success { if test.Success {
success = console.Bold + console.Green + "Passed" + console.Normal success = console.StyleBoldGreen.Render("Passed")
} else { } else {
success = console.Bold + console.Red + "Failed!" + console.Normal success = console.StyleBoldRed.Render("Failed!")
} }
errorMsg := "N/A" errorMsg := "N/A"
if test.Err != "" { if test.Err != "" {
@@ -234,7 +234,7 @@ func displayTrafficEncoderTests(running bool, tests *clientpb.TrafficEncoderTest
if running { if running {
con.Println() con.Println()
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 lineCount += 2
} }
+101 -51
View File
@@ -23,12 +23,13 @@ package help
*/ */
import ( import (
"bytes"
"fmt" "fmt"
"image/color"
"strings" "strings"
"text/template"
"charm.land/lipgloss/v2"
consts "github.com/bishopfox/sliver/client/constants" consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/theme"
) )
const ( const (
@@ -436,8 +437,9 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
mountHelp = `[[.Bold]]Command:[[.Normal]] mount mountHelp = `[[.Bold]]Command:[[.Normal]] mount
[[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.` [[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.`
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode] executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode or PE]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process. [[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process. On Windows targets, PE input can be
converted with Donut before execution and tuned with [[.Bold]]--shellcode-*[[.Normal]] flags.
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]] [[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command: Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
@@ -1418,24 +1420,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.` 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 // GetHelpFor - Get help string for a command
func GetHelpFor(cmdName []string) string { func GetHelpFor(cmdName []string) string {
if 0 < len(cmdName) { if 0 < len(cmdName) {
@@ -1446,34 +1430,100 @@ func GetHelpFor(cmdName []string) string {
return "" return ""
} }
// FormatHelpTmpl - Applies format template to help string type helpStyleState struct {
bold bool
underline bool
fgSet bool
fg color.Color
}
func (s *helpStyleState) reset() {
s.bold = false
s.underline = false
s.fgSet = false
s.fg = nil
}
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 { func FormatHelpTmpl(helpStr string) string {
outputBuf := bytes.NewBufferString("") var (
tmpl, _ := template.New("help").Delims("[[", "]]").Parse(helpStr) out strings.Builder
tmpl.Execute(outputBuf, struct { state helpStyleState
Normal string )
Bold string state.reset()
Underline string
Black string i := 0
Red string for i < len(helpStr) {
Green string j := strings.Index(helpStr[i:], "[[.")
Orange string if j < 0 {
Blue string out.WriteString(state.render(helpStr[i:]))
Purple string break
Cyan string }
Gray string j += i
}{ if j > i {
Normal: normal, out.WriteString(state.render(helpStr[i:j]))
Bold: bold, }
Underline: underline,
Black: black, k := strings.Index(helpStr[j:], "]]")
Red: red, if k < 0 {
Green: green, out.WriteString(state.render(helpStr[j:]))
Orange: orange, break
Blue: blue, }
Purple: purple, token := helpStr[j+3 : j+k]
Cyan: cyan, if !state.apply(token) {
Gray: gray, // Unknown token; preserve it as plain text.
}) out.WriteString(helpStr[j : j+k+2])
return outputBuf.String() }
i = j + k + 2
}
return out.String()
} }
+62 -43
View File
@@ -5,9 +5,10 @@ import (
"strconv" "strconv"
"strings" "strings"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/util" "github.com/bishopfox/sliver/util"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"golang.org/x/term" "golang.org/x/term"
) )
@@ -61,13 +62,19 @@ type editorModel struct {
} }
var ( var (
cursorStyle = lipgloss.NewStyle().Reverse(true) lineStyle = lipgloss.NewStyle()
modifiedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
modifiedCursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215")).Reverse(true)
statusStyle = lipgloss.NewStyle().Reverse(true)
lineStyle = lipgloss.NewStyle()
) )
const (
ansiReverseOn = "\x1b[7m"
ansiReverseOff = "\x1b[0m"
)
func modifiedStyle() lipgloss.Style {
// Read from the global console theme so theme.yaml changes are reflected.
return console.StyleWarning
}
func newEditorModel(data []byte, filename string, offset int) *editorModel { func newEditorModel(data []byte, filename string, offset int) *editorModel {
copyData := append([]byte(nil), data...) copyData := append([]byte(nil), data...)
model := &editorModel{ model := &editorModel{
@@ -105,30 +112,31 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height m.height = msg.Height
m.ensureCursorVisible() m.ensureCursorVisible()
return m, nil return m, nil
case tea.KeyMsg: case tea.KeyPressMsg:
if m.clearMessage { if m.clearMessage {
m.message = "" m.message = ""
m.clearMessage = false m.clearMessage = false
} }
if msg.Type == tea.KeyCtrlC { key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
m.action = actionQuit m.action = actionQuit
return m, tea.Quit return m, tea.Quit
} }
switch m.mode { switch m.mode {
case modeCommand: case modeCommand:
return m.handleCommand(msg) return m.handleCommand(key)
case modeHexEdit: case modeHexEdit:
return m.handleHexEdit(msg) return m.handleHexEdit(key)
case modeASCIIEdit: case modeASCIIEdit:
return m.handleASCIIEdit(msg) return m.handleASCIIEdit(key)
default: default:
return m.handleNormal(msg) return m.handleNormal(key)
} }
} }
return m, nil return m, nil
} }
func (m *editorModel) View() string { func (m *editorModel) View() tea.View {
if m.width == 0 || m.height == 0 { if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 { if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w m.width = w
@@ -151,19 +159,33 @@ func (m *editorModel) View() string {
} }
if m.height < 2 { if m.height < 2 {
return b.String() return hexEditorView(b.String())
} }
b.WriteByte('\n') b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine())) b.WriteString(renderStatusBar(m.width, m.statusLine()))
if m.height < 3 { if m.height < 3 {
return b.String() return hexEditorView(b.String())
} }
b.WriteByte('\n') b.WriteByte('\n')
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine())) b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
return b.String() return hexEditorView(b.String())
}
func renderStatusBar(width int, text string) string {
return ansiReverseOn + lineStyle.Width(width).Render(text) + ansiReverseOff
}
func renderCursorCell(cell string) string {
return ansiReverseOn + cell + ansiReverseOff
}
func hexEditorView(content string) tea.View {
v := tea.NewView(content)
v.AltScreen = true
return v
} }
func (m *editorModel) result() editorResult { func (m *editorModel) result() editorResult {
@@ -176,8 +198,8 @@ func (m *editorModel) result() editorResult {
} }
} }
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
switch msg.Type { switch key.Code {
case tea.KeyEsc: case tea.KeyEsc:
m.mode = modeNormal m.mode = modeNormal
m.command = "" m.command = ""
@@ -192,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
case tea.KeyBackspace: case tea.KeyBackspace:
m.command = popRune(m.command) m.command = popRune(m.command)
case tea.KeyRunes: default:
m.command += string(msg.Runes) if key.Text != "" {
m.command += key.Text
}
} }
return m, nil return m, nil
} }
func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *editorModel) handleHexEdit(key tea.Key) (tea.Model, tea.Cmd) {
switch msg.Type { switch key.Code {
case tea.KeyEsc: case tea.KeyEsc:
m.mode = modeNormal m.mode = modeNormal
m.nibbleSet = false m.nibbleSet = false
@@ -221,8 +245,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case tea.KeyDown: case tea.KeyDown:
m.moveDown() m.moveDown()
m.nibbleSet = false m.nibbleSet = false
case tea.KeyRunes: default:
for _, r := range msg.Runes { for _, r := range key.Text {
val, ok := hexNibble(r) val, ok := hexNibble(r)
if !ok { if !ok {
continue continue
@@ -241,8 +265,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *editorModel) handleASCIIEdit(key tea.Key) (tea.Model, tea.Cmd) {
switch msg.Type { switch key.Code {
case tea.KeyEsc: case tea.KeyEsc:
m.mode = modeNormal m.mode = modeNormal
case tea.KeyLeft: case tea.KeyLeft:
@@ -253,8 +277,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp() m.moveUp()
case tea.KeyDown: case tea.KeyDown:
m.moveDown() m.moveDown()
case tea.KeyRunes: default:
for _, r := range msg.Runes { for _, r := range key.Text {
if r < 0 || r > 0xFF { if r < 0 || r > 0xFF {
continue continue
} }
@@ -266,8 +290,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
switch msg.Type { switch key.Code {
case tea.KeyEsc: case tea.KeyEsc:
m.pending = 0 m.pending = 0
case tea.KeyLeft: case tea.KeyLeft:
@@ -278,8 +302,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp() m.moveUp()
case tea.KeyDown: case tea.KeyDown:
m.moveDown() m.moveDown()
case tea.KeyRunes: default:
for _, r := range msg.Runes { for _, r := range key.Text {
if m.handleNormalRune(r) { if m.handleNormalRune(r) {
break break
} }
@@ -399,18 +423,13 @@ func (m *editorModel) renderLine(lineIndex int) string {
asciiRaw := string(printableByte(byteVal)) asciiRaw := string(printableByte(byteVal))
isModified := m.isModified(idx) isModified := m.isModified(idx)
if idx == m.cursor { if idx == m.cursor {
if isModified { hexCells[i] = renderCursorCell(hexRaw)
hexCells[i] = modifiedCursorStyle.Render(hexRaw) asciiCells[i] = renderCursorCell(asciiRaw)
asciiCells[i] = modifiedCursorStyle.Render(asciiRaw)
} else {
hexCells[i] = cursorStyle.Render(hexRaw)
asciiCells[i] = cursorStyle.Render(asciiRaw)
}
continue continue
} }
if isModified { if isModified {
hexCells[i] = modifiedStyle.Render(hexRaw) hexCells[i] = modifiedStyle().Render(hexRaw)
asciiCells[i] = modifiedStyle.Render(asciiRaw) asciiCells[i] = modifiedStyle().Render(asciiRaw)
continue continue
} }
hexCells[i] = hexRaw hexCells[i] = hexRaw
+12 -2
View File
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/command/filesystem" "github.com/bishopfox/sliver/client/command/filesystem"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms" "github.com/bishopfox/sliver/client/forms"
@@ -15,8 +16,9 @@ import (
"github.com/bishopfox/sliver/protobuf/sliverpb" "github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util" "github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/util/encoders" "github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -174,7 +176,15 @@ func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func runHexEditor(data []byte, filename string, offset int) (editorResult, error) { func runHexEditor(data []byte, filename string, offset int) (editorResult, error) {
model := newEditorModel(data, filename, offset) model := newEditorModel(data, filename, offset)
program := tea.NewProgram(model, tea.WithAltScreen()) width, height := 80, 24
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
finalModel, err := program.Run() finalModel, err := program.Run()
if err != nil { if err != nil {
return editorResult{}, err return editorResult{}, err
+38 -38
View File
@@ -70,47 +70,47 @@ func InfoCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if session != nil { if session != nil {
con.Printf(console.Bold+" Session ID: %s%s\n", console.Normal, session.ID) con.Printf("%s %s\n", console.StyleBold.Render(" Session ID:"), session.ID)
con.Printf(console.Bold+" Name: %s%s\n", console.Normal, session.Name) con.Printf("%s %s\n", console.StyleBold.Render(" Name:"), session.Name)
con.Printf(console.Bold+" Hostname: %s%s\n", console.Normal, session.Hostname) con.Printf("%s %s\n", console.StyleBold.Render(" Hostname:"), session.Hostname)
con.Printf(console.Bold+" UUID: %s%s\n", console.Normal, session.UUID) con.Printf("%s %s\n", console.StyleBold.Render(" UUID:"), session.UUID)
con.Printf(console.Bold+" Username: %s%s\n", console.Normal, session.Username) con.Printf("%s %s\n", console.StyleBold.Render(" Username:"), session.Username)
con.Printf(console.Bold+" UID: %s%s\n", console.Normal, session.UID) con.Printf("%s %s\n", console.StyleBold.Render(" UID:"), session.UID)
con.Printf(console.Bold+" GID: %s%s\n", console.Normal, session.GID) con.Printf("%s %s\n", console.StyleBold.Render(" GID:"), session.GID)
con.Printf(console.Bold+" PID: %s%d\n", console.Normal, session.PID) con.Printf("%s %d\n", console.StyleBold.Render(" PID:"), session.PID)
con.Printf(console.Bold+" OS: %s%s\n", console.Normal, session.OS) con.Printf("%s %s\n", console.StyleBold.Render(" OS:"), session.OS)
con.Printf(console.Bold+" Version: %s%s\n", console.Normal, session.Version) con.Printf("%s %s\n", console.StyleBold.Render(" Version:"), session.Version)
con.Printf(console.Bold+" Locale: %s%s\n", console.Normal, session.Locale) con.Printf("%s %s\n", console.StyleBold.Render(" Locale:"), session.Locale)
con.Printf(console.Bold+" Arch: %s%s\n", console.Normal, session.Arch) con.Printf("%s %s\n", console.StyleBold.Render(" Arch:"), session.Arch)
con.Printf(console.Bold+" Active C2: %s%s\n", console.Normal, session.ActiveC2) con.Printf("%s %s\n", console.StyleBold.Render(" Active C2:"), session.ActiveC2)
con.Printf(console.Bold+" Remote Address: %s%s\n", console.Normal, session.RemoteAddress) con.Printf("%s %s\n", console.StyleBold.Render(" Remote Address:"), session.RemoteAddress)
con.Printf(console.Bold+" Proxy URL: %s%s\n", console.Normal, session.ProxyURL) con.Printf("%s %s\n", console.StyleBold.Render(" Proxy URL:"), session.ProxyURL)
con.Printf(console.Bold+"Reconnect Interval: %s%s\n", console.Normal, time.Duration(session.ReconnectInterval).String()) con.Printf("%s %s\n", console.StyleBold.Render("Reconnect Interval:"), 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("%s %s\n", console.StyleBold.Render(" First Contact:"), 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(" Last Checkin:"), con.FormatDateDelta(time.Unix(session.LastCheckin, 0), true, false))
} else if beacon != nil { } else if beacon != nil {
con.Printf(console.Bold+" Beacon ID: %s%s\n", console.Normal, beacon.ID) con.Printf("%s %s\n", console.StyleBold.Render(" Beacon ID:"), beacon.ID)
con.Printf(console.Bold+" Name: %s%s\n", console.Normal, beacon.Name) con.Printf("%s %s\n", console.StyleBold.Render(" Name:"), beacon.Name)
con.Printf(console.Bold+" Hostname: %s%s\n", console.Normal, beacon.Hostname) con.Printf("%s %s\n", console.StyleBold.Render(" Hostname:"), beacon.Hostname)
con.Printf(console.Bold+" UUID: %s%s\n", console.Normal, beacon.UUID) con.Printf("%s %s\n", console.StyleBold.Render(" UUID:"), beacon.UUID)
con.Printf(console.Bold+" Username: %s%s\n", console.Normal, beacon.Username) con.Printf("%s %s\n", console.StyleBold.Render(" Username:"), beacon.Username)
con.Printf(console.Bold+" UID: %s%s\n", console.Normal, beacon.UID) con.Printf("%s %s\n", console.StyleBold.Render(" UID:"), beacon.UID)
con.Printf(console.Bold+" GID: %s%s\n", console.Normal, beacon.GID) con.Printf("%s %s\n", console.StyleBold.Render(" GID:"), beacon.GID)
con.Printf(console.Bold+" PID: %s%d\n", console.Normal, beacon.PID) con.Printf("%s %d\n", console.StyleBold.Render(" PID:"), beacon.PID)
con.Printf(console.Bold+" OS: %s%s\n", console.Normal, beacon.OS) con.Printf("%s %s\n", console.StyleBold.Render(" OS:"), beacon.OS)
con.Printf(console.Bold+" Version: %s%s\n", console.Normal, beacon.Version) con.Printf("%s %s\n", console.StyleBold.Render(" Version:"), beacon.Version)
con.Printf(console.Bold+" Locale: %s%s\n", console.Normal, beacon.Locale) con.Printf("%s %s\n", console.StyleBold.Render(" Locale:"), beacon.Locale)
con.Printf(console.Bold+" Arch: %s%s\n", console.Normal, beacon.Arch) con.Printf("%s %s\n", console.StyleBold.Render(" Arch:"), beacon.Arch)
con.Printf(console.Bold+" Active C2: %s%s\n", console.Normal, beacon.ActiveC2) con.Printf("%s %s\n", console.StyleBold.Render(" Active C2:"), beacon.ActiveC2)
con.Printf(console.Bold+" Remote Address: %s%s\n", console.Normal, beacon.RemoteAddress) con.Printf("%s %s\n", console.StyleBold.Render(" Remote Address:"), beacon.RemoteAddress)
con.Printf(console.Bold+" Proxy URL: %s%s\n", console.Normal, beacon.ProxyURL) con.Printf("%s %s\n", console.StyleBold.Render(" Proxy URL:"), beacon.ProxyURL)
con.Printf(console.Bold+" Interval: %s%s\n", console.Normal, time.Duration(beacon.Interval).String()) con.Printf("%s %s\n", console.StyleBold.Render(" Interval:"), time.Duration(beacon.Interval).String())
con.Printf(console.Bold+" Jitter: %s%s\n", console.Normal, time.Duration(beacon.Jitter).String()) con.Printf("%s %s\n", console.StyleBold.Render(" Jitter:"), 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("%s %s\n", console.StyleBold.Render(" First Contact:"), 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("%s %s\n", console.StyleBold.Render(" Last Checkin:"), 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(" Next Checkin:"), con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), true, true))
} else { } else {
con.PrintErrorf("No target session, see `help %s`\n", consts.InfoStr) con.PrintErrorf("No target session, see `help %s`\n", consts.InfoStr)
+27
View File
@@ -1,6 +1,25 @@
package jobs 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 ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate" "github.com/bishopfox/sliver/client/command/generate"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
@@ -103,6 +122,10 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("long-poll-timeout", "T", "1s", "server-side long poll timeout") f.StringP("long-poll-timeout", "T", "1s", "server-side long poll timeout")
f.StringP("long-poll-jitter", "J", "2s", "server-side long poll jitter") 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 // HTTPS
httpsCmd := &cobra.Command{ httpsCmd := &cobra.Command{
@@ -129,9 +152,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("disable-randomized-jarm", "E", false, "disable randomized jarm fingerprints") f.BoolP("disable-randomized-jarm", "E", false, "disable randomized jarm fingerprints")
}) })
flags.BindFlagCompletions(httpsCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(httpsCmd, func(comp *carapace.ActionMap) {
(*comp)["website"] = WebsiteNameCompleter(con)
(*comp)["cert"] = carapace.ActionFiles().Tag("certificate file") (*comp)["cert"] = carapace.ActionFiles().Tag("certificate file")
(*comp)["key"] = carapace.ActionFiles().Tag("key file") (*comp)["key"] = carapace.ActionFiles().Tag("key file")
}) })
completers.RegisterLocalFilePathFlagCompletions(httpsCmd, "cert", "key")
registerWebsiteFlagCompletion(httpsCmd, "website", con)
// Staging listeners // Staging listeners
stageCmd := &cobra.Command{ stageCmd := &cobra.Command{
@@ -161,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["key"] = carapace.ActionFiles().Tag("key file") (*comp)["key"] = carapace.ActionFiles().Tag("key file")
(*comp)["compress"] = carapace.ActionValues([]string{"zlib", "gzip", "deflate9", "none"}...).Tag("compression formats") (*comp)["compress"] = carapace.ActionValues([]string{"zlib", "gzip", "deflate9", "none"}...).Tag("compression formats")
}) })
completers.RegisterLocalFilePathFlagCompletions(stageCmd, "cert", "key")
return []*cobra.Command{jobsCmd, mtlsCmd, wgCmd, dnsCmd, httpCmd, httpsCmd, stageCmd} return []*cobra.Command{jobsCmd, mtlsCmd, wgCmd, dnsCmd, httpCmd, httpsCmd, stageCmd}
} }
+38
View File
@@ -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
}
+100
View File
@@ -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
}
+3
View File
@@ -1,6 +1,7 @@
package loot package loot
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace" "github.com/rsteube/carapace"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -50,6 +51,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}) })
carapace.Gen(lootAddCmd).PositionalCompletion( carapace.Gen(lootAddCmd).PositionalCompletion(
carapace.ActionFiles().Tag("local loot file").Usage("The local file path to the loot")) carapace.ActionFiles().Tag("local loot file").Usage("The local file path to the loot"))
completers.RegisterLocalFilePathPositionalCompletion(lootAddCmd, 0)
lootRemoteCmd := &cobra.Command{ lootRemoteCmd := &cobra.Command{
Use: consts.LootRemoteStr, Use: consts.LootRemoteStr,
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save loot") (*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save loot")
(*comp)["filter"] = FileTypeCompleter(con) (*comp)["filter"] = FileTypeCompleter(con)
}) })
completers.RegisterLocalFilePathFlagCompletion(lootFetchCmd, "save")
lootRmCmd := &cobra.Command{ lootRmCmd := &cobra.Command{
Use: consts.RmStr, Use: consts.RmStr,
+1 -1
View File
@@ -82,7 +82,7 @@ func PrintLootFile(loot *clientpb.Loot, con *console.SliverClient) {
return return
} }
if loot.File.Name != "" { 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.File.Data != nil && 0 < len(loot.File.Data) {
if loot.FileType == clientpb.FileType_TEXT || isText(loot.File.Data) { if loot.FileType == clientpb.FileType_TEXT || isText(loot.File.Data) {
+4 -7
View File
@@ -20,7 +20,6 @@ package monitor
import ( import (
"context" "context"
"fmt"
"github.com/bishopfox/sliver/client/command/settings" "github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -99,14 +98,12 @@ func PrintWTConfig(configs *clientpb.MonitoringProviders, con *console.SliverCli
"APIPassword", "APIPassword",
}) })
color := console.Normal
for _, config := range configs.Providers { for _, config := range configs.Providers {
row := table.Row{} row := table.Row{}
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.ID)) row = append(row, config.ID)
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.Type)) row = append(row, config.Type)
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.APIKey)) row = append(row, config.APIKey)
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.APIPassword)) row = append(row, config.APIPassword)
tw.AppendRow(row) tw.AppendRow(row)
} }
+2 -2
View File
@@ -75,7 +75,7 @@ func PrintIfconfig(ifconfig *sliverpb.Ifconfig, all bool, con *console.SliverCli
for index, iface := range interfaces { for index, iface := range interfaces {
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableWithBordersStyle(con)) 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{ tw.SetColumnConfigs([]table.ColumnConfig{
{Name: "#", AutoMerge: true}, {Name: "#", AutoMerge: true},
{Name: "IP Address", 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) { 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 { } else if all {
ips = append(ips, fmt.Sprintf("%s", ip)) ips = append(ips, fmt.Sprintf("%s", ip))
} }
+5 -5
View File
@@ -104,11 +104,11 @@ func PrintNetstat(netstat *sliverpb.Netstat, implantPID int32, activeC2 string,
} }
if entry.Process != nil && entry.Process.Pid == implantPID { if entry.Process != nil && entry.Process.Pid == implantPID {
tw.AppendRow(table.Row{ tw.AppendRow(table.Row{
fmt.Sprintf(console.Green+"%s"+console.Normal, entry.Protocol), console.StyleGreen.Render(entry.Protocol),
fmt.Sprintf(console.Green+"%s"+console.Normal, srcAddr), console.StyleGreen.Render(srcAddr),
fmt.Sprintf(console.Green+"%s"+console.Normal, dstAddr), console.StyleGreen.Render(dstAddr),
fmt.Sprintf(console.Green+"%s"+console.Normal, entry.SkState), console.StyleGreen.Render(entry.SkState),
fmt.Sprintf(console.Green+"%s"+console.Normal, pid), console.StyleGreen.Render(pid),
}) })
} else { } else {
tw.AppendRow(table.Row{entry.Protocol, srcAddr, dstAddr, entry.SkState, pid}) tw.AppendRow(table.Row{entry.Protocol, srcAddr, dstAddr, entry.SkState, pid})
+3 -3
View File
@@ -50,7 +50,7 @@ func displayOperators(operators []*clientpb.Operator, con *console.SliverClient)
}) })
for _, operator := range operators { for _, operator := range operators {
tw.AppendRow(table.Row{ tw.AppendRow(table.Row{
console.Bold + operator.Name + console.Normal, console.StyleBold.Render(operator.Name),
status(operator.Online), status(operator.Online),
}) })
} }
@@ -59,7 +59,7 @@ func displayOperators(operators []*clientpb.Operator, con *console.SliverClient)
func status(isOnline bool) string { func status(isOnline bool) string {
if isOnline { 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")
} }
+1 -1
View File
@@ -81,7 +81,7 @@ func PrintPivotListenerDetails(listener *sliverpb.PivotListener, con *console.Sl
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con)) 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.AppendSeparator()
tw.AppendHeader(table.Row{ tw.AppendHeader(table.Row{
"ID", "ID",
+2
View File
@@ -1,6 +1,7 @@
package processes package processes
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace" "github.com/rsteube/carapace"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -56,6 +57,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles() (*comp)["save"] = carapace.ActionFiles()
}) })
completers.RegisterLocalFilePathFlagCompletion(procdumpCmd, "save")
terminateCmd := &cobra.Command{ terminateCmd := &cobra.Command{
Use: consts.TerminateStr, Use: consts.TerminateStr,
+107 -107
View File
@@ -36,77 +36,77 @@ import (
"github.com/bishopfox/sliver/protobuf/sliverpb" "github.com/bishopfox/sliver/protobuf/sliverpb"
) )
// Stylizes known processes in the `ps` command // Known security tools shown by `ps`/`pstree`.
var knownSecurityTools = map[string][]string{ // Process executable -> product name.
// Process Name -> [Color, Stylized Name] var knownSecurityTools = map[string]string{
"ccSvcHst.exe": {console.Red, "Symantec Endpoint Protection"}, // Symantec Endpoint Protection (SEP) "ccSvcHst.exe": "Symantec Endpoint Protection", // Symantec Endpoint Protection (SEP)
"cb.exe": {console.Red, "Carbon Black"}, // Carbon Black "cb.exe": "Carbon Black", // Carbon Black
"RepMgr.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor "RepMgr.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
"RepUtils.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor "RepUtils.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
"RepUx.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor "RepUx.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
"RepWSC.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor "RepWSC.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
"scanhost.exe": {console.Red, "Carbon Black Cloud Sensor"}, // Carbon Black Cloud Sensor "scanhost.exe": "Carbon Black Cloud Sensor", // Carbon Black Cloud Sensor
"elastic-agent.exe": {console.Red, "Elastic Agent"}, // Elastic Agent "elastic-agent.exe": "Elastic Agent", // Elastic Agent
"elastic-endpoint.exe": {console.Red, "Elastic Agent"}, // Elastic Agent "elastic-endpoint.exe": "Elastic Agent", // Elastic Agent
"filebeat.exe": {console.Red, "Elastic Agent"}, // Elastic Agent - log shipper "filebeat.exe": "Elastic Agent", // Elastic Agent - log shipper
"metricbeat.exe": {console.Red, "Elastic Agent"}, // Elastic Agent - metric shipper "metricbeat.exe": "Elastic Agent", // Elastic Agent - metric shipper
"smartscreen.exe": {console.Red, "Windows Smart Screen"}, // Windows Defender Smart Screen "smartscreen.exe": "Windows Smart Screen", // Windows Defender Smart Screen
"MpCmdRun.exe": {console.Red, "Windows Defender"}, // Windows Defender Command-line "MpCmdRun.exe": "Windows Defender", // Windows Defender Command-line
"MonitoringHost.exe": {console.Red, "Windows Defender"}, // Microsoft Monitoring Agent "MonitoringHost.exe": "Windows Defender", // Microsoft Monitoring Agent
"HealthService.exe": {console.Red, "Windows Defender"}, // Microsoft Monitoring Agent "HealthService.exe": "Windows Defender", // Microsoft Monitoring Agent
"MsMpEng.exe": {console.Red, "Windows Defender"}, // Windows Defender (Service Executable) "MsMpEng.exe": "Windows Defender", // Windows Defender (Service Executable)
"NisSrv.exe": {console.Red, "Windows Defender"}, // Windows Defender (Network Realtime Inspection) "NisSrv.exe": "Windows Defender", // Windows Defender (Network Realtime Inspection)
"SenseIR.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Live Response Session) "SenseIR.exe": "Windows Defender MDE", // Windows Defender Endpoint (Live Response Session)
"SenseNdr.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Network Detection and Response) "SenseNdr.exe": "Windows Defender MDE", // Windows Defender Endpoint (Network Detection and Response)
"SenseSC.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Screenshot Capture Module) "SenseSC.exe": "Windows Defender MDE", // Windows Defender Endpoint (Screenshot Capture Module)
"SenseCE.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Classification Engine Module) "SenseCE.exe": "Windows Defender MDE", // Windows Defender Endpoint (Classification Engine Module)
"SenseCM.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Configuration Management Module) "SenseCM.exe": "Windows Defender MDE", // Windows Defender Endpoint (Configuration Management Module)
"SenseSampleUploader.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Sample Uploader Module) "SenseSampleUploader.exe": "Windows Defender MDE", // Windows Defender Endpoint (Sample Uploader Module)
"SenseCncProxy.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Communication Module) "SenseCncProxy.exe": "Windows Defender MDE", // Windows Defender Endpoint (Communication Module)
"MsSense.exe": {console.Red, "Windows Defender MDE"}, // Windows Defender Endpoint (Service Executable) "MsSense.exe": "Windows Defender MDE", // Windows Defender Endpoint (Service Executable)
"CSFalconService.exe": {console.Red, "CrowdStrike"}, // Crowdstrike Falcon Service "CSFalconService.exe": "CrowdStrike", // Crowdstrike Falcon Service
"CSFalconContainer.exe": {console.Red, "CrowdStrike"}, // CrowdStrike Falcon Container Security "CSFalconContainer.exe": "CrowdStrike", // CrowdStrike Falcon Container Security
"bdservicehost.exe": {console.Red, "Bitdefender"}, // Bitdefender (Total Security) "bdservicehost.exe": "Bitdefender", // Bitdefender (Total Security)
"bdagent.exe": {console.Red, "Bitdefender"}, // Bitdefender (Total Security) "bdagent.exe": "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) "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": {console.Red, "Trend Micro"}, // TM Deep Security Manager "Deep Security Manager.exe": "Trend Micro", // TM Deep Security Manager
"coreServiceShell.exe": {console.Red, "Trend Micro"}, // TM Anti-malware scan process "coreServiceShell.exe": "Trend Micro", // TM Anti-malware scan process
"ds_monitor.exe": {console.Red, "Trend Micro"}, // TM Deep Security Monitor "ds_monitor.exe": "Trend Micro", // TM Deep Security Monitor
"Notifier.exe": {console.Red, "Trend Micro"}, // TM Deep Security Notifier's process "Notifier.exe": "Trend Micro", // TM Deep Security Notifier's process
"dsa.exe": {console.Red, "Trend Micro"}, // TM Agent's main process "dsa.exe": "Trend Micro", // TM Agent's main process
"ds_nuagent.exe": {console.Red, "Trend Micro"}, // TM Advanced TLS traffic inspection "ds_nuagent.exe": "Trend Micro", // TM Advanced TLS traffic inspection
"coreFrameworkHost.exe": {console.Red, "Trend Micro"}, // TM Anti-malware scan process "coreFrameworkHost.exe": "Trend Micro", // TM Anti-malware scan process
"SentinelServiceHost.exe": {console.Red, "SentinelOne"}, // Sentinel One "SentinelServiceHost.exe": "SentinelOne", // Sentinel One
"SentinelStaticEngine.exe": {console.Red, "SentinelOne"}, // Sentinel One "SentinelStaticEngine.exe": "SentinelOne", // Sentinel One
"SentinelStaticEngineScanner.exe": {console.Red, "SentinelOne"}, // Sentinel One "SentinelStaticEngineScanner.exe": "SentinelOne", // Sentinel One
"SentinelAgent.exe": {console.Red, "SentinelOne"}, // Sentinel One "SentinelAgent.exe": "SentinelOne", // Sentinel One
"SentinelAgentWorker.exe": {console.Red, "SentinelOne"}, // Sentinel One "SentinelAgentWorker.exe": "SentinelOne", // Sentinel One
"SentinelHelperService.exe": {console.Red, "SentinelOne"}, // Sentinel One "SentinelHelperService.exe": "SentinelOne", // Sentinel One
"SentinelBrowserNativeHost.exe": {console.Red, "SentinelOne"}, // Sentinel One "SentinelBrowserNativeHost.exe": "SentinelOne", // Sentinel One
"SentinelUI.exe": {console.Red, "SentinelOne"}, // Sentinel One "SentinelUI.exe": "SentinelOne", // Sentinel One
"Sysmon.exe": {console.Red, "Sysmon"}, // Sysmon "Sysmon.exe": "Sysmon", // Sysmon
"Sysmon64.exe": {console.Red, "Sysmon64"}, // Sysmon64 "Sysmon64.exe": "Sysmon64", // Sysmon64
"CylanceSvc.exe": {console.Red, "Cylance"}, // Cylance "CylanceSvc.exe": "Cylance", // Cylance
"CylanceUI.exe": {console.Red, "Cylance"}, // Cylance "CylanceUI.exe": "Cylance", // Cylance
"TaniumClient.exe": {console.Red, "Tanium"}, // Tanium "TaniumClient.exe": "Tanium", // Tanium
"TaniumCX.exe": {console.Red, "Tanium"}, // Tanium "TaniumCX.exe": "Tanium", // Tanium
"TaniumDetectEngine.exe": {console.Red, "Tanium"}, // Tanium "TaniumDetectEngine.exe": "Tanium", // Tanium
"collector.exe": {console.Red, "Rapid 7 Collector"}, // Rapid 7 Insight Platform Collector "collector.exe": "Rapid 7 Collector", // Rapid 7 Insight Platform Collector
"ir_agent.exe": {console.Red, "Rapid 7 Insight Agent"}, // Rapid 7 Insight Agent "ir_agent.exe": "Rapid 7 Insight Agent", // Rapid 7 Insight Agent
"eguiproxy.exe": {console.Red, "ESET Security"}, // ESET Internet Security "eguiproxy.exe": "ESET Security", // ESET Internet Security
"ekrn.exe": {console.Red, "ESET Security"}, // ESET Internet Security "ekrn.exe": "ESET Security", // ESET Internet Security
"efwd.exe": {console.Red, "ESET Security"}, // ESET Internet Security "efwd.exe": "ESET Security", // ESET Internet Security
"AmSvc.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe "AmSvc.exe": "Cybereason ActiveProbe", // Cybereason ActiveProbe
"CrAmTray.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe "CrAmTray.exe": "Cybereason ActiveProbe", // Cybereason ActiveProbe
"CrsSvc.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe "CrsSvc.exe": "Cybereason ActiveProbe", // Cybereason ActiveProbe
"CybereasonAV.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe "CybereasonAV.exe": "Cybereason ActiveProbe", // Cybereason ActiveProbe
"cortex-xdr-payload.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - offline triage "cortex-xdr-payload.exe": "Palo Alto Cortex", // Cortex XDR - offline triage
"cysandbox.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - sandbox "cysandbox.exe": "Palo Alto Cortex", // Cortex XDR - sandbox
"cyuserservice.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - user service "cyuserservice.exe": "Palo Alto Cortex", // Cortex XDR - user service
"cywscsvc.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - security center service "cywscsvc.exe": "Palo Alto Cortex", // Cortex XDR - security center service
"tlaworker.exe": {console.Red, "Palo Alto Cortex"}, // Cortex XDR - local analysis worker "tlaworker.exe": "Palo Alto Cortex", // Cortex XDR - local analysis worker
"AEEngine.exe": {console.Red, "Faronics Anti-Executable"}, // Faronics Anti-Executable - security service "AEEngine.exe": "Faronics Anti-Executable", // Faronics Anti-Executable - security service
"Antiexecutable.exe": {console.Red, "Faronics Anti-Executable"}, // Faronics Anti-Executable - gui and tray icon "Antiexecutable.exe": "Faronics Anti-Executable", // Faronics Anti-Executable - gui and tray icon
} }
// PsCmd - List processes on the remote system // 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 { func findKnownSecurityProducts(ps *sliverpb.Ps) []string {
uniqProducts := map[string]string{} uniqProducts := map[string]struct{}{}
for _, proc := range ps.Processes { for _, proc := range ps.Processes {
if secTool, ok := knownSecurityTools[proc.Executable]; ok { if product, ok := knownSecurityTools[proc.Executable]; ok {
uniqProducts[secTool[1]] = secTool[0] uniqProducts[product] = struct{}{}
} }
} }
products := make([]string, 0, len(uniqProducts)) 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 { func procRow(proc *commonpb.Process, cmdLine bool, fullInfo bool, con *console.SliverClient) table.Row {
session, beacon := con.ActiveTarget.GetInteractive() session, beacon := con.ActiveTarget.GetInteractive()
color := console.Normal style := console.StyleNormal
if secTool, ok := knownSecurityTools[proc.Executable]; ok { if _, ok := knownSecurityTools[proc.Executable]; ok {
color = secTool[0] style = console.StyleRed
} }
if session != nil && proc.Pid == session.PID { if session != nil && proc.Pid == session.PID {
color = console.Green style = console.StyleGreen
} }
if beacon != nil && proc.Pid == beacon.PID { if beacon != nil && proc.Pid == beacon.PID {
color = console.Green style = console.StyleGreen
} }
var row table.Row var row table.Row
@@ -303,28 +303,28 @@ func procRow(proc *commonpb.Process, cmdLine bool, fullInfo bool, con *console.S
args = proc.Executable args = proc.Executable
} }
row = table.Row{ row = table.Row{
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid), style.Render(fmt.Sprintf("%d", proc.Pid)),
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid), style.Render(fmt.Sprintf("%d", proc.Ppid)),
fmt.Sprintf(color+"%s"+console.Normal, proc.Owner), style.Render(proc.Owner),
fmt.Sprintf(color+"%s"+console.Normal, proc.Architecture), style.Render(proc.Architecture),
fmt.Sprintf(color+"%s"+console.Normal, args), style.Render(args),
fmt.Sprintf(color+"%d"+console.Normal, proc.SessionID), style.Render(fmt.Sprintf("%d", proc.SessionID)),
} }
} else { } else {
if fullInfo { if fullInfo {
row = table.Row{ row = table.Row{
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid), style.Render(fmt.Sprintf("%d", proc.Pid)),
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid), style.Render(fmt.Sprintf("%d", proc.Ppid)),
fmt.Sprintf(color+"%s"+console.Normal, proc.Owner), style.Render(proc.Owner),
fmt.Sprintf(color+"%s"+console.Normal, proc.Architecture), style.Render(proc.Architecture),
fmt.Sprintf(color+"%s"+console.Normal, proc.Executable), style.Render(proc.Executable),
fmt.Sprintf(color+"%d"+console.Normal, proc.SessionID), style.Render(fmt.Sprintf("%d", proc.SessionID)),
} }
} else { } else {
row = table.Row{ row = table.Row{
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid), style.Render(fmt.Sprintf("%d", proc.Pid)),
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid), style.Render(fmt.Sprintf("%d", proc.Ppid)),
fmt.Sprintf(color+"%s"+console.Normal, proc.Executable), style.Render(proc.Executable),
} }
} }
} }
@@ -341,26 +341,26 @@ func procRow(proc *commonpb.Process, cmdLine bool, fullInfo bool, con *console.S
args = proc.Executable args = proc.Executable
} }
row = table.Row{ row = table.Row{
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid), style.Render(fmt.Sprintf("%d", proc.Pid)),
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid), style.Render(fmt.Sprintf("%d", proc.Ppid)),
fmt.Sprintf(color+"%s"+console.Normal, proc.Owner), style.Render(proc.Owner),
fmt.Sprintf(color+"%s"+console.Normal, proc.Architecture), style.Render(proc.Architecture),
fmt.Sprintf(color+"%s"+console.Normal, args), style.Render(args),
} }
} else { } else {
if fullInfo { if fullInfo {
row = table.Row{ row = table.Row{
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid), style.Render(fmt.Sprintf("%d", proc.Pid)),
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid), style.Render(fmt.Sprintf("%d", proc.Ppid)),
fmt.Sprintf(color+"%s"+console.Normal, proc.Owner), style.Render(proc.Owner),
fmt.Sprintf(color+"%s"+console.Normal, proc.Architecture), style.Render(proc.Architecture),
fmt.Sprintf(color+"%s"+console.Normal, proc.Executable), style.Render(proc.Executable),
} }
} else { } else {
row = table.Row{ row = table.Row{
fmt.Sprintf(color+"%d"+console.Normal, proc.Pid), style.Render(fmt.Sprintf("%d", proc.Pid)),
fmt.Sprintf(color+"%d"+console.Normal, proc.Ppid), style.Render(fmt.Sprintf("%d", proc.Ppid)),
fmt.Sprintf(color+"%s"+console.Normal, proc.Executable), style.Render(proc.Executable),
} }
} }
} }
+5 -6
View File
@@ -1,7 +1,6 @@
package processes package processes
import ( import (
"fmt"
"sort" "sort"
"github.com/xlab/treeprint" "github.com/xlab/treeprint"
@@ -108,14 +107,14 @@ func (t *PsTree) AddProcess(proc *commonpb.Process) {
} }
func (t *PsTree) filterProc(proc *commonpb.Process) string { func (t *PsTree) filterProc(proc *commonpb.Process) string {
color := console.Normal style := console.StyleNormal
if proc.Pid == t.implantPID { if proc.Pid == t.implantPID {
color = console.Green style = console.StyleGreen
} }
if secTool, ok := knownSecurityTools[proc.Executable]; ok { if _, ok := knownSecurityTools[proc.Executable]; ok {
color = secTool[0] style = console.StyleRed
} }
return fmt.Sprintf(color+"%s"+console.Normal, proc.Executable) return style.Render(proc.Executable)
} }
func (t *PsTree) String() string { func (t *PsTree) String() string {
+1 -2
View File
@@ -19,7 +19,6 @@ package reaction
*/ */
import ( import (
"fmt"
"strings" "strings"
"github.com/bishopfox/sliver/client/command/settings" "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) { func displayReactionsTable(eventType string, reactions []core.Reaction, con *console.SliverClient) {
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con)) 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() tw.AppendSeparator()
slackSpace := len(EventTypeToTitle(eventType)) - len("Commands") - len("ID") - 3 slackSpace := len(EventTypeToTitle(eventType)) - len("Commands") - len("ID") - 3
if slackSpace < 0 { if slackSpace < 0 {
+1
View File
@@ -26,6 +26,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("beacon-interval", "i", "", "beacon callback interval") f.StringP("beacon-interval", "i", "", "beacon callback interval")
f.StringP("beacon-jitter", "j", "", "beacon callback jitter (random up to)") f.StringP("beacon-jitter", "j", "", "beacon callback jitter (random up to)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds") f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.StringP("c2-uri", "c", "", "C2 URI to switch to")
}) })
renameCmd := &cobra.Command{ renameCmd := &cobra.Command{
+39
View File
@@ -20,6 +20,7 @@ package reconfig
import ( import (
"context" "context"
"net/url"
"time" "time"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -52,6 +53,7 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
var beaconJitter time.Duration var beaconJitter time.Duration
binterval, _ := cmd.Flags().GetString("beacon-interval") binterval, _ := cmd.Flags().GetString("beacon-interval")
bjitter, _ := cmd.Flags().GetString("beacon-jitter") bjitter, _ := cmd.Flags().GetString("beacon-jitter")
C2URI, _ := cmd.Flags().GetString("c2-uri")
if beacon != nil { if beacon != nil {
if binterval != "" { 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{ reconfig, err := con.Rpc.Reconfigure(context.Background(), &sliverpb.ReconfigureReq{
ReconnectInterval: int64(reconnectInterval), ReconnectInterval: int64(reconnectInterval),
BeaconInterval: int64(beaconInterval), BeaconInterval: int64(beaconInterval),
BeaconJitter: int64(beaconJitter), BeaconJitter: int64(beaconJitter),
C2URI: C2URI,
Request: con.ActiveTarget.Request(cmd), Request: con.ActiveTarget.Request(cmd),
}) })
if err != nil { if err != nil {
+2
View File
@@ -1,6 +1,7 @@
package registry package registry
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace" "github.com/rsteube/carapace"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -56,6 +57,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "name to assign loot (optional)") f.StringP("name", "n", "", "name to assign loot (optional)")
f.StringP("type", "T", "", "force a specific loot type (file/cred) if looting (optional)") f.StringP("type", "T", "", "force a specific loot type (file/cred) if looting (optional)")
}) })
completers.RegisterLocalFilePathFlagCompletion(registryReadHiveCmd, "save")
registryReadCmd.AddCommand(registryReadHiveCmd) registryReadCmd.AddCommand(registryReadHiveCmd)
registryWriteCmd := &cobra.Command{ registryWriteCmd := &cobra.Command{
+2
View File
@@ -1,6 +1,7 @@
package screenshot package screenshot
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -31,6 +32,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(screenshotCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(screenshotCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles() (*comp)["save"] = carapace.ActionFiles()
}) })
completers.RegisterLocalFilePathFlagCompletion(screenshotCmd, "save")
return []*cobra.Command{screenshotCmd} return []*cobra.Command{screenshotCmd}
} }
+2
View File
@@ -44,6 +44,7 @@ import (
"github.com/bishopfox/sliver/client/command/monitor" "github.com/bishopfox/sliver/client/command/monitor"
"github.com/bishopfox/sliver/client/command/operators" "github.com/bishopfox/sliver/client/command/operators"
"github.com/bishopfox/sliver/client/command/reaction" "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/sessions"
"github.com/bishopfox/sliver/client/command/settings" "github.com/bishopfox/sliver/client/command/settings"
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders" shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
@@ -97,6 +98,7 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
// Core // Core
bind(consts.GenericHelpGroup, bind(consts.GenericHelpGroup,
exit.Command, exit.Command,
serverctx.Commands,
licenses.Commands, licenses.Commands,
settings.Commands, settings.Commands,
alias.Commands, alias.Commands,
+609
View File
@@ -0,0 +1,609 @@
//go:build client
package serverctx
import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"sort"
"strings"
"sync"
"time"
"charm.land/lipgloss/v2"
"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/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])
}
+13
View File
@@ -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
}
+20 -20
View File
@@ -192,15 +192,15 @@ func PrintSessions(sessions map[string]*clientpb.Session, filter string, filterR
}) })
for _, session := range sessions { for _, session := range sessions {
color := console.Normal style := console.StyleNormal
if con.ActiveTarget.GetSession() != nil && con.ActiveTarget.GetSession().ID == session.ID { if con.ActiveTarget.GetSession() != nil && con.ActiveTarget.GetSession().ID == session.ID {
color = console.Green style = console.StyleGreen
} }
var SessionHealth string var SessionHealth string
if session.IsDead { if session.IsDead {
SessionHealth = console.Bold + console.Red + "[DEAD]" + console.Normal SessionHealth = console.StyleBoldRed.Render("[DEAD]")
} else { } else {
SessionHealth = console.Bold + console.Green + "[ALIVE]" + console.Normal SessionHealth = console.StyleBoldGreen.Render("[ALIVE]")
} }
burned := "" burned := ""
if session.Burned { if session.Burned {
@@ -211,33 +211,33 @@ func PrintSessions(sessions map[string]*clientpb.Session, filter string, filterR
var rowEntries []string var rowEntries []string
if wideTermWidth { if wideTermWidth {
rowEntries = []string{ rowEntries = []string{
fmt.Sprintf(color+"%s"+console.Normal, ShortSessionID(session.ID)), style.Render(ShortSessionID(session.ID)),
fmt.Sprintf(color+"%s"+console.Normal, session.Name), style.Render(session.Name),
fmt.Sprintf(color+"%s"+console.Normal, session.Transport), style.Render(session.Transport),
fmt.Sprintf(color+"%s"+console.Normal, session.RemoteAddress), style.Render(session.RemoteAddress),
fmt.Sprintf(color+"%s"+console.Normal, session.Hostname), style.Render(session.Hostname),
fmt.Sprintf(color+"%s"+console.Normal, username), style.Render(username),
fmt.Sprintf(color+"%s (%d)"+console.Normal, session.Filename, session.PID), style.Render(fmt.Sprintf("%s (%d)", session.Filename, session.PID)),
} }
if windowsSessionInList { if windowsSessionInList {
rowEntries = append(rowEntries, fmt.Sprintf(color+"%s"+console.Normal, session.Integrity)) rowEntries = append(rowEntries, style.Render(session.Integrity))
} }
rowEntries = append(rowEntries, []string{ rowEntries = append(rowEntries, []string{
fmt.Sprintf(color+"%s/%s"+console.Normal, session.OS, session.Arch), style.Render(fmt.Sprintf("%s/%s", session.OS, session.Arch)),
fmt.Sprintf(color+"%s"+console.Normal, session.Locale), style.Render(session.Locale),
con.FormatDateDelta(time.Unix(session.LastCheckin, 0), wideTermWidth, false), con.FormatDateDelta(time.Unix(session.LastCheckin, 0), wideTermWidth, false),
burned + SessionHealth, burned + SessionHealth,
}...) }...)
} else { } else {
rowEntries = []string{ rowEntries = []string{
fmt.Sprintf(color+"%s"+console.Normal, ShortSessionID(session.ID)), style.Render(ShortSessionID(session.ID)),
fmt.Sprintf(color+"%s"+console.Normal, session.Transport), style.Render(session.Transport),
fmt.Sprintf(color+"%s"+console.Normal, session.RemoteAddress), style.Render(session.RemoteAddress),
fmt.Sprintf(color+"%s"+console.Normal, session.Hostname), style.Render(session.Hostname),
fmt.Sprintf(color+"%s"+console.Normal, username), style.Render(username),
fmt.Sprintf(color+"%s/%s"+console.Normal, session.OS, session.Arch), style.Render(fmt.Sprintf("%s/%s", session.OS, session.Arch)),
burned + SessionHealth, burned + SessionHealth,
} }
} }
+13
View File
@@ -20,6 +20,7 @@ package settings
import ( import (
"errors" "errors"
"path/filepath"
"strconv" "strconv"
"strings" "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) con.PrintErrorf("Failed to apply settings form values: %s\n", err)
return 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 := table.NewWriter()
tw.SetStyle(GetTableStyle(con)) tw.SetStyle(GetTableStyle(con))
tw.AppendHeader(table.Row{"Name", "Value", "Description"}) tw.AppendHeader(table.Row{"Name", "Value", "Description"})
tw.AppendRow(table.Row{"Tables", con.Settings.TableStyle, "Set the stylization of tables"}) 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 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{"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"}) 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 { if width < 1 {
return errors.New("small terminal width must be 1 or greater") return errors.New("small terminal width must be 1 or greater")
} }
promptStyle := assets.NormalizePromptStyle(result.PromptStyle)
settings.TableStyle = result.TableStyle settings.TableStyle = result.TableStyle
settings.AutoAdult = result.AutoAdult settings.AutoAdult = result.AutoAdult
@@ -230,5 +242,6 @@ func applySettingsForm(settings *assets.ClientSettings, result *forms.SettingsFo
settings.VimMode = result.VimMode settings.VimMode = result.VimMode
settings.UserConnect = result.UserConnect settings.UserConnect = result.UserConnect
settings.ConsoleLogs = result.ConsoleLogs settings.ConsoleLogs = result.ConsoleLogs
settings.PromptStyle = promptStyle
return nil return nil
} }
+2 -2
View File
@@ -218,12 +218,12 @@ func PaginateTable(tw table.Writer, skipPages int, overflow bool, interactive bo
} }
con.Println() con.Println()
} else { } 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 { } else {
if 1 < len(pages) { 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 break
} }
+5 -2
View File
@@ -6,7 +6,10 @@ Implements the 'shell' command group for the Sliver client console. Handlers map
## Go Files ## 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_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. - `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.
+42
View File
@@ -5,6 +5,7 @@ import (
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants" consts "github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "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") 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} return []*cobra.Command{shellCmd}
} }
+30
View File
@@ -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)
}
+148
View File
@@ -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
}
+150
View File
@@ -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
View File
@@ -19,24 +19,34 @@ package shell
*/ */
import ( import (
"bufio"
"context" "context"
"io" "io"
"log" "log"
"os" "os"
"strconv"
"strings"
"time"
"github.com/bishopfox/sliver/client/command/settings" "github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core" "github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/sliverpb" "github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/term" "golang.org/x/term"
) )
const ( const (
windows = "windows" darwin = "darwin"
darwin = "darwin" linux = "linux"
linux = "linux" )
type shellResult int
const (
shellExited shellResult = iota
shellDetached
shellAttachFailed
) )
// ShellCmd - Start an interactive shell on the remote system. // 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 { if con.ActiveTarget.GetSession().OS != linux && con.ActiveTarget.GetSession().OS != darwin {
noPty = true // Sliver's PTYs are only supported on linux/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.Println()
con.PrintInfof("Wait approximately 10 seconds after exit, and press <enter> to continue\n") con.PrintInfof("Shell management: `shell ls`, `shell attach <id>`\n")
con.PrintInfof("Opening shell tunnel (EOF to exit) ...\n\n") con.PrintInfof("Escape: press Ctrl-] to return to the Sliver client\n")
session := con.ActiveTarget.GetSession() session := con.ActiveTarget.GetSession()
if session == nil { if session == nil {
return return shellAttachFailed
} }
// Create an RPC tunnel, then start it before binding the shell to the newly created tunnel if existing == nil {
ctxTunnel, cancelTunnel := context.WithCancel(context.Background()) con.PrintInfof("Opening shell tunnel ...\n\n")
} else {
rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{ con.PrintInfof("Re-attaching to shell %d ...\n\n", existing.ID)
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)
}
} }
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{ var (
Request: con.ActiveTarget.Request(cmd), tunnel *core.TunnelIO
Path: shellPath, managed *managedShell
EnablePTY: !noPty, enablePTY bool
Rows: rows, oldState *term.State
Cols: cols, stateSaved bool
TunnelID: tunnel.ID, stopPtySize = func() {}
}) err error
if err != nil { )
con.PrintErrorf("%s\n", err)
return 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())
if shell.Response != nil && shell.Response.Err != "" { defer cancelTunnel()
con.PrintErrorf("Error: %s\n", shell.Response.Err)
_, err = con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{ rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{
TunnelID: tunnel.ID,
SessionID: session.ID, SessionID: session.ID,
}) })
if err != nil { if err != nil {
con.PrintErrorf("RPC Error: %s\n", err) con.PrintErrorf("%s\n", err)
return shellAttachFailed
} }
return log.Printf("Created new tunnel with id: %d, binding to shell ...", rpcTunnel.TunnelID)
}
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)
var oldState *term.State // Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object.
if !noPty { 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) oldState, err = term.MakeRaw(0)
log.Printf("Saving terminal state: %v", oldState) log.Printf("Saving terminal state: %v", oldState)
if err != nil { if err != nil {
con.PrintErrorf("Failed to save terminal state") con.PrintErrorf("Failed to save terminal state\n")
return 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() {} detached, _ := runAttachedIO(tunnel, con)
if !noPty { if detached {
stopPtyResize = startPtyResizeWatcher(con, cmd, tunnel.ID) managed.SetOutput(io.Discard)
} managed.setState(shellStateDetached)
defer stopPtyResize() return shellDetached
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)
} }
if !noPty { managed.SetOutput(io.Discard)
log.Printf("Restoring terminal state ...") managed.setState(shellStateClosing)
term.Restore(0, oldState) shells.Remove(managed.ID)
}
log.Printf("Exit interactive") // "exit" should return immediately; tunnel close/cleanup is best-effort in the background.
bufio.NewWriter(os.Stdout).Flush() 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 + ")"
} }
@@ -1,6 +1,7 @@
package shellcodeencoders package shellcodeencoders
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -48,7 +49,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["arch"] = ShellcodeEncoderArchCompleter(con) (*comp)["arch"] = ShellcodeEncoderArchCompleter(con)
(*comp)["output"] = carapace.ActionFiles().Tag("output file/directory") (*comp)["output"] = carapace.ActionFiles().Tag("output file/directory")
}) })
completers.RegisterLocalFilePathFlagCompletion(shellcodeEncodersEncodeCmd, "output")
carapace.Gen(shellcodeEncodersEncodeCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file")) carapace.Gen(shellcodeEncodersEncodeCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
completers.RegisterLocalFilePathPositionalAnyCompletion(shellcodeEncodersEncodeCmd)
shellcodeEncodersCmd.AddCommand(shellcodeEncodersEncodeCmd) shellcodeEncodersCmd.AddCommand(shellcodeEncodersEncodeCmd)
return []*cobra.Command{shellcodeEncodersCmd} return []*cobra.Command{shellcodeEncodersCmd}
@@ -1,6 +1,7 @@
package sgn package sgn
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace" "github.com/rsteube/carapace"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -34,7 +35,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["arch"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures") (*comp)["arch"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures")
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save shellcode") (*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save shellcode")
}) })
completers.RegisterLocalFilePathFlagCompletion(shikataGaNaiCmd, "save")
carapace.Gen(shikataGaNaiCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file")) carapace.Gen(shikataGaNaiCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
completers.RegisterLocalFilePathPositionalCompletion(shikataGaNaiCmd, 0)
return []*cobra.Command{shikataGaNaiCmd} return []*cobra.Command{shikataGaNaiCmd}
} }
+2 -2
View File
@@ -190,7 +190,7 @@ func SelectMultipleBeaconsAndSessions(con *console.SliverClient) ([]*clientpb.Se
session.Username, session.Username,
fmt.Sprintf("%s/%s", session.OS, session.Arch), fmt.Sprintf("%s/%s", session.OS, session.Arch),
) )
fmt.Fprintf(table, option+"\n") fmt.Fprintln(table, option)
o := strings.ReplaceAll(option, "\t", "") o := strings.ReplaceAll(option, "\t", "")
sessionOptionMap[o] = session sessionOptionMap[o] = session
} }
@@ -206,7 +206,7 @@ func SelectMultipleBeaconsAndSessions(con *console.SliverClient) ([]*clientpb.Se
beacon.Username, beacon.Username,
fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch), fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch),
) )
fmt.Fprintf(table, option+"\n") fmt.Fprintln(table, option)
o := strings.ReplaceAll(option, "\t", "") o := strings.ReplaceAll(option, "\t", "")
beaconOptionMap[o] = beacon beaconOptionMap[o] = beacon
} }
+1 -1
View File
@@ -126,7 +126,7 @@ func filterTasksByTaskType(taskType string, tasks []*clientpb.BeaconTask) []*cli
func PrintTask(task *clientpb.BeaconTask, con *console.SliverClient) { func PrintTask(task *clientpb.BeaconTask, con *console.SliverClient) {
tw := table.NewWriter() tw := table.NewWriter()
tw.SetStyle(settings.GetTableWithBordersStyle(con)) 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.AppendSeparator()
tw.AppendRow(table.Row{"State", emojiState(task.State) + " " + prettyState(strings.Title(task.State))}) tw.AppendRow(table.Row{"State", emojiState(task.State) + " " + prettyState(strings.Title(task.State))})
tw.AppendRow(table.Row{"Description", task.Description}) tw.AppendRow(table.Row{"Description", task.Description})
+4 -4
View File
@@ -93,13 +93,13 @@ func PrintBeaconTasks(tasks []*clientpb.BeaconTask, cmd *cobra.Command, con *con
func prettyState(state string) string { func prettyState(state string) string {
switch strings.ToLower(state) { switch strings.ToLower(state) {
case "pending": case "pending":
return console.Bold + state + console.Normal return console.StyleBold.Render(state)
case "sent": case "sent":
return console.Bold + console.Orange + state + console.Normal return console.StyleBoldOrange.Render(state)
case "completed": case "completed":
return console.Bold + console.Green + state + console.Normal return console.StyleBoldGreen.Render(state)
case "canceled": case "canceled":
return console.Bold + console.Gray + state + console.Normal return console.StyleBoldGray.Render(state)
default: default:
return state return state
} }
+1
View File
@@ -33,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
(*comp)["proxy"] = completers.LocalProxyCompleter() (*comp)["proxy"] = completers.LocalProxyCompleter()
}) })
completers.RegisterLocalFilePathFlagCompletion(updateCmd, "save")
versionCmd := &cobra.Command{ versionCmd := &cobra.Command{
Use: consts.VersionStr, Use: consts.VersionStr,
+3
View File
@@ -1,6 +1,7 @@
package wasm package wasm
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -35,9 +36,11 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["file"] = carapace.ActionFiles() (*comp)["file"] = carapace.ActionFiles()
(*comp)["dir"] = carapace.ActionDirectories() (*comp)["dir"] = carapace.ActionDirectories()
}) })
completers.RegisterLocalFilePathFlagCompletion(wasmCmd, "file")
wasmComp := carapace.Gen(wasmCmd) wasmComp := carapace.Gen(wasmCmd)
wasmComp.PositionalCompletion(carapace.ActionFiles().Usage("wasm/wasi module file (.wasm)")) wasmComp.PositionalCompletion(carapace.ActionFiles().Usage("wasm/wasi module file (.wasm)"))
wasmComp.PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the wasm module (optional)")) wasmComp.PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the wasm module (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(wasmCmd, 0)
wasmLsCmd := &cobra.Command{ wasmLsCmd := &cobra.Command{
Use: consts.LsStr, Use: consts.LsStr,
+1 -1
View File
@@ -278,7 +278,7 @@ func WasmLsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
cacheLine = " (cached)" cacheLine = " (cached)"
} else { } else {
wasmRegistrationCache[idOf(con)] = append(wasmRegistrationCache[idOf(con)], extName) 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) con.PrintInfof(" %s%s\n", extName, cacheLine)
} }
+17 -1
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"strings" "strings"
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -66,14 +67,28 @@ func Commands(con *console.SliverClient) []*cobra.Command {
websitesCmd.AddCommand(websitesShowCmd) websitesCmd.AddCommand(websitesShowCmd)
websitesRmCmd := &cobra.Command{ websitesRmCmd := &cobra.Command{
Use: consts.RmStr, Use: consts.RmStr + " [name]",
Short: "Remove an entire website and all of its contents", Short: "Remove an entire website and all of its contents",
Long: help.GetHelpFor([]string{consts.WebsitesStr, consts.RmStr}), Long: help.GetHelpFor([]string{consts.WebsitesStr, consts.RmStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
WebsiteRmCmd(cmd, con, args) WebsiteRmCmd(cmd, con, args)
}, },
} }
carapace.Gen(websitesRmCmd).PositionalCompletion(WebsiteNameCompleter(con)) 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) websitesCmd.AddCommand(websitesRmCmd)
websitesRmWebContentCmd := &cobra.Command{ websitesRmWebContentCmd := &cobra.Command{
@@ -113,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["content"] = carapace.ActionFiles().Tag("content directory/files") (*comp)["content"] = carapace.ActionFiles().Tag("content directory/files")
(*comp)["website"] = WebsiteNameCompleter(con) (*comp)["website"] = WebsiteNameCompleter(con)
}) })
completers.RegisterLocalFilePathFlagCompletion(websitesContentCmd, "content")
websitesCmd.AddCommand(websitesContentCmd) websitesCmd.AddCommand(websitesContentCmd)
websitesContentTypeCmd := &cobra.Command{ websitesContentTypeCmd := &cobra.Command{
+34 -1
View File
@@ -20,8 +20,10 @@ package websites
import ( import (
"context" "context"
"fmt"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb" "github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -32,12 +34,43 @@ func WebsiteRmCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
if len(args) > 0 { if len(args) > 0 {
name = 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, Name: name,
}) })
if err != nil { if err != nil {
con.PrintErrorf("Failed to remove website %s", err) con.PrintErrorf("Failed to remove website %s", err)
return 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)
}
}
+2
View File
@@ -1,6 +1,7 @@
package wireguard package wireguard
import ( import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags" "github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help" "github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console" "github.com/bishopfox/sliver/client/console"
@@ -31,6 +32,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(wgConfigCmd, func(comp *carapace.ActionMap) { flags.BindFlagCompletions(wgConfigCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save config") (*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save config")
}) })
completers.RegisterLocalFilePathFlagCompletion(wgConfigCmd, "save")
return []*cobra.Command{wgConfigCmd} return []*cobra.Command{wgConfigCmd}
} }
+160
View File
@@ -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
View File
@@ -35,6 +35,7 @@ import (
consts "github.com/bishopfox/sliver/client/constants" consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/core" "github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/client/spin" "github.com/bishopfox/sliver/client/spin"
"github.com/bishopfox/sliver/client/theme"
"github.com/bishopfox/sliver/client/version" "github.com/bishopfox/sliver/client/version"
"github.com/bishopfox/sliver/protobuf/clientpb" "github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb" "github.com/bishopfox/sliver/protobuf/commonpb"
@@ -46,6 +47,7 @@ import (
"github.com/reeflective/readline" "github.com/reeflective/readline"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/exp/slog" "golang.org/x/exp/slog"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -54,32 +56,10 @@ const (
) )
const ( const (
// ANSI Colors. // Terminal control sequences (not "styling").
Normal = "\033[0m" Clearln = "\r\x1b[2K"
Black = "\033[30m" UpN = "\033[%dA"
Red = "\033[31m" DownN = "\033[%dB"
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
) )
// Observer - A function to call when the sessions changes. // Observer - A function to call when the sessions changes.
@@ -106,6 +86,21 @@ type SliverClient struct {
stdoutPipeWriter *os.File stdoutPipeWriter *os.File
stdoutPipeDone chan struct{} stdoutPipeDone chan struct{}
stdoutPipeOnce sync.Once 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. // 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. // thus has not started monitoring any server events, or started the application.
func NewConsole(isServer bool) *SliverClient { func NewConsole(isServer bool) *SliverClient {
assets.Setup(false, false) assets.Setup(false, false)
_ = theme.LoadAndSetCurrentTheme()
ApplyTheme(theme.Current())
settings, _ := assets.LoadSettings() settings, _ := assets.LoadSettings()
con := &SliverClient{ con := &SliverClient{
@@ -126,7 +123,10 @@ func NewConsole(isServer bool) *SliverClient {
BeaconTaskCallbacksMutex: &sync.Mutex{}, BeaconTaskCallbacksMutex: &sync.Mutex{},
IsServer: isServer, IsServer: isServer,
Settings: settings, 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 // The active target needs access to the console
// to automatically switch between command menus. // 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. // 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 // 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. // 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 { func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, grpcConn *grpc.ClientConn, details *ConnectionDetails, serverCmds, sliverCmds console.Commands, run bool, rcScript string) error {
con.Rpc = rpc
con.IsCLI = !run con.IsCLI = !run
con.serverCmds = serverCmds con.serverCmds = serverCmds
con.sliverCmds = sliverCmds con.sliverCmds = sliverCmds
@@ -188,39 +187,29 @@ func StartClient(con *SliverClient, rpc rpcpb.SliverRPCClient, serverCmds, slive
sliver := con.App.Menu(consts.ImplantMenu) sliver := con.App.Menu(consts.ImplantMenu)
sliver.SetCommands(sliverCmds) sliver.SetCommands(sliverCmds)
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, con.allowServerRootCommands) con.applyConnectionHooksOnce()
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)
// console logger // console logger
if con.Settings.ConsoleLogs { if con.Settings.ConsoleLogs {
// Classic logs // Classic logs
consoleLog := getConsoleLogFile() consoleLog := getConsoleLogFile()
consoleLogStream, err := con.ClientLogStream("json") con.ensureJSONRemoteWriter()
if err != nil { con.setupLogger(consoleLog, con.jsonRemoteWriter)
log.Printf("Could not get client json log stream: %s", err)
}
con.setupLogger(consoleLog, consoleLogStream)
defer consoleLog.Close() defer consoleLog.Close()
// Ascii cast sessions (complete terminal interface). // Ascii cast sessions (complete terminal interface) are only useful
asciicastLog := getConsoleAsciicastFile() // for the interactive console. In CLI mode they would clobber stdout.
defer asciicastLog.Close() if !con.IsCLI {
asciicastLog := getConsoleAsciicastFile()
defer asciicastLog.Close()
asciicastStream, err := con.ClientLogStream("asciicast") con.ensureAsciicastRemoteWriter()
if err != nil { con.setupAsciicastRecord(asciicastLog, con.asciicastRemoteWriter)
log.Printf("Could not get client asciicast log stream: %s", err)
} }
con.setupAsciicastRecord(asciicastLog, asciicastStream) }
if err := con.SetConnection(rpc, grpcConn, details); err != nil {
return err
} }
if rcScript != "" { if rcScript != "" {
@@ -319,10 +308,29 @@ func (con *SliverClient) runRCLine(serverCmds, sliverCmds console.Commands, line
return nil return nil
} }
func (con *SliverClient) startEventLoop() { func (con *SliverClient) applyConnectionHooksOnce() {
eventStream, err := con.Rpc.Events(context.Background(), &commonpb.Empty{}) 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 { if err != nil {
fmt.Printf(Warn+"%s\n", err) fmt.Printf("%s%s\n", Warn, err)
return return
} }
for { for {
@@ -337,7 +345,7 @@ func (con *SliverClient) startEventLoop() {
switch event.EventType { switch event.EventType {
case consts.CanaryEvent: 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) sessions := con.GetSessionsByName(event.Session.Name)
for _, session := range sessions { for _, session := range sessions {
shortID := strings.Split(session.ID, "-")[0] shortID := strings.Split(session.ID, "-")[0]
@@ -346,7 +354,7 @@ func (con *SliverClient) startEventLoop() {
case consts.WatchtowerEvent: case consts.WatchtowerEvent:
msg := string(event.Data) 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) sessions := con.GetSessionsByName(event.Session.Name)
for _, session := range sessions { for _, session := range sessions {
shortID := strings.Split(session.ID, "-")[0] shortID := strings.Split(session.ID, "-")[0]
@@ -462,7 +470,7 @@ func (con *SliverClient) triggerReactions(event *clientpb.Event) {
for _, reaction := range reactions { for _, reaction := range reactions {
for _, line := range reaction.Commands { 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) err := con.App.ActiveMenu().RunCommandLine(context.Background(), line)
if err != nil { if err != nil {
con.PrintErrorf("Reaction command error: %s\n", err) 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 { func (con *SliverClient) GetPrompt() string {
prompt := Underline + "sliver" + Normal promptStyle := assets.PromptStyleHost
if con.IsServer { if con.Settings != nil {
prompt = Bold + "[server] " + Normal + Underline + "sliver" + Normal promptStyle = assets.NormalizePromptStyle(con.Settings.PromptStyle)
} }
if con.ActiveTarget.GetSession() != nil {
prompt += fmt.Sprintf(Bold+Red+" (%s)%s", con.ActiveTarget.GetSession().Name, Normal) if promptStyle == assets.PromptStyleCustom {
} else if con.ActiveTarget.GetBeacon() != nil { src := assets.DefaultPromptTemplate
prompt += fmt.Sprintf(Bold+Blue+" (%s)%s", con.ActiveTarget.GetBeacon().Name, Normal) 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 += " > " prompt += " > "
return Clearln + prompt return Clearln + prompt
} }
func (con *SliverClient) PrintLogo() { 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 { 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 := "" dirty := ""
if serverVer.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) serverSemVer := fmt.Sprintf("%d.%d.%d", serverVer.Major, serverVer.Minor, serverVer.Patch)
fmt.Printf("%sServer v%s - %s%s\r\n", Info, serverSemVer, serverVer.Commit, dirty)
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)
if version.GitCommit != serverVer.Commit { 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") fmt.Println(Info + "Welcome to the sliver shell, please type 'help' for options\r")
if serverVer.Major != int32(version.SemanticVersion()[0]) { 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() con.CheckLastUpdate()
} }
@@ -564,7 +638,7 @@ func (con *SliverClient) CheckLastUpdate() {
day := 24 * time.Hour day := 24 * time.Hour
if compiledAt.Add(30 * day).Before(now) { if compiledAt.Add(30 * day).Before(now) {
if lastUpdate == nil || lastUpdate.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 { func (con *SliverClient) GetSessionsByName(name string) []*clientpb.Session {
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{}) sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
if err != nil { if err != nil {
fmt.Printf(Warn+"%s\n", err) fmt.Printf("%s%s\n", Warn, err)
return nil return nil
} }
matched := []*clientpb.Session{} 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() interval = time.Since(t).Round(time.Second).String()
} }
if color { if color {
interval = fmt.Sprintf("%s%s%s", Bold+Red, interval, Normal) interval = StyleBoldRed.Render(interval)
} }
} else { } else {
if includeDate { 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() interval = time.Until(t).Round(time.Second).String()
} }
if color { if color {
interval = fmt.Sprintf("%s%s%s", Bold+Green, interval, Normal) interval = StyleBoldGreen.Render(interval)
} }
} }
return interval return interval
@@ -789,7 +863,7 @@ type ActiveTarget struct {
// GetSessionInteractive - Get the active target(s). // GetSessionInteractive - Get the active target(s).
func (s *ActiveTarget) GetInteractive() (*clientpb.Session, *clientpb.Beacon) { func (s *ActiveTarget) GetInteractive() (*clientpb.Session, *clientpb.Beacon) {
if s.session == nil && s.beacon == nil { 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 nil, nil
} }
return s.session, s.beacon return s.session, s.beacon
@@ -803,7 +877,7 @@ func (s *ActiveTarget) Get() (*clientpb.Session, *clientpb.Beacon) {
// GetSessionInteractive - GetSessionInteractive the active session. // GetSessionInteractive - GetSessionInteractive the active session.
func (s *ActiveTarget) GetSessionInteractive() *clientpb.Session { func (s *ActiveTarget) GetSessionInteractive() *clientpb.Session {
if s.session == nil { 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 nil
} }
return s.session return s.session
@@ -817,7 +891,7 @@ func (s *ActiveTarget) GetSession() *clientpb.Session {
// GetBeaconInteractive - Get beacon interactive the active session. // GetBeaconInteractive - Get beacon interactive the active session.
func (s *ActiveTarget) GetBeaconInteractive() *clientpb.Beacon { func (s *ActiveTarget) GetBeaconInteractive() *clientpb.Beacon {
if s.beacon == nil { 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 nil
} }
return s.beacon return s.beacon
@@ -1037,82 +1111,60 @@ var abilities = []string{
"jump-start", "jump-start",
} }
var asciiLogos = []string{ type asciiLogoStyle int
Red + `
██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
▒██ ▒ ▓██▒ ▓██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒
░ ▓██▄ ▒██░ ▒██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒
▒ ██▒▒██░ ░██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄
▒██████▒▒░██████▒░██░ ▒▀█░ ░▒████▒░██▓ ▒██▒
▒ ▒▓▒ ▒ ░░ ▒░▓ ░░▓ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░
░ ░▒ ░ ░░ ░ ▒ ░ ▒ ░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░
░ ░ ░ ░ ░ ▒ ░ ░░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░
` + Normal,
Green + ` const (
███████╗██╗ ██╗██╗ ██╗███████╗██████╗ logoStyleRed asciiLogoStyle = iota
██╔════╝██║ ██║██║ ██║██╔════╝██╔══██╗ logoStyleGreen
███████╗██║ ██║██║ ██║█████╗ ██████╔╝ logoStyleBoldGray
╚════██║██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ )
███████║███████╗██║ ╚████╔╝ ███████╗██║ ██║
╚══════╝╚══════╝╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
` + Normal,
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.--. |
| :/\: || :/\: || (\/) || :(): || (\/) || :(): | | :/\: || :/\: || (\/) || :(): || (\/) || :(): |
| :\/: || (__) || :\/: || ()() || :\/: || ()() | | :\/: || (__) || :\/: || ()() || :\/: || ()() |
| '--'S|| '--'L|| '--'I|| '--'V|| '--'E|| '--'R| | '--'S|| '--'L|| '--'I|| '--'V|| '--'E|| '--'R|
` + "`------'`------'`------'`------'`------'`------'" + ` ` + "`------'`------'`------'`------'`------'`------'" + `
` + Normal, `},
Purple + `
****@@ @@****
@@@@@@***@ @***@@@@@@
@%%@@@%%#***@ @***#%%@@@%%@
%%%%%##%%%%****@ @****%%%%##%%%%%
%%%%#####%%%%#*###@ @#####%%%%#####%%%%
@%%%*@#####%%%%@####@ @####@%%%%#####@#%%%@
%%%+@**#####%%%%#####@ @#####%%%%#####**@+%%%@
#%%=+*+**###%#%%%###### ######%%%%%###**+*+=%%%
%#%===++@*####%@@@@###### ######@@@@%####*@++===%%%
@#@--===+**%%%@@@@@@#####%% %%#####@@@@@@%%#**+===--@#@
#@----%=+*%%%%@@@%%%####%%%@ @ @ @%%%####%%%@@@%%%%*+=%----@#
#%----=#++*%%%%@%%%%%@%%%%%%%%%@ @%%%%%%%%%@%%%%%@%%%%*++#=----%#
%+----===+**@%%@#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%@**+===----+%
@=----==++***@%@#%%%%%%%@%%%%@%%%%%%%%%%%%%%%%%%%%@%%%%@%%%%%%%#@%@***++==-----@
-----==+#%**##@@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%@@##**%#+==-----
%----==+**%%%%@@@#@%%%%%%%%%%@%%%%%%%%%%%%%%%%%%@%%%%%%%%%%@#@@@%%%%*++==----%
=--#+=+**#@%%####%####%%%%@@@@@%%%%%%##%%%%%%@@@@@@%%%####%####%%@#**+=+#--=
+-==+*%%%%########%#######%@@@@@@%%%%%#%%@@@@@@%#######%########%%%%*+==-=
-----==++**#%######################%@@@%%%%@@@%######################%#**++==-----
=--===+**######@##############%%%%%%%%%%%%%%%%%%%%##############@######**+===--=
%-==+%@@@#######%%%%%%%%%@@%%%%%%%%%%%%%%%%%%%%%%%%@@%%%%%%%%%#######@@@%+==-%
@=@@@@@@@@#%%%%%%%%%%%%%%@@%%%%%%%%%%%%%%%%%%%%%%@@@%%%%%%%%%%%%%#@@@@@@@@=@
@@@@@@@@@@@@@%%%%%%%%%%%%@@@@%%%%%%%%**%%%%%%%%@@@@%%%%%%%%%%%%@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@%%#%@@%%%%@@@@@%%%%%@#**#@%%%%%@@@@@%%%%@@%#%%@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@%#####%#%%%%%%%@%###**###%@%%%%%%%#######%@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@*##%%%%%%%%%%%%%##**##%%%%%%%%%%%%###*@@@@@@@@@@@@@@@@@@@@@@@
@%%%%%@@@@@@@@@@@@@@@@@@@ %%%%%%%%%%%%%##**##%%%%%%%%%%%%% @@@@@@@@@@@@@@@@@@@%%%%%@
@%%%%%%%%%%%%%%@@@@@@@@@@@@ . %%%%%@%%%%%##**##%%%%%@%%%%% @@@@@@@@@@@@%%%%%%%%%%%%%%@
%%%%%%%@+. % :@%%%%%%%%@@@@@@@@@%@%%@@@@##**##@@@@%%@%@@@@@@@@@%%%%%%%%@: % ..+@%%%%%%%
%#. . : .-@%%%%%@@@@@@@@@@@@@@##**##@@@@@@@@@@@@@@%%%%%@-. : .*%
@%%@* #..:--=++*@%@@@@@@@@@@@@##**##@@@@@@@@@@@@@@*++==-:. # *@%%@
%%%%%%@..*.:+% ..::+-==++++@-=@@@@@@@@@@@%####%@@@@@@@@@@@=-@++++==-+::.. #+:.*..@%%%%%%
@###%.......::%-==++**#@++#*: .:-+@@@@@@@@@@%####%@@@@@@@@@@+-:. .*#++@#**++==-%::.......%###@
#%..........:::-==%+*******@* . .:-+@@@@@@@@@@####@@@@@@@@@@+-: #@*******+%=--:::..........%#
@@@+:...::::--=++**@***##*==+ :-+@@@.@@@@%%##%%@@@@.@@@+-: ++=*##***@**++=--::::...:+@@@
@%%%***###%@#****##@##++++++. .:=*@+ @@@@%%##%%@@@@ =@*=:. .+++++=##@##****#@%###***%%%@
@@=====+++++***#####%%%%@++++**#.:=*@ .@@%%####%%@@. @*=:.#***+++@%%%%#####***+++++=====@@
@@@#+++++***##%%%%%***##%%*# @@*------*@@ #*%%##***%%%%%##***+++++#@@@
@@@@%%%@%%%%%. **--------** .%%%%%%%%%@@@@
@%%%% %@@@@@@@@@@@@@ %%%%@
@@ @::@@@@@@@@@@::@ @@
-::::::@@@@:::::::
*-----========-----+
@###%%%@@@@%%%###@
@%%%%%%@
` + Normal,
} }
+111 -2
View File
@@ -26,6 +26,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"time" "time"
"github.com/bishopfox/sliver/client/assets" "github.com/bishopfox/sliver/client/assets"
@@ -51,6 +52,109 @@ func (l *ConsoleClientLogger) Write(buf []byte) (int, error) {
return len(buf), err 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 // ClientLogStream requires a log stream name, used to save the logs
// going through this stream in a specific log subdirectory/file. // going through this stream in a specific log subdirectory/file.
func (con *SliverClient) ClientLogStream(name string) (*ConsoleClientLogger, error) { 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) con.jsonHandler = slog.NewJSONHandler(logWriter, jsonOptions)
// Log all commands before running them. // 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. // 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...)) 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. // PrintErrorf prints an error message immediately below the last line of output.
+233
View File
@@ -0,0 +1,233 @@
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"
)
type promptPalette struct {
Default string
Mods map[int]string
}
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: p.Default,
Mods: map[int]string{},
}
for k, v := range p.Mods {
out.Mods[k] = 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
}
+153
View File
@@ -0,0 +1,153 @@
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 (
"image/color"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/theme"
)
// 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) color.Color {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
return lipgloss.Color("#ffffff")
}
paletteMod := func(p theme.Palette, mod int) color.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("[+] ")
}
Loaded 100 of 467 files, more files were not shown because too many files have changed in this diff. Show more