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
0xOral c8be5a5b1b add --spoof feature 2026-02-16 02:27:59 +02:00
380 changed files with 46749 additions and 7161 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,
+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"
)
@@ -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)
}
@@ -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 -1
View File
@@ -22,6 +22,7 @@ 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 = {
@@ -41,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>
) => {
@@ -541,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-") &&
@@ -596,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;
@@ -629,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;
+70 -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,
@@ -158,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>
@@ -243,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={() => {
+1231 -4
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -26,6 +26,7 @@
"framer-motion": "^12.23.22",
"fuse.js": "^7.1.0",
"lunr": "^2.3.9",
"mermaid": "^11.12.2",
"next": "^16.1.6",
"next-themes": "^0.4.6",
"react": "^19.2.0",
+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' 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'`;
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>
+14 -10
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 (
@@ -1,4 +1,4 @@
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 systems like Linux or MacOS. If you only have a Windows machine, you should be able to use the Windows Subsystem for Linux (WSL).
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:
@@ -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.
+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
@@ -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>
+53 -21
View File
@@ -1,7 +1,7 @@
import Youtube from "@/components/youtube";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Card, CardFooter, CardHeader } from "@heroui/react";
import { Accordion, AccordionItem, Button, Card, CardFooter } from "@heroui/react";
import { NextPage } from "next";
import Head from "next/head";
import React from "react";
@@ -13,9 +13,9 @@ type Talk = {
};
type TalkSection = {
title: "Workshops" | "General Tradecraft";
key: "workshops" | "general-tradecraft" | "community";
title: "Workshops" | "General Tradecraft" | "Community";
description: string;
badge: string;
talks: Talk[];
};
@@ -50,19 +50,53 @@ const generalTradecraftTalks: Talk[] = [
}
];
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.",
badge: "Workshop",
talks: workshopTalks,
},
{
key: "general-tradecraft",
title: "General Tradecraft",
description: "Broader offensive engineering and tradecraft talks.",
badge: "Tradecraft",
talks: generalTradecraftTalks,
},
{
key: "community",
title: "Community",
description: "Community-created videos listed in Community Guides.",
talks: communityTalks,
},
];
const TalksIndexPage: NextPage = () => {
@@ -80,25 +114,23 @@ const TalksIndexPage: NextPage = () => {
<title>Sliver Talks</title>
</Head>
<div className="mt-6 px-4 pb-8 sm:px-6 lg:px-12">
{talkSections.map((section, sectionIndex) => {
return (
<section
key={section.title}
className={sectionIndex === 0 ? "" : "mt-8"}
<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="mb-4">
<h2 className="text-3xl font-semibold">{section.title}</h2>
<p className="mt-2 text-sm text-default-500">{section.description}</p>
</div>
<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">
<CardHeader className="absolute z-10 top-1 flex-col items-end">
</CardHeader>
<Youtube
className="w-full"
url={talk.url}
@@ -128,9 +160,9 @@ const TalksIndexPage: NextPage = () => {
</div>
))}
</div>
</section>
);
})}
</AccordionItem>
))}
</Accordion>
</div>
</>
);
+14 -10
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 (
+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=
+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)
}
})
}
+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()
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;
+1086 -1078
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -85,6 +85,8 @@ service SliverRPC {
// *** Implants ***
rpc Generate(clientpb.GenerateReq) returns (clientpb.Generate);
rpc GenerateSpoofMetadata(clientpb.GenerateSpoofMetadataReq)
returns (commonpb.Empty);
rpc GenerateExternal(clientpb.ExternalGenerateReq)
returns (clientpb.ExternalImplantConfig);
rpc GenerateExternalSaveBuild(clientpb.ExternalImplantBinary)
+36
View File
@@ -81,6 +81,7 @@ type SliverRPCClient interface {
HostIOCRm(ctx context.Context, in *clientpb.IOC, opts ...grpc.CallOption) (*commonpb.Empty, error)
// *** Implants ***
Generate(ctx context.Context, in *clientpb.GenerateReq, opts ...grpc.CallOption) (*clientpb.Generate, error)
GenerateSpoofMetadata(ctx context.Context, in *clientpb.GenerateSpoofMetadataReq, opts ...grpc.CallOption) (*commonpb.Empty, error)
GenerateExternal(ctx context.Context, in *clientpb.ExternalGenerateReq, opts ...grpc.CallOption) (*clientpb.ExternalImplantConfig, error)
GenerateExternalSaveBuild(ctx context.Context, in *clientpb.ExternalImplantBinary, opts ...grpc.CallOption) (*commonpb.Empty, error)
GenerateExternalGetBuildConfig(ctx context.Context, in *clientpb.ImplantBuild, opts ...grpc.CallOption) (*clientpb.ExternalImplantConfig, error)
@@ -685,6 +686,15 @@ func (c *sliverRPCClient) Generate(ctx context.Context, in *clientpb.GenerateReq
return out, nil
}
func (c *sliverRPCClient) GenerateSpoofMetadata(ctx context.Context, in *clientpb.GenerateSpoofMetadataReq, opts ...grpc.CallOption) (*commonpb.Empty, error) {
out := new(commonpb.Empty)
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/GenerateSpoofMetadata", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *sliverRPCClient) GenerateExternal(ctx context.Context, in *clientpb.ExternalGenerateReq, opts ...grpc.CallOption) (*clientpb.ExternalImplantConfig, error) {
out := new(clientpb.ExternalImplantConfig)
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/GenerateExternal", in, out, opts...)
@@ -2113,6 +2123,7 @@ type SliverRPCServer interface {
HostIOCRm(context.Context, *clientpb.IOC) (*commonpb.Empty, error)
// *** Implants ***
Generate(context.Context, *clientpb.GenerateReq) (*clientpb.Generate, error)
GenerateSpoofMetadata(context.Context, *clientpb.GenerateSpoofMetadataReq) (*commonpb.Empty, error)
GenerateExternal(context.Context, *clientpb.ExternalGenerateReq) (*clientpb.ExternalImplantConfig, error)
GenerateExternalSaveBuild(context.Context, *clientpb.ExternalImplantBinary) (*commonpb.Empty, error)
GenerateExternalGetBuildConfig(context.Context, *clientpb.ImplantBuild) (*clientpb.ExternalImplantConfig, error)
@@ -2413,6 +2424,9 @@ func (UnimplementedSliverRPCServer) HostIOCRm(context.Context, *clientpb.IOC) (*
func (UnimplementedSliverRPCServer) Generate(context.Context, *clientpb.GenerateReq) (*clientpb.Generate, error) {
return nil, status.Errorf(codes.Unimplemented, "method Generate not implemented")
}
func (UnimplementedSliverRPCServer) GenerateSpoofMetadata(context.Context, *clientpb.GenerateSpoofMetadataReq) (*commonpb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method GenerateSpoofMetadata not implemented")
}
func (UnimplementedSliverRPCServer) GenerateExternal(context.Context, *clientpb.ExternalGenerateReq) (*clientpb.ExternalImplantConfig, error) {
return nil, status.Errorf(codes.Unimplemented, "method GenerateExternal not implemented")
}
@@ -3679,6 +3693,24 @@ func _SliverRPC_Generate_Handler(srv interface{}, ctx context.Context, dec func(
return interceptor(ctx, in, info, handler)
}
func _SliverRPC_GenerateSpoofMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(clientpb.GenerateSpoofMetadataReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SliverRPCServer).GenerateSpoofMetadata(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpcpb.SliverRPC/GenerateSpoofMetadata",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SliverRPCServer).GenerateSpoofMetadata(ctx, req.(*clientpb.GenerateSpoofMetadataReq))
}
return interceptor(ctx, in, info, handler)
}
func _SliverRPC_GenerateExternal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(clientpb.ExternalGenerateReq)
if err := dec(in); err != nil {
@@ -6393,6 +6425,10 @@ var SliverRPC_ServiceDesc = grpc.ServiceDesc{
MethodName: "Generate",
Handler: _SliverRPC_Generate_Handler,
},
{
MethodName: "GenerateSpoofMetadata",
Handler: _SliverRPC_GenerateSpoofMetadata_Handler,
},
{
MethodName: "GenerateExternal",
Handler: _SliverRPC_GenerateExternal_Handler,
+16 -1
View File
@@ -100,6 +100,8 @@ func StartDNSListener(bindIface string, lport uint16, domains []string, canaries
server.startInitGC()
dnsLog.Infof("Starting DNS listener for %v (canaries: %v) ...", domains, canaries)
dns.HandleFunc(".", func(writer dns.ResponseWriter, req *dns.Msg) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns request callback")
started := time.Now()
server.HandleDNSRequest(domains, canaries, writer, req)
dnsLog.Debugf("DNS server took %s", time.Since(started))
@@ -226,6 +228,8 @@ func (s *DNSSession) IncomingPendingEnvelope(msgID uint32, size uint32) *Pending
// ForwardCompletedEnvelope - Reassembles and forwards envelopes to core
func (s *DNSSession) ForwardCompletedEnvelope(msgID uint32, pending *PendingEnvelope) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns ForwardCompletedEnvelope")
dnsLog.Debugf("[dns] dns session id: %d, msg id: %d completed message", s.ID, msgID)
s.incomingMutex.Lock()
delete(s.incomingEnvelopes, msgID) // Remove pending message
@@ -373,6 +377,8 @@ func (s *SliverDNSServer) startInitGC() {
s.gcStop = make(chan struct{})
s.gcDone = make(chan struct{})
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns init gc")
ticker := time.NewTicker(defaultPendingDNSInitGCInterval)
defer ticker.Stop()
defer close(s.gcDone)
@@ -420,6 +426,8 @@ func (s *SliverDNSServer) gcPendingInits() {
// ---------------------------
// Handles all DNS queries, first we determine if the query is C2 or a canary
func (s *SliverDNSServer) HandleDNSRequest(domains []string, canaries bool, writer dns.ResponseWriter, req *dns.Msg) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns HandleDNSRequest")
if req == nil {
dnsLog.Info("req can not be nil")
return
@@ -682,6 +690,8 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
}
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns session send loop")
dnsLog.Debugf("[dns] starting implant conn send loop")
for envelope := range dnsSession.ImplantConn.Send {
dnsSession.StageOutgoingEnvelope(envelope)
@@ -830,7 +840,10 @@ func (s *SliverDNSServer) handleDataFromImplant(domain string, msg *dnspb.DNSMes
pending := dnsSession.IncomingPendingEnvelope(msg.ID, msg.Size)
complete := pending.Insert(msg)
if complete {
go dnsSession.ForwardCompletedEnvelope(msg.ID, pending)
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns completed envelope dispatch")
dnsSession.ForwardCompletedEnvelope(msg.ID, pending)
}()
}
resp := new(dns.Msg)
@@ -981,6 +994,8 @@ func (s *SliverDNSServer) handleNOP(domain string, msg *dnspb.DNSMessage, checks
func (s *SliverDNSServer) handleCanary(req *dns.Msg) *dns.Msg {
// Don't block, return error as fast as possible
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns handleCanary")
reqDomain := strings.ToLower(req.Question[0].Name)
if !strings.HasSuffix(reqDomain, ".") {
reqDomain += "." // Ensure we have the FQDN
+15 -8
View File
@@ -68,12 +68,12 @@ var (
)
const (
DefaultMaxBodyLength = 2 * 1024 * 1024 * 1024 // 2Gb
DefaultMaxUnauthBodyLength = 8 * 1024 * 1024 // 8Mb
DefaultHTTPTimeout = time.Minute
DefaultLongPollTimeout = time.Second
DefaultLongPollJitter = time.Second
minPollTimeout = time.Second
DefaultMaxBodyLength = 2 * 1024 * 1024 * 1024 // 2Gb
DefaultMaxUnauthBodyLength = 8 * 1024 * 1024 // 8Mb
DefaultHTTPTimeout = time.Minute
DefaultLongPollTimeout = time.Second
DefaultLongPollJitter = time.Second
minPollTimeout = time.Second
)
var (
@@ -574,7 +574,7 @@ func (s *SliverHTTPC2) startSessionHandler(resp http.ResponseWriter, req *http.R
s.defaultHandler(resp, req)
return
}
data, err := encoder.Decode(body)
data, err := decodeReqBodyWithMaxLen(encoder, body, int64(DefaultMaxUnauthBodyLength))
if err != nil {
httpLog.Errorf("Failed to decode body %s", err)
s.defaultHandler(resp, req)
@@ -720,7 +720,7 @@ func (s *SliverHTTPC2) readReqBody(httpSession *HTTPSession, resp http.ResponseW
return nil, err
}
data, err := encoder.Decode(body)
data, err := decodeReqBodyWithMaxLen(encoder, body, int64(DefaultMaxBodyLength))
if err != nil {
httpLog.Warnf("Failed to decode body %s", err)
s.defaultHandler(resp, req)
@@ -735,6 +735,13 @@ func (s *SliverHTTPC2) readReqBody(httpSession *HTTPSession, resp http.ResponseW
return plaintext, err
}
func decodeReqBodyWithMaxLen(encoder sliverEncoders.Encoder, body []byte, maxDecodedLen int64) ([]byte, error) {
if limitedDecoder, ok := encoder.(sliverEncoders.LimitedDecoder); ok {
return limitedDecoder.DecodeWithMaxLen(body, maxDecodedLen)
}
return encoder.Decode(body)
}
func (s *SliverHTTPC2) getServerPollTimeout() time.Duration {
min := s.ServerConf.LongPollTimeout
max := s.ServerConf.LongPollTimeout + s.ServerConf.LongPollJitter
+12
View File
@@ -126,6 +126,8 @@ func StartMutualTLSListener(bindIface string, port uint16) (net.Listener, error)
}
func acceptSliverConnections(ln net.Listener) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls acceptSliverConnections")
for {
conn, err := ln.Accept()
if err != nil {
@@ -140,6 +142,8 @@ func acceptSliverConnections(ln net.Listener) {
}
func handleSliverConnection(conn net.Conn) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls handleSliverConnection")
mtlsLog.Infof("Accepted incoming connection: %s", conn.RemoteAddr())
implantConn := core.NewImplantConnection(consts.MtlsStr, conn.RemoteAddr().String())
@@ -174,6 +178,8 @@ func (c *mtlsBufferedConn) Read(p []byte) (int, error) {
}
func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnection) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls handleSliverConnectionYamux")
session, err := yamux.Server(conn, nil)
if err != nil {
mtlsLog.Errorf("Failed to initialize yamux session: %v", err)
@@ -196,6 +202,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
go func() {
defer closeDone()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux accept loop")
for {
stream, err := session.Accept()
if err != nil {
@@ -216,6 +223,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
defer func() {
<-streamSem
}()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux stream")
defer stream.Close()
envelope, err := socketReadEnvelope(stream)
@@ -239,6 +247,8 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
if handler, ok := handlers[envelope.Type]; ok {
mtlsLog.Debugf("Received new mtls message type %d, data: %s", envelope.Type, envelope.Data)
go func(envelope *sliverpb.Envelope) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls message handler")
respEnvelope := handler(implantConn, envelope.Data)
if respEnvelope != nil {
implantConn.Send <- respEnvelope
@@ -251,6 +261,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
go func() {
defer closeDone()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux sender loop")
for {
select {
case envelope := <-implantConn.Send:
@@ -264,6 +275,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
defer func() {
<-sendSem
}()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux sender stream")
stream, err := session.Open()
if err != nil {
+4 -1
View File
@@ -5,6 +5,7 @@ package c2_test
import (
"context"
"fmt"
"io"
"net"
"os"
"path/filepath"
@@ -321,7 +322,9 @@ func startTestBeacon(t *testing.T, conn net.Conn, beaconID string) *testBeacon {
t.Fatalf("write yamux preface: %v", err)
}
muxSession, err := yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
cfg.LogOutput = io.Discard
muxSession, err := yamux.Client(conn, cfg)
if err != nil {
t.Fatalf("start yamux client session: %v", err)
}
+4 -1
View File
@@ -5,6 +5,7 @@ package c2_test
import (
"context"
"fmt"
"io"
"net"
"os"
"path/filepath"
@@ -353,7 +354,9 @@ func startTestImplant(t *testing.T, conn net.Conn) func() {
t.Fatalf("write yamux preface: %v", err)
}
muxSession, err := yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
cfg.LogOutput = io.Discard
muxSession, err := yamux.Client(conn, cfg)
if err != nil {
t.Fatalf("start yamux client session: %v", err)
}
+9
View File
@@ -0,0 +1,9 @@
package c2
import "runtime/debug"
func recoverAndLogPanic(logf func(string, ...interface{}), scope string) {
if recovered := recover(); recovered != nil {
logf("Recovered panic in %s: %v\n%s", scope, recovered, string(debug.Stack()))
}
}
+30 -2
View File
@@ -28,6 +28,7 @@ import (
"io"
"net"
"net/netip"
"strings"
"sync"
"github.com/bishopfox/sliver/protobuf/sliverpb"
@@ -118,14 +119,25 @@ func StartWGListener(port uint16, netstackPort uint16, keyExchangeListenPort uin
return nil, nil, nil, err
}
validPeerCount := 0
for k, v := range peers {
tunPeerIP := strings.TrimSpace(v)
if tunPeerIP == "" {
wgLog.Warnf("Skipping wireguard peer %q with empty tunnel IP in the database", k)
continue
}
if _, err := netip.ParseAddr(tunPeerIP); err != nil {
wgLog.Warnf("Skipping wireguard peer %q with invalid tunnel IP %q: %v", k, tunPeerIP, err)
continue
}
validPeerCount++
fmt.Fprintf(wgConf, "public_key=%s\n", k)
fmt.Fprintf(wgConf, "allowed_ip=%s/32\n", v)
fmt.Fprintf(wgConf, "allowed_ip=%s/32\n", tunPeerIP)
}
// Set wg device config
if err := dev.IpcSetOperation(bufio.NewReader(wgConf)); err != nil {
return nil, nil, nil, err
return nil, nil, nil, fmt.Errorf("failed to apply wireguard interface config (%d valid peer entries): %w", validPeerCount, err)
}
err = dev.Up()
@@ -156,6 +168,8 @@ func StartWGListener(port uint16, netstackPort uint16, keyExchangeListenPort uin
// acceptKeyExchangeConnection - accept connections to key exchange socket
func acceptKeyExchangeConnection(ln net.Listener) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard acceptKeyExchangeConnection")
wgLog.Printf("Polling for connections to key exchange listener")
for {
conn, err := ln.Accept()
@@ -176,6 +190,8 @@ func acceptKeyExchangeConnection(ln net.Listener) {
// Generate new implant wg keys. Generate new unique IP for implant.
// Write all retrieved data to socket connection.
func handleKeyExchangeConnection(conn net.Conn) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleKeyExchangeConnection")
wgLog.Infof("Handling connection to key exchange listener")
defer conn.Close()
@@ -201,6 +217,8 @@ func handleKeyExchangeConnection(conn net.Conn) {
}
func acceptWGSliverConnections(ln net.Listener) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard acceptWGSliverConnections")
for {
conn, err := ln.Accept()
if err != nil {
@@ -215,6 +233,8 @@ func acceptWGSliverConnections(ln net.Listener) {
}
func handleWGSliverConnection(conn net.Conn) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleWGSliverConnection")
wgLog.Infof("Accepted incoming connection: %s", conn.RemoteAddr())
implantConn := core.NewImplantConnection("wg", conn.RemoteAddr().String())
@@ -250,6 +270,8 @@ func (c *wgBufferedConn) Read(p []byte) (int, error) {
}
func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnection) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleWGSliverConnectionYamux")
session, err := yamux.Server(conn, nil)
if err != nil {
wgLog.Errorf("Failed to initialize yamux session: %v", err)
@@ -272,6 +294,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
go func() {
defer closeDone()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux accept loop")
for {
stream, err := session.Accept()
if err != nil {
@@ -292,6 +315,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
defer func() {
<-streamSem
}()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux stream")
defer stream.Close()
envelope, err := socketWGReadEnvelope(stream)
@@ -314,6 +338,8 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
if handler, ok := handlers[envelope.Type]; ok {
go func(envelope *sliverpb.Envelope) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard message handler")
respEnvelope := handler(implantConn, envelope.Data)
if respEnvelope != nil {
implantConn.Send <- respEnvelope
@@ -326,6 +352,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
go func() {
defer closeDone()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux sender loop")
for {
select {
case envelope := <-implantConn.Send:
@@ -339,6 +366,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
defer func() {
<-sendSem
}()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux sender stream")
stream, err := session.Open()
if err != nil {
+4 -1
View File
@@ -4,6 +4,7 @@ package c2_test
import (
"context"
"io"
"net"
"os"
"path/filepath"
@@ -316,7 +317,9 @@ func startTestWGBeacon(t *testing.T, conn net.Conn, beaconID string) *wgTestBeac
t.Fatalf("write yamux preface: %v", err)
}
muxSession, err := yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
cfg.LogOutput = io.Discard
muxSession, err := yamux.Client(conn, cfg)
if err != nil {
t.Fatalf("start yamux client session: %v", err)
}
+4 -1
View File
@@ -5,6 +5,7 @@ package c2_test
import (
"context"
"fmt"
"io"
"net"
"os"
"path/filepath"
@@ -328,7 +329,9 @@ func startTestWGImplant(t *testing.T, conn net.Conn) func() {
t.Fatalf("write yamux preface: %v", err)
}
muxSession, err := yamux.Client(conn, nil)
cfg := yamux.DefaultConfig()
cfg.LogOutput = io.Discard
muxSession, err := yamux.Client(conn, cfg)
if err != nil {
t.Fatalf("start yamux client session: %v", err)
}
+16 -5
View File
@@ -22,6 +22,8 @@ import (
"encoding/hex"
"errors"
"fmt"
"net/netip"
"strings"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/server/core"
@@ -136,6 +138,14 @@ func saveWGKeys(isPeer bool, wgPeerTunIP string, privKey string, pubKey string)
var result *gorm.DB
if isPeer {
wgPeerTunIP = strings.TrimSpace(wgPeerTunIP)
if wgPeerTunIP == "" {
return errors.New("wg peer tunnel IP cannot be empty")
}
if _, err := netip.ParseAddr(wgPeerTunIP); err != nil {
return fmt.Errorf("invalid wg peer tunnel IP %q: %w", wgPeerTunIP, err)
}
wgPeerModels := &models.WGPeer{
PrivKey: privKey,
PubKey: pubKey,
@@ -150,11 +160,12 @@ func saveWGKeys(isPeer bool, wgPeerTunIP string, privKey string, pubKey string)
}
result = dbSession.Create(&wgKeysModel)
}
core.EventBroker.Publish(core.Event{
EventType: consts.WireGuardNewPeer,
Data: []byte(fmt.Sprintf("public_key=%s\nallowed_ip=%s/32\n", pubKey, wgPeerTunIP)),
})
if isPeer {
core.EventBroker.Publish(core.Event{
EventType: consts.WireGuardNewPeer,
Data: []byte(fmt.Sprintf("public_key=%s\nallowed_ip=%s/32\n", pubKey, wgPeerTunIP)),
})
}
return result.Error
}
+2 -2
View File
@@ -141,12 +141,12 @@ var rootCmd = &cobra.Command{
serverConfig := configs.GetServerConfig()
listenerJobs, err := db.ListenerJobs()
if err != nil {
fmt.Println(err)
fmt.Printf("[!] Failed to load persistent listener jobs: %s\n", err)
}
err = StartPersistentJobs(listenerJobs)
if err != nil {
fmt.Println(err)
fmt.Printf("[!] %s\n", err)
}
if serverConfig.DaemonMode {
daemon.Start(daemon.BlankHost, daemon.BlankPort, serverConfig.DaemonConfig.Tailscale)
+147 -55
View File
@@ -1,10 +1,12 @@
package cli
import (
"errors"
"fmt"
"log"
"os"
"runtime/debug"
"strings"
"github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
@@ -68,12 +70,12 @@ var daemonCmd = &cobra.Command{
listenerJobs, err := db.ListenerJobs()
if err != nil {
fmt.Println(err)
fmt.Printf("[!] Failed to load persistent listener jobs: %s\n", err)
}
err = StartPersistentJobs(listenerJobs)
if err != nil {
fmt.Println(err)
fmt.Printf("[!] %s\n", err)
}
daemon.Start(lhost, uint16(lport), tailscale)
@@ -81,60 +83,150 @@ var daemonCmd = &cobra.Command{
}
func StartPersistentJobs(listenerJobs []*clientpb.ListenerJob) error {
if len(listenerJobs) > 0 {
// StartPersistentJobs - Start persistent jobs
for _, j := range listenerJobs {
listenerJob, err := db.ListenerByJobID(j.JobID)
if err != nil {
return err
}
switch j.Type {
case constants.HttpStr:
job, err := c2.StartHTTPListenerJob(listenerJob.HTTPConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.HttpsStr:
job, err := c2.StartHTTPListenerJob(listenerJob.HTTPConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.MtlsStr:
job, err := c2.StartMTLSListenerJob(listenerJob.MTLSConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.WGStr:
job, err := c2.StartWGListenerJob(listenerJob.WGConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.DnsStr:
job, err := c2.StartDNSListenerJob(listenerJob.DNSConf)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
case constants.MultiplayerModeStr:
id, err := console.JobStartClientListener(listenerJob.MultiConf)
if err != nil {
return err
}
j.JobID = uint32(id)
case constants.TCPListenerStr:
job, err := c2.StartTCPStagerListenerJob(listenerJob.TCPConf.Host, uint16(listenerJob.TCPConf.Port), listenerJob.TCPConf.ProfileName, listenerJob.TCPConf.Data)
if err != nil {
return err
}
j.JobID = uint32(job.ID)
}
db.UpdateHTTPC2Listener(j)
if len(listenerJobs) == 0 {
return nil
}
var startupErrs []error
for _, j := range listenerJobs {
if j == nil {
startupErrs = append(startupErrs, errors.New("persistent startup entry is nil"))
continue
}
listenerJob, err := db.ListenerByJobID(j.JobID)
if err != nil {
startupErrs = append(startupErrs, fmt.Errorf("persistent %s listener (saved job id=%d): failed to load config: %w", j.Type, j.JobID, err))
continue
}
startupContext := persistentJobStartupContext(j.Type, j.JobID, listenerJob)
savedJobID := j.JobID
jobID, err := startPersistentListenerJob(j.Type, listenerJob)
if err != nil {
startupErrs = append(startupErrs, fmt.Errorf("%s: failed to start: %w", startupContext, err))
continue
}
j.JobID = jobID
if err := db.UpdateListenerJobID(savedJobID, jobID); err != nil {
startupErrs = append(startupErrs, fmt.Errorf("%s: started with new job id=%d but failed to update listener record: %w", startupContext, j.JobID, err))
}
}
return nil
if len(startupErrs) == 0 {
return nil
}
return fmt.Errorf("persistent listener startup errors (%d/%d failed): %w", len(startupErrs), len(listenerJobs), errors.Join(startupErrs...))
}
func startPersistentListenerJob(jobType string, listenerJob *clientpb.ListenerJob) (uint32, error) {
switch jobType {
case constants.HttpStr, constants.HttpsStr:
if listenerJob.HTTPConf == nil {
return 0, errors.New("missing HTTP listener configuration")
}
job, err := c2.StartHTTPListenerJob(listenerJob.HTTPConf)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
case constants.MtlsStr:
if listenerJob.MTLSConf == nil {
return 0, errors.New("missing mTLS listener configuration")
}
job, err := c2.StartMTLSListenerJob(listenerJob.MTLSConf)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
case constants.WGStr:
if listenerJob.WGConf == nil {
return 0, errors.New("missing WireGuard listener configuration")
}
job, err := c2.StartWGListenerJob(listenerJob.WGConf)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
case constants.DnsStr:
if listenerJob.DNSConf == nil {
return 0, errors.New("missing DNS listener configuration")
}
job, err := c2.StartDNSListenerJob(listenerJob.DNSConf)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
case constants.MultiplayerModeStr:
if listenerJob.MultiConf == nil {
return 0, errors.New("missing multiplayer listener configuration")
}
id, err := console.JobStartClientListener(listenerJob.MultiConf)
if err != nil {
return 0, err
}
return uint32(id), nil
case constants.TCPListenerStr:
if listenerJob.TCPConf == nil {
return 0, errors.New("missing TCP stager listener configuration")
}
job, err := c2.StartTCPStagerListenerJob(listenerJob.TCPConf.Host, uint16(listenerJob.TCPConf.Port), listenerJob.TCPConf.ProfileName, listenerJob.TCPConf.Data)
if err != nil {
return 0, err
}
return uint32(job.ID), nil
default:
return 0, fmt.Errorf("unsupported listener type %q", jobType)
}
}
func persistentJobStartupContext(jobType string, savedJobID uint32, listenerJob *clientpb.ListenerJob) string {
prefix := fmt.Sprintf("persistent %s listener (saved job id=%d)", jobType, savedJobID)
if listenerJob == nil {
return prefix
}
switch jobType {
case constants.HttpStr, constants.HttpsStr:
if listenerJob.HTTPConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s domain=%q]", prefix, listenerBind(listenerJob.HTTPConf.Host, listenerJob.HTTPConf.Port), listenerJob.HTTPConf.Domain)
case constants.MtlsStr:
if listenerJob.MTLSConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s]", prefix, listenerBind(listenerJob.MTLSConf.Host, listenerJob.MTLSConf.Port))
case constants.WGStr:
if listenerJob.WGConf == nil {
return prefix
}
return fmt.Sprintf("%s [wg_port=%d netstack_port=%d key_port=%d]", prefix, listenerJob.WGConf.Port, listenerJob.WGConf.NPort, listenerJob.WGConf.KeyPort)
case constants.DnsStr:
if listenerJob.DNSConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s domains=%s]", prefix, listenerBind(listenerJob.DNSConf.Host, listenerJob.DNSConf.Port), strings.Join(listenerJob.DNSConf.Domains, ","))
case constants.MultiplayerModeStr:
if listenerJob.MultiConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s]", prefix, listenerBind(listenerJob.MultiConf.Host, listenerJob.MultiConf.Port))
case constants.TCPListenerStr:
if listenerJob.TCPConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s profile=%q]", prefix, listenerBind(listenerJob.TCPConf.Host, listenerJob.TCPConf.Port), listenerJob.TCPConf.ProfileName)
default:
return prefix
}
}
func listenerBind(host string, port uint32) string {
host = strings.TrimSpace(host)
if host == "" {
host = "*"
}
return fmt.Sprintf("%s:%d", host, port)
}
+9 -2
View File
@@ -27,6 +27,7 @@ import (
"github.com/bishopfox/sliver/server/log"
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
@@ -66,7 +67,10 @@ func hostsSessionCallback(session *Session) {
}
if errors.Is(err, gorm.ErrRecordNotFound) {
coreLog.Infof("Session %v is from a new host", session.ID)
err := dbSession.Create(&models.Host{
err := dbSession.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "host_uuid"}},
DoNothing: true,
}).Create(&models.Host{
HostUUID: uuid.FromStringOrNil(session.UUID),
Hostname: session.Hostname,
OSVersion: session.OS,
@@ -94,7 +98,10 @@ func hostsBeaconCallback(beacon *models.Beacon) {
}
if errors.Is(err, gorm.ErrRecordNotFound) {
coreLog.Infof("Beacon %v is from a new host", beacon.ID)
err := dbSession.Create(&models.Host{
err := dbSession.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "host_uuid"}},
DoNothing: true,
}).Create(&models.Host{
HostUUID: uuid.FromStringOrNil(beacon.UUID.String()),
Hostname: beacon.Hostname,
OSVersion: beacon.OS,
+41 -3
View File
@@ -528,12 +528,50 @@ func SaveC2Listener(listenerConf *clientpb.ListenerJob) error {
}
func UpdateHTTPC2Listener(listenerConf *clientpb.ListenerJob) error {
dbListener := models.ListenerJobFromProtobuf(listenerConf)
dbSession := Session()
result := dbSession.Save(dbListener)
if listenerConf == nil {
return errors.New("listener config is nil")
}
if listenerConf.ID == "" {
return errors.New("listener config id is empty")
}
listenerID := uuid.FromStringOrNil(listenerConf.ID)
if listenerID == uuid.Nil {
return fmt.Errorf("invalid listener config id %q", listenerConf.ID)
}
result := Session().
Model(&models.ListenerJob{}).
Where(&models.ListenerJob{ID: listenerID}).
Updates(map[string]any{
"job_id": listenerConf.JobID,
"type": listenerConf.Type,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("listener record %s not found", listenerConf.ID)
}
return nil
}
// UpdateListenerJobID - Update a listener record's job id while preserving listener configuration rows.
func UpdateListenerJobID(oldJobID uint32, newJobID uint32) error {
if oldJobID == newJobID {
return nil
}
result := Session().
Model(&models.ListenerJob{}).
Where("job_id = ?", oldJobID).
Update("job_id", newJobID)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("listener job id %d not found", oldJobID)
}
return nil
}
+4
View File
@@ -464,6 +464,10 @@ func RandomPaths(httpC2PathSegments []*clientpb.HTTPC2PathSegment, minPaths int3
}
func randomSample(values []*clientpb.HTTPC2PathSegment, min int32, max int32) []*clientpb.HTTPC2PathSegment {
if len(values) == 0 {
return nil
}
count := int32(util.Intn(len(values)))
if count < min {
count = min
+33
View File
@@ -0,0 +1,33 @@
package models
import (
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestRandomSampleEmptyValues(t *testing.T) {
sample := randomSample(nil, 1, 3)
if len(sample) != 0 {
t.Fatalf("expected empty sample, got %d entries", len(sample))
}
}
func TestRandomPathSegmentsWithNoSegments(t *testing.T) {
defer func() {
if recovered := recover(); recovered != nil {
t.Fatalf("RandomPathSegments panicked with empty input: %v", recovered)
}
}()
config := &clientpb.HTTPC2ImplantConfig{
MinFileGen: 1,
MaxFileGen: 3,
MinPathGen: 0,
MaxPathGen: 3,
}
segments := RandomPathSegments(config)
if len(segments) != 0 {
t.Fatalf("expected no path segments, got %d", len(segments))
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ func (w *Website) ToProtobuf(webContentDir string) *clientpb.Website {
if err != nil {
continue
}
WebContents[webcontent.ID.String()] = webcontent.ToProtobuf(&contents)
WebContents[webcontent.Path] = webcontent.ToProtobuf(&contents)
}
return &clientpb.Website{
ID: w.ID.String(),
Loaded 100 of 380 files, more files were not shown because too many files have changed in this diff. Show more