mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
96
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82d979436b | ||
|
|
d2632e75bd | ||
|
|
a55831fb44 | ||
|
|
39d4edec43 | ||
|
|
bbdb08e7a4 | ||
|
|
39b488bd25 | ||
|
|
e7b8a24534 | ||
|
|
17b9d611f8 | ||
|
|
ed9075d065 | ||
|
|
1f7221933d | ||
|
|
6333e0691a | ||
|
|
3bbaf80510 | ||
|
|
25726d8105 | ||
|
|
67e9a7f37f | ||
|
|
a5b6935da6 | ||
|
|
2e36f45bdf | ||
|
|
3e06a9d5ee | ||
|
|
952b2df49d | ||
|
|
9769f1bb57 | ||
|
|
f5f2c80ad3 | ||
|
|
bb2edf63cf | ||
|
|
cd711afe8a | ||
|
|
f9bbbb34bb | ||
|
|
64b91c56c0 | ||
|
|
a6c7867a26 | ||
|
|
7f25c3bb06 | ||
|
|
153cf29551 | ||
|
|
b85161e797 | ||
|
|
ed906fcffe | ||
|
|
3bbcf5697a | ||
|
|
4a425d2fd6 | ||
|
|
d0f752e118 | ||
|
|
7167621577 | ||
|
|
c3582fc203 | ||
|
|
07c95b203f | ||
|
|
958e44deef | ||
|
|
836395a2d6 | ||
|
|
1a7a1c9b2d | ||
|
|
4ba285796f | ||
|
|
a05dfad100 | ||
|
|
0eb29ad9b8 | ||
|
|
ad8b329f85 | ||
|
|
c6206257b6 | ||
|
|
8bbf06c156 | ||
|
|
4d4eab998f | ||
|
|
c081f17380 | ||
|
|
fafbac3fa6 | ||
|
|
d9c89c81cf | ||
|
|
061221905b | ||
|
|
d0600e2be7 | ||
|
|
26abbe3860 | ||
|
|
55a386ae1c | ||
|
|
2bfdb218ff | ||
|
|
715b84e799 | ||
|
|
faea2ff639 | ||
|
|
1b253f8dec | ||
|
|
eb38724322 | ||
|
|
05dedf4473 | ||
|
|
28c6b82a79 | ||
|
|
1f553496e3 | ||
|
|
cd5db464e0 | ||
|
|
0bf0ac6c7b | ||
|
|
0cf5a47cfd | ||
|
|
8d6759924b | ||
|
|
8d5dbd2050 | ||
|
|
804e149113 | ||
|
|
9ced4cfd50 | ||
|
|
c8be5a5b1b | ||
|
|
b7abf98c95 | ||
|
|
918ac1bb91 | ||
|
|
1e57818312 | ||
|
|
c44c14dd46 | ||
|
|
35f41cd62d | ||
|
|
4eff1cc053 | ||
|
|
16981da431 | ||
|
|
3830c76292 | ||
|
|
73de93d6fb | ||
|
|
c7770c2f6e | ||
|
|
21db908ce4 | ||
|
|
b7b0247652 | ||
|
|
3dc7dc03d0 | ||
|
|
229452d7ed | ||
|
|
d29ce9a08b | ||
|
|
46f2b2b402 | ||
|
|
6ee2006387 | ||
|
|
2fd13ee2bc | ||
|
|
2697568336 | ||
|
|
5013048b0b | ||
|
|
627b15572c | ||
|
|
42c40c9b8d | ||
|
|
405b282fed | ||
|
|
e32cd36e01 | ||
|
|
492a2aeed6 | ||
|
|
d6dafb0391 | ||
|
|
49a1bcfd7d | ||
|
|
a8d1bf6fb1 |
No files matched your search
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,8 @@ The server and client support MacOS, Windows, and Linux. Implants are supported
|
||||
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://goreportcard.com/report/github.com/BishopFox/sliver) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
|
||||
|
||||
### Features
|
||||
|
||||
- Dynamic code generation
|
||||
@@ -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.
|
||||
|
||||
@@ -124,4 +124,5 @@ under certain conditions; type 'licenses' for details.`)
|
||||
saveAssetVersion(appDir)
|
||||
}
|
||||
_, _ = LoadSettings()
|
||||
_, _ = LoadSpoofMetadataConfig()
|
||||
}
|
||||
@@ -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: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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]",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestHTTPAndHTTPSWebsiteFlagCompletionRegistered(t *testing.T) {
|
||||
cmds := Commands(&console.SliverClient{})
|
||||
|
||||
httpCmd := commandByUse(cmds, consts.HttpStr)
|
||||
if httpCmd == nil {
|
||||
t.Fatalf("missing %q command", consts.HttpStr)
|
||||
}
|
||||
if _, ok := httpCmd.GetFlagCompletionFunc("website"); !ok {
|
||||
t.Fatalf("%q missing website flag completion func", consts.HttpStr)
|
||||
}
|
||||
|
||||
httpsCmd := commandByUse(cmds, consts.HttpsStr)
|
||||
if httpsCmd == nil {
|
||||
t.Fatalf("missing %q command", consts.HttpsStr)
|
||||
}
|
||||
if _, ok := httpsCmd.GetFlagCompletionFunc("website"); !ok {
|
||||
t.Fatalf("%q missing website flag completion func", consts.HttpsStr)
|
||||
}
|
||||
}
|
||||
|
||||
func commandByUse(cmds []*cobra.Command, use string) *cobra.Command {
|
||||
for _, cmd := range cmds {
|
||||
if cmd.Use == use {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package jobs
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// WebsiteNameCompleter completes the names of available websites.
|
||||
func WebsiteNameCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
websites, err := websiteNameValues(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("Failed to list websites %s", err)
|
||||
}
|
||||
if len(websites) == 0 {
|
||||
return carapace.ActionMessage("no available websites")
|
||||
}
|
||||
|
||||
return carapace.ActionValues(websites...).Tag("websites").Usage("website name")
|
||||
})
|
||||
}
|
||||
|
||||
func registerWebsiteFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
values, err := websiteNameValues(con)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
|
||||
func websiteNameValues(con *console.SliverClient) ([]string, error) {
|
||||
results := make([]string, 0)
|
||||
if con == nil || con.Rpc == nil {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
websites, err := con.Rpc.Websites(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ws := range websites.Websites {
|
||||
results = append(results, ws.Name)
|
||||
}
|
||||
sort.Strings(results)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func filterCompletionValues(values []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -26,6 +26,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("beacon-interval", "i", "", "beacon callback interval")
|
||||
f.StringP("beacon-jitter", "j", "", "beacon callback jitter (random up to)")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.StringP("c2-uri", "c", "", "C2 URI to switch to")
|
||||
})
|
||||
|
||||
renameCmd := &cobra.Command{
|
||||
|
||||
@@ -20,6 +20,7 @@ package reconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -52,6 +53,7 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
var beaconJitter time.Duration
|
||||
binterval, _ := cmd.Flags().GetString("beacon-interval")
|
||||
bjitter, _ := cmd.Flags().GetString("beacon-jitter")
|
||||
C2URI, _ := cmd.Flags().GetString("c2-uri")
|
||||
|
||||
if beacon != nil {
|
||||
if binterval != "" {
|
||||
@@ -73,10 +75,47 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
if C2URI != "" {
|
||||
// validate uri
|
||||
newURI, err := url.Parse(C2URI)
|
||||
if err != nil || newURI.Scheme == "" || newURI.Host == "" {
|
||||
con.PrintErrorf("Invalid C2 URI: %s (expected format: protocol://host:port)\n", C2URI)
|
||||
return
|
||||
}
|
||||
|
||||
// validate the protocol is the same as the beacon (cuz it compiled with only one protocol)
|
||||
var activeC2 string
|
||||
if beacon != nil {
|
||||
activeC2 = beacon.ActiveC2
|
||||
} else if session != nil {
|
||||
activeC2 = session.ActiveC2
|
||||
}
|
||||
|
||||
if activeC2 != "" {
|
||||
currentURI, err := url.Parse(activeC2)
|
||||
if err == nil {
|
||||
// must be the same protocol
|
||||
if newURI.Scheme != currentURI.Scheme {
|
||||
con.PrintErrorf("Cannot switch protocol from %s to %s (protocol must match compiled implant)\n",
|
||||
currentURI.Scheme, newURI.Scheme)
|
||||
return
|
||||
}
|
||||
// for now only support the same Host/C2 Server (becuase the implant crypto keys problem)
|
||||
if newURI.Hostname() != currentURI.Hostname() {
|
||||
con.PrintWarnf("Switching to a different host (%s -> %s). This only works if both servers share the same crypto keys.\n",
|
||||
currentURI.Hostname(), newURI.Hostname())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
con.PrintInfof("Updating C2 endpoint to: %s\n", C2URI)
|
||||
}
|
||||
|
||||
reconfig, err := con.Rpc.Reconfigure(context.Background(), &sliverpb.ReconfigureReq{
|
||||
ReconnectInterval: int64(reconnectInterval),
|
||||
BeaconInterval: int64(beaconInterval),
|
||||
BeaconJitter: int64(beaconJitter),
|
||||
C2URI: C2URI,
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,10 @@ Implements the 'shell' command group for the Sliver client console. Handlers map
|
||||
|
||||
## Go Files
|
||||
|
||||
- `commands.go` – Exposes the interactive shell command and binds its options.
|
||||
- `commands.go` – Exposes the shell command group (`shell`, `shell ls`, `shell attach`, `shell kill`) and binds options.
|
||||
- `completers.go` – Shell-specific command completion helpers.
|
||||
- `filter-reader_generic.go` – Provides output filtering for shell streams on POSIX targets.
|
||||
- `filter-reader_windows.go` – Implements CRLF-aware filtering and decoding for Windows shell sessions.
|
||||
- `shell.go` – Launches interactive command shells over RPC and manages IO loops.
|
||||
- `ioloop.go` – Handles attached shell stdin forwarding, escape handling, and fast local detach/exit behavior.
|
||||
- `manager.go` – Tracks managed shell tunnels so detached shells can be listed and re-attached.
|
||||
- `shell.go` – Launches/attaches shell tunnels and coordinates managed shell lifecycle.
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -28,5 +29,46 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
shellLsCmd := &cobra.Command{
|
||||
Use: consts.LsStr,
|
||||
Short: "List managed local shell tunnels",
|
||||
Long: help.GetHelpFor([]string{consts.ShellStr}),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ShellLsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
shellCmd.AddCommand(shellLsCmd)
|
||||
|
||||
shellAttachCmd := &cobra.Command{
|
||||
Use: "attach <id>",
|
||||
Short: "Attach to a managed local shell tunnel",
|
||||
Long: help.GetHelpFor([]string{consts.ShellStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ShellAttachCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, shellAttachCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
shellCmd.AddCommand(shellAttachCmd)
|
||||
carapace.Gen(shellAttachCmd).PositionalCompletion(ShellIDCompleter(con).Usage("managed shell ID"))
|
||||
|
||||
shellKillCmd := &cobra.Command{
|
||||
Use: consts.KillStr + " <id>",
|
||||
Short: "Kill a managed shell and clean up remote process state",
|
||||
Long: help.GetHelpFor([]string{consts.ShellStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ShellKillCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, shellKillCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("force", "F", false, "force process cleanup via terminate rpc")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
shellCmd.AddCommand(shellKillCmd)
|
||||
carapace.Gen(shellKillCmd).PositionalCompletion(ShellIDCompleter(con).Usage("managed shell ID"))
|
||||
|
||||
return []*cobra.Command{shellCmd}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/rsteube/carapace"
|
||||
)
|
||||
|
||||
// ShellIDCompleter completes IDs of managed local shells.
|
||||
func ShellIDCompleter(_ *console.SliverClient) carapace.Action {
|
||||
callback := func(_ carapace.Context) carapace.Action {
|
||||
results := make([]string, 0)
|
||||
|
||||
managed := shells.List()
|
||||
if len(managed) == 0 {
|
||||
return carapace.ActionMessage("no managed shells")
|
||||
}
|
||||
|
||||
for _, sh := range managed {
|
||||
results = append(results, strconv.Itoa(sh.ID))
|
||||
results = append(results, fmt.Sprintf("%s, pid=%d, state=%s", sessionLabel(sh.SessionID, sh.SessionName), sh.Pid, sh.State()))
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("managed shells")
|
||||
}
|
||||
|
||||
return carapace.ActionCallback(callback)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
)
|
||||
|
||||
// runAttachedIO forwards local stdin to a shell tunnel while attached to the shell.
|
||||
// It returns:
|
||||
// - detached=true when user hit escape (Ctrl-])
|
||||
// - closeRequested=true when user requested shell close (EOF/exit/logout)
|
||||
func runAttachedIO(tunnel *core.TunnelIO, con *console.SliverClient) (detached bool, closeRequested bool) {
|
||||
// We can't pipe stdin directly into the tunnel: if the gRPC tunnel send path blocks
|
||||
// (network hang, server gone, etc.) tunnel.Write can block forever and "lock" the
|
||||
// client in shell mode. Instead, buffer stdin locally and look for a local escape key.
|
||||
const (
|
||||
shellEscapeByte = byte(0x1d) // Ctrl-]
|
||||
stdinBufSize = 32 * 1024
|
||||
stdinQueueDepth = 128
|
||||
)
|
||||
|
||||
stdinQueue := make(chan []byte, stdinQueueDepth)
|
||||
stopWriter := make(chan struct{})
|
||||
var writerWG sync.WaitGroup
|
||||
writerWG.Add(1)
|
||||
|
||||
go func() {
|
||||
defer writerWG.Done()
|
||||
defer func() {
|
||||
// Tunnel teardown can race with writes (server-side close, disconnect, etc.).
|
||||
// Don't let a send-on-closed-channel panic bring down the whole client.
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Shell tunnel writer stopped: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopWriter:
|
||||
return
|
||||
case data, ok := <-stdinQueue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-stopWriter:
|
||||
return
|
||||
case tunnel.Send <- data:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
close(stopWriter)
|
||||
close(stdinQueue)
|
||||
writerWG.Wait()
|
||||
}()
|
||||
|
||||
stdin := newFilterReader(os.Stdin)
|
||||
buf := make([]byte, stdinBufSize)
|
||||
lineBuf := make([]byte, 0, 128)
|
||||
inEscSeq := false
|
||||
log.Printf("Reading from stdin (escape=Ctrl-]) ...")
|
||||
|
||||
for {
|
||||
n, err := stdin.Read(buf)
|
||||
if n > 0 {
|
||||
data := buf[:n]
|
||||
if i := bytes.IndexByte(data, shellEscapeByte); i >= 0 {
|
||||
if i > 0 {
|
||||
// Best-effort: don't block stdin if the tunnel is wedged.
|
||||
dataCopy := append([]byte(nil), data[:i]...)
|
||||
select {
|
||||
case stdinQueue <- dataCopy:
|
||||
default:
|
||||
}
|
||||
}
|
||||
detached = true
|
||||
break
|
||||
}
|
||||
|
||||
exitRequested := false
|
||||
for _, b := range data {
|
||||
if inEscSeq {
|
||||
if b >= 0x40 && b <= 0x7e {
|
||||
inEscSeq = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch b {
|
||||
case '\r', '\n':
|
||||
line := strings.TrimSpace(string(lineBuf))
|
||||
if line == "exit" || line == "logout" {
|
||||
exitRequested = true
|
||||
}
|
||||
lineBuf = lineBuf[:0]
|
||||
case 0x1b:
|
||||
inEscSeq = true
|
||||
case 0x08, 0x7f:
|
||||
if len(lineBuf) > 0 {
|
||||
lineBuf = lineBuf[:len(lineBuf)-1]
|
||||
}
|
||||
default:
|
||||
// Keep printable bytes only; ignore control sequences (arrows, etc.).
|
||||
if b >= 0x20 && b <= 0x7e {
|
||||
lineBuf = append(lineBuf, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataCopy := append([]byte(nil), data...)
|
||||
select {
|
||||
case stdinQueue <- dataCopy:
|
||||
default:
|
||||
// Drop input if the tunnel send path is blocked; still allow escape.
|
||||
}
|
||||
|
||||
if exitRequested {
|
||||
closeRequested = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
con.PrintErrorf("Error reading from stdin: %s\n", err)
|
||||
}
|
||||
closeRequested = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Exit interactive")
|
||||
bufio.NewWriter(os.Stdout).Flush()
|
||||
return detached, closeRequested
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
)
|
||||
|
||||
type shellManager struct {
|
||||
mu sync.RWMutex
|
||||
nextID int
|
||||
shells map[int]*managedShell
|
||||
}
|
||||
|
||||
func newShellManager() *shellManager {
|
||||
return &shellManager{
|
||||
nextID: 1,
|
||||
shells: map[int]*managedShell{},
|
||||
}
|
||||
}
|
||||
|
||||
var shells = newShellManager()
|
||||
|
||||
type shellState string
|
||||
|
||||
const (
|
||||
shellStateAttached shellState = "attached"
|
||||
shellStateDetached shellState = "detached"
|
||||
shellStateClosing shellState = "closing"
|
||||
)
|
||||
|
||||
type managedShell struct {
|
||||
ID int
|
||||
SessionID string
|
||||
SessionName string
|
||||
Path string
|
||||
Pid uint32
|
||||
TunnelID uint64
|
||||
EnablePTY bool
|
||||
CreatedAt time.Time
|
||||
|
||||
mu sync.RWMutex
|
||||
state shellState
|
||||
tunnel *core.TunnelIO
|
||||
output *swapWriter
|
||||
readerWG sync.WaitGroup
|
||||
}
|
||||
|
||||
func (s *managedShell) State() shellState {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.state
|
||||
}
|
||||
|
||||
func (s *managedShell) setState(state shellState) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.state = state
|
||||
}
|
||||
|
||||
func (s *managedShell) Tunnel() *core.TunnelIO {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.tunnel
|
||||
}
|
||||
|
||||
func (s *managedShell) SetOutput(w io.Writer) {
|
||||
s.output.Set(w)
|
||||
}
|
||||
|
||||
func (s *managedShell) startReader(remove func()) {
|
||||
s.readerWG.Add(1)
|
||||
go func() {
|
||||
defer s.readerWG.Done()
|
||||
_, err := io.Copy(s.output, s.tunnel)
|
||||
log.Printf("Shell tunnel reader (id=%d tunnel=%d) exited: %v", s.ID, s.TunnelID, err)
|
||||
remove()
|
||||
}()
|
||||
}
|
||||
|
||||
// swapWriter is an io.Writer whose underlying destination can be swapped at runtime.
|
||||
// This is used so detached shells can keep draining output without clobbering the REPL.
|
||||
type swapWriter struct {
|
||||
mu sync.RWMutex
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newSwapWriter(w io.Writer) *swapWriter {
|
||||
return &swapWriter{w: w}
|
||||
}
|
||||
|
||||
func (s *swapWriter) Set(w io.Writer) {
|
||||
s.mu.Lock()
|
||||
s.w = w
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *swapWriter) Write(p []byte) (int, error) {
|
||||
s.mu.RLock()
|
||||
w := s.w
|
||||
s.mu.RUnlock()
|
||||
if w == nil {
|
||||
return len(p), nil
|
||||
}
|
||||
return w.Write(p)
|
||||
}
|
||||
|
||||
func (m *shellManager) Add(sh *managedShell) int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
id := m.nextID
|
||||
m.nextID++
|
||||
|
||||
sh.ID = id
|
||||
m.shells[id] = sh
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (m *shellManager) Remove(id int) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.shells, id)
|
||||
}
|
||||
|
||||
func (m *shellManager) Get(id int) (*managedShell, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
sh, ok := m.shells[id]
|
||||
return sh, ok
|
||||
}
|
||||
|
||||
func (m *shellManager) List() []*managedShell {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
results := make([]*managedShell, 0, len(m.shells))
|
||||
for _, sh := range m.shells {
|
||||
results = append(results, sh)
|
||||
}
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].ID < results[j].ID
|
||||
})
|
||||
return results
|
||||
}
|
||||
+351
-90
@@ -19,24 +19,34 @@ package shell
|
||||
*/
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
windows = "windows"
|
||||
darwin = "darwin"
|
||||
linux = "linux"
|
||||
darwin = "darwin"
|
||||
linux = "linux"
|
||||
)
|
||||
|
||||
type shellResult int
|
||||
|
||||
const (
|
||||
shellExited shellResult = iota
|
||||
shellDetached
|
||||
shellAttachFailed
|
||||
)
|
||||
|
||||
// ShellCmd - Start an interactive shell on the remote system.
|
||||
@@ -55,113 +65,364 @@ func ShellCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if con.ActiveTarget.GetSession().OS != linux && con.ActiveTarget.GetSession().OS != darwin {
|
||||
noPty = true // Sliver's PTYs are only supported on linux/darwin
|
||||
}
|
||||
runInteractive(cmd, shellPath, noPty, con)
|
||||
con.Println("Shell exited")
|
||||
|
||||
result := runInteractive(cmd, shellPath, noPty, con, nil)
|
||||
if result == shellDetached {
|
||||
con.Println("Shell detached")
|
||||
} else if result == shellExited {
|
||||
con.Println("Shell exited")
|
||||
}
|
||||
}
|
||||
|
||||
func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *console.SliverClient) {
|
||||
// ShellLsCmd lists local shell tunnels currently managed by the client.
|
||||
func ShellLsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
managed := shells.List()
|
||||
if len(managed) == 0 {
|
||||
con.PrintInfof("No managed shells\n")
|
||||
return
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"State",
|
||||
"Session",
|
||||
"PID",
|
||||
"PTY",
|
||||
"Tunnel ID",
|
||||
"Path",
|
||||
"Age",
|
||||
})
|
||||
|
||||
now := time.Now()
|
||||
for _, sh := range managed {
|
||||
age := now.Sub(sh.CreatedAt).Round(time.Second).String()
|
||||
if sh.CreatedAt.IsZero() {
|
||||
age = "-"
|
||||
}
|
||||
pid := "-"
|
||||
if sh.Pid > 0 {
|
||||
pid = strconv.FormatUint(uint64(sh.Pid), 10)
|
||||
}
|
||||
|
||||
tw.AppendRow(table.Row{
|
||||
sh.ID,
|
||||
sh.State(),
|
||||
sessionLabel(sh.SessionID, sh.SessionName),
|
||||
pid,
|
||||
sh.EnablePTY,
|
||||
sh.TunnelID,
|
||||
sh.Path,
|
||||
age,
|
||||
})
|
||||
}
|
||||
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
// ShellAttachCmd re-attaches local stdin/stdout to a managed shell tunnel.
|
||||
func ShellAttachCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if len(args) < 1 {
|
||||
con.PrintErrorf("Usage: shell attach <id>\n")
|
||||
return
|
||||
}
|
||||
|
||||
shellID, err := strconv.Atoi(args[0])
|
||||
if err != nil || shellID <= 0 {
|
||||
con.PrintErrorf("Invalid shell id: %s\n", args[0])
|
||||
return
|
||||
}
|
||||
|
||||
sh, ok := shells.Get(shellID)
|
||||
if !ok {
|
||||
con.PrintErrorf("Unknown shell id: %d\n", shellID)
|
||||
return
|
||||
}
|
||||
if sh.State() == shellStateAttached {
|
||||
con.PrintErrorf("Shell %d is already attached\n", shellID)
|
||||
return
|
||||
}
|
||||
if sh.State() == shellStateClosing {
|
||||
con.PrintErrorf("Shell %d is closing\n", shellID)
|
||||
return
|
||||
}
|
||||
tunnel := sh.Tunnel()
|
||||
if tunnel == nil || !tunnel.IsOpen() {
|
||||
con.PrintErrorf("Shell %d is no longer active\n", shellID)
|
||||
shells.Remove(shellID)
|
||||
return
|
||||
}
|
||||
|
||||
session := con.ActiveTarget.GetSession()
|
||||
if session == nil || session.ID != sh.SessionID {
|
||||
con.PrintErrorf("Shell %d belongs to %s; switch targets before attaching\n", shellID, sessionLabel(sh.SessionID, sh.SessionName))
|
||||
return
|
||||
}
|
||||
|
||||
result := runInteractive(cmd, sh.Path, !sh.EnablePTY, con, sh)
|
||||
if result == shellDetached {
|
||||
con.Println("Shell detached")
|
||||
} else if result == shellExited {
|
||||
con.Println("Shell exited")
|
||||
}
|
||||
}
|
||||
|
||||
// ShellKillCmd kills a managed shell from the REPL and cleans up remote state.
|
||||
func ShellKillCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if len(args) < 1 {
|
||||
con.PrintErrorf("Usage: shell kill <id>\n")
|
||||
return
|
||||
}
|
||||
|
||||
shellID, err := strconv.Atoi(args[0])
|
||||
if err != nil || shellID <= 0 {
|
||||
con.PrintErrorf("Invalid shell id: %s\n", args[0])
|
||||
return
|
||||
}
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
sh, ok := shells.Get(shellID)
|
||||
if !ok {
|
||||
con.PrintErrorf("Unknown shell id: %d\n", shellID)
|
||||
return
|
||||
}
|
||||
|
||||
session := con.ActiveTarget.GetSession()
|
||||
if session == nil || session.ID != sh.SessionID {
|
||||
con.PrintErrorf("Shell %d belongs to %s; switch targets before killing\n", shellID, sessionLabel(sh.SessionID, sh.SessionName))
|
||||
return
|
||||
}
|
||||
|
||||
sh.setState(shellStateClosing)
|
||||
sh.SetOutput(io.Discard)
|
||||
shells.Remove(sh.ID)
|
||||
|
||||
// Best-effort graceful request before closing the tunnel.
|
||||
requestRemoteShellExit(sh.Tunnel())
|
||||
|
||||
if err := closeShellTunnel(con, sh.TunnelID, sh.SessionID); err != nil {
|
||||
con.PrintWarnf("Error closing shell tunnel %d: %s\n", sh.TunnelID, err)
|
||||
}
|
||||
|
||||
if force && sh.Pid > 1 {
|
||||
terminate, err := con.Rpc.Terminate(context.Background(), &sliverpb.TerminateReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Pid: int32(sh.Pid),
|
||||
Force: force,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintWarnf("Error terminating shell process %d: %s\n", sh.Pid, err)
|
||||
} else if terminate.Response != nil && terminate.Response.GetErr() != "" {
|
||||
errMsg := terminate.Response.GetErr()
|
||||
// If shell already exited, that is an acceptable cleanup outcome.
|
||||
if strings.Contains(strings.ToLower(errMsg), "no such process") ||
|
||||
strings.Contains(strings.ToLower(errMsg), "process not found") {
|
||||
con.PrintInfof("Shell process %d already exited\n", sh.Pid)
|
||||
} else {
|
||||
con.PrintWarnf("Terminate response for shell process %d: %s\n", sh.Pid, errMsg)
|
||||
}
|
||||
} else {
|
||||
con.PrintInfof("Shell process %d terminated\n", sh.Pid)
|
||||
}
|
||||
}
|
||||
|
||||
if force {
|
||||
con.PrintInfof("Shell %d killed (force)\n", sh.ID)
|
||||
} else {
|
||||
con.PrintInfof("Shell %d killed\n", sh.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *console.SliverClient, existing *managedShell) shellResult {
|
||||
con.Println()
|
||||
con.PrintInfof("Wait approximately 10 seconds after exit, and press <enter> to continue\n")
|
||||
con.PrintInfof("Opening shell tunnel (EOF to exit) ...\n\n")
|
||||
con.PrintInfof("Shell management: `shell ls`, `shell attach <id>`\n")
|
||||
con.PrintInfof("Escape: press Ctrl-] to return to the Sliver client\n")
|
||||
|
||||
session := con.ActiveTarget.GetSession()
|
||||
if session == nil {
|
||||
return
|
||||
return shellAttachFailed
|
||||
}
|
||||
|
||||
// Create an RPC tunnel, then start it before binding the shell to the newly created tunnel
|
||||
ctxTunnel, cancelTunnel := context.WithCancel(context.Background())
|
||||
|
||||
rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{
|
||||
SessionID: session.ID,
|
||||
})
|
||||
defer cancelTunnel()
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Created new tunnel with id: %d, binding to shell ...", rpcTunnel.TunnelID)
|
||||
|
||||
// Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object
|
||||
tunnel := core.GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
|
||||
|
||||
var rows uint32
|
||||
var cols uint32
|
||||
if !noPty {
|
||||
colsInt, rowsInt, err := term.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil || rowsInt <= 0 || colsInt <= 0 {
|
||||
colsInt, rowsInt, err = term.GetSize(int(os.Stdin.Fd()))
|
||||
}
|
||||
if err == nil && rowsInt > 0 && colsInt > 0 {
|
||||
rows = uint32(rowsInt)
|
||||
cols = uint32(colsInt)
|
||||
}
|
||||
if existing == nil {
|
||||
con.PrintInfof("Opening shell tunnel ...\n\n")
|
||||
} else {
|
||||
con.PrintInfof("Re-attaching to shell %d ...\n\n", existing.ID)
|
||||
}
|
||||
|
||||
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: shellPath,
|
||||
EnablePTY: !noPty,
|
||||
Rows: rows,
|
||||
Cols: cols,
|
||||
TunnelID: tunnel.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
//
|
||||
if shell.Response != nil && shell.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s\n", shell.Response.Err)
|
||||
_, err = con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{
|
||||
TunnelID: tunnel.ID,
|
||||
var (
|
||||
tunnel *core.TunnelIO
|
||||
managed *managedShell
|
||||
enablePTY bool
|
||||
oldState *term.State
|
||||
stateSaved bool
|
||||
stopPtySize = func() {}
|
||||
err error
|
||||
)
|
||||
|
||||
if existing == nil {
|
||||
// Create an RPC tunnel, then start it before binding the shell to the newly created tunnel.
|
||||
ctxTunnel, cancelTunnel := context.WithCancel(context.Background())
|
||||
defer cancelTunnel()
|
||||
|
||||
rpcTunnel, err := con.Rpc.CreateTunnel(ctxTunnel, &sliverpb.Tunnel{
|
||||
SessionID: session.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("RPC Error: %s\n", err)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return shellAttachFailed
|
||||
}
|
||||
return
|
||||
}
|
||||
defer tunnel.Close()
|
||||
log.Printf("Bound remote shell pid %d to tunnel %d", shell.Pid, shell.TunnelID)
|
||||
con.PrintInfof("Started remote shell with pid %d\n\n", shell.Pid)
|
||||
log.Printf("Created new tunnel with id: %d, binding to shell ...", rpcTunnel.TunnelID)
|
||||
|
||||
var oldState *term.State
|
||||
if !noPty {
|
||||
// Start() takes an RPC tunnel and creates a local Reader/Writer tunnel object.
|
||||
tunnel = core.GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
|
||||
|
||||
var rows uint32
|
||||
var cols uint32
|
||||
if !noPty {
|
||||
colsInt, rowsInt, err := term.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil || rowsInt <= 0 || colsInt <= 0 {
|
||||
colsInt, rowsInt, err = term.GetSize(int(os.Stdin.Fd()))
|
||||
}
|
||||
if err == nil && rowsInt > 0 && colsInt > 0 {
|
||||
rows = uint32(rowsInt)
|
||||
cols = uint32(colsInt)
|
||||
}
|
||||
}
|
||||
|
||||
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: shellPath,
|
||||
EnablePTY: !noPty,
|
||||
Rows: rows,
|
||||
Cols: cols,
|
||||
TunnelID: tunnel.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
core.GetTunnels().Close(tunnel.ID)
|
||||
return shellAttachFailed
|
||||
}
|
||||
if shell.Response != nil && shell.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s\n", shell.Response.Err)
|
||||
go backgroundCloseShell(con, tunnel.ID, session.ID)
|
||||
return shellAttachFailed
|
||||
}
|
||||
|
||||
path := shellPath
|
||||
if path == "" {
|
||||
path = shell.Path
|
||||
}
|
||||
enablePTY = !noPty
|
||||
managed = &managedShell{
|
||||
SessionID: session.ID,
|
||||
SessionName: session.Name,
|
||||
Path: path,
|
||||
Pid: shell.Pid,
|
||||
TunnelID: tunnel.ID,
|
||||
EnablePTY: enablePTY,
|
||||
CreatedAt: time.Now(),
|
||||
state: shellStateAttached,
|
||||
tunnel: tunnel,
|
||||
output: newSwapWriter(os.Stdout),
|
||||
}
|
||||
managed.ID = shells.Add(managed)
|
||||
managed.startReader(func() {
|
||||
shells.Remove(managed.ID)
|
||||
})
|
||||
|
||||
log.Printf("Bound remote shell pid %d to tunnel %d", shell.Pid, shell.TunnelID)
|
||||
con.PrintInfof("Started remote shell [%d] with pid %d\n\n", managed.ID, shell.Pid)
|
||||
} else {
|
||||
managed = existing
|
||||
tunnel = managed.Tunnel()
|
||||
enablePTY = managed.EnablePTY
|
||||
managed.setState(shellStateAttached)
|
||||
managed.SetOutput(os.Stdout)
|
||||
con.PrintInfof("Attached shell [%d] (pid %d)\n\n", managed.ID, managed.Pid)
|
||||
}
|
||||
|
||||
if enablePTY {
|
||||
oldState, err = term.MakeRaw(0)
|
||||
log.Printf("Saving terminal state: %v", oldState)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to save terminal state")
|
||||
return
|
||||
con.PrintErrorf("Failed to save terminal state\n")
|
||||
managed.SetOutput(io.Discard)
|
||||
managed.setState(shellStateDetached)
|
||||
return shellAttachFailed
|
||||
}
|
||||
stateSaved = true
|
||||
stopPtySize = startPtyResizeWatcher(con, cmd, tunnel.ID)
|
||||
}
|
||||
defer stopPtySize()
|
||||
|
||||
if stateSaved {
|
||||
defer func() {
|
||||
log.Printf("Restoring terminal state ...")
|
||||
term.Restore(0, oldState)
|
||||
}()
|
||||
}
|
||||
|
||||
stopPtyResize := func() {}
|
||||
if !noPty {
|
||||
stopPtyResize = startPtyResizeWatcher(con, cmd, tunnel.ID)
|
||||
}
|
||||
defer stopPtyResize()
|
||||
|
||||
log.Printf("Starting stdin/stdout shell ...")
|
||||
go func() {
|
||||
n, err := io.Copy(os.Stdout, tunnel)
|
||||
log.Printf("Wrote %d bytes to stdout", n)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error writing to stdout: %s", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
log.Printf("Reading from stdin ...")
|
||||
n, err := io.Copy(tunnel, newFilterReader(os.Stdin))
|
||||
log.Printf("Read %d bytes from stdin", n)
|
||||
if err != nil && err != io.EOF {
|
||||
con.PrintErrorf("Error reading from stdin: %s\n", err)
|
||||
detached, _ := runAttachedIO(tunnel, con)
|
||||
if detached {
|
||||
managed.SetOutput(io.Discard)
|
||||
managed.setState(shellStateDetached)
|
||||
return shellDetached
|
||||
}
|
||||
|
||||
if !noPty {
|
||||
log.Printf("Restoring terminal state ...")
|
||||
term.Restore(0, oldState)
|
||||
}
|
||||
managed.SetOutput(io.Discard)
|
||||
managed.setState(shellStateClosing)
|
||||
shells.Remove(managed.ID)
|
||||
|
||||
log.Printf("Exit interactive")
|
||||
bufio.NewWriter(os.Stdout).Flush()
|
||||
// "exit" should return immediately; tunnel close/cleanup is best-effort in the background.
|
||||
go backgroundCloseShell(con, tunnel.ID, session.ID)
|
||||
|
||||
return shellExited
|
||||
}
|
||||
|
||||
func backgroundCloseShell(con *console.SliverClient, tunnelID uint64, sessionID string) {
|
||||
err := closeShellTunnel(con, tunnelID, sessionID)
|
||||
if err != nil {
|
||||
log.Printf("Background close tunnel %d failed: %v", tunnelID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func closeShellTunnel(con *console.SliverClient, tunnelID uint64, sessionID string) error {
|
||||
core.GetTunnels().Close(tunnelID)
|
||||
if con == nil || con.Rpc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := con.Rpc.CloseTunnel(context.Background(), &sliverpb.Tunnel{
|
||||
TunnelID: tunnelID,
|
||||
SessionID: sessionID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func requestRemoteShellExit(tunnel *core.TunnelIO) {
|
||||
if tunnel == nil || !tunnel.IsOpen() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, payload := range [][]byte{[]byte("exit\n"), []byte("logout\n")} {
|
||||
func(data []byte) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Shell exit write skipped: %v", r)
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case tunnel.Send <- append([]byte(nil), data...):
|
||||
default:
|
||||
}
|
||||
}(payload)
|
||||
}
|
||||
}
|
||||
|
||||
func sessionLabel(sessionID, sessionName string) string {
|
||||
if sessionName == "" {
|
||||
return sessionID
|
||||
}
|
||||
return sessionName + " (" + sessionID + ")"
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -1139,7 +1139,7 @@ func (l asciiLogo) Render() string {
|
||||
|
||||
var asciiLogos = []asciiLogo{
|
||||
{style: logoStyleRed, art: `
|
||||
██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
|
||||
██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
|
||||
▒██ ▒ ▓██▒ ▓██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒
|
||||
░ ▓██▄ ▒██░ ▒██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒
|
||||
▒ ██▒▒██░ ░██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Spinner } from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
const LoadingState: React.FC = () => {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center">
|
||||
<Spinner label="Loading ..." labelColor="foreground" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingState;
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
oneLight,
|
||||
} from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import AsciinemaPlayer from "./asciinema";
|
||||
import Mermaid from "./mermaid";
|
||||
import Youtube from "./youtube";
|
||||
|
||||
export type MarkdownProps = {
|
||||
key?: string;
|
||||
markdown: string;
|
||||
};
|
||||
|
||||
@@ -42,6 +42,49 @@ type HeadingProps = ComponentPropsWithoutRef<"h1"> & {
|
||||
node?: unknown;
|
||||
};
|
||||
|
||||
const parseMermaidMinHeight = (lang: string, node: any): number | undefined => {
|
||||
const [, ...langOptionParts] = lang.split(":");
|
||||
const langOptions = langOptionParts.join(":").trim();
|
||||
|
||||
const nodeMeta =
|
||||
typeof node?.data?.meta === "string"
|
||||
? node.data.meta
|
||||
: typeof node?.meta === "string"
|
||||
? node.meta
|
||||
: typeof node?.properties?.metastring === "string"
|
||||
? node.properties.metastring
|
||||
: "";
|
||||
|
||||
const optionSources = [langOptions, nodeMeta]
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const parseValue = (raw: string): number | undefined => {
|
||||
const directNumber = raw.match(/^(\d{2,4})$/);
|
||||
if (directNumber) {
|
||||
return Number.parseInt(directNumber[1], 10);
|
||||
}
|
||||
|
||||
const keyedNumber = raw.match(
|
||||
/(?:^|[,\s;])(?:min[-_]?h(?:eight)?|h(?:eight)?)\s*[:=]\s*(\d{2,4})\b/i
|
||||
);
|
||||
if (keyedNumber) {
|
||||
return Number.parseInt(keyedNumber[1], 10);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
for (const source of optionSources) {
|
||||
const parsed = parseValue(source);
|
||||
if (parsed !== undefined && Number.isFinite(parsed)) {
|
||||
return Math.min(1200, Math.max(120, parsed));
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const mergeClassNames = (
|
||||
...classes: Array<string | false | null | undefined>
|
||||
) => {
|
||||
@@ -252,7 +295,6 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<div className="relative">
|
||||
<div className={proseClassName}>
|
||||
<Markdown
|
||||
key={props.key || `${Math.random()}`}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
...headingComponents,
|
||||
@@ -543,6 +585,12 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
// a monaco code block in a <pre> tag
|
||||
const { children, className, node, ...rest } = preProps as any;
|
||||
const childClass = (children as any)?.props?.className;
|
||||
if (
|
||||
typeof childClass === "string" &&
|
||||
childClass.startsWith("language-mermaid")
|
||||
) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
if (
|
||||
childClass &&
|
||||
childClass.startsWith("language-") &&
|
||||
@@ -598,7 +646,8 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
const lang = languageClass
|
||||
? languageClass.replace("language-", "")
|
||||
: "plaintext";
|
||||
const normalizedLang = lang.toLowerCase();
|
||||
const [baseLang] = lang.split(":");
|
||||
const normalizedLang = baseLang.toLowerCase();
|
||||
const childValue = Array.isArray(children)
|
||||
? children.join("")
|
||||
: children;
|
||||
@@ -631,6 +680,15 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (normalizedLang === "mermaid") {
|
||||
return (
|
||||
<Mermaid
|
||||
diagram={sourceCode.replace(/\n$/, "")}
|
||||
minHeight={parseMermaidMinHeight(lang, node)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (inline || normalizedLang === "plaintext") {
|
||||
return (
|
||||
<code
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Themes } from "@/util/themes";
|
||||
import { useTheme } from "next-themes";
|
||||
import React from "react";
|
||||
|
||||
type MermaidProps = {
|
||||
diagram: string;
|
||||
minHeight?: number;
|
||||
};
|
||||
|
||||
const Mermaid = ({ diagram, minHeight }: MermaidProps) => {
|
||||
const { theme } = useTheme();
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const normalizedMinHeight =
|
||||
Number.isFinite(minHeight) && minHeight
|
||||
? Math.min(1200, Math.max(120, Math.floor(minHeight)))
|
||||
: undefined;
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const target = containerRef.current;
|
||||
const source = diagram.trim();
|
||||
|
||||
if (!target || !source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderDiagram = async () => {
|
||||
try {
|
||||
const mermaidModule = await import("mermaid");
|
||||
const mermaid = mermaidModule.default;
|
||||
const mermaidTheme = theme === Themes.LIGHT ? "default" : "dark";
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "strict",
|
||||
theme: mermaidTheme,
|
||||
});
|
||||
|
||||
const graphId = `sliver-mermaid-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 10)}`;
|
||||
const { svg, bindFunctions } = await mermaid.render(graphId, source);
|
||||
|
||||
if (cancelled || !containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
containerRef.current.innerHTML = svg;
|
||||
bindFunctions?.(containerRef.current);
|
||||
|
||||
if (normalizedMinHeight) {
|
||||
const svgEl = containerRef.current.querySelector("svg");
|
||||
if (svgEl) {
|
||||
const viewBox = svgEl.getAttribute("viewBox") || "";
|
||||
const parts = viewBox
|
||||
.split(/\s+/)
|
||||
.map((part) => Number.parseFloat(part))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
|
||||
let naturalWidth = 0;
|
||||
let naturalHeight = 0;
|
||||
if (parts.length === 4) {
|
||||
naturalWidth = parts[2] || 0;
|
||||
naturalHeight = parts[3] || 0;
|
||||
}
|
||||
|
||||
if ((naturalWidth <= 0 || naturalHeight <= 0) && svgEl.getBBox) {
|
||||
try {
|
||||
const bounds = svgEl.getBBox();
|
||||
naturalWidth = bounds.width;
|
||||
naturalHeight = bounds.height;
|
||||
} catch {
|
||||
// No-op: leave natural dimensions unset if bbox is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
if (naturalWidth > 0 && naturalHeight > 0 && naturalHeight < normalizedMinHeight) {
|
||||
const scale = normalizedMinHeight / naturalHeight;
|
||||
const targetWidth = Math.round(naturalWidth * scale);
|
||||
const targetHeight = Math.round(naturalHeight * scale);
|
||||
|
||||
svgEl.setAttribute("width", `${targetWidth}`);
|
||||
svgEl.setAttribute("height", `${targetHeight}`);
|
||||
svgEl.style.width = `${targetWidth}px`;
|
||||
svgEl.style.height = `${targetHeight}px`;
|
||||
svgEl.style.maxWidth = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setError(null);
|
||||
} catch (renderError) {
|
||||
if (cancelled || !containerRef.current) {
|
||||
return;
|
||||
}
|
||||
containerRef.current.innerHTML = "";
|
||||
const message =
|
||||
renderError instanceof Error
|
||||
? renderError.message
|
||||
: "Unknown Mermaid rendering error";
|
||||
setError(message);
|
||||
}
|
||||
};
|
||||
|
||||
void renderDiagram();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (target) {
|
||||
target.innerHTML = "";
|
||||
}
|
||||
};
|
||||
}, [diagram, normalizedMinHeight, theme]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="not-prose my-6 text-sm text-rose-700 dark:text-rose-300">
|
||||
Failed to render Mermaid diagram: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="not-prose my-6 overflow-x-auto">
|
||||
<div
|
||||
className="mx-auto flex w-fit min-w-max items-center justify-center"
|
||||
style={normalizedMinHeight ? { minHeight: normalizedMinHeight } : undefined}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="min-w-max [&_svg]:h-auto [&_svg]:max-w-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mermaid;
|
||||
@@ -3,7 +3,7 @@
|
||||
import { SliversIcon } from "@/components/icons/slivers";
|
||||
import { useSearchContext } from "@/util/search-context";
|
||||
import { Themes } from "@/util/themes";
|
||||
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faBars,
|
||||
faHome,
|
||||
@@ -133,6 +133,17 @@ export default function TopNavbar(props: TopNavbarProps) {
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
|
||||
<NavbarItem isActive={router.pathname.startsWith("/talks")}>
|
||||
<Button
|
||||
variant="light"
|
||||
color={router.pathname === "/talks" ? "primary" : "default"}
|
||||
as={Link}
|
||||
onPress={() => router.push("/talks")}
|
||||
>
|
||||
Talks
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
|
||||
<NavbarItem isActive={router.pathname.startsWith("/docs")}>
|
||||
<Button
|
||||
variant="light"
|
||||
@@ -147,43 +158,69 @@ export default function TopNavbar(props: TopNavbarProps) {
|
||||
|
||||
<NavbarContent as="div" justify="end" className="hidden md:flex">
|
||||
{renderSearchInput("w-64")}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
isDisabled={!mounted}
|
||||
<Tooltip
|
||||
content={theme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
isDisabled={!mounted}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open("https://github.com/BishopFox/sliver", "_blank");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
</Button>
|
||||
<Tooltip content="Join Discord">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="View on GitHub">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open("https://github.com/BishopFox/sliver", "_blank");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</NavbarContent>
|
||||
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
isDisabled={!mounted}
|
||||
<Tooltip
|
||||
content={theme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
onPress={() => setIsMobileMenuOpen((current) => !current)}
|
||||
>
|
||||
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(theme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
isDisabled={!mounted}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content={isMobileMenuOpen ? "Close menu" : "Open menu"}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
onPress={() => setIsMobileMenuOpen((current) => !current)}
|
||||
>
|
||||
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Navbar>
|
||||
|
||||
@@ -212,6 +249,16 @@ export default function TopNavbar(props: TopNavbarProps) {
|
||||
>
|
||||
Tutorials
|
||||
</Button>
|
||||
<Button
|
||||
variant={router.pathname.startsWith("/talks") ? "solid" : "light"}
|
||||
color={router.pathname.startsWith("/talks") ? "primary" : "default"}
|
||||
onPress={() => {
|
||||
router.push("/talks");
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
Talks
|
||||
</Button>
|
||||
<Button
|
||||
variant={router.pathname.startsWith("/docs") ? "solid" : "light"}
|
||||
color={router.pathname.startsWith("/docs") ? "primary" : "default"}
|
||||
@@ -222,6 +269,18 @@ export default function TopNavbar(props: TopNavbarProps) {
|
||||
>
|
||||
Docs
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
startContent={<FontAwesomeIcon icon={faDiscord} />}
|
||||
>
|
||||
Discord
|
||||
</Button>
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
|
||||
@@ -1,19 +1,240 @@
|
||||
import React from "react";
|
||||
|
||||
type YoutubePlayerState = {
|
||||
PLAYING: number;
|
||||
};
|
||||
|
||||
type YoutubePlayerStateChangeEvent = {
|
||||
data: number;
|
||||
};
|
||||
|
||||
type YoutubePlayer = {
|
||||
destroy?: () => void;
|
||||
};
|
||||
|
||||
type YoutubePlayerCtor = new (
|
||||
element: HTMLIFrameElement | string,
|
||||
options?: {
|
||||
events?: {
|
||||
onStateChange?: (event: YoutubePlayerStateChangeEvent) => void;
|
||||
};
|
||||
}
|
||||
) => YoutubePlayer;
|
||||
|
||||
type YoutubeNamespace = {
|
||||
Player: YoutubePlayerCtor;
|
||||
PlayerState: YoutubePlayerState;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
YT?: YoutubeNamespace;
|
||||
onYouTubeIframeAPIReady?: () => void;
|
||||
__sliverYoutubeApiPromise?: Promise<YoutubeNamespace>;
|
||||
}
|
||||
}
|
||||
|
||||
export type YoutubeProps = {
|
||||
className?: string;
|
||||
iframeClassName?: string;
|
||||
|
||||
width?: number;
|
||||
height?: number;
|
||||
responsive?: boolean;
|
||||
title?: string;
|
||||
startAt?: number;
|
||||
|
||||
embedId: string;
|
||||
embedId?: string;
|
||||
url?: string;
|
||||
|
||||
onInteract?: () => void;
|
||||
onPlay?: () => void;
|
||||
};
|
||||
|
||||
const YOUTUBE_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
|
||||
|
||||
function parseYoutubeEmbedIdFromUrl(input: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(input);
|
||||
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
||||
const parts = parsed.pathname.split("/").filter(Boolean);
|
||||
|
||||
let candidate = "";
|
||||
if (host === "youtu.be" && parts.length > 0) {
|
||||
candidate = parts[0] || "";
|
||||
} else if (host === "youtube.com" || host === "m.youtube.com") {
|
||||
if (parts[0] === "watch") {
|
||||
candidate = parsed.searchParams.get("v") || "";
|
||||
} else if ((parts[0] === "embed" || parts[0] === "shorts") && parts.length > 1) {
|
||||
candidate = parts[1] || "";
|
||||
}
|
||||
}
|
||||
|
||||
return YOUTUBE_ID_PATTERN.test(candidate) ? candidate : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveYoutubeEmbedId(embedId?: string, url?: string): string | null {
|
||||
const idCandidate = (embedId || "").trim();
|
||||
if (YOUTUBE_ID_PATTERN.test(idCandidate)) {
|
||||
return idCandidate;
|
||||
}
|
||||
if (idCandidate) {
|
||||
const parsedFromEmbedId = parseYoutubeEmbedIdFromUrl(idCandidate);
|
||||
if (parsedFromEmbedId) {
|
||||
return parsedFromEmbedId;
|
||||
}
|
||||
}
|
||||
|
||||
const urlCandidate = (url || "").trim();
|
||||
if (urlCandidate) {
|
||||
return parseYoutubeEmbedIdFromUrl(urlCandidate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadYoutubeIframeApi(): Promise<YoutubeNamespace> {
|
||||
if (typeof window === "undefined") {
|
||||
return Promise.reject(new Error("YouTube API cannot load during SSR"));
|
||||
}
|
||||
if (window.YT?.Player) {
|
||||
return Promise.resolve(window.YT);
|
||||
}
|
||||
if (window.__sliverYoutubeApiPromise) {
|
||||
return window.__sliverYoutubeApiPromise;
|
||||
}
|
||||
|
||||
window.__sliverYoutubeApiPromise = new Promise<YoutubeNamespace>((resolve, reject) => {
|
||||
const previousReadyHandler = window.onYouTubeIframeAPIReady;
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
previousReadyHandler?.();
|
||||
if (window.YT?.Player) {
|
||||
resolve(window.YT);
|
||||
} else {
|
||||
reject(new Error("YouTube API loaded without Player support"));
|
||||
}
|
||||
};
|
||||
|
||||
const existingScript = document.querySelector<HTMLScriptElement>(
|
||||
"script[data-youtube-iframe-api='true']"
|
||||
);
|
||||
if (existingScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://www.youtube.com/iframe_api";
|
||||
script.async = true;
|
||||
script.dataset.youtubeIframeApi = "true";
|
||||
script.onerror = () => reject(new Error("Failed to load YouTube iframe API"));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return window.__sliverYoutubeApiPromise;
|
||||
}
|
||||
|
||||
export default function Youtube(props: YoutubeProps) {
|
||||
const {
|
||||
className,
|
||||
iframeClassName,
|
||||
width,
|
||||
height,
|
||||
responsive,
|
||||
title,
|
||||
startAt,
|
||||
embedId: embedIdProp,
|
||||
url,
|
||||
onInteract,
|
||||
onPlay,
|
||||
} = props;
|
||||
const embedId = resolveYoutubeEmbedId(embedIdProp, url);
|
||||
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
|
||||
const onPlayRef = React.useRef(onPlay);
|
||||
|
||||
React.useEffect(() => {
|
||||
onPlayRef.current = onPlay;
|
||||
}, [onPlay]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!embedId || !onPlayRef.current || !iframeRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let player: YoutubePlayer | null = null;
|
||||
let disposed = false;
|
||||
|
||||
loadYoutubeIframeApi()
|
||||
.then((YT) => {
|
||||
if (disposed || !iframeRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
player = new YT.Player(iframeRef.current, {
|
||||
events: {
|
||||
onStateChange: (event) => {
|
||||
if (event.data === YT.PlayerState.PLAYING) {
|
||||
onPlayRef.current?.();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// No-op: if API fails, videos still load via standard iframe behavior.
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
player?.destroy?.();
|
||||
};
|
||||
}, [embedId]);
|
||||
|
||||
if (!embedId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startAtParam =
|
||||
Number.isFinite(startAt) && startAt && startAt > 0
|
||||
? `&start=${Math.floor(startAt)}`
|
||||
: "";
|
||||
const enableJsApi = onPlay ? "&enablejsapi=1" : "";
|
||||
const src = `https://www.youtube-nocookie.com/embed/${embedId}?rel=0${startAtParam}${enableJsApi}`;
|
||||
const playerTitle = title || "YouTube video player";
|
||||
const isResponsive = responsive ?? true;
|
||||
const handleInteract = () => onInteract?.();
|
||||
|
||||
if (isResponsive) {
|
||||
return (
|
||||
<div className={className} onPointerDown={handleInteract}>
|
||||
<div className="relative w-full overflow-hidden rounded-xl bg-black pb-[56.25%]">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
className={`absolute left-0 top-0 h-full w-full ${iframeClassName || ""}`.trim()}
|
||||
src={src}
|
||||
title={playerTitle}
|
||||
loading="lazy"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div className={className} onPointerDown={handleInteract}>
|
||||
<iframe
|
||||
width={`${props.width ? props.width : 640}`}
|
||||
height={`${props.height ? props.height : 360}`}
|
||||
src={`https://youtu.be/embed/${props.embedId}`}
|
||||
ref={iframeRef}
|
||||
width={`${width ? width : 640}`}
|
||||
height={`${height ? height : 360}`}
|
||||
className={iframeClassName}
|
||||
src={src}
|
||||
title={playerTitle}
|
||||
loading="lazy"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||
|
||||
const config = [
|
||||
...nextCoreWebVitals,
|
||||
{
|
||||
// Monaco bundles are vendor artifacts, not authored source files.
|
||||
ignores: ["public/js/**"],
|
||||
},
|
||||
];
|
||||
|
||||
export default config;
|
||||
@@ -1,6 +1,3 @@
|
||||
const { spawnSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
@@ -11,18 +8,6 @@ const nextConfig = {
|
||||
experimental: {
|
||||
scrollRestoration: true,
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
if (isServer) {
|
||||
const result = spawnSync(process.execPath, [path.join(__dirname, 'prebuild/run-prebuild.js')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error('[prebuild] webpack hook failed');
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
Generated
+3716
-1029
File diff suppressed because it is too large.
Load diff
@@ -3,10 +3,10 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run prebuild && concurrently -k -n prebuild,dev \"npm run prebuild:watch -- --skip-initial\" \"next dev\"",
|
||||
"build": "npm run prebuild && next build",
|
||||
"dev": "npm run prebuild && concurrently -k -n prebuild,dev \"npm run prebuild:watch -- --skip-initial\" \"next dev --webpack\"",
|
||||
"build": "npm run prebuild && next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint .",
|
||||
"prebuild": "node ./prebuild/run-prebuild.js",
|
||||
"prebuild:watch": "node ./prebuild/watch.js",
|
||||
"offline": "npm run build && zip -r www.zip ./out",
|
||||
@@ -26,7 +26,8 @@
|
||||
"framer-motion": "^12.23.22",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lunr": "^2.3.9",
|
||||
"next": "^15.5.9",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "^16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
@@ -44,7 +45,7 @@
|
||||
"autoprefixer": "^10.4.21",
|
||||
"concurrently": "^9.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-next": "^15.5.4",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -2,7 +2,7 @@ import Document, { Head, Html, Main, NextScript } from "next/document";
|
||||
import React from "react";
|
||||
|
||||
// This generates the https: and wss: "connect-src" directives based on the above backends list so its a little easier to edit.
|
||||
const CSP = `default-src 'none'; script-src 'self' 'unsafe-eval'; img-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-src https://youtu.be; connect-src 'self'`;
|
||||
const CSP = `default-src 'none'; script-src 'self' 'unsafe-eval' 'sha256-HSN8D632WcMJaQDjNrIwVtSeQLAfKAVZRSyJiv6bjQw=' https://www.youtube.com https://s.ytimg.com; img-src 'self' data: https://i.ytimg.com https://*.ytimg.com; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-src https://youtu.be https://www.youtube.com https://youtube.com https://www.youtube-nocookie.com; connect-src 'self'`;
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: any) {
|
||||
@@ -15,7 +15,7 @@ class MyDocument extends Document {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en" suppressHydrationWarning>
|
||||
<Html lang="en" className="dark" suppressHydrationWarning>
|
||||
<Head>
|
||||
<meta httpEquiv="Content-Security-Policy" content={CSP} />
|
||||
</Head>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import MarkdownViewer from "@/components/markdown";
|
||||
import LoadingState from "@/components/loading-state";
|
||||
import { Docs } from "@/util/docs";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
|
||||
@@ -34,14 +35,17 @@ const DocsIndexPage: NextPage = () => {
|
||||
});
|
||||
|
||||
const params = useSearchParams();
|
||||
const [name, setName] = React.useState("");
|
||||
const [markdown, setMarkdown] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
const _name = params.get("name");
|
||||
setName(_name || "");
|
||||
setMarkdown(docs?.docs.find((doc) => doc.name === _name)?.content || "");
|
||||
}, [params, docs]);
|
||||
const name = params.get("name") || "";
|
||||
const markdown = React.useMemo(() => {
|
||||
return docs?.docs.find((doc) => doc.name === name)?.content || "";
|
||||
}, [docs, name]);
|
||||
const hasNameQueryInPath = React.useMemo(() => {
|
||||
const query = router.asPath.split("?")[1];
|
||||
if (!query) {
|
||||
return false;
|
||||
}
|
||||
return new URLSearchParams(query).has("name");
|
||||
}, [router.asPath]);
|
||||
|
||||
const [filterValue, setFilterValue] = React.useState("");
|
||||
const fuse = React.useMemo(() => {
|
||||
@@ -79,8 +83,8 @@ const DocsIndexPage: NextPage = () => {
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
if (isLoading || !docs) {
|
||||
return <div>Loading...</div>;
|
||||
if (isLoading || !docs || (hasNameQueryInPath && !name)) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -127,7 +131,7 @@ const DocsIndexPage: NextPage = () => {
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
|
||||
<aside className="hidden lg:block lg:col-span-3">
|
||||
<div className="sticky top-24 ml-4 flex flex-col gap-3">
|
||||
<div className="sticky top-16 ml-4 flex flex-col gap-3">
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="Filter..."
|
||||
@@ -171,7 +175,7 @@ const DocsIndexPage: NextPage = () => {
|
||||
<Divider />
|
||||
<CardBody>
|
||||
<MarkdownViewer
|
||||
key={name || `${Math.random()}`}
|
||||
key={name}
|
||||
markdown={markdown || ""}
|
||||
/>
|
||||
</CardBody>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
You'll want to compile from a MacOS or Linux machine. Compiling from native Windows is possible in theory, but none of the asset scripts are designed to run on Windows. You can cross-compile the Windows server/client binaries from a better operating system like Linux or MacOS. If you only have a Windows machine, see "Windows Builds" below (TL;DR: use WSL).
|
||||
|
||||
# Sliver v1.6.x
|
||||
You'll want to compile Sliver from a MacOS or Linux machine. Compiling from native Windows is possible in theory, but none of the scripts are designed to run on Windows. You can cross-compile the Windows server/client binaries from a better operating system like Linux or MacOS. If you only have a Windows machine, you should be able to use the Windows Subsystem for Linux (WSL).
|
||||
|
||||
To compile from source you'll need:
|
||||
|
||||
@@ -62,7 +60,7 @@ The Docker build includes mingw and Metasploit, so it can take a while to build
|
||||
|
||||
### Windows Builds
|
||||
|
||||
If all you have is a Windows machine, the easiest way to build Sliver is using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) and following the Linux/cross-compile instructions above. To cross-compile a native Windows binary use `make windows-amd64` and copy it to your Windows file system (i.e. `/mnt/c/Users/foo/Desktop`) and run it using a terminal that supports ANSI sequences such as the [Windows Terminal](https://github.com/microsoft/terminal).
|
||||
If all you have is a Windows machine, the easiest way to build Sliver is using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) and following the Linux/cross-compile instructions above. To cross-compile a Windows binary use `make windows-amd64` and copy it to your Windows file system (i.e. `/mnt/c/Users/foo/Desktop`) and, you will need to run it using a terminal that supports ANSI sequences such as the [Windows Terminal](https://github.com/microsoft/terminal).
|
||||
|
||||
# Developers
|
||||
|
||||
|
||||
@@ -1,94 +1,131 @@
|
||||
### General Server Configuration
|
||||
|
||||
The Sliver server has a configuration file located in the `configs` sub-directory of the [`SLIVER_ROOT_DIR`](/docs?name=Environment+Variables); by default this is `~/.sliver/configs/server.json`. If no configuration file exists, a default configuration will be generated and written to disk on startup. The default configuration is shown below:
|
||||
The Sliver server configuration file is located in the `configs` sub-directory of [`SLIVER_ROOT_DIR`](/docs?name=Environment+Variables); by default this is `~/.sliver/configs/server.yaml`.
|
||||
|
||||
If no configuration file exists, a default YAML configuration is generated and written to disk on startup. If a legacy `server.json` file exists, Sliver reads it, writes `server.yaml`, and renames the old file to `.server.json-old`.
|
||||
|
||||
#### Default Server Config
|
||||
|
||||
```json
|
||||
{
|
||||
"daemon_mode": false,
|
||||
"daemon": {
|
||||
"host": "",
|
||||
"port": 31337
|
||||
},
|
||||
"logs": {
|
||||
"level": 4,
|
||||
"grpc_unary_payloads": false,
|
||||
"grpc_stream_payloads": false
|
||||
},
|
||||
"grpc": {
|
||||
"keepalive": {
|
||||
"min_time_seconds": 30,
|
||||
"permit_without_stream": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```yaml
|
||||
daemon_mode: false
|
||||
daemon:
|
||||
host: ""
|
||||
port: 31337
|
||||
tailscale: false
|
||||
logs:
|
||||
level: 4
|
||||
grpc_unary_payloads: false
|
||||
grpc_stream_payloads: false
|
||||
tls_key_logger: false
|
||||
grpc:
|
||||
keepalive:
|
||||
min_time_seconds: 30
|
||||
permit_without_stream: true
|
||||
watch_tower: null
|
||||
go_proxy: ""
|
||||
http_default:
|
||||
headers:
|
||||
- method: GET
|
||||
name: Cache-Control
|
||||
value: no-store, no-cache, must-revalidate
|
||||
probability: 100
|
||||
notifications:
|
||||
enabled: false
|
||||
services: {}
|
||||
cc: {}
|
||||
cxx: {}
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
- `daemon_mode` - Enable [daemon mode](/docs?name=Daemon+Mode)
|
||||
- `daemon` - An object containing options related to `daemon_mode`, these values are only used when `daemon_mode` is set to `true`.
|
||||
- `host` - What network interface to bind the `daemon_mode` client listener to. By default this is an empty string, which indicates binding to all interfaces.
|
||||
- `port` - TCP port to bind the `daemon_mode` client listener to.
|
||||
- `logs` - An object containing configuration options for server logs
|
||||
- `level` - The `logrus` logging level (1 - 5), by default this is set to `4`, which is the `INFO` level; increasing this value increases the verbosity of the logs e.g. `DEBUG` is `5` and `SILENT` is `1`
|
||||
- `grpc_unary_payloads` - Log gRPC unary payloads
|
||||
- `grpc_stream_payloads` - Log gRPC streaming payloads
|
||||
- `grpc` - An object containing options for the server gRPC stack
|
||||
- `keepalive` - gRPC keepalive enforcement configuration
|
||||
- `min_time_seconds` - Minimum amount of time (in seconds) between client pings before the server sends a GOAWAY with `too_many_pings`
|
||||
- `permit_without_stream` - Allow client pings when there are no active streams
|
||||
- `daemon_mode` - Enable [daemon mode](/docs?name=Daemon+Mode).
|
||||
- `daemon` - Options for daemon mode.
|
||||
- `host` - Interface to bind the daemon listener to. Empty string means all interfaces.
|
||||
- `port` - TCP port for the daemon listener.
|
||||
- `tailscale` - Enable Tailscale for daemon listener setup.
|
||||
- `logs` - Server logging options.
|
||||
- `level` - `logrus` level (`0`-`6`, clamped by Sliver). `4` is `INFO`, `5` is `DEBUG`, `6` is `TRACE`.
|
||||
- `grpc_unary_payloads` - Log gRPC unary payloads.
|
||||
- `grpc_stream_payloads` - Log gRPC streaming payloads.
|
||||
- `tls_key_logger` - Enable TLS key logging (for debugging/traffic analysis only; sensitive).
|
||||
- `grpc.keepalive` - gRPC keepalive enforcement configuration.
|
||||
- `min_time_seconds` - Minimum time (seconds) between client pings before sending GOAWAY (`too_many_pings`).
|
||||
- `permit_without_stream` - Allow client pings when there are no active streams.
|
||||
- `watch_tower` - Optional API keys for Watchtower integrations.
|
||||
- `vt_api_key` - VirusTotal API key.
|
||||
- `xforce_api_key` - IBM X-Force API key.
|
||||
- `xforce_api_password` - IBM X-Force API password.
|
||||
- `go_proxy` - Optional `GOPROXY` override used by server-side builds.
|
||||
- `http_default.headers` - Default HTTP headers for anonymous server responses.
|
||||
- `method` - HTTP method to apply to (for example `GET`).
|
||||
- `name` - Header name.
|
||||
- `value` - Header value.
|
||||
- `probability` - Percent chance (0-100) that the header is added.
|
||||
- `notifications` - Server notification config. See [Notifications](/docs?name=Notifications) for the full schema.
|
||||
- `cc` - Optional compiler overrides map (`GOOS/GOARCH` -> C compiler path).
|
||||
- `cxx` - Optional compiler overrides map (`GOOS/GOARCH` -> C++ compiler path).
|
||||
|
||||
## Database Configuration
|
||||
|
||||
Sliver supports SQL database configurations. By default this configuration file is located at `~/.sliver/configs/database.json`:
|
||||
Sliver supports SQL database configurations in `~/.sliver/configs/database.yaml`.
|
||||
|
||||
If no configuration file exists, a default YAML configuration is generated and written to disk on startup. If a legacy `database.json` exists, Sliver reads it, writes `database.yaml`, and renames the old file to `.database.json-old`.
|
||||
|
||||
#### Default Database Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"dialect": "sqlite3",
|
||||
"database": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"host": "",
|
||||
"port": 0,
|
||||
"params": null,
|
||||
"max_idle_conns": 10,
|
||||
"max_open_conns": 100,
|
||||
"log_level": "warn"
|
||||
}
|
||||
```yaml
|
||||
dialect: sqlite3
|
||||
database: ""
|
||||
username: ""
|
||||
password: ""
|
||||
host: ""
|
||||
port: 0
|
||||
params:
|
||||
cache: shared
|
||||
pragmas:
|
||||
busy_timeout: "5000"
|
||||
journal_mode: WAL
|
||||
synchronous: NORMAL
|
||||
temp_store: MEMORY
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
log_level: warn
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
- `dialect` - Defines the type of database to connect to, Sliver supports `mysql`, `postgresql`, and `sqlite3`. By default an internal version of `sqlite3` is used, which is saved to `~/.sliver/sliver.db` by default.
|
||||
- `database` - The name of the database to connect to, note that this option is ignored when using `sqlite3`.
|
||||
- `dialect` - Database backend (`mysql`, `postgresql`, or `sqlite3`).
|
||||
- `database` - Database name (ignored for `sqlite3`).
|
||||
- `username` - Database username.
|
||||
- `password` - Database password.
|
||||
- `host` - Database host.
|
||||
- `port` - Database TCP port.
|
||||
- `params` - Is a key/value string map that can be used to pass dialect-specific options to the DSN/connection string. This can be used, for example, to enable SSL/TLS on certain database connections.
|
||||
- `max_idle_conns` - Max idle connections in the database connection pool.
|
||||
- `max_open_conns` - Max open connections in the database connection pool.
|
||||
- `log_level` - Configures the log level for database logging, can be set to: `silent`, `error`, `warn`, or `info`. Default level is `warn`, note that setting this option to `info` may result in sensitive information being logged to disk!
|
||||
- `params` - Key/value map of dialect-specific DSN parameters.
|
||||
- `pragmas` - SQLite pragma map. Used when `dialect` is `sqlite3` and `params` does not already set `_pragma`.
|
||||
- `max_idle_conns` - Max idle DB connections (minimum enforced value is `1`).
|
||||
- `max_open_conns` - Max open DB connections (minimum enforced value is `1`).
|
||||
- `log_level` - GORM DB logging level (`silent`, `error`, `warn`, `info`).
|
||||
|
||||
With the default SQLite configuration, Sliver stores the DB at `~/.sliver/sliver.db`.
|
||||
|
||||
# Client Configuration
|
||||
|
||||
### Operator
|
||||
|
||||
Client configurations must be generated by the server (they contain per-user key pairs). For more details see [multiplayer mode](/docs?name=Multi-player+Mode), and example configuration is shown below. Client configuration files can be copied into `~/.sliver-client/configs/`:
|
||||
Operator client configs are generated by the server and should be copied into `~/.sliver-client/configs/`. These files are currently JSON (`*.cfg`), not YAML.
|
||||
|
||||
For details on generating them, see [multiplayer mode](/docs?name=Multi-player+Mode).
|
||||
|
||||
```json
|
||||
{
|
||||
"operator": "moloch",
|
||||
"operator": "alice",
|
||||
"token": "<operator-auth-token>",
|
||||
"lhost": "localhost",
|
||||
"lport": 31337,
|
||||
"ca_certificate": "-----BEGIN CERTIFICATE-----\nMIIBoTCCASegAwIBAgIRAPhwFTptJKJM4Bef71nA29YwCgYIKoZIzj0EAwMwCzEJ\nMAcGA1UEChMAMB4XDTIwMDExNDIyNTY1NloXDTIzMDExMzIyNTY1NlowCzEJMAcG\nA1UEChMAMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmvGJZ8zDqLxge2PfhK1QcdOi\n6kEV8vgz/S1fSiU9h21JDVaX+FFhG2cYIzr1Q3zE2Ml+pUdnwUSk24pFFTUhjlsO\n4H2YQC/W46DXSa8VQdvpfDaTPYLzXuBhsdVcn2rdo08wTTAOBgNVHQ8BAf8EBAMC\nAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMB\nAf8wCwYDVR0RBAQwAoIAMAoGCCqGSM49BAMDA2gAMGUCMQDOj0sVrVoJSkN4qiqn\nS9wMFNeVOZ+5TVZOOpTn19nc4C/wq9jwdYRHW1dlmvWu2LMCMCm2y1TkaWLa/i4t\nArTCIiCmNVRxAR1xFsWama9yv7wFY0+5xKS7W944418v0jYJKA==\n-----END CERTIFICATE-----\n",
|
||||
"private_key": "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDBido1KtKSwQah/WIoGkDZDX2WPXdexUVAmi0tf6Pd9vK5pfpt2II+n\n8BqkV0ifyjugBwYFK4EEACKhZANiAATHATh7e8uX/MN5mokguQf4ywXgTaihYD//\nyraEUcZAYsrCtDHmdAH356GPrxlPSpwyWhFV3fIQJKI2Hf/8/mAd3wqCGQQzq5Mt\ncM0B36+vP9wOGsTI/tc32+0glRZoxo8=\n-----END EC PRIVATE KEY-----\n",
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\nMIIBiTCCAQ+gAwIBAgIQZLa8QUXWDnuht3G9+g9yKTAKBggqhkjOPQQDAzALMQkw\nBwYDVQQKEwAwHhcNMTkwNzIyMjE1MzAzWhcNMjIwNzIxMjE1MzAzWjAcMQkwBwYD\nVQQKEwAxDzANBgNVBAMTBm1vbG9jaDB2MBAGByqGSM49AgEGBSuBBAAiA2IABMcB\nOHt7y5f8w3maiSC5B/jLBeBNqKFgP//KtoRRxkBiysK0MeZ0AffnoY+vGU9KnDJa\nEVXd8hAkojYd//z+YB3fCoIZBDOrky1wzQHfr68/3A4axMj+1zfb7SCVFmjGj6Mn\nMCUwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAoGCCqGSM49\nBAMDA2gAMGUCMH0hbeghZfMkhp9Wf2a7eqj8IruwKH6vQql/6nxQ8aunfCUUxWo8\nOrxxaqp+bGCCCQIxAK5Icqww8m9llhprfzENaFZUCkpyvBMm9EB8B69SBFBHcVFm\n7bqHjFFMH39g2JCy9Q==\n-----END CERTIFICATE-----\n"
|
||||
"ca_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
|
||||
"private_key": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n",
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `operator` field in this configuration file only modifies the value displayed by the client when selecting configurations, modifying it will not change what name the server uses to identify the user. The server determines the user based on the `CommonName` in the certificate. If you want to change the user name you need to regenerate the client configuration with the new name.
|
||||
**Note:** The `operator` field is mostly a local label in the client. Server-side identity is derived from the client certificate (`CommonName`) and token material generated by the server. If you need to rename an operator, generate a new client configuration.
|
||||
@@ -1,14 +1,14 @@
|
||||
**NOTE:** Any platform can cross-compile a standalone executable to any other platform out of the box; you primarily need cross-compilers when using CGO-based formats like `--format shared`, and for `--format shellcode` on macOS/Linux.
|
||||
Any platform can cross-compile a standalone executable to any other platform out of the box; you primarily need cross-compilers when using CGO-based formats like `--format shared`, and for `--format shellcode` on macOS/Linux.
|
||||
|
||||
Sliver can tell you which platforms it can likely target based on the server's platform and available cross-compilers by running the `generate info` command in the console.
|
||||
|
||||
Sliver supports [External Builders](/docs?name=External+Builders), which can be used to easily cross-compile implants.
|
||||
|
||||
## From Linux to MacOS/Windows
|
||||
## From Linux to MacOS
|
||||
|
||||
Sliver embeds a Zig cross-compiler out of the box, so Windows shared library and shellcode implants from Linux do not require mingw-w64.
|
||||
|
||||
To compile MacOS shared library implants from Linux, we recommend using https://github.com/tpoechtrager/osxcross by default Sliver will look in `/opt/osxcross` but you can override this via [environment variables](/docs?name=Environment+Variables). If you do not have a MacOS-based machine you can use GitHub Actions' MacOS instances to build OSXCross.
|
||||
To compile MacOS shared library or shellcode payloads from Linux, we recommend using [OSX Cross](https://github.com/tpoechtrager/osxcross) by default Sliver will look in `/opt/osxcross` but you can override this via [environment variables](/docs?name=Environment+Variables). If you do not have a MacOS-based machine you can use GitHub Actions' MacOS instances to build OSXCross.
|
||||
|
||||
**NOTE:** Sliver expects the root of the osxcross git repo to be located at `/opt/osxcross` and the actual binaries in `/opt/osxcross/target/bin`.
|
||||
|
||||
|
||||
@@ -1,34 +1,53 @@
|
||||
Sliver supports running in "daemon mode," which automatically starts a client listener (but not an interactive console). In order to connect to a server running in daemon mode you'll need to use [multiplayer mode](/docs?name=Multi-player+Mode).
|
||||
Sliver supports running in daemon mode, which automatically starts the multiplayer client listener (but not an interactive console). To connect to a daemonized server, use [multiplayer mode](/docs?name=Multi-player+Mode) operator configs.
|
||||
|
||||
There are two ways to start the server in daemon mode:
|
||||
There are two ways to run in daemon mode:
|
||||
|
||||
1. Using the command line interface: `sliver-server daemon`
|
||||
2. Set `daemon_mode` to `true` in the `configs/server.json` located in `SLIVER_ROOT_DIR` (by default: `~/.sliver/configs/server.json`). The listener can be configured via the `daemon` object. The process will respond gracefully to `SIGTERM` on Unix-like operating systems.
|
||||
1. Start daemon directly from CLI: `sliver-server daemon`
|
||||
2. Set `daemon_mode: true` in `~/.sliver/configs/server.yaml`, then start normally with `sliver-server`
|
||||
|
||||
#### Example Config
|
||||
If `server.yaml` does not exist, Sliver generates it. If a legacy `server.json` exists, Sliver migrates it to YAML.
|
||||
|
||||
```
|
||||
$ cat ~/.sliver/configs/server.json
|
||||
{
|
||||
"daemon_mode": true,
|
||||
"daemon": {
|
||||
"host": "",
|
||||
"port": 31337
|
||||
},
|
||||
"logs": {
|
||||
"level": 5,
|
||||
"grpc_unary_payloads": false,
|
||||
"grpc_stream_payloads": false
|
||||
},
|
||||
"grpc": {
|
||||
"keepalive": {
|
||||
"min_time_seconds": 30,
|
||||
"permit_without_stream": true
|
||||
}
|
||||
}
|
||||
}
|
||||
### CLI Behavior
|
||||
|
||||
- `sliver-server daemon` supports:
|
||||
- `-l, --lhost` multiplayer listener host
|
||||
- `-p, --lport` multiplayer listener port
|
||||
- `-t, --tailscale` enable Tailscale listener
|
||||
- `-f, --force` force unpack static assets
|
||||
- For `sliver-server daemon`, `--lhost` and `--lport` override config values. If omitted, Sliver uses `daemon.host` and `daemon.port` from `server.yaml`.
|
||||
- For normal startup (`sliver-server`) with `daemon_mode: true`, Sliver uses `daemon.tailscale` from `server.yaml`.
|
||||
|
||||
### Example Config
|
||||
|
||||
```yaml
|
||||
daemon_mode: true
|
||||
daemon:
|
||||
host: ""
|
||||
port: 31337
|
||||
tailscale: false
|
||||
logs:
|
||||
level: 4
|
||||
grpc_unary_payloads: false
|
||||
grpc_stream_payloads: false
|
||||
tls_key_logger: false
|
||||
grpc:
|
||||
keepalive:
|
||||
min_time_seconds: 30
|
||||
permit_without_stream: true
|
||||
```
|
||||
|
||||
#### systemd
|
||||
### Generating Operator Configs Without Console
|
||||
|
||||
With this config you can easily set up a [systemd service](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) or init script. See the [Linux install script](/docs?name=Linux+Install+Script) for an example.
|
||||
Since daemon mode does not provide an interactive server console, generate operator configs using the server CLI:
|
||||
|
||||
```bash
|
||||
./sliver-server operator --name zer0cool --lhost 1.2.3.4 --permissions all --save zer0cool.cfg
|
||||
```
|
||||
|
||||
### Shutdown Behavior
|
||||
|
||||
On Unix-like systems, daemon mode handles `SIGTERM` and exits cleanly.
|
||||
|
||||
### systemd
|
||||
|
||||
With daemon mode enabled in `server.yaml`, you can run Sliver under [systemd](https://www.linode.com/docs/quick-answers/linux/start-service-at-boot/) or another init system. See the [Linux install script](/docs?name=Linux+Install+Script) for an example.
|
||||
@@ -0,0 +1,179 @@
|
||||
Executable metadata spoofing lets you post-process a generated build and copy metadata/resource details from a donor executable.
|
||||
|
||||
## Overview
|
||||
|
||||
The feature is enabled with the `--spoof-metadata` flag on generate commands.
|
||||
|
||||
- Current format support: `pe` (Windows PE)
|
||||
- Config file: `<client app dir>/spoof-metadata.yaml`
|
||||
- Default client app dir: `~/.sliver-client`
|
||||
|
||||
On first client startup, Sliver creates `spoof-metadata.yaml` if it does not exist.
|
||||
|
||||
## Flag Usage
|
||||
|
||||
`--spoof-metadata` is off by default.
|
||||
|
||||
- No flag: metadata spoofing is not applied.
|
||||
- `--spoof-metadata` (no argument): use `<client app dir>/spoof-metadata.yaml`.
|
||||
- `--spoof-metadata /path/to/donor.exe`: use the provided donor file directly.
|
||||
|
||||
Path mode validation:
|
||||
|
||||
- Currently only supported for Windows PE targets.
|
||||
- Target output format must be executable, service, or shared library.
|
||||
- Donor PE machine type must match target architecture (`386`, `amd64`, `arm64`).
|
||||
- For shared library targets, donor must be a DLL; for executable/service targets, donor must not be a DLL.
|
||||
|
||||
When path mode is used, Sliver only uses the donor file bytes as `pe.source` and does not read override fields from `spoof-metadata.yaml`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Start the client once to generate `~/.sliver-client/spoof-metadata.yaml`.
|
||||
2. Edit `pe.source.path` to point to a donor PE file.
|
||||
3. Generate with `--spoof-metadata`.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
sliver > generate --os windows --arch amd64 --format exe --mtls 10.0.0.5 --spoof-metadata -N win-agent
|
||||
```
|
||||
|
||||
Direct donor path example:
|
||||
|
||||
```bash
|
||||
sliver > generate --os windows --arch amd64 --format exe --mtls 10.0.0.5 --spoof-metadata /opt/samples/notepad.exe -N win-agent
|
||||
```
|
||||
|
||||
## Config Format
|
||||
|
||||
Top-level config:
|
||||
|
||||
```yaml
|
||||
pe:
|
||||
source:
|
||||
name: metadata-source.exe
|
||||
path: /absolute/path/to/donor.exe
|
||||
```
|
||||
|
||||
`pe.source` is required when using `--spoof-metadata`.
|
||||
|
||||
Fields that can carry binary content (such as `source` and `icon`) support:
|
||||
|
||||
- `name`: logical filename sent in protobuf metadata
|
||||
- `path`: local file path read by the client
|
||||
- `base64`: inline base64 bytes
|
||||
|
||||
`name` and `path` are not duplicates:
|
||||
|
||||
- `path` controls where bytes are loaded from on the client machine.
|
||||
- `name` is the filename value sent to the server.
|
||||
|
||||
If `path` is set and `name` is omitted, Sliver uses `basename(path)` automatically.
|
||||
|
||||
Set only one of `path` or `base64` for each object.
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal Path-Based Config
|
||||
|
||||
```yaml
|
||||
pe:
|
||||
source:
|
||||
name: metadata-source.exe
|
||||
path: /opt/samples/notepad.exe
|
||||
```
|
||||
|
||||
### Include Icon Donor Bytes
|
||||
|
||||
```yaml
|
||||
pe:
|
||||
source:
|
||||
name: metadata-source.exe
|
||||
path: /opt/samples/notepad.exe
|
||||
icon:
|
||||
name: icon.ico
|
||||
path: /opt/samples/app.ico
|
||||
```
|
||||
|
||||
### Inline Base64 Config
|
||||
|
||||
```yaml
|
||||
pe:
|
||||
source:
|
||||
name: metadata-source.exe
|
||||
base64: TVqQAAMAAAAEAAAA...
|
||||
icon:
|
||||
name: icon.ico
|
||||
base64: AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAA...
|
||||
```
|
||||
|
||||
### Advanced Optional PE Structure Fields
|
||||
|
||||
These fields are optional and act as PE metadata overrides:
|
||||
|
||||
```yaml
|
||||
pe:
|
||||
source:
|
||||
name: metadata-source.exe
|
||||
path: /opt/samples/notepad.exe
|
||||
|
||||
resource_directory:
|
||||
major_version: 1
|
||||
minor_version: 0
|
||||
time_date_stamp: 1712345678
|
||||
number_of_named_entries: 0
|
||||
number_of_id_entries: 0
|
||||
|
||||
resource_directory_entries:
|
||||
- name: 1
|
||||
offset_to_data: 0
|
||||
|
||||
resource_data_entries:
|
||||
- offset_to_data: 0
|
||||
size: 0
|
||||
code_page: 0
|
||||
reserved: 0
|
||||
|
||||
export_directory:
|
||||
major_version: 1
|
||||
minor_version: 0
|
||||
time_date_stamp: 1712345678
|
||||
number_of_functions: 0
|
||||
number_of_names: 0
|
||||
```
|
||||
|
||||
## Override Behavior
|
||||
|
||||
Override order for `pe`:
|
||||
|
||||
1. Sliver clones metadata/resources from `pe.source`.
|
||||
2. Sliver applies explicit override fields from your config.
|
||||
|
||||
This means:
|
||||
|
||||
- If an override field is set, it replaces the cloned value.
|
||||
- If an override field is omitted, the cloned donor value is kept.
|
||||
|
||||
Resource override details:
|
||||
|
||||
- `resource_directory` overrides the root `IMAGE_RESOURCE_DIRECTORY` fields.
|
||||
- `resource_directory_entries` overrides the first N root directory entries in order.
|
||||
- `resource_data_entries` overrides the first N resource data entries discovered in resource-tree order.
|
||||
|
||||
Export override details:
|
||||
|
||||
- `export_directory` overrides fields in the PE export directory when that directory exists.
|
||||
- If a PE has no export directory, this override has no effect.
|
||||
|
||||
Important:
|
||||
|
||||
- In protobuf/YAML, scalar zero values are real override values when the parent block is present.
|
||||
- When setting partial `resource_directory` values, include `number_of_named_entries` and `number_of_id_entries` from the original header unless you intentionally want to change them.
|
||||
|
||||
## Notes
|
||||
|
||||
- The client reads local file paths and sends bytes to the server over gRPC.
|
||||
- Spoofing is applied only after a build completes successfully.
|
||||
- The server applies spoofing to the selected existing build artifact.
|
||||
- PE support is implemented first; the config is structured for future executable formats.
|
||||
@@ -2,22 +2,13 @@ Sliver supports "external builders," which allow a Sliver server to offload impl
|
||||
|
||||
External builders can also be used to create custom modifications to the implant source code, or potentially replace the default Sliver implant entirely.
|
||||
|
||||
```
|
||||
MacOS .dylib Implant Builds
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────────┐ ┌─────┴─────┐
|
||||
│ MacOS │ Multiplayer │ Linux │
|
||||
│ Builder ├────────────────────────►│ Server │
|
||||
│ │ │ │
|
||||
└───────────┘ └───────────┘
|
||||
▲
|
||||
┌───────────┐ │
|
||||
│Windows │ Multiplayer │
|
||||
│ Operator ├─────────────────────────────┘
|
||||
│ │
|
||||
└───────────┘
|
||||
```mermaid:minh=340
|
||||
flowchart LR
|
||||
subgraph "External MacOS Builder"
|
||||
builder["MacOS Builder"] -- "Multiplayer" --> server["Linux Server"]
|
||||
operator["Windows Operator"] -- "Multiplayer" --> server
|
||||
server -- ".dylib build task" --> builder
|
||||
end
|
||||
```
|
||||
|
||||
## Builders
|
||||
@@ -32,8 +23,6 @@ Any `sliver-server` binary can be started as a builder process using [operator c
|
||||
|
||||
When started as a builder, the Sliver process will mirror log output to stdout by default, however this can be disabled (see `sliver-server builder --help`).
|
||||
|
||||
**⚠️ IMPORTANT:** Make sure the builder and server have identical `http-c2.json` configuration files to avoid incompatibility problems.
|
||||
|
||||
**⚠️ IMPORTANT:** Builders must have unique names, by default the builder's hostname will be used, but this can be changed using the `--name` cli flag.
|
||||
|
||||
#### External Builds
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
This page summarizes implant payload format compatibility by target OS/architecture (`GOOS/GOARCH`).
|
||||
|
||||
## Common Platforms
|
||||
|
||||
| Target OS/Arch | Executable | Shared Library | Shellcode | Service |
|
||||
| --------------- | ---------- | -------------- | --------- | ------- |
|
||||
| `windows/386` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `windows/amd64` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `windows/arm64` | ⚠️ | ⚠️ | ❌ | ⚠️ |
|
||||
| `linux/386` | ✅ | ✅ | ❌ | N/A |
|
||||
| `linux/amd64` | ✅ | ✅ | ✅ | N/A |
|
||||
| `linux/arm64` | ✅ | ✅ | ✅ | N/A |
|
||||
| `darwin/amd64` | ✅ | ✅ | ❌ | N/A |
|
||||
| `darwin/arm64` | ✅ | ✅ | ✅ | N/A |
|
||||
|
||||
### Important Notes
|
||||
|
||||
- `✅` = first-class support in Sliver's built-in target matrix.
|
||||
- `⚠️` = generic/experimental target (not first-class; may fail depending on toolchain/target).
|
||||
- `❌` = not currently supported for that payload format.
|
||||
- `Service` is a Windows-only format.
|
||||
- `Shellcode` is currently supported for `windows/{386,amd64}`, `linux/{amd64,arm64}`, and `darwin/arm64`.
|
||||
- `Shellcode` and `Shared Library` for MacOS may require a [cross-compiler](/docs?name=Cross-compiling+Implants) or an [external builder](/docs?name=External+Builders) depending on your platform.
|
||||
- On macOS hosts, targeting `linux/386` for shared library/shellcode builds is currently not reliable.
|
||||
|
||||
Use `generate info` in the Sliver console to see what your current server can build with its configured toolchains.
|
||||
|
||||
## All GOOS/GOARCH Targets
|
||||
|
||||
The table below covers all `GOOS/GOARCH` entries from `go tool dist list` (Go 1.24+), including common platforms:
|
||||
|
||||
| Target GOOS/GOARCH | Common Platform | Executable | Shared Library | Shellcode | Service |
|
||||
| ------------------ | --------------- | ---------- | -------------- | --------- | ------- |
|
||||
| `aix/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `android/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `android/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `android/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `android/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `darwin/amd64` | Yes | ✅ | ✅ | ❌ | N/A |
|
||||
| `darwin/arm64` | Yes | ✅ | ✅ | ✅ | N/A |
|
||||
| `dragonfly/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `freebsd/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `illumos/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `ios/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `ios/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `js/wasm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/386` | Yes | ✅ | ✅ | ❌ | N/A |
|
||||
| `linux/amd64` | Yes | ✅ | ✅ | ✅ | N/A |
|
||||
| `linux/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/arm64` | Yes | ✅ | ✅ | ✅ | N/A |
|
||||
| `linux/loong64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/mips` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/mips64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/mips64le` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/mipsle` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/ppc64le` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `linux/s390x` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `netbsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `netbsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `netbsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `netbsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/arm64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/ppc64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `openbsd/riscv64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `plan9/386` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `plan9/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `plan9/arm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `solaris/amd64` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `wasip1/wasm` | No | ⚠️ | ❌ | ❌ | N/A |
|
||||
| `windows/386` | Yes | ✅ | ✅ | ✅ | ✅ |
|
||||
| `windows/amd64` | Yes | ✅ | ✅ | ✅ | ✅ |
|
||||
| `windows/arm64` | Yes | ⚠️ | ⚠️ | ❌ | ⚠️ |
|
||||
@@ -0,0 +1,124 @@
|
||||
This page documents the current implant C2 WireGuard transport implementation used between `server/` and `implant/` code paths.
|
||||
|
||||
**Scope:** This is about implant callbacks over the `wg` listener, not operator multiplayer mode.
|
||||
|
||||
## Listener and Tunnel Setup
|
||||
|
||||
Server-side setup is implemented in `server/c2/wireguard.go` (`StartWGListener`):
|
||||
|
||||
1. Creates a netstack TUN with server-side tunnel IP `100.64.0.1`.
|
||||
1. Loads or generates server WireGuard keys.
|
||||
1. Configures a WireGuard device with known peer keys and `/32` allowed IPs.
|
||||
1. Starts two TCP listeners inside the WireGuard tunnel:
|
||||
- key exchange port (default `1337`)
|
||||
- C2 transport port (default `8888`)
|
||||
1. Exposes the WireGuard UDP listener (default `53`).
|
||||
|
||||
Defaults come from `server/rpc/rpc-jobs.go`:
|
||||
|
||||
- WireGuard UDP listener: `53`
|
||||
- In-tunnel C2 TCP port (`NPort`): `8888`
|
||||
- In-tunnel key exchange TCP port (`KeyPort`): `1337`
|
||||
|
||||
## Implant Connection Sequence
|
||||
|
||||
Implant-side transport is implemented in `implant/sliver/transports/wireguard/wireguard.go`:
|
||||
|
||||
1. The implant is generated with embedded WireGuard values:
|
||||
- initial implant private key
|
||||
- server public key
|
||||
- peer TUN IP
|
||||
- key-exchange and C2-in-tunnel ports
|
||||
1. Implant brings up an initial WireGuard interface using those embedded values.
|
||||
1. Implant dials the server key-exchange endpoint at `100.64.0.1:<key-exchange-port>`.
|
||||
1. Server returns session WireGuard material and a session tunnel IP.
|
||||
1. Implant tears down the initial WireGuard interface.
|
||||
1. Implant brings up a new WireGuard interface using the session values.
|
||||
1. Implant dials `100.64.0.1:<tcp-comms-port>`.
|
||||
1. Implant sends `MUX/1` yamux preface and starts yamux.
|
||||
|
||||
After repeated failures, the implant will refresh session keys by running key exchange again.
|
||||
|
||||
## Key Exchange Payload Format
|
||||
|
||||
The key-exchange response payload is a fixed 146-byte message:
|
||||
|
||||
```mermaid:minh=444
|
||||
%%{init: {'packet': {'bitsPerRow': 40, 'bitWidth': 24, 'rowHeight': 72}}}%%
|
||||
packet-beta
|
||||
title WireGuard Session Key Exchange Payload (146 bytes)
|
||||
0-63: "Sess PrivKey (hex)"
|
||||
64: "|"
|
||||
65-128: "Server PubKey (hex)"
|
||||
129: "|"
|
||||
130-145: "Sess TUN IP (16B)"
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `Sess PrivKey (hex)` is the new implant WireGuard private key (64 hex chars).
|
||||
- `Server PubKey (hex)` is the server WireGuard public key (64 hex chars).
|
||||
- `Sess TUN IP (16B)` is raw `net.IP` bytes (not dotted string encoding).
|
||||
|
||||
## C2 Envelope Wire Format (Per Yamux Stream)
|
||||
|
||||
Each yamux stream carries a single framed envelope:
|
||||
|
||||
`[raw minisign signature | uint32 little-endian payload length | protobuf envelope payload]`
|
||||
|
||||
```mermaid:minh=444
|
||||
%%{init: {'packet': {'bitsPerRow': 32, 'bitWidth': 30, 'rowHeight': 72}}}%%
|
||||
packet-beta
|
||||
title WireGuard C2 Envelope Frame (byte offsets, per yamux stream)
|
||||
0-1: "Alg"
|
||||
2-9: "Key ID"
|
||||
10-73: "Ed25519 Signature"
|
||||
74-77: "Len"
|
||||
78-109: "Payload (protobuf sliverpb.Envelope, continues...)"
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Diagram offsets are bytes (not bits) for readability.
|
||||
- `Alg` = LE `uint16` (`0x6445`, EdDSA).
|
||||
- `Key ID` = LE `uint64`.
|
||||
- `Len` = LE `uint32` protobuf payload length.
|
||||
- Raw signature size is fixed at 74 bytes: `2 + 8 + 64`.
|
||||
- Length is payload length only (it does not include signature bytes).
|
||||
|
||||
## Envelope Payload
|
||||
|
||||
The payload is `sliverpb.Envelope` from `protobuf/sliverpb/sliver.proto`:
|
||||
|
||||
- `ID` (`int64`) request/response correlation
|
||||
- `Type` (`uint32`) message type
|
||||
- `Data` (`bytes`) message body
|
||||
- `UnknownMessageType` (`bool`) set by implants for unsupported message types
|
||||
|
||||
## Message Authentication Details
|
||||
|
||||
Like mTLS, WireGuard C2 signs each envelope payload in addition to transport-layer encryption.
|
||||
|
||||
### Implant -> Server
|
||||
|
||||
- Implant signs marshaled envelope payloads in `implant/sliver/transports/wireguard/wireguard.go` (`WriteEnvelope`).
|
||||
- Signing key derivation is deterministic from implant `PeerPrivateKey`:
|
||||
- seed = `sha256("env-signing-v1:" + peerPrivateKey)`
|
||||
- ed25519 keypair from that seed
|
||||
- key ID = first 8 bytes of `blake2b(pubKey)` as LE `uint64`
|
||||
- Server verifies in `server/c2/wireguard.go` (`socketWGReadEnvelope`) by:
|
||||
- checking `EdDSA` algorithm
|
||||
- resolving key ID to an implant signing public key
|
||||
- verifying signature with ed25519
|
||||
|
||||
### Server -> Implant
|
||||
|
||||
- Server signs payloads in `server/c2/wireguard.go` (`socketWGWriteEnvelope`) using server minisign private key.
|
||||
- Implant verifies in `implant/sliver/transports/wireguard/wireguard.go` (`ReadEnvelope`) using embedded server minisign public key (`MinisignVerifyRaw`).
|
||||
|
||||
## Runtime Behavior
|
||||
|
||||
- Server rejects legacy WireGuard C2 connections that do not send the `MUX/1` preface.
|
||||
- Session mode sends in-band pings every 2 minutes (`PingInterval`) using `MsgPing`.
|
||||
- Beacon mode and session mode both use one envelope per yamux stream.
|
||||
- Server enforces max inbound payload size of `(2 * 1024 * 1024 * 1024) - 1` bytes.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import Youtube from "@/components/youtube";
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Accordion, AccordionItem, Button, Card, CardFooter } from "@heroui/react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import React from "react";
|
||||
|
||||
type Talk = {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type TalkSection = {
|
||||
key: "workshops" | "general-tradecraft" | "community";
|
||||
title: "Workshops" | "General Tradecraft" | "Community";
|
||||
description: string;
|
||||
talks: Talk[];
|
||||
};
|
||||
|
||||
const workshopTalks: Talk[] = [
|
||||
{
|
||||
title:"Getting Started with Sliver v1.6",
|
||||
description: "Introductory workshop covering basics and new features.",
|
||||
url: "https://www.youtube.com/watch?v=IOiyXYp1lDc",
|
||||
},
|
||||
{
|
||||
title: "Building Traffic Encoders",
|
||||
description: "From concept to practical encoder strategy in Sliver.",
|
||||
url: "https://www.youtube.com/watch?v=6unwFhurm-E",
|
||||
},
|
||||
{
|
||||
title: "Sliver Staging and Automation",
|
||||
description: "Workflow patterns for payload staging and repeatable ops.",
|
||||
url: "https://www.youtube.com/watch?v=vuQ5tG5kelI&feature=youtu.be",
|
||||
},
|
||||
];
|
||||
|
||||
const generalTradecraftTalks: Talk[] = [
|
||||
{
|
||||
title: "Offensive WASM",
|
||||
description: "Applying WebAssembly techniques in offensive tradecraft.",
|
||||
url: "https://www.youtube.com/watch?v=RnSLsnP4imQ",
|
||||
},
|
||||
{
|
||||
title: "The Sliver C2 Framework",
|
||||
description: "General discussion of C2 design.",
|
||||
url: "https://www.youtube.com/watch?v=tkjMZuZ_8nw",
|
||||
}
|
||||
];
|
||||
|
||||
const communityTalks: Talk[] = [
|
||||
{
|
||||
title: "Community Guide Video 1",
|
||||
description: "Linked from Community Guides.",
|
||||
url: "https://youtu.be/3R6WKUgN0K4?t=456",
|
||||
},
|
||||
{
|
||||
title: "Community Guide Video 2",
|
||||
description: "Linked from Community Guides.",
|
||||
url: "https://www.youtube.com/watch?v=QO_1UMaiWHk",
|
||||
},
|
||||
{
|
||||
title: "Community Guide Video 3",
|
||||
description: "Linked from Community Guides.",
|
||||
url: "https://www.youtube.com/watch?v=izMMmOaLn9g",
|
||||
},
|
||||
{
|
||||
title: "Community Guide Video 4",
|
||||
description: "Linked from Community Guides.",
|
||||
url: "https://www.youtube.com/watch?v=qIbrozlf2wM",
|
||||
},
|
||||
{
|
||||
title: "Community Guide Video 5",
|
||||
description: "Linked from Community Guides.",
|
||||
url: "https://www.youtube.com/watch?v=CKfjLnEMfvI",
|
||||
},
|
||||
];
|
||||
|
||||
const talkSections: TalkSection[] = [
|
||||
{
|
||||
key: "workshops",
|
||||
title: "Workshops",
|
||||
description: "Hands-on workshop recordings focused on Sliver workflows.",
|
||||
talks: workshopTalks,
|
||||
},
|
||||
{
|
||||
key: "general-tradecraft",
|
||||
title: "General Tradecraft",
|
||||
description: "Broader offensive engineering and tradecraft talks.",
|
||||
talks: generalTradecraftTalks,
|
||||
},
|
||||
{
|
||||
key: "community",
|
||||
title: "Community",
|
||||
description: "Community-created videos listed in Community Guides.",
|
||||
talks: communityTalks,
|
||||
},
|
||||
];
|
||||
|
||||
const TalksIndexPage: NextPage = () => {
|
||||
const [footerHidden, setFooterHidden] = React.useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const hideFooterFor = React.useCallback((url: string) => {
|
||||
setFooterHidden((prev) => (prev[url] ? prev : { ...prev, [url]: true }));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver Talks</title>
|
||||
</Head>
|
||||
<div className="mt-6 px-4 pb-8 sm:px-6 lg:px-12">
|
||||
<Accordion
|
||||
selectionMode="multiple"
|
||||
defaultExpandedKeys={["workshops", "general-tradecraft"]}
|
||||
showDivider={false}
|
||||
className="px-0"
|
||||
>
|
||||
{talkSections.map((section) => (
|
||||
<AccordionItem
|
||||
key={section.key}
|
||||
aria-label={section.title}
|
||||
title={section.title}
|
||||
subtitle={section.description}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-9">
|
||||
{section.talks.map((talk) => (
|
||||
<div key={talk.url} className="sm:col-span-1 lg:col-span-3">
|
||||
<Card isFooterBlurred className="relative z-0 overflow-hidden">
|
||||
<Youtube
|
||||
className="w-full"
|
||||
url={talk.url}
|
||||
title={talk.title}
|
||||
onPlay={() => hideFooterFor(talk.url)}
|
||||
/>
|
||||
|
||||
{footerHidden[talk.url] ? null : (
|
||||
<CardFooter className="absolute bottom-0 z-10 bg-black/40 border-t-1 border-default-600 dark:border-default-100">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<p className="text-xs text-white/80">{talk.description}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="warning"
|
||||
size="sm"
|
||||
className="ml-auto"
|
||||
onPress={() => {
|
||||
window.open(talk.url, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
>
|
||||
Watch <FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TalksIndexPage;
|
||||
@@ -1,4 +1,5 @@
|
||||
import MarkdownViewer from "@/components/markdown";
|
||||
import LoadingState from "@/components/loading-state";
|
||||
import { Tutorials } from "@/util/tutorials";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchTutorials as fetchTutorialsContent } from "@/util/content-fetchers";
|
||||
@@ -34,14 +35,17 @@ const TutorialsIndexPage: NextPage = () => {
|
||||
});
|
||||
|
||||
const params = useSearchParams();
|
||||
const [name, setName] = React.useState("");
|
||||
const [markdown, setMarkdown] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
const _name = params.get("name");
|
||||
setName(_name || "");
|
||||
setMarkdown(tutorials?.tutorials.find((tutorial) => tutorial.name === _name)?.content || "");
|
||||
}, [params, tutorials]);
|
||||
const name = params.get("name") || "";
|
||||
const markdown = React.useMemo(() => {
|
||||
return tutorials?.tutorials.find((tutorial) => tutorial.name === name)?.content || "";
|
||||
}, [tutorials, name]);
|
||||
const hasNameQueryInPath = React.useMemo(() => {
|
||||
const query = router.asPath.split("?")[1];
|
||||
if (!query) {
|
||||
return false;
|
||||
}
|
||||
return new URLSearchParams(query).has("name");
|
||||
}, [router.asPath]);
|
||||
|
||||
const [filterValue, setFilterValue] = React.useState("");
|
||||
const fuse = React.useMemo(() => {
|
||||
@@ -79,8 +83,8 @@ const TutorialsIndexPage: NextPage = () => {
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
if (isLoading || !tutorials) {
|
||||
return <div>Loading...</div>;
|
||||
if (isLoading || !tutorials || (hasNameQueryInPath && !name)) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -127,8 +131,9 @@ const TutorialsIndexPage: NextPage = () => {
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
|
||||
<aside className="hidden lg:block lg:col-span-3">
|
||||
<div className="sticky top-24 ml-4 flex flex-col gap-3">
|
||||
<div className="sticky top-16 ml-4 flex flex-col gap-3">
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="Filter..."
|
||||
startContent={<FontAwesomeIcon icon={faSearch} />}
|
||||
value={filterValue}
|
||||
@@ -170,7 +175,7 @@ const TutorialsIndexPage: NextPage = () => {
|
||||
<Divider />
|
||||
<CardBody>
|
||||
<MarkdownViewer
|
||||
key={name || `${Math.random()}`}
|
||||
key={name}
|
||||
markdown={markdown || ""}
|
||||
/>
|
||||
</CardBody>
|
||||
|
||||
@@ -7,6 +7,37 @@
|
||||
Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
/* Keep layout width stable when page content toggles vertical scrolling. */
|
||||
html,
|
||||
body {
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
/*
|
||||
Keep html as the single scrolling element. Setting overflow on both html
|
||||
and body breaks sticky sidebar behavior on docs/tutorial pages.
|
||||
*/
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* Fallback for engines that do not support scrollbar-gutter. */
|
||||
@supports not (scrollbar-gutter: stable) {
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Desktop sidebar pages are sensitive to viewport-width changes when the page
|
||||
scrollbar appears/disappears. Keep the page scrollbar track persistent.
|
||||
*/
|
||||
@media (min-width: 1024px) {
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
code,
|
||||
pre {
|
||||
font-family: var(--font-mono);
|
||||
@@ -19,6 +50,7 @@ pre {
|
||||
.sliver-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.7) transparent;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.sliver-scrollbar::-webkit-scrollbar {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -11,10 +15,12 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
@@ -25,5 +31,7 @@
|
||||
"prebuild/generate-tutorials.js",
|
||||
"util/__generated__/prebuild-version.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
+202
-206
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -1351,6 +1351,12 @@ func reconfigureHandler(data []byte, resp RPCResponse) {
|
||||
if reconfigReq.BeaconJitter != 0 {
|
||||
transports.SetJitter(reconfigReq.BeaconJitter)
|
||||
}
|
||||
if reconfigReq.C2URI != "" {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[*] Received new C2 URI: %s", reconfigReq.C2URI)
|
||||
// {{end}}
|
||||
transports.SetC2URI(reconfigReq.C2URI)
|
||||
}
|
||||
// {{end}}
|
||||
|
||||
reconfigResp := &sliverpb.Reconfigure{}
|
||||
|
||||
@@ -125,6 +125,8 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
Type: sliverpb.MsgTunnelClose,
|
||||
Data: tunnelClose,
|
||||
}
|
||||
|
||||
systemShell.Wait()
|
||||
}
|
||||
|
||||
for _, rc := range tunnel.Readers {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -152,6 +152,13 @@ func beaconStartup() {
|
||||
log.Printf("Next beacon = %v", beacon)
|
||||
// {{end}}
|
||||
if beacon != nil {
|
||||
// skip stale beacons when c2-uri has changed
|
||||
if c2 := transports.GetC2URI(); c2 != "" && c2 != beacon.ActiveC2 {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[beacon] skipping stale beacon for %s, c2-uri is %s", beacon.ActiveC2, c2)
|
||||
// {{end}}
|
||||
continue
|
||||
}
|
||||
err := beaconMainLoop(beacon)
|
||||
if err != nil {
|
||||
connectionErrors++
|
||||
@@ -160,6 +167,10 @@ func beaconStartup() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// skip 1m reconnect sleep if c2-uri just changed
|
||||
if c2 := transports.GetC2URI(); c2 != "" && (beacon == nil || c2 != beacon.ActiveC2) {
|
||||
continue
|
||||
}
|
||||
reconnect := transports.GetReconnectInterval()
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("Reconnect sleep: %s", reconnect)
|
||||
@@ -287,6 +298,14 @@ func beaconMainLoop(beacon *transports.Beacon) error {
|
||||
case <-shortCircuit:
|
||||
// Short circuit current duration with no error
|
||||
}
|
||||
|
||||
// check if reconfig used to set a new C2-URI
|
||||
if c2 := transports.GetC2URI(); c2 != "" && c2 != beacon.ActiveC2 {
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[beacon] C2 URI changed to %s, reconnecting...", c2)
|
||||
// {{end}}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -36,6 +36,18 @@ const (
|
||||
strategySequential = "s"
|
||||
)
|
||||
|
||||
var (
|
||||
dynamicC2URI = ""
|
||||
)
|
||||
|
||||
func SetC2URI(uri string) {
|
||||
dynamicC2URI = uri
|
||||
}
|
||||
|
||||
func GetC2URI() string {
|
||||
return dynamicC2URI
|
||||
}
|
||||
|
||||
// C2Generator - Creates a stream of C2 URLs based on a connection strategy
|
||||
func C2Generator(abort <-chan struct{}, temporaryC2 ...string) <-chan *url.URL {
|
||||
// {{if .Config.Debug}}
|
||||
@@ -78,6 +90,12 @@ func C2Generator(abort <-chan struct{}, temporaryC2 ...string) <-chan *url.URL {
|
||||
default:
|
||||
next = c2Servers[c2Counter%uint(len(c2Servers))]()
|
||||
}
|
||||
|
||||
// check if reconfig used to set a new C2-URI
|
||||
if dynamic := GetC2URI(); dynamic != "" {
|
||||
next = dynamic
|
||||
}
|
||||
|
||||
c2Counter++
|
||||
if ^uint(0) < c2Counter {
|
||||
panic("counter overflow")
|
||||
|
||||
+3134
-2280
File diff suppressed because it is too large.
Load diff
@@ -215,10 +215,75 @@ message ImplantConfig {
|
||||
bool TrafficEncodersEnabled = 152;
|
||||
repeated string TrafficEncoders = 153;
|
||||
string Extension = 154;
|
||||
reserved 155;
|
||||
reserved "SpoofData";
|
||||
|
||||
repeated commonpb.File Assets = 200;
|
||||
}
|
||||
|
||||
message SpoofMetadataFile {
|
||||
string Name = 1;
|
||||
bytes Data = 2;
|
||||
}
|
||||
|
||||
message IMAGE_RESOURCE_DIRECTORY {
|
||||
uint32 Characteristics = 1;
|
||||
uint32 TimeDateStamp = 2;
|
||||
// uint16 in PE headers; encoded as uint32 for protobuf transport.
|
||||
uint32 MajorVersion = 3;
|
||||
// uint16 in PE headers; encoded as uint32 for protobuf transport.
|
||||
uint32 MinorVersion = 4;
|
||||
// uint16 in PE headers; encoded as uint32 for protobuf transport.
|
||||
uint32 NumberOfNamedEntries = 5;
|
||||
// uint16 in PE headers; encoded as uint32 for protobuf transport.
|
||||
uint32 NumberOfIdEntries = 6;
|
||||
}
|
||||
|
||||
message IMAGE_RESOURCE_DIRECTORY_ENTRY {
|
||||
uint32 Name = 1;
|
||||
uint32 OffsetToData = 2;
|
||||
}
|
||||
|
||||
message IMAGE_RESOURCE_DATA_ENTRY {
|
||||
uint32 OffsetToData = 1;
|
||||
uint32 Size = 2;
|
||||
uint32 CodePage = 3;
|
||||
uint32 Reserved = 4;
|
||||
}
|
||||
|
||||
message IMAGE_EXPORT_DIRECTORY {
|
||||
uint32 Characteristics = 1;
|
||||
uint32 TimeDateStamp = 2;
|
||||
// uint16 in PE headers; encoded as uint32 for protobuf transport.
|
||||
uint32 MajorVersion = 3;
|
||||
// uint16 in PE headers; encoded as uint32 for protobuf transport.
|
||||
uint32 MinorVersion = 4;
|
||||
uint32 Name = 5;
|
||||
uint32 Base = 6;
|
||||
uint32 NumberOfFunctions = 7;
|
||||
uint32 NumberOfNames = 8;
|
||||
uint32 AddressOfFunctions = 9;
|
||||
uint32 AddressOfNames = 10;
|
||||
uint32 AddressOfNameOrdinals = 11;
|
||||
}
|
||||
|
||||
message PESpoofMetadataConfig {
|
||||
// Reference PE used to clone metadata/resources/timestamps.
|
||||
SpoofMetadataFile Source = 1;
|
||||
// Reserved for future standalone icon mutation support.
|
||||
SpoofMetadataFile Icon = 2;
|
||||
IMAGE_RESOURCE_DIRECTORY ResourceDirectory = 10;
|
||||
repeated IMAGE_RESOURCE_DIRECTORY_ENTRY ResourceDirectoryEntries = 11;
|
||||
repeated IMAGE_RESOURCE_DATA_ENTRY ResourceDataEntries = 12;
|
||||
IMAGE_EXPORT_DIRECTORY ExportDirectory = 13;
|
||||
}
|
||||
|
||||
message SpoofMetadataConfig {
|
||||
// Keep executable formats separated so additional formats can be added later
|
||||
// without changing request semantics.
|
||||
PESpoofMetadataConfig PE = 1;
|
||||
}
|
||||
|
||||
message TrafficEncoder {
|
||||
uint64 ID = 1;
|
||||
commonpb.File Wasm = 2;
|
||||
@@ -484,7 +549,19 @@ message GenerateStageReq {
|
||||
string Compress = 8;
|
||||
}
|
||||
|
||||
message Generate { commonpb.File File = 1; }
|
||||
message Generate {
|
||||
commonpb.File File = 1;
|
||||
string ImplantName = 2;
|
||||
string ImplantBuildID = 3;
|
||||
}
|
||||
|
||||
message GenerateSpoofMetadataReq {
|
||||
string ImplantBuildID = 1;
|
||||
string ImplantName = 2;
|
||||
uint64 ResourceID = 3;
|
||||
|
||||
SpoofMetadataConfig SpoofMetadata = 10;
|
||||
}
|
||||
|
||||
message MSFReq {
|
||||
string Payload = 1;
|
||||
|
||||
Loaded 100 of 408 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user