Compare commits

...
Author SHA1 Message Date
moloch-- f65f98bf65 Improve websites content-type sniffing issue #2033 2026-01-28 18:14:27 -08:00
moloch-- cee2cc76c4 Convert tui-settings to yaml 2026-01-28 18:06:41 -08:00
moloch-- daa33b6301 Convert server configs to yaml syntax, with backwards compatibility 2026-01-28 17:59:34 -08:00
moloch-- ef52973536 Convert configs to yml 2026-01-28 17:39:14 -08:00
moloch-- 6a7bd0803a Update --save help 2026-01-28 17:21:35 -08:00
moloch-- 6c70c1c1e6 Add signature validation logic 2026-01-28 17:20:24 -08:00
moloch-- ddc222b8a2 Improvements to update 2026-01-28 17:07:47 -08:00
moloch-- 5506a582ac Add SliverPublicKey 2026-01-28 16:45:32 -08:00
Joe 63d872c98b Merge pull request #2142 from BishopFox/fix/sgn
Fix/sgn
2026-01-28 17:31:46 -07:00
moloch-- ef9f5dde0d Fix unit tests 2026-01-28 07:21:38 -08:00
moloch-- e35b7fdb97 Update sgn to v0.0.5 2026-01-27 21:28:21 -08:00
moloch-- efbbf382eb Merge branch 'master' into fix/sgn 2026-01-27 21:26:42 -08:00
moloch-- 0b8d00900d v1.6.7 2026-01-27 21:06:39 -08:00
moloch-- 9077596f25 Damn it Claude 2026-01-27 21:04:30 -08:00
moloch-- 0a8104a32e Damn it Claude 2026-01-27 21:03:25 -08:00
moloch-- 15a0ba4c1e Damn it Claude 2026-01-27 21:01:55 -08:00
moloch-- d452c8a676 Write .bin even if sgn fails 2026-01-27 20:49:28 -08:00
Joe fb74b491fa Merge pull request #2141 from BishopFox/fix/console-inputs
Update readline/console deps
2026-01-27 21:38:20 -07:00
29 changed files with 1494 additions and 196 deletions

No files matched your search

+3
View File
@@ -29,11 +29,14 @@ MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 24
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
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
+1
View File
@@ -43,3 +43,4 @@ See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
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.
+10 -5
View File
@@ -34,12 +34,19 @@ const (
SliverClientDirName = ".sliver-client"
versionFileName = "version"
envVarName = "SLIVER_CLIENT_ROOT_DIR"
)
// GetRootAppDir - Get the Sliver app dir ~/.sliver-client/
func GetRootAppDir() string {
user, _ := user.Current()
dir := filepath.Join(user.HomeDir, SliverClientDirName)
value := os.Getenv(envVarName)
var dir string
if len(value) == 0 {
user, _ := user.Current()
dir = filepath.Join(user.HomeDir, SliverClientDirName)
} else {
dir = value
}
if _, err := os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0700)
if err != nil {
@@ -116,7 +123,5 @@ under certain conditions; type 'licenses' for details.`)
}
saveAssetVersion(appDir)
}
if _, err := os.Stat(filepath.Join(appDir, settingsFileName)); os.IsNotExist(err) {
SaveSettings(nil)
}
_, _ = LoadSettings()
}
+27
View File
@@ -0,0 +1,27 @@
package assets
import (
"os"
"path/filepath"
"strings"
)
func legacyBackupPath(legacyPath string) string {
dir := filepath.Dir(legacyPath)
base := filepath.Base(legacyPath)
name := strings.TrimSuffix(base, filepath.Ext(base))
return filepath.Join(dir, "."+name+".json-old")
}
func renameLegacyConfig(legacyPath string) error {
if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
return nil
}
backupPath := legacyBackupPath(legacyPath)
if _, err := os.Stat(backupPath); err == nil {
if err := os.Remove(backupPath); err != nil {
return err
}
}
return os.Rename(legacyPath, backupPath)
}
+39 -16
View File
@@ -22,35 +22,58 @@ import (
"encoding/json"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
const (
settingsFileName = "tui-settings.json"
settingsFileName = "tui-settings.yaml"
settingsLegacyFileName = "tui-settings.json"
)
// ClientSettings - Client JSON config
type ClientSettings struct {
TableStyle string `json:"tables"`
AutoAdult bool `json:"autoadult"`
BeaconAutoResults bool `json:"beacon_autoresults"`
SmallTermWidth int `json:"small_term_width"`
AlwaysOverflow bool `json:"always_overflow"`
VimMode bool `json:"vim_mode"`
UserConnect bool `json:"user_connect"`
ConsoleLogs bool `json:"console_logs"`
TableStyle string `json:"tables" yaml:"tables"`
AutoAdult bool `json:"autoadult" yaml:"autoadult"`
BeaconAutoResults bool `json:"beacon_autoresults" yaml:"beacon_autoresults"`
SmallTermWidth int `json:"small_term_width" yaml:"small_term_width"`
AlwaysOverflow bool `json:"always_overflow" yaml:"always_overflow"`
VimMode bool `json:"vim_mode" yaml:"vim_mode"`
UserConnect bool `json:"user_connect" yaml:"user_connect"`
ConsoleLogs bool `json:"console_logs" yaml:"console_logs"`
}
// LoadSettings - Load the client settings from disk
func LoadSettings() (*ClientSettings, error) {
rootDir, _ := filepath.Abs(GetRootAppDir())
data, err := os.ReadFile(filepath.Join(rootDir, settingsFileName))
if err != nil {
settingsPath := filepath.Join(rootDir, settingsFileName)
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
settings := defaultSettings()
migratedLegacy := false
data, err := os.ReadFile(settingsPath)
if err == nil {
if err = yaml.Unmarshal(data, settings); err != nil {
return defaultSettings(), err
}
} else if !os.IsNotExist(err) {
return defaultSettings(), err
} else if data, err = os.ReadFile(legacyPath); err == nil {
if err = json.Unmarshal(data, settings); err != nil {
return defaultSettings(), err
}
migratedLegacy = true
} else if !os.IsNotExist(err) {
return defaultSettings(), err
}
settings := defaultSettings()
err = json.Unmarshal(data, settings)
if err != nil {
return defaultSettings(), err
if err := SaveSettings(settings); err != nil {
return settings, err
}
if migratedLegacy {
if err := renameLegacyConfig(legacyPath); err != nil {
return settings, err
}
}
return settings, nil
}
@@ -73,7 +96,7 @@ func SaveSettings(settings *ClientSettings) error {
if settings == nil {
settings = defaultSettings()
}
data, err := json.MarshalIndent(settings, "", " ")
data, err := yaml.Marshal(settings)
if err != nil {
return err
}
+115
View File
@@ -0,0 +1,115 @@
package assets
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestLoadSettingsParsesYAML(t *testing.T) {
t.Setenv(envVarName, t.TempDir())
rootDir, _ := filepath.Abs(GetRootAppDir())
settingsPath := filepath.Join(rootDir, settingsFileName)
data := []byte(`tables: "Compact"
autoadult: true
beacon_autoresults: false
small_term_width: 120
always_overflow: true
vim_mode: true
user_connect: true
console_logs: false
`)
if err := os.WriteFile(settingsPath, data, 0o600); err != nil {
t.Fatalf("failed to write yaml settings: %v", err)
}
settings, err := LoadSettings()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if settings.TableStyle != "Compact" {
t.Fatalf("expected TableStyle %q, got %q", "Compact", settings.TableStyle)
}
if !settings.AutoAdult {
t.Fatalf("expected AutoAdult true")
}
if settings.BeaconAutoResults {
t.Fatalf("expected BeaconAutoResults false")
}
if settings.SmallTermWidth != 120 {
t.Fatalf("expected SmallTermWidth %d, got %d", 120, settings.SmallTermWidth)
}
if !settings.AlwaysOverflow {
t.Fatalf("expected AlwaysOverflow true")
}
if !settings.VimMode {
t.Fatalf("expected VimMode true")
}
if !settings.UserConnect {
t.Fatalf("expected UserConnect true")
}
if settings.ConsoleLogs {
t.Fatalf("expected ConsoleLogs false")
}
}
func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
t.Setenv(envVarName, t.TempDir())
rootDir, _ := filepath.Abs(GetRootAppDir())
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
legacy := &ClientSettings{
TableStyle: "Legacy",
AutoAdult: true,
BeaconAutoResults: false,
SmallTermWidth: 200,
AlwaysOverflow: true,
VimMode: true,
UserConnect: true,
ConsoleLogs: false,
}
data, err := json.MarshalIndent(legacy, "", " ")
if err != nil {
t.Fatalf("failed to marshal legacy settings: %v", err)
}
if err := os.WriteFile(legacyPath, data, 0o600); err != nil {
t.Fatalf("failed to write legacy settings: %v", err)
}
settings, err := LoadSettings()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if settings.TableStyle != "Legacy" {
t.Fatalf("expected TableStyle %q, got %q", "Legacy", settings.TableStyle)
}
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
t.Fatalf("expected yaml settings to exist: %v", err)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("expected legacy settings to be renamed: %v", err)
}
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
t.Fatalf("expected legacy backup to exist: %v", err)
}
}
func TestLoadSettingsWritesDefault(t *testing.T) {
t.Setenv(envVarName, t.TempDir())
settings, err := LoadSettings()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if settings.TableStyle != "SliverDefault" {
t.Fatalf("expected default TableStyle %q, got %q", "SliverDefault", settings.TableStyle)
}
rootDir, _ := filepath.Abs(GetRootAppDir())
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
t.Fatalf("expected default settings to be written: %v", err)
}
}
+7
View File
@@ -1,6 +1,7 @@
package mcp
import (
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
slivermcp "github.com/bishopfox/sliver/client/mcp"
"github.com/spf13/cobra"
@@ -26,6 +27,12 @@ func McpStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
ServerVersion: version,
}.WithDefaults()
msg := `Do you know what prompt injection is and are you an adult?`
if !settings.IsUserAnAdultWithPrompt(con, msg) {
con.PrintErrorf("Failed to start MCP server, the user is not qualified to use feature\n")
return
}
if err := slivermcp.Start(cfg, con.Rpc); err != nil {
con.PrintErrorf("%s\n", err)
return
+6 -1
View File
@@ -41,11 +41,16 @@ func SettingsAutoAdultCmd(cmd *cobra.Command, con *console.SliverClient, args []
// IsUserAnAdult - This should be called for any dangerous (OPSEC-wise) functions.
func IsUserAnAdult(con *console.SliverClient) bool {
return IsUserAnAdultWithPrompt(con, "This action is bad OPSEC, are you an adult?")
}
// IsUserAnAdultWithPrompt prompts with a custom message before continuing.
func IsUserAnAdultWithPrompt(con *console.SliverClient, prompt string) bool {
if GetAutoAdult(con) {
return true
}
confirm := false
_ = forms.Confirm("This action is bad OPSEC, are you an adult?", &confirm)
_ = forms.Confirm(prompt, &confirm)
return confirm
}
+2 -1
View File
@@ -25,8 +25,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("update", false, updateCmd, func(f *pflag.FlagSet) {
f.BoolP("prereleases", "P", false, "include pre-released (unstable) versions")
f.StringP("proxy", "p", "", "specify a proxy url (e.g. http://localhost:8080)")
f.StringP("save", "s", "", "save downloaded files to specific directory (default user home dir)")
f.StringP("save", "s", "", "save downloaded files to specific directory (default downloads dir)")
f.BoolP("insecure", "I", false, "skip tls certificate validation")
f.Bool("force", false, "skip version comparison and download the latest release")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
+408 -36
View File
@@ -21,6 +21,7 @@ package update
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
@@ -40,8 +41,19 @@ import (
"github.com/bishopfox/sliver/client/version"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/util/minisign"
"github.com/cheggaaa/pb/v3"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var (
SliverPublicKey string
)
const (
downloadAttempts = 3
downloadBackoff = 750 * time.Millisecond
)
// UpdateCmd - Check for updates.
@@ -94,7 +106,13 @@ func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
con.Printf("\nChecking for updates ... ")
prereleases, _ := cmd.Flags().GetBool("prereleases")
release, err := version.CheckForUpdates(client, prereleases)
force, _ := cmd.Flags().GetBool("force")
var release *version.Release
if force {
release, err = version.LatestRelease(client, prereleases)
} else {
release, err = version.CheckForUpdates(client, prereleases)
}
con.Printf("done!\n\n")
if err != nil {
con.PrintErrorf("Update check failed %s\n", err)
@@ -107,9 +125,13 @@ func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
con.PrintErrorf("%s\n", err)
return
}
updateAvailable(con, client, release, saveTo)
updateAvailable(con, client, release, saveTo, force)
} else {
con.PrintInfof("No new releases.\n")
if force {
con.PrintInfof("No releases found.\n")
} else {
con.PrintInfof("No new releases.\n")
}
}
now := time.Now()
lastCheck := fmt.Appendf(nil, "%d", now.Unix())
@@ -191,84 +213,434 @@ func findAssetFor(prefix string, suffixes []string, assets []version.Asset) *ver
}
func serverAssetForGOOS(assets []version.Asset) *version.Asset {
suffixes := []string{fmt.Sprintf("_%s.zip", runtime.GOOS), runtime.GOOS}
if runtime.GOOS == "darwin" {
suffixes = []string{"_macos.zip", "_macos"}
if runtime.GOARCH == "arm64" {
suffixes = []string{"_macos-arm64.zip", "_macos-arm64"}
}
}
suffixes := assetSuffixes(runtime.GOOS, runtime.GOARCH)
prefix := "sliver-server"
return findAssetFor(prefix, suffixes, assets)
}
func clientAssetForGOOS(assets []version.Asset) *version.Asset {
suffixes := []string{fmt.Sprintf("_%s.zip", runtime.GOOS), runtime.GOOS}
if runtime.GOOS == "darwin" {
suffixes = []string{"_macos.zip", "_macos"}
if runtime.GOARCH == "arm64" {
suffixes = []string{"_macos-arm64.zip", "_macos-arm64"}
}
}
suffixes := assetSuffixes(runtime.GOOS, runtime.GOARCH)
prefix := "sliver-client"
return findAssetFor(prefix, suffixes, assets)
}
func updateAvailable(con *console.SliverClient, client *http.Client, release *version.Release, saveTo string) {
func assetSuffixes(goos, goarch string) []string {
suffixes := make([]string, 0, 8)
seen := make(map[string]struct{}, 8)
add := func(suffix string) {
if suffix == "" {
return
}
if _, ok := seen[suffix]; ok {
return
}
seen[suffix] = struct{}{}
suffixes = append(suffixes, suffix)
}
add(fmt.Sprintf("_%s-%s.zip", goos, goarch))
add(fmt.Sprintf("_%s-%s", goos, goarch))
add(fmt.Sprintf("_%s.zip", goos))
add(fmt.Sprintf("_%s", goos))
if goos == "darwin" {
add(fmt.Sprintf("_macos-%s.zip", goarch))
add(fmt.Sprintf("_macos-%s", goarch))
add("_macos.zip")
add("_macos")
}
return suffixes
}
func updateAvailable(con *console.SliverClient, client *http.Client, release *version.Release, saveTo string, force bool) {
serverAsset := serverAssetForGOOS(release.Assets)
clientAsset := clientAssetForGOOS(release.Assets)
con.Printf("New version available %s\n", release.TagName)
msg := "New version available"
if force {
msg = "Latest release available"
}
con.Printf("%s %s\n", msg, release.TagName)
if serverAsset != nil {
con.Printf(" - Server: %s\n", util.ByteCountBinary(int64(serverAsset.Size)))
}
if clientAsset != nil {
con.Printf(" - Client: %s\n", util.ByteCountBinary(int64(clientAsset.Size)))
}
if serverAsset == nil {
con.PrintWarnf("No server asset available for %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
if clientAsset == nil {
con.PrintWarnf("No client asset available for %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
con.Println()
confirm := false
forms.Confirm("Download update?", &confirm)
if confirm {
con.Printf("Please wait ...")
err := downloadAsset(client, serverAsset, saveTo)
if serverAsset == nil && clientAsset == nil {
con.PrintWarnf("No matching assets found for %s/%s\n", runtime.GOOS, runtime.GOARCH)
return
}
publicKey, err := loadSliverPublicKey()
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
err = downloadAsset(client, clientAsset, saveTo)
if err != nil {
con.PrintErrorf("%s\n", err)
return
downloads := []struct {
label string
asset *version.Asset
}{
{label: "server", asset: serverAsset},
{label: "client", asset: clientAsset},
}
for _, item := range downloads {
if item.asset == nil {
continue
}
con.Printf("Downloading %s update from %s ...\n", item.label, item.asset.BrowserDownloadURL)
if err := downloadAssetWithSignature(con, client, item.asset, release.Assets, saveTo, publicKey); err != nil {
con.PrintErrorf("Failed to download %s update: %s\n", item.label, err)
return
}
con.Println(console.Clearln)
}
con.Println(console.Clearln)
con.PrintInfof("Saved updates to: %s\n", saveTo)
}
}
func downloadAsset(client *http.Client, asset *version.Asset, saveTo string) error {
func downloadAssetWithSignature(con *console.SliverClient, client *http.Client, asset *version.Asset, assets []version.Asset, saveTo string, publicKey minisign.PublicKey) error {
if asset == nil {
return errors.New("no asset available for this platform")
}
downloadURL, err := url.Parse(asset.BrowserDownloadURL)
if err != nil {
return err
}
assetFileName := filepath.Base(downloadURL.Path)
finalPath := filepath.Join(saveTo, assetFileName)
limit := int64(asset.Size)
writer, err := os.Create(filepath.Join(saveTo, assetFileName))
sigURL := asset.BrowserDownloadURL + ".minisig"
var sigLimit int64
if sigAsset := signatureAssetFor(assetFileName, assets); sigAsset != nil && sigAsset.BrowserDownloadURL != "" {
sigURL = sigAsset.BrowserDownloadURL
sigLimit = int64(sigAsset.Size)
}
assetTempPath, err := downloadWithRetries(client, asset.BrowserDownloadURL, saveTo, int64(asset.Size), "asset")
if err != nil {
return fmt.Errorf("download asset: %w", err)
}
sigData, err := downloadBytesWithRetries(client, sigURL, sigLimit, "signature")
if err != nil {
cleanupAssetFiles(assetTempPath, finalPath)
return fmt.Errorf("download signature: %w", err)
}
if err := verifyMinisignSignature(assetTempPath, sigData, assetFileName, publicKey); err != nil {
cleanupAssetFiles(assetTempPath, finalPath)
return err
}
defer writer.Close()
con.PrintSuccessf("Signature verified for %s\n", finalPath)
resp, err := client.Get(asset.BrowserDownloadURL)
if err != nil {
if err := replaceFile(assetTempPath, finalPath); err != nil {
os.Remove(assetTempPath)
return err
}
return nil
}
func signatureAssetFor(assetFileName string, assets []version.Asset) *version.Asset {
want := assetFileName + ".minisig"
for _, asset := range assets {
if asset.Name == want {
return &asset
}
}
return nil
}
func loadSliverPublicKey() (minisign.PublicKey, error) {
if strings.TrimSpace(SliverPublicKey) == "" {
return minisign.PublicKey{}, errors.New("minisign public key not set at build time")
}
var publicKey minisign.PublicKey
if err := publicKey.UnmarshalText([]byte(SliverPublicKey)); err != nil {
return minisign.PublicKey{}, fmt.Errorf("invalid minisign public key: %w", err)
}
return publicKey, nil
}
type httpStatusError struct {
url string
status int
}
func (e httpStatusError) Error() string {
return fmt.Sprintf("unexpected HTTP status %d for %s", e.status, e.url)
}
func downloadWithRetries(client *http.Client, downloadURL, saveTo string, limit int64, label string) (string, error) {
var lastErr error
backoff := downloadBackoff
for attempt := 1; attempt <= downloadAttempts; attempt++ {
path, err := downloadOnce(client, downloadURL, saveTo, limit, label)
if err == nil {
return path, nil
}
lastErr = err
if !isRetryableDownloadError(err) || attempt == downloadAttempts {
break
}
time.Sleep(backoff)
backoff *= 2
}
return "", lastErr
}
func downloadBytesWithRetries(client *http.Client, downloadURL string, limit int64, label string) ([]byte, error) {
var lastErr error
backoff := downloadBackoff
for attempt := 1; attempt <= downloadAttempts; attempt++ {
data, err := downloadBytesOnce(client, downloadURL, limit, label)
if err == nil {
return data, nil
}
lastErr = err
if !isRetryableDownloadError(err) || attempt == downloadAttempts {
break
}
time.Sleep(backoff)
backoff *= 2
}
return nil, lastErr
}
func downloadOnce(client *http.Client, downloadURL, saveTo string, limit int64, label string) (string, error) {
parsedURL, err := url.Parse(downloadURL)
if err != nil {
return "", err
}
baseName := filepath.Base(parsedURL.Path)
tmpFile, err := os.CreateTemp(saveTo, baseName+".tmp-*")
if err != nil {
return "", err
}
success := false
defer func() {
if !success {
tmpFile.Close()
os.Remove(tmpFile.Name())
}
}()
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, downloadURL, nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
bar := pb.Full.Start64(limit)
barReader := bar.NewProxyReader(resp.Body)
io.Copy(writer, barReader)
bar.Finish()
if resp.StatusCode != http.StatusOK {
return "", httpStatusError{url: downloadURL, status: resp.StatusCode}
}
if limit <= 0 && resp.ContentLength > 0 {
limit = resp.ContentLength
}
var reader io.Reader = resp.Body
var bar *pb.ProgressBar
if limit > 0 {
bar = pb.Full.Start64(limit)
bar.Set("prefix", fmt.Sprintf("%s: ", label))
bar.Set(pb.Bytes, true)
bar.Set(pb.CleanOnFinish, true)
bar.Set(pb.ReturnSymbol, "\r")
if term.IsTerminal(int(os.Stdin.Fd())) {
bar.Set(pb.Terminal, true)
}
reader = bar.NewProxyReader(reader)
}
n, copyErr := io.Copy(tmpFile, reader)
if bar != nil {
bar.Finish()
}
if copyErr != nil {
return "", copyErr
}
if limit > 0 && n != limit {
return "", fmt.Errorf("downloaded %d bytes, expected %d", n, limit)
}
if err := tmpFile.Sync(); err != nil {
return "", err
}
if err := tmpFile.Close(); err != nil {
return "", err
}
success = true
return tmpFile.Name(), nil
}
func downloadBytesOnce(client *http.Client, downloadURL string, limit int64, label string) ([]byte, error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, downloadURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, httpStatusError{url: downloadURL, status: resp.StatusCode}
}
if limit <= 0 && resp.ContentLength > 0 {
limit = resp.ContentLength
}
var reader io.Reader = resp.Body
var bar *pb.ProgressBar
if limit > 0 {
bar = pb.Full.Start64(limit)
bar.Set("prefix", fmt.Sprintf("%s: ", label))
bar.Set(pb.Bytes, true)
bar.Set(pb.CleanOnFinish, true)
bar.Set(pb.ReturnSymbol, "\r")
if term.IsTerminal(int(os.Stdin.Fd())) {
bar.Set(pb.Terminal, true)
}
reader = bar.NewProxyReader(reader)
}
data, readErr := readAllWithLimit(reader, limit)
if bar != nil {
bar.Finish()
}
if readErr != nil {
return nil, readErr
}
return data, nil
}
func isRetryableDownloadError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, io.ErrUnexpectedEOF) {
return true
}
var netErr net.Error
if errors.As(err, &netErr) {
return netErr.Timeout() || netErr.Temporary()
}
var statusErr httpStatusError
if errors.As(err, &statusErr) {
switch statusErr.status {
case http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return true
default:
return false
}
}
return false
}
func verifyMinisignSignature(artifactPath string, sigData []byte, expectedFile string, publicKey minisign.PublicKey) error {
artifact, err := os.Open(artifactPath)
if err != nil {
return fmt.Errorf("open artifact: %w", err)
}
defer artifact.Close()
reader := minisign.NewReader(artifact)
if _, err := io.Copy(io.Discard, reader); err != nil {
return fmt.Errorf("stream artifact: %w", err)
}
if !reader.Verify(publicKey, sigData) {
return errors.New("signature verification failed")
}
if err := verifyTrustedComment(sigData, expectedFile); err != nil {
return err
}
return nil
}
func readAllWithLimit(reader io.Reader, limit int64) ([]byte, error) {
if limit <= 0 {
return io.ReadAll(reader)
}
lr := &io.LimitedReader{R: reader, N: limit + 1}
data, err := io.ReadAll(lr)
if err != nil {
return nil, err
}
if int64(len(data)) > limit {
return nil, fmt.Errorf("downloaded %d bytes, expected %d", len(data), limit)
}
if int64(len(data)) != limit {
return nil, fmt.Errorf("downloaded %d bytes, expected %d", len(data), limit)
}
return data, nil
}
func verifyTrustedComment(signature []byte, expectedFile string) error {
var sig minisign.Signature
if err := sig.UnmarshalText(signature); err != nil {
return fmt.Errorf("parse signature: %w", err)
}
fileField := trustedCommentFile(sig.TrustedComment)
if fileField == "" {
return fmt.Errorf("trusted comment missing file field: %q", sig.TrustedComment)
}
if fileField != expectedFile {
return fmt.Errorf("trusted comment file mismatch: expected %q, got %q", expectedFile, fileField)
}
return nil
}
func trustedCommentFile(trustedComment string) string {
for _, field := range strings.Fields(trustedComment) {
if after, ok := strings.CutPrefix(field, "file:"); ok {
return after
}
}
return ""
}
func replaceFile(src, dst string) error {
if err := os.Remove(dst); err != nil && !os.IsNotExist(err) {
return err
}
return os.Rename(src, dst)
}
func cleanupAssetFiles(tempPath, finalPath string) {
if tempPath != "" {
_ = os.Remove(tempPath)
}
if finalPath != "" {
_ = os.Remove(finalPath)
}
}
@@ -22,10 +22,12 @@ import (
"context"
"errors"
"io"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
@@ -117,7 +119,7 @@ func webAddFile(web *clientpb.WebsiteAddContent, webpath string, contentType str
}
if contentType == "" {
contentType = sniffContentType(file)
contentType = sniffContentType(file, contentPath)
}
web.Contents[webpath] = &clientpb.WebContent{
@@ -135,13 +137,19 @@ func confirmAddDirectory() bool {
return confirm
}
func sniffContentType(out *os.File) string {
func sniffContentType(out *os.File, contentPath string) string {
ext := strings.ToLower(filepath.Ext(contentPath))
if ext != "" {
contentType := mime.TypeByExtension(ext)
if contentType != "" {
return contentType
}
}
out.Seek(0, io.SeekStart)
buffer := make([]byte, fileSampleSize)
_, err := out.Read(buffer)
if err != nil {
return defaultMimeType
}
contentType := http.DetectContentType(buffer)
return contentType
return http.DetectContentType(buffer)
}
+9
View File
@@ -227,6 +227,15 @@ func (con *SliverClient) PrintSuccessf(format string, args ...any) {
con.printf(Clearln+Success+format, args...)
}
// PrintSuccess prints a success message with a default or provided message.
func (con *SliverClient) PrintSuccess(args ...any) {
if len(args) == 0 {
con.PrintSuccessf("Success")
return
}
con.PrintSuccessf("%s", fmt.Sprint(args...))
}
// PrintWarnf a warning message immediately below the last line of output.
func (con *SliverClient) PrintWarnf(format string, args ...any) {
logger := slog.New(con.jsonHandler)
+65 -20
View File
@@ -63,6 +63,68 @@ func (r *Release) Published() (time.Time, error) {
// occurs we don't really try to recover. If client is nil we just use the Go
// default client with default settings.
func CheckForUpdates(client *http.Client, prereleases bool) (*Release, error) {
releases, err := fetchReleases(client)
if err != nil {
return nil, err
}
if len(releases) == 0 {
return nil, nil
}
current := canonicalSemver(Version)
if current == "" {
current = "v0.0.0"
}
for _, release := range releases {
if release == nil {
continue
}
if release.Prerelease && !prereleases {
continue
}
releaseVersion := canonicalSemver(release.TagName)
if releaseVersion == "" {
continue
}
if semver.Compare(releaseVersion, current) > 0 {
return release, nil
}
}
return nil, nil
}
// LatestRelease - Returns the latest release without comparing to the current version.
func LatestRelease(client *http.Client, prereleases bool) (*Release, error) {
releases, err := fetchReleases(client)
if err != nil {
return nil, err
}
if len(releases) == 0 {
return nil, nil
}
var latest *Release
var latestVersion string
for _, release := range releases {
if release == nil {
continue
}
if release.Prerelease && !prereleases {
continue
}
releaseVersion := canonicalSemver(release.TagName)
if releaseVersion == "" {
continue
}
if latest == nil || semver.Compare(releaseVersion, latestVersion) > 0 {
latest = release
latestVersion = releaseVersion
}
}
return latest, nil
}
func fetchReleases(client *http.Client) ([]*Release, error) {
skip := os.Getenv(skipCheckEnv)
if skip != "" || GithubReleasesURL == "" {
return nil, nil
@@ -84,27 +146,10 @@ func CheckForUpdates(client *http.Client, prereleases bool) (*Release, error) {
if resp.StatusCode != 200 {
return nil, errors.New("API returned non-200 status code")
}
releases := &[]*Release{}
err = json.Unmarshal(body, releases)
if err != nil {
releases := []*Release{}
if err := json.Unmarshal(body, &releases); err != nil {
return nil, err
}
current := canonicalSemver(Version)
if current == "" {
current = "v0.0.0"
}
for _, release := range *releases {
if release.Prerelease && !prereleases {
continue
}
releaseVersion := canonicalSemver(release.TagName)
if releaseVersion == "" {
continue
}
if semver.Compare(releaseVersion, current) > 0 {
return release, nil
}
}
return nil, nil
return releases, nil
}
+1 -1
View File
@@ -38,7 +38,7 @@ require (
github.com/miekg/dns v1.1.70
github.com/moloch--/asciicast v0.1.1
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e
github.com/moloch--/sgn v0.0.4
github.com/moloch--/sgn v0.0.5
github.com/ncruces/go-sqlite3 v0.30.4
github.com/reeflective/console v0.1.25
github.com/reeflective/readline v1.1.4
+2 -2
View File
@@ -358,8 +358,8 @@ github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e h1:IkFCPlAa0iTiLxc
github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e/go.mod h1:eYeI6cQ5YHhHt9i0BBW0zc1DaQnb4ZMXsSPuEV/V5Og=
github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989 h1:g0EfxAWzDGYarN0RAPibsjz2M/f0pAqYNiEpCPWRDok=
github.com/moloch--/readline v0.0.0-20260128035102-9f373ad18989/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
github.com/moloch--/sgn v0.0.4 h1:90IC5rx3TmPEfrqPnENB0yyiqcbcNk9LqezXx7uruF0=
github.com/moloch--/sgn v0.0.4/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
github.com/moloch--/sgn v0.0.5 h1:92GFaRpe+353uhqzFiNKXKgmTiD4dBxnvb8gd4MaV/g=
github.com/moloch--/sgn v0.0.5/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+1
View File
@@ -136,6 +136,7 @@ var rootCmd = &cobra.Command{
cryptography.AgeServerKeyPair()
cryptography.MinisignServerPrivateKey()
c2.SetupDefaultC2Profiles()
_, _ = configs.LoadCrackConfig()
serverConfig := configs.GetServerConfig()
listenerJobs, err := db.ListenerJobs()
+2
View File
@@ -11,6 +11,7 @@ import (
"github.com/bishopfox/sliver/server/assets"
"github.com/bishopfox/sliver/server/c2"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/configs"
"github.com/bishopfox/sliver/server/console"
"github.com/bishopfox/sliver/server/cryptography"
"github.com/bishopfox/sliver/server/daemon"
@@ -63,6 +64,7 @@ var daemonCmd = &cobra.Command{
certs.SetupWGKeys()
cryptography.AgeServerKeyPair()
cryptography.MinisignServerPrivateKey()
_, _ = configs.LoadCrackConfig()
listenerJobs, err := db.ListenerJobs()
if err != nil {
+83 -25
View File
@@ -26,17 +26,19 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/server/assets"
"github.com/bishopfox/sliver/server/log"
"gopkg.in/yaml.v3"
)
const (
crackConfigFileName = "crack.json"
crackConfigFileName = "crack.yaml"
crackLegacyConfigFileName = "crack.json"
)
var (
crackConfigLog = log.NamedLogger("config", "crack")
)
// getCrackConfigPath - File path to config.json
// getCrackConfigPath - File path to config.yaml
func getCrackConfigPath() string {
appDir := assets.GetRootAppDir()
confPath := filepath.Join(appDir, "configs", crackConfigFileName)
@@ -44,6 +46,41 @@ func getCrackConfigPath() string {
return confPath
}
func getCrackLegacyConfigPath() string {
appDir := assets.GetRootAppDir()
return filepath.Join(appDir, "configs", crackLegacyConfigFileName)
}
type crackConfigYAML struct {
AutoFire bool `yaml:"AutoFire"`
MaxFileSize int64 `yaml:"MaxFileSize"`
ChunkSize int64 `yaml:"ChunkSize"`
MaxDiskUsage int64 `yaml:"MaxDiskUsage"`
}
func crackConfigFromYAML(data []byte) (*clientpb.CrackConfig, error) {
var raw crackConfigYAML
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, err
}
return &clientpb.CrackConfig{
AutoFire: raw.AutoFire,
MaxFileSize: raw.MaxFileSize,
ChunkSize: raw.ChunkSize,
MaxDiskUsage: raw.MaxDiskUsage,
}, nil
}
func crackConfigToYAML(config *clientpb.CrackConfig) ([]byte, error) {
raw := crackConfigYAML{
AutoFire: config.AutoFire,
MaxFileSize: config.MaxFileSize,
ChunkSize: config.ChunkSize,
MaxDiskUsage: config.MaxDiskUsage,
}
return yaml.Marshal(raw)
}
// Save - Save config file to disk
func SaveCrackConfig(config *clientpb.CrackConfig) error {
configPath := getCrackConfigPath()
@@ -55,7 +92,7 @@ func SaveCrackConfig(config *clientpb.CrackConfig) error {
return err
}
}
data, err := json.MarshalIndent(config, "", " ")
data, err := crackConfigToYAML(config)
if err != nil {
return err
}
@@ -70,6 +107,7 @@ func SaveCrackConfig(config *clientpb.CrackConfig) error {
// LoadCrackConfig - Get config value
func LoadCrackConfig() (*clientpb.CrackConfig, error) {
configPath := getCrackConfigPath()
legacyPath := getCrackLegacyConfigPath()
const defaultMaxFileSize = 10 * 1024 * 1024 * 1024 // 10GB - max size of any one file
const defaultMaxDiskUsage = 5 * defaultMaxFileSize // 50GB - total disk usage for all files
const maxChunkSize = 1024 * 1024 * 1024 // 1GB - max size of any one chunk
@@ -80,15 +118,28 @@ func LoadCrackConfig() (*clientpb.CrackConfig, error) {
ChunkSize: defaultChunkSize,
MaxDiskUsage: defaultMaxDiskUsage,
}
migratedLegacy := false
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
data, err := os.ReadFile(configPath)
if err != nil {
crackConfigLog.Errorf("Failed to read crack config file %s", err)
return config, err
}
err = json.Unmarshal(data, config)
parsed, err := crackConfigFromYAML(data)
if err != nil {
crackConfigLog.Errorf("Failed to parse crack config file %s", err)
return config, err
}
config = parsed
} else if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
data, err := os.ReadFile(legacyPath)
if err != nil {
crackConfigLog.Errorf("Failed to read legacy crack config file %s", err)
return config, err
}
err = json.Unmarshal(data, config)
if err != nil {
crackConfigLog.Errorf("Failed to parse legacy crack config file %s", err)
return &clientpb.CrackConfig{
AutoFire: true,
MaxFileSize: defaultMaxFileSize,
@@ -96,33 +147,40 @@ func LoadCrackConfig() (*clientpb.CrackConfig, error) {
MaxDiskUsage: defaultMaxDiskUsage,
}, err
}
if config.ChunkSize < 1024 {
crackConfigLog.Warnf("Invalid chunk size %d, using default %d", config.ChunkSize, defaultChunkSize)
config.ChunkSize = defaultChunkSize
}
if config.MaxFileSize < 1024 {
crackConfigLog.Warnf("Invalid max file size %d, using default %d", config.MaxFileSize, defaultMaxFileSize)
config.MaxFileSize = defaultMaxFileSize
}
if maxChunkSize < config.ChunkSize {
crackConfigLog.Warnf("Chunk size is too large, setting to %d", maxChunkSize)
config.ChunkSize = maxChunkSize
}
if config.MaxFileSize < config.ChunkSize {
crackConfigLog.Warnf("Chunk size is larger than max file size, setting to %d", config.MaxFileSize)
config.ChunkSize = config.MaxFileSize
}
if config.MaxDiskUsage < config.MaxFileSize {
crackConfigLog.Warnf("Max disk usage is smaller than max file size, setting to %d", config.MaxFileSize)
config.MaxDiskUsage = config.MaxFileSize
}
return config, nil
migratedLegacy = true
crackConfigLog.Infof("Migrating legacy config %s to %s", legacyPath, configPath)
} else {
crackConfigLog.Debugf("Crack config file does not exist, using defaults")
}
if config.ChunkSize < 1024 {
crackConfigLog.Warnf("Invalid chunk size %d, using default %d", config.ChunkSize, defaultChunkSize)
config.ChunkSize = defaultChunkSize
}
if config.MaxFileSize < 1024 {
crackConfigLog.Warnf("Invalid max file size %d, using default %d", config.MaxFileSize, defaultMaxFileSize)
config.MaxFileSize = defaultMaxFileSize
}
if maxChunkSize < config.ChunkSize {
crackConfigLog.Warnf("Chunk size is too large, setting to %d", maxChunkSize)
config.ChunkSize = maxChunkSize
}
if config.MaxFileSize < config.ChunkSize {
crackConfigLog.Warnf("Chunk size is larger than max file size, setting to %d", config.MaxFileSize)
config.ChunkSize = config.MaxFileSize
}
if config.MaxDiskUsage < config.MaxFileSize {
crackConfigLog.Warnf("Max disk usage is smaller than max file size, setting to %d", config.MaxFileSize)
config.MaxDiskUsage = config.MaxFileSize
}
err := SaveCrackConfig(config) // This updates the config with any missing fields
if err != nil {
crackConfigLog.Errorf("Failed to save default config %s", err)
return config, err
}
if migratedLegacy {
if err := renameLegacyConfig(legacyPath); err != nil {
crackConfigLog.Errorf("Failed to rename legacy config %s", err)
}
}
return config, nil
}
+101
View File
@@ -0,0 +1,101 @@
package configs
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestCrackConfigParsesYAML(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
configPath := getCrackConfigPath()
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
t.Fatalf("failed to create config dir: %v", err)
}
data := []byte(`AutoFire: false
MaxFileSize: 2048
ChunkSize: 1024
MaxDiskUsage: 4096
`)
if err := os.WriteFile(configPath, data, 0600); err != nil {
t.Fatalf("failed to write yaml config: %v", err)
}
config, err := LoadCrackConfig()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if config.AutoFire {
t.Fatalf("expected AutoFire false")
}
if config.MaxFileSize != 2048 {
t.Fatalf("expected MaxFileSize %d, got %d", 2048, config.MaxFileSize)
}
if config.ChunkSize != 1024 {
t.Fatalf("expected ChunkSize %d, got %d", 1024, config.ChunkSize)
}
if config.MaxDiskUsage != 4096 {
t.Fatalf("expected MaxDiskUsage %d, got %d", 4096, config.MaxDiskUsage)
}
}
func TestCrackConfigWritesDefault(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
config, err := LoadCrackConfig()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if config == nil {
t.Fatalf("expected default crack config")
}
if _, err := os.Stat(getCrackConfigPath()); err != nil {
t.Fatalf("expected default config file to exist: %v", err)
}
}
func TestCrackConfigMigratesLegacyJSON(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
configDir := filepath.Dir(getCrackConfigPath())
if err := os.MkdirAll(configDir, 0700); err != nil {
t.Fatalf("failed to create config dir: %v", err)
}
legacyPath := filepath.Join(configDir, crackLegacyConfigFileName)
legacy := &clientpb.CrackConfig{
AutoFire: false,
MaxFileSize: 4096,
ChunkSize: 2048,
MaxDiskUsage: 8192,
}
data, err := json.MarshalIndent(legacy, "", " ")
if err != nil {
t.Fatalf("failed to marshal legacy config: %v", err)
}
if err := os.WriteFile(legacyPath, data, 0600); err != nil {
t.Fatalf("failed to write legacy config: %v", err)
}
config, err := LoadCrackConfig()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if config.MaxFileSize != 4096 {
t.Fatalf("expected MaxFileSize %d, got %d", 4096, config.MaxFileSize)
}
if _, err := os.Stat(getCrackConfigPath()); err != nil {
t.Fatalf("expected migrated yaml config file to exist: %v", err)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("expected legacy config to be renamed: %v", err)
}
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
t.Fatalf("expected legacy backup file to exist: %v", err)
}
}
+43 -15
View File
@@ -30,6 +30,7 @@ import (
"github.com/bishopfox/sliver/server/assets"
"github.com/bishopfox/sliver/server/log"
"gopkg.in/yaml.v3"
)
const (
@@ -40,7 +41,8 @@ const (
// MySQL - MySQL protocol
MySQL = "mysql"
databaseConfigFileName = "database.json"
databaseConfigFileName = "database.yaml"
databaseLegacyConfigFileName = "database.json"
)
var (
@@ -57,7 +59,7 @@ var (
}
)
// GetDatabaseConfigPath - File path to config.json
// GetDatabaseConfigPath - File path to config.yaml
func GetDatabaseConfigPath() string {
appDir := assets.GetRootAppDir()
databaseConfigPath := filepath.Join(appDir, "configs", databaseConfigFileName)
@@ -65,22 +67,27 @@ func GetDatabaseConfigPath() string {
return databaseConfigPath
}
func getDatabaseLegacyConfigPath() string {
appDir := assets.GetRootAppDir()
return filepath.Join(appDir, "configs", databaseLegacyConfigFileName)
}
// DatabaseConfig - Server config
type DatabaseConfig struct {
Dialect string `json:"dialect"`
Database string `json:"database"`
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
Port uint16 `json:"port"`
Dialect string `json:"dialect" yaml:"dialect"`
Database string `json:"database" yaml:"database"`
Username string `json:"username" yaml:"username"`
Password string `json:"password" yaml:"password"`
Host string `json:"host" yaml:"host"`
Port uint16 `json:"port" yaml:"port"`
Params map[string]string `json:"params"`
Pragmas map[string]string `json:"pragmas"`
Params map[string]string `json:"params" yaml:"params"`
Pragmas map[string]string `json:"pragmas" yaml:"pragmas"`
MaxIdleConns int `json:"max_idle_conns"`
MaxOpenConns int `json:"max_open_conns"`
MaxIdleConns int `json:"max_idle_conns" yaml:"max_idle_conns"`
MaxOpenConns int `json:"max_open_conns" yaml:"max_open_conns"`
LogLevel string `json:"log_level"`
LogLevel string `json:"log_level" yaml:"log_level"`
}
// DSN - Get the db connections string
@@ -170,7 +177,7 @@ func (c *DatabaseConfig) Save() error {
return err
}
}
data, err := json.MarshalIndent(c, "", " ")
data, err := yaml.Marshal(c)
if err != nil {
return err
}
@@ -185,18 +192,33 @@ func (c *DatabaseConfig) Save() error {
// GetDatabaseConfig - Get config value
func GetDatabaseConfig() *DatabaseConfig {
configPath := GetDatabaseConfigPath()
legacyPath := getDatabaseLegacyConfigPath()
config := getDefaultDatabaseConfig()
migratedLegacy := false
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
data, err := os.ReadFile(configPath)
if err != nil {
databaseConfigLog.Errorf("Failed to read config file %s", err)
return config
}
err = json.Unmarshal(data, config)
err = yaml.Unmarshal(data, config)
if err != nil {
databaseConfigLog.Errorf("Failed to parse config file %s", err)
return config
}
} else if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
data, err := os.ReadFile(legacyPath)
if err != nil {
databaseConfigLog.Errorf("Failed to read legacy config file %s", err)
return config
}
err = json.Unmarshal(data, config)
if err != nil {
databaseConfigLog.Errorf("Failed to parse legacy config file %s", err)
return config
}
migratedLegacy = true
databaseConfigLog.Infof("Migrating legacy config %s to %s", legacyPath, configPath)
} else {
databaseConfigLog.Warnf("Config file does not exist, using defaults")
}
@@ -213,6 +235,12 @@ func GetDatabaseConfig() *DatabaseConfig {
err := config.Save() // This updates the config with any missing fields
if err != nil {
databaseConfigLog.Errorf("Failed to save default config %s", err)
return config
}
if migratedLegacy {
if err := renameLegacyConfig(legacyPath); err != nil {
databaseConfigLog.Errorf("Failed to rename legacy config %s", err)
}
}
return config
}
+129
View File
@@ -0,0 +1,129 @@
package configs
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestDatabaseConfigParsesYAML(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
configPath := GetDatabaseConfigPath()
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
t.Fatalf("failed to create config dir: %v", err)
}
data := []byte(`dialect: mysql
database: "sliver"
username: "tester"
password: "secret"
host: "127.0.0.1"
port: 3306
params:
charset: "utf8mb4"
pragmas: {}
max_idle_conns: 7
max_open_conns: 15
log_level: "info"
`)
if err := os.WriteFile(configPath, data, 0600); err != nil {
t.Fatalf("failed to write yaml config: %v", err)
}
config := GetDatabaseConfig()
if config.Dialect != MySQL {
t.Fatalf("expected dialect %q, got %q", MySQL, config.Dialect)
}
if config.Database != "sliver" {
t.Fatalf("expected database %q, got %q", "sliver", config.Database)
}
if config.Username != "tester" {
t.Fatalf("expected username %q, got %q", "tester", config.Username)
}
if config.Password != "secret" {
t.Fatalf("expected password %q, got %q", "secret", config.Password)
}
if config.Host != "127.0.0.1" {
t.Fatalf("expected host %q, got %q", "127.0.0.1", config.Host)
}
if config.Port != 3306 {
t.Fatalf("expected port %d, got %d", 3306, config.Port)
}
if config.Params["charset"] != "utf8mb4" {
t.Fatalf("expected charset param %q, got %q", "utf8mb4", config.Params["charset"])
}
if config.MaxIdleConns != 7 {
t.Fatalf("expected max_idle_conns %d, got %d", 7, config.MaxIdleConns)
}
if config.MaxOpenConns != 15 {
t.Fatalf("expected max_open_conns %d, got %d", 15, config.MaxOpenConns)
}
if config.LogLevel != "info" {
t.Fatalf("expected log_level %q, got %q", "info", config.LogLevel)
}
}
func TestDatabaseConfigWritesDefault(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
config := GetDatabaseConfig()
if config.Dialect != Sqlite {
t.Fatalf("expected default dialect %q, got %q", Sqlite, config.Dialect)
}
if _, err := os.Stat(GetDatabaseConfigPath()); err != nil {
t.Fatalf("expected default config file to exist: %v", err)
}
}
func TestDatabaseConfigMigratesLegacyJSON(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
configDir := filepath.Dir(GetDatabaseConfigPath())
if err := os.MkdirAll(configDir, 0700); err != nil {
t.Fatalf("failed to create config dir: %v", err)
}
legacyPath := filepath.Join(configDir, databaseLegacyConfigFileName)
legacy := &DatabaseConfig{
Dialect: Postgres,
Database: "sliver",
Username: "legacy",
Password: "pass",
Host: "db.internal",
Port: 5432,
Params: map[string]string{"sslmode": "disable"},
Pragmas: map[string]string{},
MaxIdleConns: 4,
MaxOpenConns: 9,
LogLevel: "debug",
}
data, err := json.MarshalIndent(legacy, "", " ")
if err != nil {
t.Fatalf("failed to marshal legacy config: %v", err)
}
if err := os.WriteFile(legacyPath, data, 0600); err != nil {
t.Fatalf("failed to write legacy config: %v", err)
}
config := GetDatabaseConfig()
if config.Dialect != Postgres {
t.Fatalf("expected dialect %q, got %q", Postgres, config.Dialect)
}
if config.Database != "sliver" {
t.Fatalf("expected database %q, got %q", "sliver", config.Database)
}
if config.Params["sslmode"] != "disable" {
t.Fatalf("expected sslmode param %q, got %q", "disable", config.Params["sslmode"])
}
if _, err := os.Stat(GetDatabaseConfigPath()); err != nil {
t.Fatalf("expected migrated yaml config file to exist: %v", err)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("expected legacy config to be renamed: %v", err)
}
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
t.Fatalf("expected legacy backup file to exist: %v", err)
}
}
+27
View File
@@ -0,0 +1,27 @@
package configs
import (
"os"
"path/filepath"
"strings"
)
func legacyBackupPath(legacyPath string) string {
dir := filepath.Dir(legacyPath)
base := filepath.Base(legacyPath)
name := strings.TrimSuffix(base, filepath.Ext(base))
return filepath.Join(dir, "."+name+".json-old")
}
func renameLegacyConfig(legacyPath string) error {
if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
return nil
}
backupPath := legacyBackupPath(legacyPath)
if _, err := os.Stat(backupPath); err == nil {
if err := os.Remove(backupPath); err != nil {
return err
}
}
return os.Rename(legacyPath, backupPath)
}
+130 -62
View File
@@ -24,20 +24,21 @@ import (
"path/filepath"
"github.com/bishopfox/sliver/server/assets"
"github.com/bishopfox/sliver/server/db/models"
"github.com/bishopfox/sliver/server/log"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
const (
serverConfigFileName = "server.json"
serverConfigFileName = "server.yaml"
serverLegacyConfigFileName = "server.json"
)
var (
serverConfigLog = log.NamedLogger("config", "server")
)
// GetServerConfigPath - File path to config.json
// GetServerConfigPath - File path to config.yaml
func GetServerConfigPath() string {
appDir := assets.GetRootAppDir()
serverConfigPath := filepath.Join(appDir, "configs", serverConfigFileName)
@@ -45,104 +46,150 @@ func GetServerConfigPath() string {
return serverConfigPath
}
func getServerLegacyConfigPath() string {
appDir := assets.GetRootAppDir()
return filepath.Join(appDir, "configs", serverLegacyConfigFileName)
}
// LogConfig - Server logging config
type LogConfig struct {
Level int `json:"level"`
GRPCUnaryPayloads bool `json:"grpc_unary_payloads"`
GRPCStreamPayloads bool `json:"grpc_stream_payloads"`
TLSKeyLogger bool `json:"tls_key_logger"`
Level int `json:"level" yaml:"level"`
GRPCUnaryPayloads bool `json:"grpc_unary_payloads" yaml:"grpc_unary_payloads"`
GRPCStreamPayloads bool `json:"grpc_stream_payloads" yaml:"grpc_stream_payloads"`
TLSKeyLogger bool `json:"tls_key_logger" yaml:"tls_key_logger"`
}
// DaemonConfig - Configure daemon mode
type DaemonConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Tailscale bool `json:"tailscale"`
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
Tailscale bool `json:"tailscale" yaml:"tailscale"`
}
// JobConfig - Restart Jobs on Load
type JobConfig struct {
Multiplayer []*MultiplayerJobConfig `json:"multiplayer"`
MTLS []*MTLSJobConfig `json:"mtls,omitempty"`
WG []*WGJobConfig `json:"wg,omitempty"`
DNS []*DNSJobConfig `json:"dns,omitempty"`
HTTP []*HTTPJobConfig `json:"http,omitempty"`
Multiplayer []*MultiplayerJobConfig `json:"multiplayer" yaml:"multiplayer"`
MTLS []*MTLSJobConfig `json:"mtls,omitempty" yaml:"mtls,omitempty"`
WG []*WGJobConfig `json:"wg,omitempty" yaml:"wg,omitempty"`
DNS []*DNSJobConfig `json:"dns,omitempty" yaml:"dns,omitempty"`
HTTP []*HTTPJobConfig `json:"http,omitempty" yaml:"http,omitempty"`
}
type MultiplayerJobConfig struct {
Host string `json:"host"`
Port uint16 `json:"port"`
JobID string `json:"job_id"`
Tailscale bool `json:"tailscale"`
Host string `json:"host" yaml:"host"`
Port uint16 `json:"port" yaml:"port"`
JobID string `json:"job_id" yaml:"job_id"`
Tailscale bool `json:"tailscale" yaml:"tailscale"`
}
// MTLSJobConfig - Per-type job configs
type MTLSJobConfig struct {
Host string `json:"host"`
Port uint16 `json:"port"`
JobID string `json:"job_id"`
Host string `json:"host" yaml:"host"`
Port uint16 `json:"port" yaml:"port"`
JobID string `json:"job_id" yaml:"job_id"`
}
// WGJobConfig - Per-type job configs
type WGJobConfig struct {
Port uint16 `json:"port"`
NPort uint16 `json:"nport"`
KeyPort uint16 `json:"key_port"`
JobID string `json:"job_id"`
Port uint16 `json:"port" yaml:"port"`
NPort uint16 `json:"nport" yaml:"nport"`
KeyPort uint16 `json:"key_port" yaml:"key_port"`
JobID string `json:"job_id" yaml:"job_id"`
}
// DNSJobConfig - Persistent DNS job config
type DNSJobConfig struct {
Domains []string `json:"domains"`
Canaries bool `json:"canaries"`
Host string `json:"host"`
Port uint16 `json:"port"`
JobID string `json:"job_id"`
EnforceOTP bool `json:"enforce_otp"`
Domains []string `json:"domains" yaml:"domains"`
Canaries bool `json:"canaries" yaml:"canaries"`
Host string `json:"host" yaml:"host"`
Port uint16 `json:"port" yaml:"port"`
JobID string `json:"job_id" yaml:"job_id"`
EnforceOTP bool `json:"enforce_otp" yaml:"enforce_otp"`
}
// HTTPJobConfig - Persistent HTTP job config
type HTTPJobConfig struct {
Domain string `json:"domain"`
Host string `json:"host"`
Port uint16 `json:"port"`
Secure bool `json:"secure"`
Website string `json:"website"`
Cert []byte `json:"cert"`
Key []byte `json:"key"`
ACME bool `json:"acme"`
JobID string `json:"job_id"`
EnforceOTP bool `json:"enforce_otp"`
LongPollTimeout int64 `json:"long_poll_timeout"`
LongPollJitter int64 `json:"long_poll_jitter"`
RandomizeJARM bool `json:"randomize_jarm"`
Domain string `json:"domain" yaml:"domain"`
Host string `json:"host" yaml:"host"`
Port uint16 `json:"port" yaml:"port"`
Secure bool `json:"secure" yaml:"secure"`
Website string `json:"website" yaml:"website"`
Cert []byte `json:"cert" yaml:"cert"`
Key []byte `json:"key" yaml:"key"`
ACME bool `json:"acme" yaml:"acme"`
JobID string `json:"job_id" yaml:"job_id"`
EnforceOTP bool `json:"enforce_otp" yaml:"enforce_otp"`
LongPollTimeout int64 `json:"long_poll_timeout" yaml:"long_poll_timeout"`
LongPollJitter int64 `json:"long_poll_jitter" yaml:"long_poll_jitter"`
RandomizeJARM bool `json:"randomize_jarm" yaml:"randomize_jarm"`
}
// WatchTowerConfig - Watch Tower job config
type WatchTowerConfig struct {
VTApiKey string `json:"vt_api_key"`
XForceApiKey string `json:"xforce_api_key"`
XForceApiPassword string `json:"xforce_api_password"`
VTApiKey string `json:"vt_api_key" yaml:"vt_api_key"`
XForceApiKey string `json:"xforce_api_key" yaml:"xforce_api_key"`
XForceApiPassword string `json:"xforce_api_password" yaml:"xforce_api_password"`
}
// http server defaults for anonymous requests
type HttpDefaultHeader struct {
Method string `json:"method" yaml:"method"`
Name string `json:"name" yaml:"name"`
Value string `json:"value" yaml:"value"`
Probability int32 `json:"probability" yaml:"probability"`
}
type httpDefaultHeaderLegacy struct {
Method string `json:"method" yaml:"method"`
Name string `json:"name" yaml:"name"`
Value string `json:"value" yaml:"value"`
Probability int32 `json:"probability" yaml:"probability"`
ID string `json:"id" yaml:"id"`
HttpC2ServerConfigID *string `json:"httpc2serverconfigid" yaml:"httpc2serverconfigid"`
HttpC2ImplantConfigID *string `json:"httpc2implantconfigid" yaml:"httpc2implantconfigid"`
}
func (h *HttpDefaultHeader) UnmarshalJSON(data []byte) error {
var raw httpDefaultHeaderLegacy
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
h.Method = raw.Method
h.Name = raw.Name
h.Value = raw.Value
h.Probability = raw.Probability
return nil
}
func (h *HttpDefaultHeader) UnmarshalYAML(node *yaml.Node) error {
var raw httpDefaultHeaderLegacy
if err := node.Decode(&raw); err != nil {
return err
}
h.Method = raw.Method
h.Name = raw.Name
h.Value = raw.Value
h.Probability = raw.Probability
return nil
}
type HttpDefaultConfig struct {
Headers []models.HttpC2Header `json:"headers"`
Headers []HttpDefaultHeader `json:"headers" yaml:"headers"`
}
// ServerConfig - Server config
type ServerConfig struct {
DaemonMode bool `json:"daemon_mode"`
DaemonConfig *DaemonConfig `json:"daemon"`
Logs *LogConfig `json:"logs"`
Watchtower *WatchTowerConfig `json:"watch_tower"`
GoProxy string `json:"go_proxy"`
HTTPDefaults *HttpDefaultConfig `json:"http_default"`
DonutBypass int `json:"donut_bypass"` // 1=skip, 2=abort on fail, 3=continue on fail.
DaemonMode bool `json:"daemon_mode" yaml:"daemon_mode"`
DaemonConfig *DaemonConfig `json:"daemon" yaml:"daemon"`
Logs *LogConfig `json:"logs" yaml:"logs"`
Watchtower *WatchTowerConfig `json:"watch_tower" yaml:"watch_tower"`
GoProxy string `json:"go_proxy" yaml:"go_proxy"`
HTTPDefaults *HttpDefaultConfig `json:"http_default" yaml:"http_default"`
DonutBypass int `json:"donut_bypass" yaml:"donut_bypass"` // 1=skip, 2=abort on fail, 3=continue on fail.
// 'GOOS/GOARCH' -> CC path
CC map[string]string `json:"cc"`
CXX map[string]string `json:"cxx"`
CC map[string]string `json:"cc" yaml:"cc"`
CXX map[string]string `json:"cxx" yaml:"cxx"`
}
// Save - Save config file to disk
@@ -156,7 +203,7 @@ func (c *ServerConfig) Save() error {
return err
}
}
data, err := json.MarshalIndent(c, "", " ")
data, err := yaml.Marshal(c)
if err != nil {
return err
}
@@ -171,18 +218,33 @@ func (c *ServerConfig) Save() error {
// GetServerConfig - Get config value
func GetServerConfig() *ServerConfig {
configPath := GetServerConfigPath()
legacyPath := getServerLegacyConfigPath()
config := getDefaultServerConfig()
migratedLegacy := false
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
data, err := os.ReadFile(configPath)
if err != nil {
serverConfigLog.Errorf("Failed to read config file %s", err)
return config
}
err = json.Unmarshal(data, config)
err = yaml.Unmarshal(data, config)
if err != nil {
serverConfigLog.Errorf("Failed to parse config file %s", err)
return config
}
} else if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
data, err := os.ReadFile(legacyPath)
if err != nil {
serverConfigLog.Errorf("Failed to read legacy config file %s", err)
return config
}
err = json.Unmarshal(data, config)
if err != nil {
serverConfigLog.Errorf("Failed to parse legacy config file %s", err)
return config
}
migratedLegacy = true
serverConfigLog.Infof("Migrating legacy config %s to %s", legacyPath, configPath)
} else {
serverConfigLog.Warnf("Config file does not exist, using defaults")
}
@@ -202,6 +264,12 @@ func GetServerConfig() *ServerConfig {
err := config.Save() // This updates the config with any missing fields
if err != nil {
serverConfigLog.Errorf("Failed to save default config %s", err)
return config
}
if migratedLegacy {
if err := renameLegacyConfig(legacyPath); err != nil {
serverConfigLog.Errorf("Failed to rename legacy config %s", err)
}
}
return config
}
@@ -219,8 +287,8 @@ func getDefaultServerConfig() *ServerConfig {
GRPCStreamPayloads: false,
},
HTTPDefaults: &HttpDefaultConfig{
Headers: []models.HttpC2Header{
models.HttpC2Header{
Headers: []HttpDefaultHeader{
{
Method: "GET",
Name: "Cache-Control",
Value: "no-store, no-cache, must-revalidate",
+194
View File
@@ -0,0 +1,194 @@
package configs
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestServerConfigParsesYAML(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
configPath := GetServerConfigPath()
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
t.Fatalf("failed to create config dir: %v", err)
}
data := []byte(`daemon_mode: true
daemon:
host: "127.0.0.1"
port: 4444
tailscale: true
logs:
level: 5
grpc_unary_payloads: true
grpc_stream_payloads: true
tls_key_logger: true
watch_tower:
vt_api_key: "vt"
xforce_api_key: "xforce"
xforce_api_password: "secret"
go_proxy: "https://proxy.example"
http_default:
headers:
- method: "POST"
name: "X-Test"
value: "abc"
probability: 50
donut_bypass: 2
cc:
linux/amd64: "/usr/bin/cc"
cxx:
linux/amd64: "/usr/bin/c++"
`)
if err := os.WriteFile(configPath, data, 0600); err != nil {
t.Fatalf("failed to write yaml config: %v", err)
}
config := GetServerConfig()
if !config.DaemonMode {
t.Fatalf("expected daemon_mode true")
}
if config.DaemonConfig == nil || config.DaemonConfig.Port != 4444 {
t.Fatalf("expected daemon port %d, got %v", 4444, config.DaemonConfig)
}
if config.Logs == nil || config.Logs.Level != 5 {
t.Fatalf("expected logs level %d, got %v", 5, config.Logs)
}
if config.Logs == nil || !config.Logs.TLSKeyLogger {
t.Fatalf("expected tls_key_logger true")
}
if config.Watchtower == nil || config.Watchtower.VTApiKey != "vt" {
t.Fatalf("expected watch_tower vt_api_key %q, got %v", "vt", config.Watchtower)
}
if config.GoProxy != "https://proxy.example" {
t.Fatalf("expected go_proxy %q, got %q", "https://proxy.example", config.GoProxy)
}
if config.HTTPDefaults == nil || len(config.HTTPDefaults.Headers) != 1 {
t.Fatalf("expected 1 http default header, got %v", config.HTTPDefaults)
}
header := config.HTTPDefaults.Headers[0]
if header.Method != "POST" || header.Name != "X-Test" || header.Value != "abc" || header.Probability != 50 {
t.Fatalf("unexpected header values: %#v", header)
}
if config.DonutBypass != 2 {
t.Fatalf("expected donut_bypass %d, got %d", 2, config.DonutBypass)
}
if config.CC["linux/amd64"] != "/usr/bin/cc" {
t.Fatalf("expected cc override %q, got %q", "/usr/bin/cc", config.CC["linux/amd64"])
}
if config.CXX["linux/amd64"] != "/usr/bin/c++" {
t.Fatalf("expected cxx override %q, got %q", "/usr/bin/c++", config.CXX["linux/amd64"])
}
}
func TestServerConfigWritesDefault(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
config := GetServerConfig()
if config.DaemonConfig == nil {
t.Fatalf("expected default daemon config")
}
if _, err := os.Stat(GetServerConfigPath()); err != nil {
t.Fatalf("expected default config file to exist: %v", err)
}
}
func TestServerConfigMigratesLegacyJSON(t *testing.T) {
t.Setenv("SLIVER_ROOT_DIR", t.TempDir())
configDir := filepath.Dir(GetServerConfigPath())
if err := os.MkdirAll(configDir, 0700); err != nil {
t.Fatalf("failed to create config dir: %v", err)
}
legacyPath := filepath.Join(configDir, serverLegacyConfigFileName)
type legacyHeader struct {
ID string `json:"id"`
HttpC2ServerConfigID *string `json:"httpc2serverconfigid"`
HttpC2ImplantConfigID *string `json:"httpc2implantconfigid"`
Method string `json:"method"`
Name string `json:"name"`
Value string `json:"value"`
Probability int32 `json:"probability"`
}
type legacyHTTPDefaults struct {
Headers []legacyHeader `json:"headers"`
}
type legacyServerConfig struct {
DaemonMode bool `json:"daemon_mode"`
DaemonConfig *DaemonConfig `json:"daemon"`
Logs *LogConfig `json:"logs"`
Watchtower *WatchTowerConfig `json:"watch_tower"`
GoProxy string `json:"go_proxy"`
HTTPDefaults *legacyHTTPDefaults `json:"http_default"`
DonutBypass int `json:"donut_bypass"`
CC map[string]string `json:"cc"`
CXX map[string]string `json:"cxx"`
}
legacy := &legacyServerConfig{
DaemonMode: true,
DaemonConfig: &DaemonConfig{
Host: "127.0.0.1",
Port: 31338,
Tailscale: false,
},
Logs: &LogConfig{
Level: 4,
GRPCUnaryPayloads: true,
GRPCStreamPayloads: true,
TLSKeyLogger: true,
},
Watchtower: &WatchTowerConfig{
VTApiKey: "vt-legacy",
XForceApiKey: "xforce-legacy",
XForceApiPassword: "secret",
},
GoProxy: "https://proxy.legacy",
HTTPDefaults: &legacyHTTPDefaults{
Headers: []legacyHeader{
{
ID: "00000000-0000-0000-0000-000000000000",
Method: "GET",
Name: "Cache-Control",
Value: "no-cache",
Probability: 42,
},
},
},
DonutBypass: 2,
CC: map[string]string{"linux/amd64": "/usr/bin/cc"},
CXX: map[string]string{"linux/amd64": "/usr/bin/c++"},
}
data, err := json.MarshalIndent(legacy, "", " ")
if err != nil {
t.Fatalf("failed to marshal legacy config: %v", err)
}
if err := os.WriteFile(legacyPath, data, 0600); err != nil {
t.Fatalf("failed to write legacy config: %v", err)
}
config := GetServerConfig()
if !config.DaemonMode || config.DaemonConfig == nil || config.DaemonConfig.Port != 31338 {
t.Fatalf("expected legacy daemon config to load")
}
if config.Watchtower == nil || config.Watchtower.VTApiKey != "vt-legacy" {
t.Fatalf("expected legacy watch_tower to load")
}
if config.GoProxy != "https://proxy.legacy" {
t.Fatalf("expected legacy go_proxy %q, got %q", "https://proxy.legacy", config.GoProxy)
}
if config.HTTPDefaults == nil || len(config.HTTPDefaults.Headers) != 1 {
t.Fatalf("expected legacy http defaults to load")
}
if _, err := os.Stat(GetServerConfigPath()); err != nil {
t.Fatalf("expected migrated yaml config file to exist: %v", err)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("expected legacy config to be renamed: %v", err)
}
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
t.Fatalf("expected legacy backup file to exist: %v", err)
}
}
+6 -1
View File
@@ -136,6 +136,10 @@ func TestSliverSharedLibWindows(t *testing.T) {
// 386
multiLibrary(t, "windows", "386", true)
multiLibrary(t, "windows", "386", false)
// Shared Library Shellcode
multiWindowsLibraryShellcode(t, true)
multiWindowsLibraryShellcode(t, false)
}
func TestSliverExecutableLinux(t *testing.T) {
@@ -554,7 +558,8 @@ func multiWindowsLibraryShellcode(t *testing.T, debug bool) {
Debug: debug,
ObfuscateSymbols: true,
Format: clientpb.OutputFormat_SHELLCODE,
IsSharedLib: true,
IsShellcode: true,
IsSharedLib: false,
Exports: []string{"FoobarW"},
IncludeMTLS: true,
IncludeHTTP: true,
+6 -1
View File
@@ -20,10 +20,13 @@ package rpc
import (
"context"
"fmt"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/server/sgn"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ShellcodeEncode - Encode a piece shellcode
@@ -35,9 +38,11 @@ func (rpc *Server) ShellcodeEncoder(ctx context.Context, req *clientpb.Shellcode
switch req.Encoder {
case clientpb.ShellcodeEncoder_SHIKATA_GA_NAI:
rpcLog.Infof("[rpc] Shellcode encoder request for: SHIKATA_GA_NAI")
rpcLog.Infof("[rpc] Encoding shellcode (%d bytes) for architecture %s with %d iterations and badchars: %v", len(req.Data), req.Architecture, req.Iterations, req.BadChars)
resp.Data, err = sgn.EncodeShellcode(req.Data, req.Architecture, int(req.Iterations), req.BadChars)
if err != nil {
resp.Response.Err = err.Error()
rpcLog.Errorf("[rpc] Failed to encode shellcode: %v", err)
return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to encode shellcode (%s)", err))
}
default:
resp.Response.Err = "Unknown encoder"
+8 -5
View File
@@ -56,9 +56,12 @@ type SGNConfig struct {
// EncodeShellcode - Encode a shellcode
func EncodeShellcode(shellcode []byte, arch string, iterations int, badChars []byte) ([]byte, error) {
cfg := SGNConfig{
Architecture: arch,
Iterations: iterations,
BadChars: badChars,
Architecture: arch,
Iterations: iterations,
BadChars: badChars,
PlainDecoder: false,
Safe: true,
MaxObfuscation: 100,
}
return EncodeShellcodeWithConfig(shellcode, cfg)
}
@@ -83,10 +86,10 @@ func EncodeShellcodeWithConfig(shellcode []byte, cfg SGNConfig) ([]byte, error)
needsConstraints := cfg.Asci || len(cfg.BadChars) > 0
seed := sgnpkg.GetRandomByte()
const maxAttempts = 512
const maxAttempts = 64
var lastErr error
for attempt := 0; attempt < maxAttempts; attempt++ {
for attempt := range maxAttempts {
encoder, err := newEncoderWithConfig(arch, cfg)
if err != nil {
sgnLog.Errorf("[sgn] EncodeShellcode setup failed: %s", err)
+56
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"math/rand"
"strings"
keystone "github.com/moloch--/go-keystone"
)
@@ -197,6 +198,12 @@ func (encoder Encoder) Assemble(asm string) ([]byte, bool) {
return nil, false
}
asm = normalizeAssembly(asm)
if asm == "" {
return []byte{}, true
}
asm = addCodeDirective(asm, encoder.architecture)
ks, err := keystone.NewEngine(keystone.ARCH_X86, mode)
if err != nil {
return nil, false
@@ -227,6 +234,12 @@ func (encoder Encoder) GetAssemblySize(asm string) int {
return -1
}
asm = normalizeAssembly(asm)
if asm == "" {
return 0
}
asm = addCodeDirective(asm, encoder.architecture)
ks, err := keystone.NewEngine(keystone.ARCH_X86, mode)
if err != nil {
return -1
@@ -247,6 +260,49 @@ func (encoder Encoder) GetAssemblySize(asm string) int {
return len(bin)
}
func normalizeAssembly(asm string) string {
if asm == "" {
return ""
}
cleaned := strings.ReplaceAll(asm, ";", "\n")
lines := strings.Split(cleaned, "\n")
out := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
out = append(out, line)
}
return strings.Join(out, "\n")
}
func addCodeDirective(asm string, arch int) string {
if asm == "" || hasCodeDirective(asm) {
return asm
}
switch arch {
case 32:
return ".code32\n" + asm
case 64:
return ".code64\n" + asm
default:
return asm
}
}
func hasCodeDirective(asm string) bool {
for _, line := range strings.Split(asm, "\n") {
trimmed := strings.ToLower(strings.TrimSpace(line))
if strings.HasPrefix(trimmed, ".code") {
return true
}
}
return false
}
// GenerateIPToStack function generates instructions series that pushes the instruction pointer to stack
func (encoder Encoder) GenerateIPToStack() []byte {
+1 -1
View File
@@ -540,7 +540,7 @@ github.com/moloch--/go-keystone
# github.com/moloch--/memmod v0.0.0-20230225130813-fd77d905589e
## explicit; go 1.17
github.com/moloch--/memmod
# github.com/moloch--/sgn v0.0.4
# github.com/moloch--/sgn v0.0.5
## explicit; go 1.23
github.com/moloch--/sgn/pkg
# github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6